Commit d79a638

Anton Golub <antongolub@antongolub.com>
2024-04-02 16:11:35
feat: let `cwdHook` be configurable (#765)
* feat: let `cwdHook` be configurable * test: add `$.cwdHook` test * feat: disable $.cwdHook by default BREAKING CHANGE: affects legacy cd() flow * test: fix pkg.test.js cd() * chore: rename `setupSmth` helpers to `useSmth`
1 parent b75c71a
src/core.ts
@@ -14,7 +14,7 @@
 
 import assert from 'node:assert'
 import { spawn, spawnSync, StdioNull, StdioPipe } from 'node:child_process'
-import { AsyncLocalStorage, createHook } from 'node:async_hooks'
+import { AsyncHook, AsyncLocalStorage, createHook } from 'node:async_hooks'
 import { Readable, Writable } from 'node:stream'
 import { inspect } from 'node:util'
 import {
@@ -38,6 +38,7 @@ import {
   parseDuration,
   quote,
   quotePowerShell,
+  noquote,
 } from './util.js'
 
 export interface Shell {
@@ -74,14 +75,18 @@ export interface Options {
 }
 
 const storage = new AsyncLocalStorage<Options>()
-const hook = createHook({
+const cwdSyncHook: AsyncHook & { enabled?: boolean } = createHook({
   init: syncCwd,
   before: syncCwd,
   promiseResolve: syncCwd,
   after: syncCwd,
   destroy: syncCwd,
 })
-hook.enable()
+
+export function syncProcessCwd(flag: boolean = true) {
+  if (flag) cwdSyncHook.enable()
+  else cwdSyncHook.disable()
+}
 
 export const defaults: Options = {
   [processCwd]: process.cwd(),
@@ -94,9 +99,7 @@ export const defaults: Options = {
   quiet: false,
   prefix: '',
   postfix: '',
-  quote: () => {
-    throw new Error('No quote function is defined: https://ï.at/no-quote-func')
-  },
+  quote: noquote,
   spawn,
   spawnSync,
   log,
@@ -104,14 +107,14 @@ export const defaults: Options = {
 }
 const isWin = process.platform == 'win32'
 
-export function setupPowerShell() {
+export function usePowerShell() {
   $.shell = which.sync('powershell.exe')
   $.prefix = ''
   $.postfix = '; exit $LastExitCode'
   $.quote = quotePowerShell
 }
 
-export function setupBash() {
+export function useBash() {
   $.shell = which.sync('bash')
   $.prefix = 'set -euo pipefail;'
   $.quote = quote
@@ -184,9 +187,8 @@ export const $: Shell & Options = new Proxy<Shell & Options>(
     },
   }
 )
-
 try {
-  setupBash()
+  useBash()
 } catch (err) {}
 
 type Resolve = (out: ProcessOutput) => void
src/globals.ts
@@ -41,7 +41,8 @@ declare global {
   var quote: typeof _.quote
   var quotePowerShell: typeof _.quotePowerShell
   var retry: typeof _.retry
-  var setupPowerShell: typeof _.setupPowerShell
+  var usePowerShell: typeof _.usePowerShell
+  var useBash: typeof _.useBash
   var sleep: typeof _.sleep
   var spinner: typeof _.spinner
   var stdin: typeof _.stdin
src/util.ts
@@ -42,6 +42,10 @@ export function normalizeMultilinePieces(
   )
 }
 
+export function noquote(): string {
+  throw new Error('No quote function is defined: https://ï.at/no-quote-func')
+}
+
 export function quote(arg: string) {
   if (/^[a-z0-9/_.\-@:=]+$/i.test(arg) || arg === '') {
     return arg
test/smoke/win32.test.js
@@ -24,7 +24,7 @@ _describe('win32', () => {
     assert.match(p.stdout, /bash/)
 
     await within(async () => {
-      setupPowerShell()
+      usePowerShell()
       assert.match($.shell, /powershell/i)
       const p = await $`get-host`
       assert.match(p.stdout, /PowerShell/)
@@ -33,7 +33,7 @@ _describe('win32', () => {
 
   test('quotePowerShell works', async () => {
     await within(async () => {
-      setupPowerShell()
+      usePowerShell()
       const p = await $`echo ${`Windows 'rulez!'`}`
       assert.match(p.stdout, /Windows 'rulez!'/)
     })
test/core.test.js
@@ -263,25 +263,26 @@ describe('core', () => {
     }
   })
 
-  test('cd() does affect parallel contexts', async () => {
+  test('cd() does not affect parallel contexts ($.cwdSyncHook enabled)', async () => {
+    syncProcessCwd()
     const cwd = process.cwd()
     try {
       fs.mkdirpSync('/tmp/zx-cd-parallel/one/two')
       await Promise.all([
         within(async () => {
           assert.equal(process.cwd(), cwd)
-          await sleep(1)
           cd('/tmp/zx-cd-parallel/one')
+          await sleep(Math.random() * 15)
           assert.ok(process.cwd().endsWith('/tmp/zx-cd-parallel/one'))
         }),
         within(async () => {
           assert.equal(process.cwd(), cwd)
-          await sleep(2)
+          await sleep(Math.random() * 15)
           assert.equal(process.cwd(), cwd)
         }),
         within(async () => {
           assert.equal(process.cwd(), cwd)
-          await sleep(3)
+          await sleep(Math.random() * 15)
           $.cwd = '/tmp/zx-cd-parallel/one/two'
           assert.equal(process.cwd(), cwd)
           assert.ok(
@@ -297,6 +298,7 @@ describe('core', () => {
     } finally {
       fs.rmSync('/tmp/zx-cd-parallel', { recursive: true })
       cd(cwd)
+      syncProcessCwd(false)
     }
   })
 
test/index.test.js
@@ -20,6 +20,9 @@ import {
   $,
   log,
   cd,
+  syncProcessCwd,
+  usePowerShell,
+  useBash,
   kill,
   ProcessOutput,
   ProcessPromise,
@@ -60,10 +63,13 @@ describe('index', () => {
     assert(ProcessOutput)
     assert(ProcessPromise)
     assert(cd)
+    assert(syncProcessCwd)
     assert(log)
     assert(kill)
     assert(defaults)
     assert(within)
+    assert(usePowerShell)
+    assert(useBash)
 
     // goods
     assert(argv)
test/package.test.js
@@ -13,10 +13,12 @@
 // limitations under the License.
 
 import assert from 'node:assert'
-import { test, describe, beforeEach } from 'node:test'
+import { test, describe, beforeEach, before, after } from 'node:test'
 import '../build/globals.js'
 
 describe('package', () => {
+  before(() => syncProcessCwd())
+  after(() => syncProcessCwd(false))
   beforeEach(async () => {
     const pack = await $`npm pack`
     await $`tar xf ${pack}`