Skip to content

Commit

Permalink
refactor: fixes by review
Browse files Browse the repository at this point in the history
  • Loading branch information
antongolub committed May 26, 2022
1 parent 7e2d04e commit 1e05234
Show file tree
Hide file tree
Showing 19 changed files with 268 additions and 174 deletions.
9 changes: 7 additions & 2 deletions package.json
Expand Up @@ -22,7 +22,8 @@
"test:cov": "c8 --reporter=html npm run test",
"test": "node zx.mjs test/full.test.mjs",
"test:zx": "npm run test zx",
"test:index": "npm run test index"
"test:index": "npm run test index",
"hooks-upd": "simple-git-hooks"
},
"dependencies": {
"@types/fs-extra": "^9.0.13",
Expand All @@ -39,7 +40,11 @@
"yaml": "^2.0.1"
},
"devDependencies": {
"c8": "^7.11.2"
"c8": "^7.11.2",
"simple-git-hooks": "^2.7.0"
},
"simple-git-hooks": {
"pre-commit": "npx prettier --single-quote --no-semi --write src test"
},
"publishConfig": {
"registry": "https://wombat-dressing-room.appspot.com"
Expand Down
23 changes: 16 additions & 7 deletions src/als.mjs
Expand Up @@ -12,13 +12,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {AsyncLocalStorage} from 'node:async_hooks'
import { AsyncLocalStorage } from 'node:async_hooks'

let root

export const als = new AsyncLocalStorage()
export const boundCtxKey = Symbol('AsyncLocalStorage bound ctx')
export const getCtx = () => als.getStore()
export const setRootCtx = (ctx) => { als.enterWith(ctx); root = ctx }
export const getRootCtx = () => root
export const runInCtx = (ctx, cb) => als.run(ctx, cb)
const als = new AsyncLocalStorage()

export function getCtx() {
return als.getStore()
}
export function setRootCtx(ctx) {
als.enterWith(ctx)
root = ctx
}
export function getRootCtx() {
return root
}
export function runInCtx(ctx, cb) {
return als.run(ctx, cb)
}
80 changes: 43 additions & 37 deletions src/core.mjs
Expand Up @@ -12,27 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {inspect} from 'node:util'
import {spawn} from 'node:child_process'
import {chalk, psTree, which} from './goods.mjs'
import {boundCtxKey, getCtx, runInCtx, setRootCtx} from './als.mjs'
import {randId} from './util.mjs'
import {printStd, printCmd} from './print.mjs'
import {formatCmd, quote} from './guards.mjs'
import { inspect } from 'node:util'
import { spawn } from 'node:child_process'
import { chalk, psTree, which } from './goods.mjs'
import { getCtx, runInCtx, setRootCtx } from './als.mjs'
import { printStd, printCmd } from './print.mjs'
import { formatCmd, quote } from './guards.mjs'

export { getCtx, runInCtx, boundCtxKey }
export { getCtx, runInCtx }

export function $(...args) {
let resolve, reject
let promise = new ProcessPromise((...args) => [resolve, reject] = args)
let promise = new ProcessPromise((...args) => ([resolve, reject] = args))

promise[boundCtxKey] = {
promise.ctx = {
...getCtx(),
id: randId(),
cmd: formatCmd(...args),
__from: (new Error().stack.split(/^\s*at\s/m)[2]).trim(),
cmd: formatCmd(...args),
__from: new Error().stack.split(/^\s*at\s/m)[2].trim(),
resolve,
reject
reject,
}

setImmediate(() => promise._run()) // Make sure all subprocesses are started, if not explicitly by await or then().
Expand All @@ -52,8 +50,7 @@ $.prefix = '' // Bash not found, no prefix.
try {
$.shell = which.sync('bash')
$.prefix = 'set -euo pipefail;'
} catch (e) {
}
} catch (e) {}

export class ProcessPromise extends Promise {
child = undefined
Expand Down Expand Up @@ -82,16 +79,20 @@ export class ProcessPromise extends Promise {
}

get exitCode() {
return this
.then(p => p.exitCode, p => p.exitCode)
return this.then(
(p) => p.exitCode,
(p) => p.exitCode
)
}

pipe(dest) {
if (typeof dest === 'string') {
throw new Error('The pipe() method does not take strings. Forgot $?')
}
if (this._resolved === true) {
throw new Error('The pipe() method shouldn\'t be called after promise is already resolved!')
throw new Error(
"The pipe() method shouldn't be called after promise is already resolved!"
)
}
this._piped = true
if (dest instanceof ProcessPromise) {
Expand All @@ -106,25 +107,23 @@ export class ProcessPromise extends Promise {
}

async kill(signal = 'SIGTERM') {
this.catch(_ => _)
this.catch((_) => _)
let children = await psTree(this.child.pid)
for (const p of children) {
try {
process.kill(p.PID, signal)
} catch (e) {
}
} catch (e) {}
}
try {
process.kill(this.child.pid, signal)
} catch (e) {
}
} catch (e) {}
}

_run() {
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().

const ctx = this[boundCtxKey]
const ctx = this.ctx
runInCtx(ctx, () => {
const {
nothrow,
Expand All @@ -136,7 +135,7 @@ export class ProcessPromise extends Promise {
maxBuffer,
__from,
resolve,
reject
reject,
} = ctx

printCmd(cmd)
Expand All @@ -147,13 +146,14 @@ export class ProcessPromise extends Promise {
stdio: [this._inheritStdin ? 'inherit' : 'pipe', 'pipe', 'pipe'],
windowsHide: true,
maxBuffer,
env
env,
})

child.on('close', (code, signal) => {

let message = `${stderr || '\n'} at ${__from}`
message += `\n exit code: ${code}${exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''}`
message += `\n exit code: ${code}${
exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''
}`
if (signal !== null) {
message += `\n signal: ${signal}`
}
Expand All @@ -164,18 +164,20 @@ export class ProcessPromise extends Promise {
stderr,
combined,
message,
});
(code === 0 || nothrow ? resolve : reject)(output)
})
;(code === 0 || nothrow ? resolve : reject)(output)
this._resolved = true
})

let stdout = '', stderr = '', combined = ''
let onStdout = data => {
let stdout = '',
stderr = '',
combined = ''
let onStdout = (data) => {
printStd(data)
stdout += data
combined += data
}
let onStderr = data => {
let onStderr = (data) => {
printStd(null, data)
stderr += data
combined += data
Expand All @@ -195,7 +197,7 @@ export class ProcessOutput extends Error {
#stderr = ''
#combined = ''

constructor({code, signal, stdout, stderr, combined, message}) {
constructor({ code, signal, stdout, stderr, combined, message }) {
super(message)
this.#code = code
this.#signal = signal
Expand Down Expand Up @@ -225,12 +227,16 @@ export class ProcessOutput extends Error {
}

[inspect.custom]() {
let stringify = (s, c) => s.length === 0 ? '\'\'' : c(inspect(s))
let stringify = (s, c) => (s.length === 0 ? "''" : c(inspect(s)))
return `ProcessOutput {
stdout: ${stringify(this.stdout, chalk.green)},
stderr: ${stringify(this.stderr, chalk.red)},
signal: ${inspect(this.signal)},
exitCode: ${(this.exitCode === 0 ? chalk.green : chalk.red)(this.exitCode)}${(exitCodeInfo(this.exitCode) ? chalk.grey(' (' + exitCodeInfo(this.exitCode) + ')') : '')}
exitCode: ${(this.exitCode === 0 ? chalk.green : chalk.red)(this.exitCode)}${
exitCodeInfo(this.exitCode)
? chalk.grey(' (' + exitCodeInfo(this.exitCode) + ')')
: ''
}
}`
}
}
Expand Down
50 changes: 32 additions & 18 deletions src/experimental.mjs
Expand Up @@ -12,37 +12,47 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {ProcessOutput, $} from './core.mjs'
import {sleep} from './goods.mjs'
import {isStr} from './util.mjs'
import { ProcessOutput, $ } from './core.mjs'
import { sleep } from './goods.mjs'
import { isString } from './util.mjs'

// Retries a command a few times. Will return after the first
// successful attempt, or will throw after specifies attempts count.
export const retry = (count = 5, delay = 0) => async (cmd, ...args) => {
while (count --> 0) try {
return await $(cmd, ...args)
} catch (p) {
if (count === 0) throw p
if (delay) await sleep(delay)
export function retry(count = 5, delay = 0) {
return async function (cmd, ...args) {
while (count-- > 0)
try {
return await $(cmd, ...args)
} catch (p) {
if (count === 0) throw p
if (delay) await sleep(delay)
}
}
}

// Runs and sets a timeout for a cmd
export const withTimeout = (timeout, signal) => async (cmd, ...args) => {
let p = $(cmd, ...args)
if (!timeout) return p
export function withTimeout(timeout, signal) {
return async function (cmd, ...args) {
let p = $(cmd, ...args)
if (!timeout) return p

let timer = setTimeout(() => p.kill(signal), timeout)
let timer = setTimeout(() => p.kill(signal), timeout)

return p.finally(() => clearTimeout(timer))
return p.finally(() => clearTimeout(timer))
}
}

// A console.log() alternative which can take ProcessOutput.
export function echo(pieces, ...args) {
let msg
let lastIdx = pieces.length - 1
if (Array.isArray(pieces) && pieces.every(isStr) && lastIdx === args.length) {
msg = args.map((a, i) => pieces[i] + stringify(a)).join('') + pieces[lastIdx]
if (
Array.isArray(pieces) &&
pieces.every(isString) &&
lastIdx === args.length
) {
msg =
args.map((a, i) => pieces[i] + stringify(a)).join('') + pieces[lastIdx]
} else {
msg = [pieces, ...args].map(stringify).join(' ')
}
Expand All @@ -58,6 +68,10 @@ function stringify(arg) {

// Starts a simple CLI spinner, and returns stop() func.
export function startSpinner(title = '') {
let i = 0, spin = () => process.stdout.write(` ${'⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'[i++ % 10]} ${title}\r`)
return (id => () => clearInterval(id))(setInterval(spin, 100))
let i = 0,
spin = () => process.stdout.write(` ${'⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏'[i++ % 10]} ${title}\r`)
return (
(id) => () =>
clearInterval(id)
)(setInterval(spin, 100))
}
2 changes: 1 addition & 1 deletion src/globals.mjs
@@ -1,3 +1,3 @@
import {registerGlobals} from './index.mjs'
import { registerGlobals } from './index.mjs'

registerGlobals()
6 changes: 3 additions & 3 deletions src/goods.mjs
Expand Up @@ -14,12 +14,12 @@

import * as globbyModule from 'globby'
import minimist from 'minimist'
import {setTimeout as sleep} from 'node:timers/promises'
import { setTimeout as sleep } from 'node:timers/promises'
import { promisify } from 'node:util'
import psTreeModule from 'ps-tree'
import nodeFetch from 'node-fetch'
import {getCtx, getRootCtx} from './als.mjs'
import {colorize} from './print.mjs'
import { getCtx, getRootCtx } from './als.mjs'
import { colorize } from './print.mjs'

export { default as chalk } from 'chalk'
export { default as fs } from 'fs-extra'
Expand Down
21 changes: 12 additions & 9 deletions src/guards.mjs
Expand Up @@ -12,32 +12,35 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {getCtx} from './als.mjs'
import { getCtx } from './als.mjs'

export function quote(arg) {
if (/^[a-z0-9/_.-]+$/i.test(arg) || arg === '') {
return arg
}
return `$'`
+ arg
return (
`$'` +
arg
.replace(/\\/g, '\\\\')
.replace(/'/g, '\\\'')
.replace(/'/g, "\\'")
.replace(/\f/g, '\\f')
.replace(/\n/g, '\\n')
.replace(/\r/g, '\\r')
.replace(/\t/g, '\\t')
.replace(/\v/g, '\\v')
.replace(/\0/g, '\\0')
+ `'`
.replace(/\0/g, '\\0') +
`'`
)
}

export const formatCmd = (pieces, ...args) => {
let cmd = pieces[0], i = 0
export function formatCmd(pieces, ...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 => quote(substitute(x))).join(' ')
s = args[i].map((x) => quote(substitute(x))).join(' ')
} else {
s = quote(substitute(args[i]))
}
Expand Down
6 changes: 2 additions & 4 deletions src/hooks.mjs
Expand Up @@ -12,14 +12,12 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {boundCtxKey} from './als.mjs'

export function nothrow(promise) {
promise[boundCtxKey].nothrow = true
promise.ctx.nothrow = true
return promise
}

export function quiet(promise) {
promise[boundCtxKey].verbose = false
promise.ctx.verbose = false
return promise
}

0 comments on commit 1e05234

Please sign in to comment.