Commit d09a117

Anton Golub <antongolub@antongolub.com>
2022-06-04 10:19:21
fix getCtx() (#425)
* fix: bind fetch output to log() * fix: use root ctx as getCtx fallback closes #424 * refactor: simplify * refactor: rm deep-proxy * chore: revert shell tweak * chore: protect promise.ctx from accidental removal * test: test promise.ctx guard * fix: use root as fallback for `runInCtx`
v6
1 parent 38e95cf
src/context.ts
@@ -20,7 +20,7 @@ export type Options = {
   cwd: string
   env: NodeJS.ProcessEnv
   prefix: string
-  shell: string
+  shell: string | boolean
   maxBuffer: number
   quote: (v: string) => string
   spawn: typeof spawn
@@ -43,13 +43,20 @@ let root: Options
 const storage = new AsyncLocalStorage<Options>()
 
 export function getCtx() {
-  return storage.getStore() as Context
+  return (storage.getStore() as Context) || getRootCtx()
 }
+
 export function setRootCtx(ctx: Options) {
   storage.enterWith(ctx)
   root = ctx
 }
+
 export function getRootCtx() {
   return root
 }
-export const runInCtx = storage.run.bind(storage)
+
+export const runInCtx = <R, TArgs extends any[]>(
+  ctx: Options = root,
+  cb: (...args: TArgs) => R,
+  ...args: TArgs
+): R => storage.run(ctx, cb, ...args)
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, Context, Options } from './context.js'
+import { runInCtx, getCtx, Context, Options, setRootCtx } from './context.js'
 import { printCmd, log } from './print.js'
 import { quote, substitute } from './guards.js'
 
@@ -52,13 +52,14 @@ export const $: Zx = function (pieces: TemplateStringsArray, ...args: any[]) {
     cmd += s + pieces[++i]
   }
 
-  promise.ctx = {
+  const ctx = {
     ...getCtx(),
     cmd,
     __from: new Error().stack!.split(/^\s*at\s/m)[2].trim(),
     resolve,
     reject,
   }
+  Object.defineProperty(promise, 'ctx', { value: ctx })
 
   setImmediate(() => promise._run()) // Make sure all subprocesses are started, if not explicitly by await or then().
 
@@ -72,6 +73,7 @@ $.spawn = spawn
 $.verbose = 2
 $.maxBuffer = 200 * 1024 * 1024 /* 200 MiB*/
 $.prefix = '' // Bash not found, no prefix.
+$.shell = true
 try {
   $.shell = which.sync('bash')
   $.prefix = 'set -euo pipefail;'
src/goods.ts
@@ -18,6 +18,7 @@ import { setTimeout as sleep } from 'node:timers/promises'
 import nodeFetch, { RequestInfo, RequestInit } from 'node-fetch'
 import { getCtx, getRootCtx } from './context.js'
 import { colorize } from './print.js'
+import { log } from './print.js'
 
 export { default as chalk } from 'chalk'
 export { default as fs } from 'fs-extra'
@@ -40,13 +41,13 @@ globbyModule)
 export const glob = globby
 
 export async function fetch(url: RequestInfo, init?: RequestInit) {
-  if (getCtx().verbose) {
-    if (typeof init !== 'undefined') {
-      console.log('$', colorize(`fetch ${url}`), init)
-    } else {
-      console.log('$', colorize(`fetch ${url}`))
-    }
-  }
+  log(
+    { scope: 'fetch' },
+    '$',
+    colorize(`fetch ${url}`),
+    init && JSON.stringify(init, null, 2)
+  )
+
   return nodeFetch(url, init)
 }
 
test/index.test.js
@@ -169,6 +169,17 @@ test('ProcessPromise: inherits native Promise', async () => {
   assert.ok(p5 !== p1)
 })
 
+test('ProcessPromise: ctx is protected from removal', async () => {
+  const p = $`echo 1`
+
+  try {
+    delete p.ctx
+    assert.unreachable()
+  } catch (e) {
+    assert.match(e.message, /Cannot delete property/)
+  }
+})
+
 test('ProcessOutput thrown as error', async () => {
   let err
   try {
package.json
@@ -51,7 +51,7 @@
     "globby": "^13.1.1",
     "ignore": "^5.2.0",
     "minimist": "^1.2.6",
-    "node-fetch": "^3.2.4",
+    "node-fetch": "^3.2.5",
     "ps-tree": "^1.2.0",
     "which": "^2.0.2",
     "yaml": "^2.1.1"