Commit 7b97d2e
Changed files (8)
src/experimental.ts
@@ -12,96 +12,5 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import assert from 'node:assert'
-import chalk from 'chalk'
-import { $, within } from './core.js'
-import { sleep } from './goods.js'
-import { Duration, parseDuration } from './util.js'
-
-export async function retry<T>(count: number, callback: () => T): Promise<T>
-export async function retry<T>(
- count: number,
- duration: Duration | Generator<number>,
- callback: () => T
-): Promise<T>
-export async function retry<T>(
- count: number,
- a: Duration | Generator<number> | (() => T),
- b?: () => T
-): Promise<T> {
- const total = count
- let callback: () => T
- let delayStatic = 0
- let delayGen: Generator<number> | undefined
- if (typeof a == 'function') {
- callback = a
- } else {
- if (typeof a == 'object') {
- delayGen = a
- } else {
- delayStatic = parseDuration(a)
- }
- assert(b)
- callback = b
- }
- let lastErr: unknown
- 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: Duration = '60s', rand: Duration = '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<T>(callback: () => T): Promise<T>
-export async function spinner<T>(title: string, callback: () => T): Promise<T>
-export async function spinner<T>(
- title: string | (() => T),
- callback?: () => T
-): Promise<T> {
- 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: T
- 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'
src/goods.ts
@@ -12,12 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import assert from 'node:assert'
import * as globbyModule from 'globby'
import minimist from 'minimist'
import nodeFetch, { RequestInfo, RequestInit } from 'node-fetch'
import { createInterface } from 'node:readline'
-import { $, ProcessOutput } from './core.js'
+import { $, within, ProcessOutput } from './core.js'
import { Duration, isString, parseDuration } from './util.js'
+import chalk from 'chalk'
export { default as chalk } from 'chalk'
export { default as fs } from 'fs-extra'
@@ -112,3 +114,91 @@ export async function stdin() {
}
return buf
}
+
+export async function retry<T>(count: number, callback: () => T): Promise<T>
+export async function retry<T>(
+ count: number,
+ duration: Duration | Generator<number>,
+ callback: () => T
+): Promise<T>
+export async function retry<T>(
+ count: number,
+ a: Duration | Generator<number> | (() => T),
+ b?: () => T
+): Promise<T> {
+ const total = count
+ let callback: () => T
+ let delayStatic = 0
+ let delayGen: Generator<number> | undefined
+ if (typeof a == 'function') {
+ callback = a
+ } else {
+ if (typeof a == 'object') {
+ delayGen = a
+ } else {
+ delayStatic = parseDuration(a)
+ }
+ assert(b)
+ callback = b
+ }
+ let lastErr: unknown
+ 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: Duration = '60s', rand: Duration = '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<T>(callback: () => T): Promise<T>
+export async function spinner<T>(title: string, callback: () => T): Promise<T>
+export async function spinner<T>(
+ title: string | (() => T),
+ callback?: () => T
+): Promise<T> {
+ 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: T
+ try {
+ result = await callback!()
+ } finally {
+ clearInterval(id)
+ process.stderr.write(' '.repeat(process.stdout.columns - 1) + '\r')
+ }
+ return result
+ })
+}
src/index.ts
@@ -14,34 +14,8 @@
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'
test/experimental.test.js
@@ -20,69 +20,4 @@ const test = suite('experimental')
$.verbose = false
-function zx(script) {
- return $`node build/cli.js --experimental --eval ${script}`
- .nothrow()
- .timeout('5s')
-}
-
-test('retry() works', async () => {
- const now = Date.now()
- let p = await zx(`
- try {
- await retry(5, '50ms', () => $\`exit 123\`)
- } catch (e) {
- echo('exitCode:', e.exitCode)
- }
- await retry(5, () => $\`exit 0\`)
- echo('success')
-`)
- assert.match(p.toString(), 'exitCode: 123')
- assert.match(p.toString(), 'success')
- assert.ok(Date.now() >= now + 50 * (5 - 1))
-})
-
-test('retry() with expBackoff() works', async () => {
- const now = Date.now()
- let p = await zx(`
- try {
- await retry(5, expBackoff('60s', 0), () => $\`exit 123\`)
- } catch (e) {
- echo('exitCode:', e.exitCode)
- }
- echo('success')
-`)
- assert.match(p.toString(), 'exitCode: 123')
- assert.match(p.toString(), 'success')
- assert.ok(Date.now() >= now + 2 + 4 + 8 + 16 + 32)
-})
-
-test('spinner() works', async () => {
- let out = await zx(`
- echo(await spinner(async () => {
- await sleep(100)
- await $\`echo hidden\`
- return $\`echo result\`
- }))
- `)
- assert.match(out.stdout, 'result')
- assert.not.match(out.stderr, 'result')
- assert.not.match(out.stderr, 'hidden')
-})
-
-test('spinner() with title works', async () => {
- let out = await zx(`
- await spinner('processing', () => sleep(100))
- `)
- assert.match(out.stderr, 'processing')
-})
-
-test('spinner() stops on throw', async () => {
- let out = await zx(`
- await spinner('processing', () => $\`wtf-cmd\`)
- `)
- assert.match(out.stderr, 'Error:')
- assert.is.not(out.exitCode, 0)
-})
-
test.run()
test/goods.test.js
@@ -21,6 +21,10 @@ const test = suite('goods')
$.verbose = false
+function zx(script) {
+ return $`node build/cli.js --eval ${script}`.nothrow().timeout('5s')
+}
+
test('question() works', async () => {
let p = $`node build/cli.js --eval "
let answer = await question('foo or bar? ', { choices: ['foo', 'bar'] })
@@ -81,4 +85,63 @@ test('sleep() works', async () => {
assert.ok(Date.now() >= now + 99)
})
+test('retry() works', async () => {
+ const now = Date.now()
+ let p = await zx(`
+ try {
+ await retry(5, '50ms', () => $\`exit 123\`)
+ } catch (e) {
+ echo('exitCode:', e.exitCode)
+ }
+ await retry(5, () => $\`exit 0\`)
+ echo('success')
+`)
+ assert.match(p.toString(), 'exitCode: 123')
+ assert.match(p.toString(), 'success')
+ assert.ok(Date.now() >= now + 50 * (5 - 1))
+})
+
+test('retry() with expBackoff() works', async () => {
+ const now = Date.now()
+ let p = await zx(`
+ try {
+ await retry(5, expBackoff('60s', 0), () => $\`exit 123\`)
+ } catch (e) {
+ echo('exitCode:', e.exitCode)
+ }
+ echo('success')
+`)
+ assert.match(p.toString(), 'exitCode: 123')
+ assert.match(p.toString(), 'success')
+ assert.ok(Date.now() >= now + 2 + 4 + 8 + 16 + 32)
+})
+
+test('spinner() works', async () => {
+ let out = await zx(`
+ echo(await spinner(async () => {
+ await sleep(100)
+ await $\`echo hidden\`
+ return $\`echo result\`
+ }))
+ `)
+ assert.match(out.stdout, 'result')
+ assert.not.match(out.stderr, 'result')
+ assert.not.match(out.stderr, 'hidden')
+})
+
+test('spinner() with title works', async () => {
+ let out = await zx(`
+ await spinner('processing', () => sleep(100))
+ `)
+ assert.match(out.stderr, 'processing')
+})
+
+test('spinner() stops on throw', async () => {
+ let out = await zx(`
+ await spinner('processing', () => $\`wtf-cmd\`)
+ `)
+ assert.match(out.stderr, 'Error:')
+ assert.is.not(out.exitCode, 0)
+})
+
test.run()
test-d/experimental.test-d.ts
@@ -13,7 +13,3 @@
// limitations under the License.
import { expectType } from 'tsd'
-import { spinner } from '../src/experimental.js'
-
-expectType<string>(await spinner(() => 'foo'))
-expectType<string>(await spinner('title', () => 'bar'))
test-d/goods.test-d.ts
@@ -12,11 +12,17 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+import { expectType } from 'tsd'
import { $ } from '../src/core.js'
-import { echo, sleep } from '../src/goods.js'
+import { echo, sleep, spinner, retry, expBackoff } from '../src/goods.js'
echo`Date is ${await $`date`}`
echo('Hello, world!')
await sleep('1s')
await sleep(1000)
+
+expectType<'foo'>(await spinner(() => 'foo' as 'foo'))
+expectType<'bar'>(await spinner('title', () => 'bar' as 'bar'))
+expectType<'foo'>(await retry(0, () => 'foo' as 'foo'))
+expectType<Generator<number, void, unknown>>(expBackoff())
README.md
@@ -228,6 +228,32 @@ let version = await within(async () => {
})
```
+### `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
The following packages are available without importing inside scripts.
@@ -386,42 +412,6 @@ files (when using `zx` executable).
let {version} = require('./package.json')
```
-## 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
### Passing env variables
@@ -563,7 +553,10 @@ jobs:
```
### 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
npm i zx@dev
npx zx@dev --install --quiet <<< 'import _ from "lodash" /* 4.17.15 */; console.log(_.VERSION)'