From 6fe3031a7f71f1b3fe6dfff1281ed7f65f4f9a9a Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Fri, 3 Jun 2022 17:39:10 +0300 Subject: [PATCH 1/8] fix: bind fetch output to log() --- src/goods.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/goods.ts b/src/goods.ts index e800410f92..b5c5d0dc40 100644 --- a/src/goods.ts +++ b/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) } From 3604b46653230481f2023159592b196527e7efb6 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Fri, 3 Jun 2022 23:37:05 +0300 Subject: [PATCH 2/8] fix: use root ctx as getCtx fallback closes #424 --- package.json | 3 ++- src/context.ts | 12 ++++------ src/core.ts | 62 +++++++++++++++++++++++++++----------------------- 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 967350cf64..442291dacf 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "coverage": "c8 --reporter=html npm test" }, "dependencies": { + "@qiwi/deep-proxy": "^1.10.3", "@types/fs-extra": "^9.0.13", "@types/minimist": "^1.2.2", "@types/node": "^17.0", @@ -51,7 +52,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" diff --git a/src/context.ts b/src/context.ts index 75fcb6f30d..1643839018 100644 --- a/src/context.ts +++ b/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 @@ -38,17 +38,15 @@ export type Context = Options & { reject: any } -let root: Options +// @ts-ignore +export let root: Options = function () {} const storage = new AsyncLocalStorage() export function getCtx() { - return storage.getStore() as Context -} -export function setRootCtx(ctx: Options) { - storage.enterWith(ctx) - root = ctx + return storage.getStore() as Context || root } + export function getRootCtx() { return root } diff --git a/src/core.ts b/src/core.ts index 9ce7770e49..49fa367c63 100644 --- a/src/core.ts +++ b/src/core.ts @@ -23,11 +23,12 @@ 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, root, Context, Options } from './context.js' import { printCmd, log } from './print.js' import { quote, substitute } from './guards.js' import psTreeModule from 'ps-tree' +import { DeepProxy } from '@qiwi/deep-proxy' const psTree = promisify(psTreeModule) @@ -35,35 +36,40 @@ interface Zx extends Options { (pieces: TemplateStringsArray, ...args: any[]): ProcessPromise } -export const $: Zx = function (pieces: TemplateStringsArray, ...args: any[]) { - let resolve, reject - let promise = new ProcessPromise((...args) => ([resolve, reject] = args)) - - let cmd = pieces[0], - i = 0 - let quote = getCtx().quote - while (i < args.length) { - let s - if (Array.isArray(args[i])) { - s = args[i].map((x: any) => quote(substitute(x))).join(' ') - } else { - s = quote(substitute(args[i])) +export const $: Zx = new DeepProxy(root as Zx, ({trapName, DEFAULT, args: _args}) => { + if (trapName === 'apply') { + const [,, [pieces, ...args]] = _args + let resolve, reject + let promise = new ProcessPromise((...args) => ([resolve, reject] = args)) + + let cmd = pieces[0], + i = 0 + let quote = getCtx().quote + while (i < args.length) { + let s + if (Array.isArray(args[i])) { + s = args[i].map((x: any) => quote(substitute(x))).join(' ') + } else { + s = quote(substitute(args[i])) + } + cmd += s + pieces[++i] } - cmd += s + pieces[++i] - } - promise.ctx = { - ...getCtx(), - cmd, - __from: new Error().stack!.split(/^\s*at\s/m)[2].trim(), - resolve, - reject, - } + promise.ctx = { + ...getCtx(), + cmd, + __from: new Error().stack!.split(/^\s*at\s/m)[2].trim(), + resolve, + reject, + } - setImmediate(() => promise._run()) // Make sure all subprocesses are started, if not explicitly by await or then(). + setImmediate(() => promise._run()) // Make sure all subprocesses are started, if not explicitly by await or then(). - return promise -} + return promise + } + + return DEFAULT +}) $.cwd = process.cwd() $.env = process.env @@ -72,12 +78,12 @@ $.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;' } catch (e) {} -setRootCtx($) export class ProcessPromise extends Promise { child?: ChildProcessByStdio @@ -184,7 +190,7 @@ export class ProcessPromise extends Promise { let options: SpawnOptionsWithStdioTuple = { cwd, - shell: typeof shell === 'string' ? shell : true, + shell, stdio: [this._inheritStdin ? 'inherit' : 'pipe', 'pipe', 'pipe'], windowsHide: true, // TODO: Surprise: maxBuffer have no effect for spawn. From f4782ef07f5960e18f7c9527209c0036127d0982 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Fri, 3 Jun 2022 23:49:50 +0300 Subject: [PATCH 3/8] refactor: simplify --- src/core.ts | 57 ++++++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/core.ts b/src/core.ts index 49fa367c63..c471b6f374 100644 --- a/src/core.ts +++ b/src/core.ts @@ -36,38 +36,41 @@ interface Zx extends Options { (pieces: TemplateStringsArray, ...args: any[]): ProcessPromise } -export const $: Zx = new DeepProxy(root as Zx, ({trapName, DEFAULT, args: _args}) => { - if (trapName === 'apply') { - const [,, [pieces, ...args]] = _args - let resolve, reject - let promise = new ProcessPromise((...args) => ([resolve, reject] = args)) - - let cmd = pieces[0], - i = 0 - let quote = getCtx().quote - while (i < args.length) { - let s - if (Array.isArray(args[i])) { - s = args[i].map((x: any) => quote(substitute(x))).join(' ') - } else { - s = quote(substitute(args[i])) - } - cmd += s + pieces[++i] +function Zx(pieces: TemplateStringsArray, ...args: any[]) { + let resolve, reject + let promise = new ProcessPromise((...args) => ([resolve, reject] = args)) + + let cmd = pieces[0], + i = 0 + let quote = getCtx().quote + while (i < args.length) { + let s + if (Array.isArray(args[i])) { + s = args[i].map((x: any) => quote(substitute(x))).join(' ') + } else { + s = quote(substitute(args[i])) } + cmd += s + pieces[++i] + } - promise.ctx = { - ...getCtx(), - cmd, - __from: new Error().stack!.split(/^\s*at\s/m)[2].trim(), - resolve, - reject, - } + promise.ctx = { + ...getCtx(), + cmd, + __from: new Error().stack!.split(/^\s*at\s/m)[2].trim(), + resolve, + reject, + } - setImmediate(() => promise._run()) // Make sure all subprocesses are started, if not explicitly by await or then(). + setImmediate(() => promise._run()) // Make sure all subprocesses are started, if not explicitly by await or then(). - return promise - } + return promise +} +export const $: Zx = new DeepProxy(root as Zx, ({trapName, DEFAULT, args: _args}) => { + if (trapName === 'apply') { + const [,, [...args]] = _args + return Zx(...args as [pieces: TemplateStringsArray, ...args: any[]]) + } return DEFAULT }) From 5a28159cf4b0bd8f1646cebe0071911e5d79fed2 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Fri, 3 Jun 2022 23:55:41 +0300 Subject: [PATCH 4/8] refactor: rm deep-proxy --- package.json | 1 - src/context.ts | 10 +++++++--- src/core.ts | 16 ++++------------ 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 442291dacf..cb0554955e 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,6 @@ "coverage": "c8 --reporter=html npm test" }, "dependencies": { - "@qiwi/deep-proxy": "^1.10.3", "@types/fs-extra": "^9.0.13", "@types/minimist": "^1.2.2", "@types/node": "^17.0", diff --git a/src/context.ts b/src/context.ts index 1643839018..5d76cff378 100644 --- a/src/context.ts +++ b/src/context.ts @@ -38,13 +38,17 @@ export type Context = Options & { reject: any } -// @ts-ignore -export let root: Options = function () {} +let root: Options const storage = new AsyncLocalStorage() export function getCtx() { - return storage.getStore() as Context || root + return (storage.getStore() as Context) || getRootCtx() +} + +export function setRootCtx(ctx: Options) { + storage.enterWith(ctx) + root = ctx } export function getRootCtx() { diff --git a/src/core.ts b/src/core.ts index c471b6f374..a7de17f356 100644 --- a/src/core.ts +++ b/src/core.ts @@ -23,12 +23,11 @@ import { inspect, promisify } from 'node:util' import { spawn } from 'node:child_process' import { chalk, which } from './goods.js' -import { runInCtx, getCtx, root, 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' import psTreeModule from 'ps-tree' -import { DeepProxy } from '@qiwi/deep-proxy' const psTree = promisify(psTreeModule) @@ -36,12 +35,12 @@ interface Zx extends Options { (pieces: TemplateStringsArray, ...args: any[]): ProcessPromise } -function Zx(pieces: TemplateStringsArray, ...args: any[]) { +export const $: Zx = function (pieces: TemplateStringsArray, ...args: any[]) { let resolve, reject let promise = new ProcessPromise((...args) => ([resolve, reject] = args)) let cmd = pieces[0], - i = 0 + i = 0 let quote = getCtx().quote while (i < args.length) { let s @@ -66,14 +65,6 @@ function Zx(pieces: TemplateStringsArray, ...args: any[]) { return promise } -export const $: Zx = new DeepProxy(root as Zx, ({trapName, DEFAULT, args: _args}) => { - if (trapName === 'apply') { - const [,, [...args]] = _args - return Zx(...args as [pieces: TemplateStringsArray, ...args: any[]]) - } - return DEFAULT -}) - $.cwd = process.cwd() $.env = process.env $.quote = quote @@ -87,6 +78,7 @@ try { $.prefix = 'set -euo pipefail;' } catch (e) {} +setRootCtx($) export class ProcessPromise extends Promise { child?: ChildProcessByStdio From 2579a4ac25d5df69a2ff8c4ce55563e6d864848f Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Sat, 4 Jun 2022 00:03:12 +0300 Subject: [PATCH 5/8] chore: revert shell tweak --- src/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core.ts b/src/core.ts index a7de17f356..58d0140d14 100644 --- a/src/core.ts +++ b/src/core.ts @@ -185,7 +185,7 @@ export class ProcessPromise extends Promise { let options: SpawnOptionsWithStdioTuple = { cwd, - shell, + shell: typeof shell === 'string' ? shell : true, stdio: [this._inheritStdin ? 'inherit' : 'pipe', 'pipe', 'pipe'], windowsHide: true, // TODO: Surprise: maxBuffer have no effect for spawn. From e3540250970a3b8881aa406f89f5319d538c2575 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Sat, 4 Jun 2022 00:13:05 +0300 Subject: [PATCH 6/8] chore: protect promise.ctx from accidental removal --- src/core.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core.ts b/src/core.ts index 58d0140d14..17182156e6 100644 --- a/src/core.ts +++ b/src/core.ts @@ -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(). From 4a5d820f6ed709a39286569e792a955c59df1333 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Sat, 4 Jun 2022 00:17:30 +0300 Subject: [PATCH 7/8] test: test promise.ctx guard --- test/index.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/index.test.js b/test/index.test.js index 4db9220e22..5377030a58 100755 --- a/test/index.test.js +++ b/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 { From e61681e76d1df4fe654ab7275a3c9ccf1029ca33 Mon Sep 17 00:00:00 2001 From: Anton Golub Date: Sat, 4 Jun 2022 00:32:31 +0300 Subject: [PATCH 8/8] fix: use root as fallback for `runInCtx` --- src/context.ts | 7 ++++++- src/core.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/context.ts b/src/context.ts index 5d76cff378..f456e7cb4e 100644 --- a/src/context.ts +++ b/src/context.ts @@ -54,4 +54,9 @@ export function setRootCtx(ctx: Options) { export function getRootCtx() { return root } -export const runInCtx = storage.run.bind(storage) + +export const runInCtx = ( + ctx: Options = root, + cb: (...args: TArgs) => R, + ...args: TArgs +): R => storage.run(ctx, cb, ...args) diff --git a/src/core.ts b/src/core.ts index 17182156e6..57cf06d54e 100644 --- a/src/core.ts +++ b/src/core.ts @@ -59,7 +59,7 @@ export const $: Zx = function (pieces: TemplateStringsArray, ...args: any[]) { resolve, reject, } - Object.defineProperty(promise, 'ctx', {value: ctx}) + Object.defineProperty(promise, 'ctx', { value: ctx }) setImmediate(() => promise._run()) // Make sure all subprocesses are started, if not explicitly by await or then().