Commit 7b97d2e

Anton Medvedev <anton@medv.io>
2023-02-27 12:21:53
Move spinner() and retry() to goods from experimental
1 parent 79222d7
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)'