Commit e652772

Anton Golub <antongolub@antongolub.com>
2022-05-31 21:32:17
refactor: simplify ctx API (#411)
* refactor: improve ctx API * test: rm magic ctx example * chore: separate zx options and ctx types * chore: reduce tests verbosity * ci: enable gh-actions for v6 branch * test: chore impr
v6
1 parent 2e45a01
.github/workflows/test.yml
@@ -2,9 +2,9 @@ name: Test
 
 on:
   push:
-    branches: [main]
+    branches: [main, v6]
   pull_request:
-    branches: [main]
+    branches: [main, v6]
 
 jobs:
   test:
src/context.ts
@@ -14,20 +14,38 @@
 
 import { AsyncLocalStorage } from 'node:async_hooks'
 
-let root: any
+export type Options = {
+  verbose: boolean
+  cwd: string
+  env: NodeJS.ProcessEnv
+  prefix: string
+  shell: string
+  maxBuffer: number
+  quote: (v: string) => string
+}
+
+export type Context = Options & {
+  nothrow?: boolean
+  cmd: string
+  __from: string
+  resolve: any
+  reject: any
+}
+
+let root: Options
 
-const storage = new AsyncLocalStorage<any>()
+const storage = new AsyncLocalStorage<Options>()
 
 export function getCtx() {
-  return storage.getStore()
+  return storage.getStore() as Context
 }
-export function setRootCtx(ctx: any) {
+export function setRootCtx(ctx: Options) {
   storage.enterWith(ctx)
   root = ctx
 }
 export function getRootCtx() {
   return root
 }
-export function runInCtx(ctx: any, cb: any) {
+export function runInCtx(ctx: Options, cb: any) {
   return storage.run(ctx, cb)
 }
src/core.ts
@@ -23,7 +23,7 @@ import { inspect, promisify } from 'node:util'
 import { spawn } from 'node:child_process'
 
 import { chalk, which } from './goods.js'
-import { runInCtx, getCtx, setRootCtx } from './context.js'
+import { runInCtx, getCtx, setRootCtx, Context } from './context.js'
 import { printStd, printCmd } from './print.js'
 import { quote, substitute } from './guards.js'
 
@@ -75,20 +75,6 @@ try {
   $.prefix = 'set -euo pipefail;'
 } catch (e) {}
 
-type Options = {
-  nothrow: boolean
-  verbose: boolean
-  cmd: string
-  cwd: string
-  env: NodeJS.ProcessEnv
-  prefix: string
-  shell: string
-  maxBuffer: number
-  __from: string
-  resolve: any
-  reject: any
-}
-
 export class ProcessPromise extends Promise<ProcessOutput> {
   child?: ChildProcessByStdio<Writable, Readable, Readable>
   _resolved = false
@@ -96,7 +82,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
   _piped = false
   _prerun: any = undefined
   _postrun: any = undefined
-  ctx?: Options
+  ctx?: Context
 
   get stdin() {
     this._inheritStdin = false
@@ -176,7 +162,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
     if (this.child) return // The _run() called from two places: then() and setTimeout().
     if (this._prerun) this._prerun() // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
 
-    runInCtx(this.ctx, () => {
+    runInCtx(this.ctx!, () => {
       const {
         nothrow,
         cmd,
src/experimental.ts
@@ -17,8 +17,6 @@ import { sleep } from './goods.js'
 import { isString } from './util.js'
 import { getCtx, runInCtx } from './context.js'
 
-export { getCtx, runInCtx }
-
 // Retries a command a few times. Will return after the first
 // successful attempt, or will throw after specifies attempts count.
 export function retry(count = 5, delay = 0) {
@@ -79,3 +77,14 @@ export function startSpinner(title = '') {
       clearInterval(id)
   )(setInterval(spin, 100))
 }
+
+export function ctx(
+  cb: Parameters<typeof runInCtx>[1]
+): ReturnType<typeof runInCtx> {
+  const _$ = Object.assign($.bind(null), getCtx())
+  function _cb() {
+    return cb(_$)
+  }
+
+  return runInCtx(_$, _cb)
+}
test/experimental.test.js
@@ -21,6 +21,7 @@ import {
   retry,
   startSpinner,
   withTimeout,
+  ctx,
 } from '../build/experimental.js'
 
 import chalk from 'chalk'
@@ -71,4 +72,42 @@ test('spinner works', async () => {
   s()
 })
 
+test('ctx() provides isolates running scopes', async () => {
+  $.verbose = true
+
+  await ctx(async ($) => {
+    $.verbose = false
+    await $`echo a`
+
+    $.verbose = true
+    await $`echo b`
+
+    $.verbose = false
+    await ctx(async ($) => {
+      await $`echo d`
+
+      await ctx(async ($) => {
+        assert.ok($.verbose === false)
+
+        await $`echo e`
+        $.verbose = true
+      })
+      $.verbose = true
+    })
+
+    await $`echo c`
+  })
+
+  await $`echo f`
+
+  await ctx(async ($) => {
+    assert.is($.verbose, true)
+    $.verbose = false
+    await $`echo g`
+  })
+
+  assert.is($.verbose, true)
+  $.verbose = false
+})
+
 test.run()
test/index.test.js
@@ -20,7 +20,9 @@ import { Writable } from 'node:stream'
 import { Socket } from 'node:net'
 import '../build/globals.js'
 import { ProcessPromise } from '../build/index.js'
-import {getCtx, runInCtx} from '../build/experimental.js'
+import { getCtx, runInCtx } from '../build/context.js'
+
+$.verbose = false
 
 test('only stdout is used during command substitution', async () => {
   let hello = await $`echo Error >&2; echo Hello`
@@ -48,6 +50,7 @@ test('undefined and empty string correctly quoted', async () => {
   $.verbose = true
   assert.is((await $`echo -n ${undefined}`).toString(), 'undefined')
   assert.is((await $`echo -n ${''}`).toString(), '')
+  $.verbose = false
 })
 
 test('can create a dir with a space in the name', async () => {
@@ -214,8 +217,6 @@ test('globby available', async () => {
   assert.is(typeof globby.isDynamicPattern, 'function')
   assert.is(typeof globby.isGitIgnored, 'function')
   assert.is(typeof globby.isGitIgnoredSync, 'function')
-  console.log(chalk.greenBright('globby available'))
-
   assert.equal(await globby('*.md'), ['README.md'])
 })
 
README.md
@@ -438,25 +438,26 @@ import {withTimeout} from 'zx/experimental'
 await withTimeout(100, 'SIGTERM')`sleep 9999`
 ```
 
-### `getCtx()` and `runInCtx()`
+### `ctx()`
 
-[async_hooks](https://nodejs.org/api/async_hooks.html) methods to manipulate bound context.
-This object is used by zx inners, so it has a significant impact on the call mechanics. Please use this carefully and wisely.
+[async_hooks](https://nodejs.org/api/async_hooks.html)-driven scope isolator.
+Creates a separate zx-context for the specified function.
 
 ```js
-import {getCtx, runInCtx} from 'zx/experimental'
+import {ctx} from 'zx/experimental'
 
-runInCtx({ ...getCtx() }, async () => {
+const _$ = $
+ctx(async ($) => {
   await sleep(10)
   cd('/foo')
   // $.cwd refers to /foo
-  // getCtx().cwd === $.cwd
+  // _$.cwd === $.cwd
 })
 
-runInCtx({ ...getCtx() }, async () => {
+ctx(async ($) => {
   await sleep(20)
-  // $.cwd refers to /foo
-  // but getCtx().cwd !== $.cwd
+  // _$.cwd refers to /foo
+  // but _$.cwd !== $.cwd
 })
 ```