Comparing version 7.1.1 to 7.2.0-dev.4868d7c
@@ -22,3 +22,2 @@ #!/usr/bin/env node | ||
import { $, chalk, fetch, ProcessOutput } from './index.js'; | ||
import { startRepl } from './repl.js'; | ||
import { randomId } from './util.js'; | ||
@@ -74,3 +73,3 @@ import { installDeps, parseDeps } from './deps.js'; | ||
if (argv.repl) { | ||
startRepl(); | ||
await (await import('./repl.js')).startRepl(); | ||
return; | ||
@@ -172,3 +171,7 @@ } | ||
let state = 'root'; | ||
let codeBlockEnd = ''; | ||
let prevLineIsEmpty = true; | ||
const jsCodeBlock = /^(```+|~~~+)(js|javascript)$/; | ||
const shCodeBlock = /^(```+|~~~+)(sh|bash)$/; | ||
const otherCodeBlock = /^(```+|~~~+)(.*)$/; | ||
for (let line of source.split('\n')) { | ||
@@ -181,13 +184,16 @@ switch (state) { | ||
} | ||
else if (/^```(js|javascript)$/.test(line)) { | ||
else if (jsCodeBlock.test(line)) { | ||
output.push(''); | ||
state = 'js'; | ||
codeBlockEnd = line.match(jsCodeBlock)[1]; | ||
} | ||
else if (/^```(sh|bash)$/.test(line)) { | ||
else if (shCodeBlock.test(line)) { | ||
output.push('await $`'); | ||
state = 'bash'; | ||
codeBlockEnd = line.match(shCodeBlock)[1]; | ||
} | ||
else if (/^```.*$/.test(line)) { | ||
else if (otherCodeBlock.test(line)) { | ||
output.push(''); | ||
state = 'other'; | ||
codeBlockEnd = line.match(otherCodeBlock)[1]; | ||
} | ||
@@ -212,3 +218,3 @@ else { | ||
case 'js': | ||
if (/^```$/.test(line)) { | ||
if (line === codeBlockEnd) { | ||
output.push(''); | ||
@@ -222,3 +228,3 @@ state = 'root'; | ||
case 'bash': | ||
if (/^```$/.test(line)) { | ||
if (line === codeBlockEnd) { | ||
output.push('`'); | ||
@@ -232,3 +238,3 @@ state = 'root'; | ||
case 'other': | ||
if (/^```$/.test(line)) { | ||
if (line === codeBlockEnd) { | ||
output.push(''); | ||
@@ -235,0 +241,0 @@ state = 'root'; |
@@ -11,5 +11,5 @@ /// <reference types="node" resolution-mode="require"/> | ||
import { Duration, noop, quote } from './util.js'; | ||
export declare type Shell = (pieces: TemplateStringsArray, ...args: any[]) => ProcessPromise; | ||
export type Shell = (pieces: TemplateStringsArray, ...args: any[]) => ProcessPromise; | ||
declare const processCwd: unique symbol; | ||
export declare type Options = { | ||
export type Options = { | ||
[processCwd]: string; | ||
@@ -27,4 +27,4 @@ cwd?: string; | ||
export declare const $: Shell & Options; | ||
declare type Resolve = (out: ProcessOutput) => void; | ||
declare type IO = StdioPipe | StdioNull; | ||
type Resolve = (out: ProcessOutput) => void; | ||
type IO = StdioPipe | StdioNull; | ||
export declare class ProcessPromise extends Promise<ProcessOutput> { | ||
@@ -80,3 +80,3 @@ child?: ChildProcess; | ||
export declare function cd(dir: string): void; | ||
export declare type LogEntry = { | ||
export type LogEntry = { | ||
kind: 'cmd'; | ||
@@ -83,0 +83,0 @@ verbose: boolean; |
@@ -50,4 +50,9 @@ // Copyright 2021 Google LLC | ||
if (process.platform == 'win32') { | ||
defaults.shell = which.sync('powershell.exe'); | ||
defaults.quote = quotePowerShell; | ||
try { | ||
defaults.shell = which.sync('powershell.exe'); | ||
defaults.quote = quotePowerShell; | ||
} | ||
catch (err) { | ||
// no powershell? | ||
} | ||
} | ||
@@ -54,0 +59,0 @@ } |
@@ -1,6 +0,1 @@ | ||
import { Duration } from './util.js'; | ||
export declare function retry<T>(count: number, callback: () => T): Promise<T>; | ||
export declare function retry<T>(count: number, duration: Duration | Generator<number>, callback: () => T): Promise<T>; | ||
export declare function expBackoff(max?: Duration, rand?: Duration): Generator<number, void, unknown>; | ||
export declare function spinner<T>(callback: () => T): Promise<T>; | ||
export declare function spinner<T>(title: string, callback: () => T): Promise<T>; | ||
export { spinner, retry, expBackoff, echo } from './goods.js'; |
@@ -14,82 +14,3 @@ // Copyright 2021 Google LLC | ||
// limitations under the License. | ||
import assert from 'node:assert'; | ||
import chalk from 'chalk'; | ||
import { $, within } from './core.js'; | ||
import { sleep } from './goods.js'; | ||
import { parseDuration } from './util.js'; | ||
export async function retry(count, a, b) { | ||
const total = count; | ||
let callback; | ||
let delayStatic = 0; | ||
let delayGen; | ||
if (typeof a == 'function') { | ||
callback = a; | ||
} | ||
else { | ||
if (typeof a == 'object') { | ||
delayGen = a; | ||
} | ||
else { | ||
delayStatic = parseDuration(a); | ||
} | ||
assert(b); | ||
callback = b; | ||
} | ||
let lastErr; | ||
let attempt = 0; | ||
while (count-- > 0) { | ||
attempt++; | ||
try { | ||
return await callback(); | ||
} | ||
catch (err) { | ||
let delay = 0; | ||
if (delayStatic > 0) | ||
delay = delayStatic; | ||
if (delayGen) | ||
delay = delayGen.next().value; | ||
$.log({ | ||
kind: 'retry', | ||
error: chalk.bgRed.white(' FAIL ') + | ||
` Attempt: ${attempt}${total == Infinity ? '' : `/${total}`}` + | ||
(delay > 0 ? `; next in ${delay}ms` : ''), | ||
}); | ||
lastErr = err; | ||
if (count == 0) | ||
break; | ||
if (delay) | ||
await sleep(delay); | ||
} | ||
} | ||
throw lastErr; | ||
} | ||
export function* expBackoff(max = '60s', rand = '100ms') { | ||
const maxMs = parseDuration(max); | ||
const randMs = parseDuration(rand); | ||
let n = 1; | ||
while (true) { | ||
const ms = Math.floor(Math.random() * randMs); | ||
yield Math.min(2 ** n++, maxMs) + ms; | ||
} | ||
} | ||
export async function spinner(title, callback) { | ||
if (typeof title == 'function') { | ||
callback = title; | ||
title = ''; | ||
} | ||
let i = 0; | ||
const spin = () => process.stderr.write(` ${'⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'[i++ % 10]} ${title}\r`); | ||
return within(async () => { | ||
$.verbose = false; | ||
const id = setInterval(spin, 100); | ||
let result; | ||
try { | ||
result = await callback(); | ||
} | ||
finally { | ||
clearInterval(id); | ||
process.stderr.write(' '.repeat(process.stdout.columns - 1) + '\r'); | ||
} | ||
return result; | ||
}); | ||
} | ||
// TODO(antonmedv): Remove this export in next v8 release. | ||
export { spinner, retry, expBackoff, echo } from './goods.js'; |
@@ -11,2 +11,3 @@ import * as globbyModule from 'globby'; | ||
export { default as os } from 'node:os'; | ||
export { ssh } from 'webpod'; | ||
export declare let argv: minimist.ParsedArgs; | ||
@@ -23,1 +24,6 @@ export declare function updateArgv(args: string[]): void; | ||
export declare function stdin(): Promise<string>; | ||
export declare function retry<T>(count: number, callback: () => T): Promise<T>; | ||
export declare function retry<T>(count: number, duration: Duration | Generator<number>, callback: () => T): Promise<T>; | ||
export declare function expBackoff(max?: Duration, rand?: Duration): Generator<number, void, unknown>; | ||
export declare function spinner<T>(callback: () => T): Promise<T>; | ||
export declare function spinner<T>(title: string, callback: () => T): Promise<T>; |
@@ -14,2 +14,3 @@ // Copyright 2022 Google LLC | ||
// limitations under the License. | ||
import assert from 'node:assert'; | ||
import * as globbyModule from 'globby'; | ||
@@ -19,4 +20,5 @@ import minimist from 'minimist'; | ||
import { createInterface } from 'node:readline'; | ||
import { $, ProcessOutput } from './core.js'; | ||
import { $, within, ProcessOutput } from './core.js'; | ||
import { isString, parseDuration } from './util.js'; | ||
import chalk from 'chalk'; | ||
export { default as chalk } from 'chalk'; | ||
@@ -28,2 +30,3 @@ export { default as fs } from 'fs-extra'; | ||
export { default as os } from 'node:os'; | ||
export { ssh } from 'webpod'; | ||
export let argv = minimist(process.argv.slice(2)); | ||
@@ -96,1 +99,77 @@ export function updateArgv(args) { | ||
} | ||
export async function retry(count, a, b) { | ||
const total = count; | ||
let callback; | ||
let delayStatic = 0; | ||
let delayGen; | ||
if (typeof a == 'function') { | ||
callback = a; | ||
} | ||
else { | ||
if (typeof a == 'object') { | ||
delayGen = a; | ||
} | ||
else { | ||
delayStatic = parseDuration(a); | ||
} | ||
assert(b); | ||
callback = b; | ||
} | ||
let lastErr; | ||
let attempt = 0; | ||
while (count-- > 0) { | ||
attempt++; | ||
try { | ||
return await callback(); | ||
} | ||
catch (err) { | ||
let delay = 0; | ||
if (delayStatic > 0) | ||
delay = delayStatic; | ||
if (delayGen) | ||
delay = delayGen.next().value; | ||
$.log({ | ||
kind: 'retry', | ||
error: chalk.bgRed.white(' FAIL ') + | ||
` Attempt: ${attempt}${total == Infinity ? '' : `/${total}`}` + | ||
(delay > 0 ? `; next in ${delay}ms` : ''), | ||
}); | ||
lastErr = err; | ||
if (count == 0) | ||
break; | ||
if (delay) | ||
await sleep(delay); | ||
} | ||
} | ||
throw lastErr; | ||
} | ||
export function* expBackoff(max = '60s', rand = '100ms') { | ||
const maxMs = parseDuration(max); | ||
const randMs = parseDuration(rand); | ||
let n = 1; | ||
while (true) { | ||
const ms = Math.floor(Math.random() * randMs); | ||
yield Math.min(2 ** n++, maxMs) + ms; | ||
} | ||
} | ||
export async function spinner(title, callback) { | ||
if (typeof title == 'function') { | ||
callback = title; | ||
title = ''; | ||
} | ||
let i = 0; | ||
const spin = () => process.stderr.write(` ${'⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'[i++ % 10]} ${title}\r`); | ||
return within(async () => { | ||
$.verbose = false; | ||
const id = setInterval(spin, 100); | ||
let result; | ||
try { | ||
result = await callback(); | ||
} | ||
finally { | ||
clearInterval(id); | ||
process.stderr.write(' '.repeat(process.stdout.columns - 1) + '\r'); | ||
} | ||
return result; | ||
}); | ||
} |
import { ProcessPromise } from './core.js'; | ||
export { $, Shell, Options, ProcessPromise, ProcessOutput, within, cd, log, LogEntry, } from './core.js'; | ||
export { argv, chalk, echo, fetch, fs, glob, globby, os, path, question, sleep, stdin, which, YAML, } from './goods.js'; | ||
export * from './core.js'; | ||
export * from './goods.js'; | ||
export { Duration, quote, quotePowerShell } from './util.js'; | ||
@@ -5,0 +5,0 @@ /** |
@@ -14,4 +14,4 @@ // Copyright 2022 Google LLC | ||
// limitations under the License. | ||
export { $, ProcessPromise, ProcessOutput, within, cd, log, } from './core.js'; | ||
export { argv, chalk, echo, fetch, fs, glob, globby, os, path, question, sleep, stdin, which, YAML, } from './goods.js'; | ||
export * from './core.js'; | ||
export * from './goods.js'; | ||
export { quote, quotePowerShell } from './util.js'; | ||
@@ -18,0 +18,0 @@ /** |
@@ -1,1 +0,1 @@ | ||
export declare function startRepl(): void; | ||
export declare function startRepl(): Promise<void>; |
@@ -17,8 +17,7 @@ // Copyright 2022 Google LLC | ||
import path from 'node:path'; | ||
import repl from 'node:repl'; | ||
import { inspect } from 'node:util'; | ||
import { ProcessOutput, defaults } from './core.js'; | ||
export function startRepl() { | ||
export async function startRepl() { | ||
defaults.verbose = false; | ||
const r = repl.start({ | ||
const r = (await import('node:repl')).start({ | ||
prompt: chalk.greenBright.bold('❯ '), | ||
@@ -25,0 +24,0 @@ useGlobal: true, |
@@ -10,4 +10,4 @@ import psTreeModule from 'ps-tree'; | ||
export declare function errnoMessage(errno: number | undefined): string; | ||
export declare type Duration = number | `${number}s` | `${number}ms`; | ||
export type Duration = number | `${number}s` | `${number}ms`; | ||
export declare function parseDuration(d: Duration): number; | ||
export declare function formatCmd(cmd?: string): string; |
{ | ||
"name": "zx", | ||
"version": "7.1.1", | ||
"version": "7.2.0-dev.4868d7c", | ||
"description": "A tool for writing better scripts.", | ||
@@ -53,23 +53,24 @@ "type": "module", | ||
"dependencies": { | ||
"@types/fs-extra": "^9.0.13", | ||
"@types/fs-extra": "^11.0.1", | ||
"@types/minimist": "^1.2.2", | ||
"@types/node": "^18.7.20", | ||
"@types/node": "^18.14.2", | ||
"@types/ps-tree": "^1.1.2", | ||
"@types/which": "^2.0.1", | ||
"chalk": "^5.0.1", | ||
"fs-extra": "^10.1.0", | ||
"globby": "^13.1.2", | ||
"minimist": "^1.2.6", | ||
"@types/which": "^2.0.2", | ||
"chalk": "^5.2.0", | ||
"fs-extra": "^11.1.0", | ||
"globby": "^13.1.3", | ||
"minimist": "^1.2.8", | ||
"node-fetch": "3.2.10", | ||
"ps-tree": "^1.2.0", | ||
"which": "^2.0.2", | ||
"yaml": "^2.1.1" | ||
"webpod": "^0.0.2", | ||
"which": "^3.0.0", | ||
"yaml": "^2.2.1" | ||
}, | ||
"devDependencies": { | ||
"@stryker-mutator/core": "^6.2.2", | ||
"c8": "^7.12.0", | ||
"madge": "^5.0.1", | ||
"prettier": "^2.7.1", | ||
"tsd": "^0.24.1", | ||
"typescript": "^4.8.3", | ||
"@stryker-mutator/core": "^6.4.1", | ||
"c8": "^7.13.0", | ||
"madge": "^6.0.0", | ||
"prettier": "^2.8.4", | ||
"tsd": "^0.25.0", | ||
"typescript": "^4.9.5", | ||
"uvu": "^0.5.6" | ||
@@ -76,0 +77,0 @@ }, |
145
README.md
@@ -38,6 +38,9 @@ # 🐚 zx | ||
[$](#command-) · [cd()](#cd) · [fetch()](#fetch) · [question()](#question) · [sleep()](#sleep) · [echo()](#echo) · [stdin()](#stdin) · [within()](#within) · | ||
[$](#command-) · [cd()](#cd) · [fetch()](#fetch) · [question()](#question) · [sleep()](#sleep) · [echo()](#echo) · [stdin()](#stdin) · [within()](#within) · [retry()](#retry) · [spinner()](#spinner) · | ||
[chalk](#chalk-package) · [fs](#fs-package) · [os](#os-package) · [path](#path-package) · [glob](#globby-package) · [yaml](#yaml-package) · [minimist](#minimist-package) · [which](#which-package) · | ||
[__filename](#__filename--__dirname) · [__dirname](#__filename--__dirname) · [require()](#require) | ||
For running commands on remote hosts, | ||
see [webpod](https://github.com/webpod/webpod). | ||
## Documentation | ||
@@ -50,2 +53,3 @@ | ||
Add the following shebang to the beginning of your `zx` scripts: | ||
```bash | ||
@@ -56,2 +60,3 @@ #!/usr/bin/env zx | ||
Now you will be able to run your script like so: | ||
```bash | ||
@@ -123,5 +128,9 @@ chmod +x ./script.mjs | ||
exitCode: Promise<number> | ||
pipe(dest): ProcessPromise | ||
kill(): Promise<void> | ||
nothrow(): this | ||
quiet(): this | ||
@@ -141,2 +150,3 @@ } | ||
readonly exitCode: number | ||
toString(): string // Combined stdout & stderr. | ||
@@ -146,3 +156,4 @@ } | ||
The output of the process is captured as-is. Usually, programs print a new line `\n` at the end. | ||
The output of the process is captured as-is. Usually, programs print a new | ||
line `\n` at the end. | ||
If `ProcessOutput` is used as an argument to some other `$` process, | ||
@@ -169,3 +180,4 @@ **zx** will use stdout and trim the new line. | ||
A wrapper around the [node-fetch](https://www.npmjs.com/package/node-fetch) package. | ||
A wrapper around the [node-fetch](https://www.npmjs.com/package/node-fetch) | ||
package. | ||
@@ -238,2 +250,28 @@ ```js | ||
### `retry()` | ||
Retries a callback for a few times. Will return after the first | ||
successful attempt, or will throw after specifies attempts count. | ||
```js | ||
let p = await retry(10, () => $`curl https://medv.io`) | ||
// With a specified delay between attempts. | ||
let p = await retry(20, '1s', () => $`curl https://medv.io`) | ||
// With an exponential backoff. | ||
let p = await retry(30, expBackoff(), () => $`curl https://medv.io`) | ||
``` | ||
### `spinner()` | ||
Starts a simple CLI spinner. | ||
```js | ||
await spinner(() => $`long-running command`) | ||
// With a message. | ||
await spinner('working...', () => $`sleep 99`) | ||
``` | ||
## Packages | ||
@@ -297,3 +335,5 @@ | ||
```js | ||
if( argv.someFlag ){ echo('yes') } | ||
if (argv.someFlag) { | ||
echo('yes') | ||
} | ||
``` | ||
@@ -384,3 +424,4 @@ | ||
In [ESM](https://nodejs.org/api/esm.html) modules, Node.js does not provide | ||
`__filename` and `__dirname` globals. As such globals are really handy in scripts, | ||
`__filename` and `__dirname` globals. As such globals are really handy in | ||
scripts, | ||
`zx` provides these for use in `.mjs` files (when using the `zx` executable). | ||
@@ -399,38 +440,2 @@ | ||
## Experimental | ||
The zx provides a few experimental functions. Please leave feedback about | ||
those features in [the discussion](https://github.com/google/zx/discussions/299). | ||
To enable new features via CLI pass `--experimental` flag. | ||
### `retry()` | ||
Retries a callback for a few times. Will return after the first | ||
successful attempt, or will throw after specifies attempts count. | ||
```js | ||
import { retry, expBackoff } from 'zx/experimental' | ||
let p = await retry(10, () => $`curl https://medv.io`) | ||
// With a specified delay between attempts. | ||
let p = await retry(20, '1s', () => $`curl https://medv.io`) | ||
// With an exponential backoff. | ||
let p = await retry(30, expBackoff(), () => $`curl https://medv.io`) | ||
``` | ||
### `spinner()` | ||
Starts a simple CLI spinner. | ||
```js | ||
import { spinner } from 'zx/experimental' | ||
await spinner(() => $`long-running command`) | ||
// With a message. | ||
await spinner('working...', () => $`sleep 99`) | ||
``` | ||
## FAQ | ||
@@ -447,6 +452,8 @@ | ||
When passing an array of values as an argument to `$`, items of the array will be escaped | ||
When passing an array of values as an argument to `$`, items of the array will | ||
be escaped | ||
individually and concatenated via space. | ||
Example: | ||
```js | ||
@@ -463,3 +470,4 @@ let files = [...] | ||
#!/usr/bin/env node | ||
import {$} from 'zx' | ||
import { $ } from 'zx' | ||
await $`date` | ||
@@ -471,3 +479,4 @@ ``` | ||
If script does not have a file extension (like `.git/hooks/pre-commit`), zx | ||
assumes that it is an [ESM](https://nodejs.org/api/modules.html#modules_module_createrequire_filename) | ||
assumes that it is | ||
an [ESM](https://nodejs.org/api/modules.html#modules_module_createrequire_filename) | ||
module. | ||
@@ -486,3 +495,3 @@ | ||
```ts | ||
import {$} from 'zx' | ||
import { $ } from 'zx' | ||
// Or | ||
@@ -497,3 +506,4 @@ import 'zx/globals' | ||
Set [`"type": "module"`](https://nodejs.org/api/packages.html#packages_type) | ||
in **package.json** and [`"module": "ESNext"`](https://www.typescriptlang.org/tsconfig/#module) | ||
in **package.json** | ||
and [`"module": "ESNext"`](https://www.typescriptlang.org/tsconfig/#module) | ||
in **tsconfig.json**. | ||
@@ -515,3 +525,3 @@ | ||
```js | ||
zx <<'EOF' | ||
zx << 'EOF' | ||
await $`pwd` | ||
@@ -534,6 +544,7 @@ EOF | ||
import sh from 'tinysh' | ||
sh.say('Hello, world!') | ||
``` | ||
Add `--install` flag to the `zx` command to install missing dependencies | ||
Add `--install` flag to the `zx` command to install missing dependencies | ||
automatically. | ||
@@ -545,3 +556,3 @@ | ||
You can also specify needed version by adding comment with `@` after | ||
You can also specify needed version by adding comment with `@` after | ||
the import. | ||
@@ -553,6 +564,19 @@ | ||
### Executing commands on remote hosts | ||
The `zx` uses [webpod](https://github.com/webpod/webpod) to execute commands on | ||
remote hosts. | ||
```js | ||
import { ssh } from 'zx' | ||
await ssh('user@host')`echo Hello, world!` | ||
``` | ||
### Attaching a profile | ||
By default `child_process` does not include aliases and bash functions. | ||
But you are still able to do it by hand. Just attach necessary directives | ||
But you are still able to do it by hand. Just attach necessary directives | ||
to the `$.prefix`. | ||
@@ -574,15 +598,18 @@ | ||
steps: | ||
- uses: actions/checkout@v3 | ||
- uses: actions/checkout@v3 | ||
- name: Build | ||
env: | ||
FORCE_COLOR: 3 | ||
run: | | ||
npx zx <<'EOF' | ||
await $`...` | ||
EOF | ||
- name: Build | ||
env: | ||
FORCE_COLOR: 3 | ||
run: | | ||
npx zx <<'EOF' | ||
await $`...` | ||
EOF | ||
``` | ||
### Canary / Beta / RC builds | ||
Impatient early adopters can try the experimental zx versions. But keep in mind: these builds are ⚠️️ __unstable__ in every sense. | ||
Impatient early adopters can try the experimental zx versions. | ||
But keep in mind: these builds are ⚠️️__beta__ in every sense. | ||
```bash | ||
@@ -589,0 +616,0 @@ npm i zx@dev |
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
75908
1527
604
14
2
+ Addedwebpod@^0.0.2
+ Added@types/fs-extra@11.0.4(transitive)
+ Added@types/jsonfile@6.1.4(transitive)
+ Addedfastq@1.19.0(transitive)
+ Addedfs-extra@11.3.0(transitive)
+ Addedwebpod@0.0.2(transitive)
+ Addedwhich@3.0.1(transitive)
- Removed@types/fs-extra@9.0.13(transitive)
- Removedfastq@1.18.0(transitive)
- Removedfs-extra@10.1.0(transitive)
- Removedwhich@2.0.2(transitive)
Updated@types/fs-extra@^11.0.1
Updated@types/node@^18.14.2
Updated@types/which@^2.0.2
Updatedchalk@^5.2.0
Updatedfs-extra@^11.1.0
Updatedglobby@^13.1.3
Updatedminimist@^1.2.8
Updatedwhich@^3.0.0
Updatedyaml@^2.2.1