Commit a741743
Changed files (20)
src/context.mjs
@@ -0,0 +1,33 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { AsyncLocalStorage } from 'node:async_hooks'
+
+let root
+
+const storage = new AsyncLocalStorage()
+
+export function getCtx() {
+ return storage.getStore()
+}
+export function setRootCtx(ctx) {
+ storage.enterWith(ctx)
+ root = ctx
+}
+export function getRootCtx() {
+ return root
+}
+export function runInCtx(ctx, cb) {
+ return storage.run(ctx, cb)
+}
src/core.mjs
@@ -0,0 +1,280 @@
+// Copyright 2021 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { inspect, promisify } from 'node:util'
+import { spawn } from 'node:child_process'
+import { chalk, which } from './goods.mjs'
+import { getCtx, runInCtx, setRootCtx } from './context.mjs'
+import { printStd, printCmd } from './print.mjs'
+import { formatCmd, quote } from './guards.mjs'
+import psTreeModule from 'ps-tree'
+
+const psTree = promisify(psTreeModule)
+
+export function $(...args) {
+ let resolve, reject
+ let promise = new ProcessPromise((...args) => ([resolve, reject] = args))
+
+ promise.ctx = {
+ ...getCtx(),
+ cmd: formatCmd(...args),
+ __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().
+
+ return promise
+}
+
+setRootCtx($)
+
+$.cwd = process.cwd()
+$.env = process.env
+$.quote = quote
+$.spawn = spawn
+$.verbose = true
+$.maxBuffer = 200 * 1024 * 1024 /* 200 MiB*/
+$.prefix = '' // Bash not found, no prefix.
+try {
+ $.shell = which.sync('bash')
+ $.prefix = 'set -euo pipefail;'
+} catch (e) {}
+
+export class ProcessPromise extends Promise {
+ child = undefined
+ _resolved = false
+ _inheritStdin = true
+ _piped = false
+ _prerun = undefined
+ _postrun = undefined
+
+ get stdin() {
+ this._inheritStdin = false
+ this._run()
+ return this.child.stdin
+ }
+
+ get stdout() {
+ this._inheritStdin = false
+ this._run()
+ return this.child.stdout
+ }
+
+ get stderr() {
+ this._inheritStdin = false
+ this._run()
+ return this.child.stderr
+ }
+
+ get 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!"
+ )
+ }
+ this._piped = true
+ if (dest instanceof ProcessPromise) {
+ dest._inheritStdin = false
+ dest._prerun = this._run.bind(this)
+ dest._postrun = () => this.stdout.pipe(dest.child.stdin)
+ return dest
+ } else {
+ this._postrun = () => this.stdout.pipe(dest)
+ return this
+ }
+ }
+
+ async kill(signal = 'SIGTERM') {
+ this.catch((_) => _)
+ let children = await psTree(this.child.pid)
+ for (const p of children) {
+ try {
+ process.kill(p.PID, signal)
+ } catch (e) {}
+ }
+ try {
+ process.kill(this.child.pid, signal)
+ } 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.ctx
+ runInCtx(ctx, () => {
+ const {
+ nothrow,
+ cmd,
+ cwd,
+ env,
+ prefix,
+ shell,
+ maxBuffer,
+ __from,
+ resolve,
+ reject,
+ } = ctx
+
+ printCmd(cmd)
+
+ let child = spawn(prefix + cmd, {
+ cwd,
+ shell: typeof shell === 'string' ? shell : true,
+ stdio: [this._inheritStdin ? 'inherit' : 'pipe', 'pipe', 'pipe'],
+ windowsHide: true,
+ maxBuffer,
+ env,
+ })
+
+ child.on('close', (code, signal) => {
+ let message = `${stderr || '\n'} at ${__from}`
+ message += `\n exit code: ${code}${
+ exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''
+ }`
+ if (signal !== null) {
+ message += `\n signal: ${signal}`
+ }
+ let output = new ProcessOutput({
+ code,
+ signal,
+ stdout,
+ stderr,
+ combined,
+ message,
+ })
+ ;(code === 0 || nothrow ? resolve : reject)(output)
+ this._resolved = true
+ })
+
+ let stdout = '',
+ stderr = '',
+ combined = ''
+ let onStdout = (data) => {
+ printStd(data)
+ stdout += data
+ combined += data
+ }
+ let onStderr = (data) => {
+ printStd(null, data)
+ stderr += data
+ combined += data
+ }
+ if (!this._piped) child.stdout.on('data', onStdout) // If process is piped, don't collect or print output.
+ child.stderr.on('data', onStderr) // Stderr should be printed regardless of piping.
+ this.child = child
+ if (this._postrun) this._postrun() // In case $1.pipe($2), after both subprocesses are running, we can pipe $1.stdout to $2.stdin.
+ })
+ }
+}
+
+export class ProcessOutput extends Error {
+ #code = null
+ #signal = null
+ #stdout = ''
+ #stderr = ''
+ #combined = ''
+
+ constructor({ code, signal, stdout, stderr, combined, message }) {
+ super(message)
+ this.#code = code
+ this.#signal = signal
+ this.#stdout = stdout
+ this.#stderr = stderr
+ this.#combined = combined
+ }
+
+ toString() {
+ return this.#combined
+ }
+
+ get stdout() {
+ return this.#stdout
+ }
+
+ get stderr() {
+ return this.#stderr
+ }
+
+ get exitCode() {
+ return this.#code
+ }
+
+ get signal() {
+ return this.#signal
+ }
+
+ [inspect.custom]() {
+ 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) + ')')
+ : ''
+ }
+}`
+ }
+}
+
+function exitCodeInfo(exitCode) {
+ return {
+ 2: 'Misuse of shell builtins',
+ 126: 'Invoked command cannot execute',
+ 127: 'Command not found',
+ 128: 'Invalid exit argument',
+ 129: 'Hangup',
+ 130: 'Interrupt',
+ 131: 'Quit and dump core',
+ 132: 'Illegal instruction',
+ 133: 'Trace/breakpoint trap',
+ 134: 'Process aborted',
+ 135: 'Bus error: "access to undefined portion of memory object"',
+ 136: 'Floating point exception: "erroneous arithmetic operation"',
+ 137: 'Kill (terminate immediately)',
+ 138: 'User-defined 1',
+ 139: 'Segmentation violation',
+ 140: 'User-defined 2',
+ 141: 'Write to pipe with no one reading',
+ 142: 'Signal raised by alarm',
+ 143: 'Termination (request to terminate)',
+ 145: 'Child process terminated, stopped (or continued*)',
+ 146: 'Continue if stopped',
+ 147: 'Stop executing temporarily',
+ 148: 'Terminal stop signal',
+ 149: 'Background process attempting to read from tty ("in")',
+ 150: 'Background process attempting to write to tty ("out")',
+ 151: 'Urgent data available on socket',
+ 152: 'CPU time limit exceeded',
+ 153: 'File size limit exceeded',
+ 154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
+ 155: 'Profiling timer expired',
+ 157: 'Pollable event',
+ 159: 'Bad syscall',
+ }[exitCode]
+}
src/experimental.mjs
@@ -12,45 +12,56 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {ProcessOutput, sleep, $} from './index.mjs'
+import { ProcessOutput, $ } from './core.mjs'
+import { sleep } from './goods.mjs'
+import { isString } from './util.mjs'
+import { getCtx, runInCtx } from './context.mjs'
+
+export { getCtx, runInCtx }
// 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(isString) && 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(' ')
}
console.log(msg)
}
-function isString(obj) {
- return typeof obj === 'string'
-}
-
function stringify(arg) {
if (arg instanceof ProcessOutput) {
return arg.toString().replace(/\n$/, '')
@@ -60,6 +71,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))
}
src/globals.mjs
@@ -1,3 +1,3 @@
-import {registerGlobals} from './index.mjs'
+import { registerGlobals } from './index.mjs'
registerGlobals()
src/goods.mjs
@@ -0,0 +1,53 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as globbyModule from 'globby'
+import minimist from 'minimist'
+import { setTimeout as sleep } from 'node:timers/promises'
+import nodeFetch from 'node-fetch'
+import { getCtx, getRootCtx } from './context.mjs'
+import { colorize } from './print.mjs'
+
+export { default as chalk } from 'chalk'
+export { default as fs } from 'fs-extra'
+export { default as which } from 'which'
+export { default as YAML } from 'yaml'
+export { default as path } from 'node:path'
+export { default as os } from 'node:os'
+export { sleep }
+
+export const argv = minimist(process.argv.slice(2))
+
+export const globby = Object.assign(function globby(...args) {
+ return globbyModule.globby(...args)
+}, globbyModule)
+
+export const glob = globby
+
+export async function fetch(url, init) {
+ if (getCtx().verbose) {
+ if (typeof init !== 'undefined') {
+ console.log('$', colorize(`fetch ${url}`), init)
+ } else {
+ console.log('$', colorize(`fetch ${url}`))
+ }
+ }
+ return nodeFetch(url, init)
+}
+
+export function cd(path) {
+ if (getCtx().verbose) console.log('$', colorize(`cd ${path}`))
+ process.chdir(path)
+ getRootCtx().cwd = getCtx().cwd = process.cwd()
+}
src/guards.mjs
@@ -0,0 +1,58 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { getCtx } from './context.mjs'
+
+export function quote(arg) {
+ if (/^[a-z0-9/_.-]+$/i.test(arg) || arg === '') {
+ return arg
+ }
+ return (
+ `$'` +
+ arg
+ .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') +
+ `'`
+ )
+}
+
+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(' ')
+ } else {
+ s = quote(substitute(args[i]))
+ }
+ cmd += s + pieces[++i]
+ }
+
+ return cmd
+}
+
+function substitute(arg) {
+ if (arg?.stdout) {
+ return arg.stdout.replace(/\n$/, '')
+ }
+ return `${arg}`
+}
src/hooks.mjs
@@ -0,0 +1,23 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export function nothrow(promise) {
+ promise.ctx.nothrow = true
+ return promise
+}
+
+export function quiet(promise) {
+ promise.ctx.verbose = false
+ return promise
+}
src/index.d.ts
@@ -1,11 +1,11 @@
// Copyright 2021 Google LLC
-//
+//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
-//
+//
// https://www.apache.org/licenses/LICENSE-2.0
-//
+//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -13,20 +13,23 @@
// limitations under the License.
declare module 'zx' {
- import {ChildProcess, spawn} from 'node:child_process'
- import {Readable, Writable} from 'node:stream'
+ import { ChildProcess, spawn } from 'node:child_process'
+ import { Readable, Writable } from 'node:stream'
import * as _fs from 'fs-extra'
import * as _globby from 'globby'
import * as _os from 'node:os'
import * as _path from 'node:path'
- import {ChalkInstance} from 'chalk'
+ import { ChalkInstance } from 'chalk'
import * as _yaml from 'yaml'
import _fetch from 'node-fetch'
- import {ParsedArgs} from 'minimist'
+ import { ParsedArgs } from 'minimist'
import * as _which from 'which'
export interface ZxTemplate {
- (pieces: TemplateStringsArray, ...args: any[]): ProcessPromise<ProcessOutput>
+ (
+ pieces: TemplateStringsArray,
+ ...args: any[]
+ ): ProcessPromise<ProcessOutput>
}
interface $ extends ZxTemplate {
@@ -45,7 +48,9 @@ declare module 'zx' {
readonly stderr: Readable
readonly exitCode: Promise<number>
- pipe(dest: ProcessPromise<ProcessOutput> | Writable): ProcessPromise<ProcessOutput>
+ pipe(
+ dest: ProcessPromise<ProcessOutput> | Writable
+ ): ProcessPromise<ProcessOutput>
kill(signal?: string | number): Promise<void>
}
@@ -62,10 +67,14 @@ declare module 'zx' {
export type QuestionOptions = { choices: string[] }
type cd = (path: string) => void
- type nothrow = (p: ProcessPromise<ProcessOutput>) => ProcessPromise<ProcessOutput>
+ type nothrow = (
+ p: ProcessPromise<ProcessOutput>
+ ) => ProcessPromise<ProcessOutput>
type question = (query?: string, options?: QuestionOptions) => Promise<string>
type sleep = (ms: number) => Promise<void>
- type quiet = (p: ProcessPromise<ProcessOutput>) => ProcessPromise<ProcessOutput>
+ type quiet = (
+ p: ProcessPromise<ProcessOutput>
+ ) => ProcessPromise<ProcessOutput>
export const $: $
export const argv: ParsedArgs
@@ -122,7 +131,7 @@ declare module 'zx/globals' {
}
declare module 'zx/experimental' {
- import {ZxTemplate} from 'zx'
+ import { ZxTemplate } from 'zx'
interface Echo {
(pieces: TemplateStringsArray, ...args: any[]): void
@@ -132,9 +141,11 @@ declare module 'zx/experimental' {
export const retry: (count?: number, delay?: number) => ZxTemplate
- export const withTimeout: (delay?: number, signal?: string | number) => ZxTemplate
+ export const withTimeout: (
+ delay?: number,
+ signal?: string | number
+ ) => ZxTemplate
type StopSpinner = () => void
export function startSpinner(title: string): StopSpinner
}
-
src/index.mjs
@@ -1,39 +1,55 @@
-// Copyright 2021 Google LLC
-//
+// Copyright 2022 Google LLC
+//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
-//
+//
// https://www.apache.org/licenses/LICENSE-2.0
-//
+//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-import fs from 'fs-extra'
-import * as globbyModule from 'globby'
-import os from 'node:os'
-import path from 'node:path'
-import {promisify, inspect} from 'node:util'
-import {spawn} from 'node:child_process'
-import {createInterface} from 'node:readline'
-import {default as nodeFetch} from 'node-fetch'
-import which from 'which'
-import chalk from 'chalk'
-import YAML from 'yaml'
-import minimist from 'minimist'
-import psTreeModule from 'ps-tree'
-
-export {chalk, fs, os, path, YAML, which}
-export const sleep = promisify(setTimeout)
-export const argv = minimist(process.argv.slice(2))
-export const globby = Object.assign(function globby(...args) {
- return globbyModule.globby(...args)
-}, globbyModule)
-export const glob = globby
-const psTree = promisify(psTreeModule)
+import {
+ argv,
+ cd,
+ chalk,
+ fetch,
+ fs,
+ glob,
+ globby,
+ path,
+ sleep,
+ which,
+ YAML,
+ os,
+} from './goods.mjs'
+import { nothrow, quiet } from './hooks.mjs'
+import { question } from './question.mjs'
+import { $, ProcessPromise, ProcessOutput } from './core.mjs'
+
+export {
+ $,
+ ProcessPromise,
+ ProcessOutput,
+ argv,
+ cd,
+ chalk,
+ fetch,
+ fs,
+ glob,
+ globby,
+ nothrow,
+ os,
+ path,
+ question,
+ quiet,
+ sleep,
+ which,
+ YAML,
+}
export function registerGlobals() {
Object.assign(global, {
@@ -46,347 +62,12 @@ export function registerGlobals() {
glob,
globby,
nothrow,
- quiet,
os,
path,
question,
+ quiet,
sleep,
- YAML,
which,
+ YAML,
})
}
-
-export function $(pieces, ...args) {
- let {
- verbose,
- shell,
- prefix,
- spawn,
- maxBuffer = 200 * 1024 * 1024 /* 200 MiB*/
- } = $
- let __from = (new Error().stack.split(/^\s*at\s/m)[2]).trim()
- let cwd = process.cwd()
-
- let cmd = pieces[0], i = 0
- while (i < args.length) {
- let s
- if (Array.isArray(args[i])) {
- s = args[i].map(x => $.quote(substitute(x))).join(' ')
- } else {
- s = $.quote(substitute(args[i]))
- }
- cmd += s + pieces[++i]
- }
-
- let resolve, reject
- let promise = new ProcessPromise((...args) => [resolve, reject] = args)
-
- promise._run = () => {
- if (promise.child) return // The _run() called from two places: then() and setTimeout().
- if (promise._prerun) promise._prerun() // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
- if (verbose && !promise._quiet) {
- printCmd(cmd)
- }
-
- let child = spawn(prefix + cmd, {
- cwd,
- shell: typeof shell === 'string' ? shell : true,
- stdio: [promise._inheritStdin ? 'inherit' : 'pipe', 'pipe', 'pipe'],
- windowsHide: true,
- maxBuffer,
- })
-
- child.on('close', (code, signal) => {
- let message = `${stderr || '\n'} at ${__from}`
- message += `\n exit code: ${code}${exitCodeInfo(code) ? ' (' + exitCodeInfo(code) + ')' : ''}`
- if (signal !== null) {
- message += `\n signal: ${signal}`
- }
- let output = new ProcessOutput({
- code,
- signal,
- stdout,
- stderr,
- combined,
- message,
- });
- (code === 0 || promise._nothrow ? resolve : reject)(output)
- promise._resolved = true
- })
-
- let stdout = '', stderr = '', combined = ''
- let onStdout = data => {
- if (verbose && !promise._quiet) process.stdout.write(data)
- stdout += data
- combined += data
- }
- let onStderr = data => {
- if (verbose && !promise._quiet) process.stderr.write(data)
- stderr += data
- combined += data
- }
- if (!promise._piped) child.stdout.on('data', onStdout) // If process is piped, don't collect or print output.
- child.stderr.on('data', onStderr) // Stderr should be printed regardless of piping.
- promise.child = child
- if (promise._postrun) promise._postrun() // In case $1.pipe($2), after both subprocesses are running, we can pipe $1.stdout to $2.stdin.
- }
- setTimeout(promise._run, 0) // Make sure all subprocesses are started, if not explicitly by await or then().
- return promise
-}
-
-$.quote = quote
-$.spawn = spawn
-$.verbose = true
-$.prefix = '' // Bash not found, no prefix.
-try {
- $.shell = which.sync('bash')
- $.prefix = 'set -euo pipefail;'
-} catch (e) {
-}
-
-export function cd(path) {
- if ($.verbose) console.log('$', colorize(`cd ${path}`))
- process.chdir(path)
-}
-
-export async function question(query, options) {
- let completer = undefined
- if (Array.isArray(options?.choices)) {
- completer = function completer(line) {
- const completions = options.choices
- const hits = completions.filter((c) => c.startsWith(line))
- return [hits.length ? hits : completions, line]
- }
- }
- const rl = createInterface({
- input: process.stdin,
- output: process.stdout,
- terminal: true,
- completer,
- })
-
- return new Promise((resolve) => rl.question(query ?? '', (answer) => {
- rl.close()
- resolve(answer)
- }))
-}
-
-export async function fetch(url, init) {
- if ($.verbose) {
- if (typeof init !== 'undefined') {
- console.log('$', colorize(`fetch ${url}`), init)
- } else {
- console.log('$', colorize(`fetch ${url}`))
- }
- }
- return nodeFetch(url, init)
-}
-
-export function nothrow(promise) {
- promise._nothrow = true
- return promise
-}
-
-export function quiet(promise) {
- promise._quiet = true
- return promise
-}
-
-export class ProcessPromise extends Promise {
- child = undefined
- _nothrow = false
- _quiet = false
- _resolved = false
- _inheritStdin = true
- _piped = false
- _prerun = undefined
- _postrun = undefined
-
- get stdin() {
- this._inheritStdin = false
- this._run()
- return this.child.stdin
- }
-
- get stdout() {
- this._inheritStdin = false
- this._run()
- return this.child.stdout
- }
-
- get stderr() {
- this._inheritStdin = false
- this._run()
- return this.child.stderr
- }
-
- get exitCode() {
- return this
- .then(p => p.exitCode)
- .catch(p => p.exitCode)
- }
-
- then(onfulfilled, onrejected) {
- if (this._run) this._run()
- return super.then(onfulfilled, onrejected)
- }
-
- 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!')
- }
- this._piped = true
- if (dest instanceof ProcessPromise) {
- dest._inheritStdin = false
- dest._prerun = this._run
- dest._postrun = () => this.stdout.pipe(dest.child.stdin)
- return dest
- } else {
- this._postrun = () => this.stdout.pipe(dest)
- return this
- }
- }
-
- async kill(signal = 'SIGTERM') {
- this.catch(_ => _)
- let children = await psTree(this.child.pid)
- for (const p of children) {
- try {
- process.kill(p.PID, signal)
- } catch (e) {
- }
- }
- try {
- process.kill(this.child.pid, signal)
- } catch (e) {
- }
- }
-}
-
-export class ProcessOutput extends Error {
- #code = null
- #signal = null
- #stdout = ''
- #stderr = ''
- #combined = ''
-
- constructor({code, signal, stdout, stderr, combined, message}) {
- super(message)
- this.#code = code
- this.#signal = signal
- this.#stdout = stdout
- this.#stderr = stderr
- this.#combined = combined
- }
-
- toString() {
- return this.#combined
- }
-
- get stdout() {
- return this.#stdout
- }
-
- get stderr() {
- return this.#stderr
- }
-
- get exitCode() {
- return this.#code
- }
-
- get signal() {
- return this.#signal
- }
-
- [inspect.custom]() {
- 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) + ')') : '')}
-}`
- }
-}
-
-function printCmd(cmd) {
- if (/\n/.test(cmd)) {
- console.log(cmd
- .split('\n')
- .map((line, i) => (i === 0 ? '$' : '>') + ' ' + colorize(line))
- .join('\n'))
- } else {
- console.log('$', colorize(cmd))
- }
-}
-
-function colorize(cmd) {
- return cmd.replace(/^[\w_.-]+(\s|$)/, substr => {
- return chalk.greenBright(substr)
- })
-}
-
-function substitute(arg) {
- if (arg instanceof ProcessOutput) {
- return arg.stdout.replace(/\n$/, '')
- }
- return `${arg}`
-}
-
-function quote(arg) {
- if (/^[a-z0-9/_.-]+$/i.test(arg) || arg === '') {
- return arg
- }
- return `$'`
- + arg
- .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')
- + `'`
-}
-
-function exitCodeInfo(exitCode) {
- return {
- 2: 'Misuse of shell builtins',
- 126: 'Invoked command cannot execute',
- 127: 'Command not found',
- 128: 'Invalid exit argument',
- 129: 'Hangup',
- 130: 'Interrupt',
- 131: 'Quit and dump core',
- 132: 'Illegal instruction',
- 133: 'Trace/breakpoint trap',
- 134: 'Process aborted',
- 135: 'Bus error: "access to undefined portion of memory object"',
- 136: 'Floating point exception: "erroneous arithmetic operation"',
- 137: 'Kill (terminate immediately)',
- 138: 'User-defined 1',
- 139: 'Segmentation violation',
- 140: 'User-defined 2',
- 141: 'Write to pipe with no one reading',
- 142: 'Signal raised by alarm',
- 143: 'Termination (request to terminate)',
- 145: 'Child process terminated, stopped (or continued*)',
- 146: 'Continue if stopped',
- 147: 'Stop executing temporarily',
- 148: 'Terminal stop signal',
- 149: 'Background process attempting to read from tty ("in")',
- 150: 'Background process attempting to write to tty ("out")',
- 151: 'Urgent data available on socket',
- 152: 'CPU time limit exceeded',
- 153: 'File size limit exceeded',
- 154: 'Signal raised by timer counting virtual time: "virtual timer expired"',
- 155: 'Profiling timer expired',
- 157: 'Pollable event',
- 159: 'Bad syscall',
- }[exitCode]
-}
src/print.mjs
@@ -0,0 +1,42 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { getCtx } from './context.mjs'
+import { chalk } from './goods.mjs'
+
+export function printCmd(cmd) {
+ if (!getCtx()?.verbose) return
+ if (/\n/.test(cmd)) {
+ console.log(
+ cmd
+ .split('\n')
+ .map((line, i) => (i === 0 ? '$' : '>') + ' ' + colorize(line))
+ .join('\n')
+ )
+ } else {
+ console.log('$', colorize(cmd))
+ }
+}
+
+export function printStd(data, err) {
+ if (!getCtx()?.verbose) return
+ if (data) process.stdout.write(data)
+ if (err) process.stderr.write(err)
+}
+
+export function colorize(cmd) {
+ return cmd.replace(/^[\w_.-]+(\s|$)/, (substr) => {
+ return chalk.greenBright(substr)
+ })
+}
src/question.mjs
@@ -0,0 +1,39 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { createInterface } from 'node:readline'
+
+export async function question(query, options) {
+ let completer = undefined
+ if (Array.isArray(options?.choices)) {
+ completer = function completer(line) {
+ const completions = options.choices
+ const hits = completions.filter((c) => c.startsWith(line))
+ return [hits.length ? hits : completions, line]
+ }
+ }
+ const rl = createInterface({
+ input: process.stdin,
+ output: process.stdout,
+ terminal: true,
+ completer,
+ })
+
+ return new Promise((resolve) =>
+ rl.question(query ?? '', (answer) => {
+ rl.close()
+ resolve(answer)
+ })
+ )
+}
src/util.mjs
@@ -0,0 +1,21 @@
+// Copyright 2022 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+export const randomId = function () {
+ return Math.random().toString(36).slice(2)
+}
+
+export function isString(obj) {
+ return typeof obj === 'string'
+}
test/fixtures/interactive.mjs
@@ -1,13 +1,13 @@
#!/usr/bin/env zx
// Copyright 2021 Google LLC
-//
+//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
-//
+//
// https://www.apache.org/licenses/LICENSE-2.0
-//
+//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
test/experimental.test.mjs
@@ -12,8 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {echo, retry, startSpinner, withTimeout} from '../src/experimental.mjs'
-import {assert, testFactory} from './test-utils.mjs'
+import { echo, retry, startSpinner, withTimeout } from '../src/experimental.mjs'
+import { assert, testFactory } from './test-utils.mjs'
import chalk from 'chalk'
const test = testFactory('experimental', import.meta)
@@ -49,7 +49,11 @@ test('withTimeout works', async () => {
test('echo works', async () => {
echo(chalk.red('foo'), chalk.green('bar'), chalk.bold('baz'))
echo`${chalk.red('foo')} ${chalk.green('bar')} ${chalk.bold('baz')}`
- echo(await $`echo ${chalk.red('foo')}`, await $`echo ${chalk.green('bar')}`, await $`echo ${chalk.bold('baz')}`)
+ echo(
+ await $`echo ${chalk.red('foo')}`,
+ await $`echo ${chalk.green('bar')}`,
+ await $`echo ${chalk.bold('baz')}`
+ )
})
test('spinner works', async () => {
test/index.test.mjs
@@ -1,23 +1,25 @@
// Copyright 2021 Google LLC
-//
+//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
-//
+//
// https://www.apache.org/licenses/LICENSE-2.0
-//
+//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
-import {inspect} from 'util'
+import { inspect } from 'node:util'
import chalk from 'chalk'
-import {Writable} from 'stream'
-import {Socket} from 'net'
+import { Writable } from 'node:stream'
+import { Socket } from 'node:net'
-import {assert, testFactory} from './test-utils.mjs'
+import { assert, testFactory } from './test-utils.mjs'
+import { ProcessPromise } from '../src/index.mjs'
+import { getCtx, runInCtx } from '../src/context.mjs'
const test = testFactory('index', import.meta)
@@ -99,19 +101,17 @@ test('Quiet mode is working', async () => {
})
test('Pipes are working', async () => {
- let {stdout} = await $`echo "hello"`
+ let { stdout } = await $`echo "hello"`
.pipe($`awk '{print $1" world"}'`)
.pipe($`tr '[a-z]' '[A-Z]'`)
assert(stdout === 'HELLO WORLD\n')
try {
- await $`echo foo`
- .pipe(fs.createWriteStream('/tmp/output.txt'))
+ await $`echo foo`.pipe(fs.createWriteStream('/tmp/output.txt'))
assert((await fs.readFile('/tmp/output.txt')).toString() === 'foo\n')
let r = $`cat`
- fs.createReadStream('/tmp/output.txt')
- .pipe(r.stdin)
+ fs.createReadStream('/tmp/output.txt').pipe(r.stdin)
assert((await r).stdout === 'foo\n')
} finally {
await fs.rm('/tmp/output.txt')
@@ -119,7 +119,7 @@ test('Pipes are working', async () => {
})
test('question', async () => {
- let p = question('foo or bar? ', {choices: ['foo', 'bar']})
+ let p = question('foo or bar? ', { choices: ['foo', 'bar'] })
setImmediate(() => {
process.stdin.emit('data', 'fo')
@@ -136,7 +136,7 @@ test('ProcessPromise', async () => {
write: function (chunk, encoding, next) {
contents += chunk.toString()
next()
- }
+ },
})
let p = $`echo 'test'`.pipe(stream)
await p
@@ -150,7 +150,29 @@ test('ProcessPromise', async () => {
} catch (p) {
err = p
}
- assert.equal(err.message, 'The pipe() method does not take strings. Forgot $?')
+ assert.equal(
+ err.message,
+ 'The pipe() method does not take strings. Forgot $?'
+ )
+})
+
+test('ProcessPromise: inherits native Promise', async () => {
+ const p1 = $`echo 1`
+ const p2 = p1.then((v) => v)
+ const p3 = p2.then((v) => v)
+ const p4 = p3.catch((v) => v)
+ const p5 = p1.finally((v) => v)
+
+ assert.ok(p1 instanceof Promise)
+ assert.ok(p1 instanceof ProcessPromise)
+ assert.ok(p2 instanceof ProcessPromise)
+ assert.ok(p3 instanceof ProcessPromise)
+ assert.ok(p4 instanceof ProcessPromise)
+ assert.ok(p5 instanceof ProcessPromise)
+ assert.ok(p1 !== p2)
+ assert.ok(p2 !== p3)
+ assert.ok(p3 !== p4)
+ assert.ok(p5 !== p1)
})
test('ProcessOutput thrown as error', async () => {
@@ -166,12 +188,16 @@ test('ProcessOutput thrown as error', async () => {
})
test('The pipe() throws if already resolved', async () => {
- let out, p = $`echo "Hello"`
+ let out,
+ p = $`echo "Hello"`
await p
try {
out = await p.pipe($`less`)
} catch (err) {
- assert.equal(err.message, `The pipe() method shouldn't be called after promise is already resolved!`)
+ assert.equal(
+ err.message,
+ `The pipe() method shouldn't be called after promise is already resolved!`
+ )
}
if (out) {
assert.fail('Expected failure!')
@@ -179,12 +205,12 @@ test('The pipe() throws if already resolved', async () => {
})
test('ProcessOutput::exitCode do not throw', async () => {
- assert(await $`grep qwerty README.md`.exitCode !== 0)
- assert(await $`[[ -f ${__filename} ]]`.exitCode === 0)
+ assert((await $`grep qwerty README.md`.exitCode) !== 0)
+ assert((await $`[[ -f ${__filename} ]]`.exitCode) === 0)
})
test('The nothrow() do not throw', async () => {
- let {exitCode} = await nothrow($`exit 42`)
+ let { exitCode } = await nothrow($`exit 42`)
assert(exitCode === 42)
})
@@ -202,14 +228,14 @@ test('globby available', async () => {
assert(await globby('test/fixtures/*'), [
'test/fixtures/interactive.mjs',
'test/fixtures/no-extension',
- 'test/fixtures/no-extension.mjs'
+ 'test/fixtures/no-extension.mjs',
])
})
test('fetch', async () => {
assert(
await fetch('https://example.com'),
- await fetch('https://example.com', {method: 'GET'})
+ await fetch('https://example.com', { method: 'GET' })
)
})
@@ -220,16 +246,16 @@ test('Executes a script from $PATH', async () => {
const envPathSeparator = isWindows ? ';' : ':'
process.env.PATH += envPathSeparator + path.resolve('/tmp/')
- const toPOSIXPath = (_path) =>
- _path.split(path.sep).join(path.posix.sep)
+ const toPOSIXPath = (_path) => _path.split(path.sep).join(path.posix.sep)
const zxPath = path.resolve('./zx.mjs')
const zxLocation = isWindows ? toPOSIXPath(zxPath) : zxPath
const scriptCode = `#!/usr/bin/env ${zxLocation}\nconsole.log('The script from path runs.')`
try {
- await $`echo ${scriptCode}`
- .pipe(fs.createWriteStream('/tmp/script-from-path', {mode: 0o744}))
+ await $`echo ${scriptCode}`.pipe(
+ fs.createWriteStream('/tmp/script-from-path', { mode: 0o744 })
+ )
await $`script-from-path`
} finally {
process.env.PATH = oldPath
@@ -239,21 +265,67 @@ test('Executes a script from $PATH', async () => {
test('The cd() works with relative paths', async () => {
let cwd = process.cwd()
+ assert.equal($.cwd, cwd)
try {
fs.mkdirpSync('/tmp/zx-cd-test/one/two')
cd('/tmp/zx-cd-test/one/two')
let p1 = $`pwd`
+ assert.ok($.cwd.endsWith('/two'))
+ assert.ok(process.cwd().endsWith('/two'))
+
cd('..')
let p2 = $`pwd`
+ assert.ok($.cwd.endsWith('/one'))
+ assert.ok(process.cwd().endsWith('/one'))
+
cd('..')
let p3 = $`pwd`
+ assert.ok(process.cwd().endsWith('/zx-cd-test'))
+ assert.ok($.cwd.endsWith('/tmp/zx-cd-test'))
- let results = (await Promise.all([p1, p2, p3]))
- .map(p => path.basename(p.stdout.trim()))
+ let results = (await Promise.all([p1, p2, p3])).map((p) =>
+ path.basename(p.stdout.trim())
+ )
assert.deepEqual(results, ['two', 'one', 'zx-cd-test'])
+ } catch (e) {
+ assert(!e, e)
+ } finally {
+ fs.rmSync('/tmp/zx-cd-test', { recursive: true })
+ cd(cwd)
+ assert.equal($.cwd, cwd)
+ }
+})
+
+test('cd() does not affect parallel contexts', async () => {
+ let cwd = process.cwd()
+ let resolve, reject
+ let promise = new ProcessPromise((...args) => ([resolve, reject] = args))
+
+ try {
+ fs.mkdirpSync('/tmp/zx-cd-parallel')
+ runInCtx({ ...getCtx() }, async () => {
+ assert.equal($.cwd, cwd)
+ await sleep(10)
+ cd('/tmp/zx-cd-parallel')
+ assert.ok(getCtx().cwd.endsWith('/zx-cd-parallel'))
+ assert.ok($.cwd.endsWith('/zx-cd-parallel'))
+ })
+
+ runInCtx({ ...getCtx() }, async () => {
+ assert.equal($.cwd, cwd)
+ assert.equal(getCtx().cwd, cwd)
+ await sleep(20)
+ assert.equal(getCtx().cwd, cwd)
+ assert.ok($.cwd.endsWith('/zx-cd-parallel'))
+ resolve()
+ })
+
+ await promise
+ } catch (e) {
+ assert(!e, e)
} finally {
- fs.rmSync('/tmp/zx-cd-test', {recursive: true})
+ fs.rmSync('/tmp/zx-cd-parallel', { recursive: true })
cd(cwd)
}
})
@@ -279,7 +351,7 @@ test('The signal is passed with kill() method', async () => {
})
test('YAML works', async () => {
- assert.deepEqual(YAML.parse(YAML.stringify({foo: 'bar'})), {foo: 'bar'})
+ assert.deepEqual(YAML.parse(YAML.stringify({ foo: 'bar' })), { foo: 'bar' })
console.log(chalk.greenBright('YAML works'))
})
test/test-utils.mjs
@@ -13,11 +13,11 @@
// limitations under the License.
import chalk from 'chalk'
-import {fileURLToPath} from 'node:url'
-import {relative} from 'node:path'
-import {sleep} from '../src/index.mjs'
+import { fileURLToPath } from 'node:url'
+import { relative } from 'node:path'
+import { setTimeout as sleep } from 'node:timers/promises'
-export {strict as assert} from 'assert'
+export { strict as assert } from 'assert'
let queued = 0
let passed = 0
@@ -29,7 +29,7 @@ let focused = 0
const singleThread = (fn) => {
let p = Promise.resolve()
return async function (...args) {
- return (p = p.catch(_ => _).then(() => fn.call(this, ...args)))
+ return (p = p.catch((_) => _).then(() => fn.call(this, ...args)))
}
}
@@ -42,12 +42,17 @@ const log = (name, group, err, file = '') => {
console.log(err)
console.log(file)
}
- console.log('\n' + chalk[err ? 'bgRedBright' : 'bgGreenBright'].black(`${chalk.inverse(' ' + group + ' ')} ${name} `))
+ console.log(
+ '\n' +
+ chalk[err ? 'bgRedBright' : 'bgGreenBright'].black(
+ `${chalk.inverse(' ' + group + ' ')} ${name} `
+ )
+ )
}
export const test = async function (name, cb, ms, focus, skip) {
const filter = RegExp(process.argv[3] || '.')
- const {group, meta} = this
+ const { group, meta } = this
const file = meta ? relative(process.cwd(), fileURLToPath(meta.url)) : ''
if (filter.test(name) || filter.test(group) || filter.test(file)) {
@@ -84,21 +89,25 @@ export const skip = async function (name, cb, ms) {
return test.call(this, name, cb, ms, false, true)
}
-export const testFactory = (group, meta) => Object.assign(
- test.bind({group, meta}), {
+export const testFactory = (group, meta) =>
+ Object.assign(test.bind({ group, meta }), {
test,
skip,
only,
group,
- meta
+ meta,
})
export const printTestDigest = () => {
- console.log('\n' +
- chalk.black.bgYellowBright(` zx version is ${require('../package.json').version} `) + '\n' +
- chalk.greenBright(` ๐บ tests passed: ${passed} `) +
- (skipped ? chalk.yellowBright(`\n ๐ง skipped: ${skipped} `) : '') +
- (failed ? chalk.redBright(`\n โ failed: ${failed} `) : '')
+ console.log(
+ '\n' +
+ chalk.black.bgYellowBright(
+ ` zx version is ${require('../package.json').version} `
+ ) +
+ '\n' +
+ chalk.greenBright(` ๐บ tests passed: ${passed} `) +
+ (skipped ? chalk.yellowBright(`\n ๐ง skipped: ${skipped} `) : '') +
+ (failed ? chalk.redBright(`\n โ failed: ${failed} `) : '')
)
failed && process.exit(1)
}
test/zx.test.mjs
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import {assert, testFactory} from './test-utils.mjs'
+import { assert, testFactory } from './test-utils.mjs'
const test = testFactory('zx', import.meta)
@@ -71,7 +71,10 @@ test('Eval script from https ref', async () => {
test('Scripts with no extension', async () => {
await $`node zx.mjs test/fixtures/no-extension`
- assert.match((await fs.readFile('test/fixtures/no-extension.mjs')).toString(), /Test file to verify no-extension didn't overwrite similarly name .mjs file./)
+ assert.match(
+ (await fs.readFile('test/fixtures/no-extension.mjs')).toString(),
+ /Test file to verify no-extension didn't overwrite similarly name .mjs file./
+ )
})
test('The require() is working from stdin', async () => {
package.json
@@ -9,6 +9,7 @@
"./globals": "./src/globals.mjs",
"./experimental": "./src/experimental.mjs",
"./cli": "./zx.mjs",
+ "./core": "./src/core.mjs",
"./package.json": "./package.json"
},
"bin": {
@@ -18,8 +19,10 @@
"node": ">= 16.0.0"
},
"scripts": {
- "test:cov": "c8 --reporter=html npm run test",
- "test": "node zx.mjs test/full.test.mjs",
+ "lint": "prettier --single-quote --no-semi --write src test",
+ "test": "npm run lint && npm run test:unit",
+ "test:unit": "node zx.mjs test/full.test.mjs",
+ "test:cov": "c8 --reporter=html npm run test:unit",
"test:zx": "npm run test zx",
"test:index": "npm run test index"
},
@@ -38,7 +41,8 @@
"yaml": "^2.0.1"
},
"devDependencies": {
- "c8": "^7.11.2"
+ "c8": "^7.11.2",
+ "prettier": "^2.6.2"
},
"publishConfig": {
"registry": "https://wombat-dressing-room.appspot.com"
README.md
@@ -359,6 +359,10 @@ outputs.
Or use a CLI argument `--quiet` to set `$.verbose = false`.
+### `$.env`
+
+Specifies env map. Defaults to `process.env`.
+
## Polyfills
### `__filename` & `__dirname`
@@ -434,6 +438,28 @@ import {withTimeout} from 'zx/experimental'
await withTimeout(100, 'SIGTERM')`sleep 9999`
```
+### `getCtx()` and `runInCtx()`
+
+[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.
+
+```js
+import {getCtx, runInCtx} from 'zx/experimental'
+
+runInCtx({ ...getCtx() }, async () => {
+ await sleep(10)
+ cd('/foo')
+ // $.cwd refers to /foo
+ // getCtx().cwd === $.cwd
+})
+
+runInCtx({ ...getCtx() }, async () => {
+ await sleep(20)
+ // $.cwd refers to /foo
+ // but getCtx().cwd !== $.cwd
+})
+```
+
## FAQ
### Passing env variables
zx.mjs
@@ -21,6 +21,7 @@ import {basename, dirname, extname, join, resolve} from 'node:path'
import url from 'node:url'
import {$, argv, fetch, ProcessOutput, registerGlobals} from './src/index.mjs'
+import {randomId} from './src/util.mjs'
await async function main() {
registerGlobals()
@@ -80,7 +81,7 @@ async function scriptFromStdin() {
if (script.length > 0) {
let filepath = join(
tmpdir(),
- Math.random().toString(36).substr(2) + '.mjs'
+ randomId() + '.mjs'
)
await fs.mkdtemp(filepath)
await writeAndImport(script, filepath, join(process.cwd(), 'stdin.mjs'))
@@ -115,7 +116,7 @@ async function importPath(filepath, origin = filepath) {
if (ext === '') {
let tmpFilename = fs.existsSync(`${filepath}.mjs`) ?
- `${basename(filepath)}-${Math.random().toString(36).substr(2)}.mjs` :
+ `${basename(filepath)}-${randomId()}.mjs` :
`${basename(filepath)}.mjs`
return await writeAndImport(