diff --git a/README.md b/README.md index 9dcc110f0d..a7aa2e960a 100644 --- a/README.md +++ b/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 }) ``` diff --git a/src/experimental.ts b/src/experimental.ts index 65a52bc2d7..899ae2136b 100644 --- a/src/experimental.ts +++ b/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,12 @@ export function startSpinner(title = '') { clearInterval(id) )(setInterval(spin, 100)) } + +export function ctx(cb: (_$: typeof $) => any) { + const _$ = Object.assign($.bind(null), getCtx()) + function _cb() { + return cb(_$) + } + + return runInCtx(_$, _cb) +} diff --git a/test/experimental.test.js b/test/experimental.test.js index d04200e131..cd2a6309d7 100644 --- a/test/experimental.test.js +++ b/test/experimental.test.js @@ -21,10 +21,14 @@ import { retry, startSpinner, withTimeout, + ctx, } from '../build/experimental.js' import chalk from 'chalk' +import { ProcessPromise } from '../build/core.js' +import { randomId } from '../build/util.js' + $.verbose = false test('retry works', async () => { @@ -71,4 +75,111 @@ 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) + + $.o = + (opts) => + (...args) => + ctx(($) => Object.assign($, opts)(...args)) + + const createHook = (opts, name = randomId(), cb = (v) => v, configurable) => { + ProcessPromise.prototype[name] = function (...args) { + Object.assign(this.ctx, opts) + return cb(this, ...args) + } + + const getP = (p, opts, $args) => + p instanceof ProcessPromise ? p : $.o(opts)(...$args) + + return (...args) => { + if (!configurable) { + const p = getP(args[0], opts, args) + return p[name]() + } + + return (...$args) => { + const p = getP($args[0], opts, $args) + return p[name](...args) + } + } + } + + const quiet = createHook({ verbose: false }, 'quiet') + const debug = createHook({ verbose: 2 }, 'debug') + + const timeout = createHook( + null, + 'timeout', + (p, t, signal) => { + if (!t) return p + let timer = setTimeout(() => p.kill(signal), t) + + return Object.assign( + p.finally(() => clearTimeout(timer)), + p + ) + }, + true + ) + + await quiet`echo 'quiet'` + await debug($`echo 'debug'`) + await $`echo 'chained'`.quiet() + + try { + await quiet(timeout(100, 'SIGKILL')`sleep 9999`) + } catch { + console.log('killed1') + } + + try { + const p = $`sleep 9999` + await quiet(timeout(100, 'SIGKILL')(p)) + } catch { + console.log('killed2') + } + + try { + await $`sleep 9999`.quiet().timeout(100, 'SIGKILL') + } catch { + console.log('killed3') + } + + $.verbose = false +}) + test.run() diff --git a/test/index.test.js b/test/index.test.js index 3b69eb81bb..16224144ce 100755 --- a/test/index.test.js +++ b/test/index.test.js @@ -20,7 +20,7 @@ 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' test('only stdout is used during command substitution', async () => { let hello = await $`echo Error >&2; echo Hello`