Commit 499d1e4

Anton Medvedev <anton@medv.io>
2022-06-05 15:38:14
Add $.log
1 parent 07e5aa1
src/core.ts
@@ -12,17 +12,22 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import { StdioNull, StdioPipe } from 'child_process'
-import { ChildProcess } from 'node:child_process'
+import { ChalkInstance } from 'chalk'
+import { RequestInit } from 'node-fetch'
+import assert from 'node:assert'
 import { AsyncLocalStorage } from 'node:async_hooks'
+import { ChildProcess, spawn, StdioNull, StdioPipe } from 'node:child_process'
 import { Readable, Writable } from 'node:stream'
 import { inspect } from 'node:util'
-import { spawn } from 'node:child_process'
-import assert from 'node:assert'
-import { ChalkInstance } from 'chalk'
 import { chalk, which } from './goods.js'
-import { printCmd } from './print.js'
-import { noop, quote, substitute, psTree, exitCodeInfo } from './util.js'
+import {
+  colorize,
+  exitCodeInfo,
+  noop,
+  psTree,
+  quote,
+  substitute,
+} from './util.js'
 
 type Shell = (pieces: TemplateStringsArray, ...args: any[]) => ProcessPromise
 
@@ -34,6 +39,7 @@ type Options = {
   prefix: string
   quote: typeof quote
   spawn: typeof spawn
+  log: typeof log
 }
 
 const storage = new AsyncLocalStorage<Options>()
@@ -47,6 +53,7 @@ function initStore(): Options {
     prefix: '',
     quote,
     spawn,
+    log,
   }
   storage.enterWith(context)
   if (process.env.ZX_VERBOSE) $.verbose = process.env.ZX_VERBOSE == 'true'
@@ -130,9 +137,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
   _run() {
     if (this.child) return // The _run() called from two places: then() and setImmediate().
     this._prerun() // In case $1.pipe($2), the $2 returned, and on $2._run() invoke $1._run().
-    if ($.verbose && !this._quiet) {
-      printCmd(this._command)
-    }
+    $.log('cmd', this._command, { source: this })
     this.child = spawn($.prefix + this._command, {
       cwd: this._cwd,
       shell: typeof $.shell === 'string' ? $.shell : true,
@@ -170,12 +175,12 @@ export class ProcessPromise extends Promise<ProcessOutput> {
       stderr = '',
       combined = ''
     let onStdout = (data: any) => {
-      if ($.verbose && !this._quiet) process.stderr.write(data)
+      $.log('stdout', data, { source: this })
       stdout += data
       combined += data
     }
     let onStderr = (data: any) => {
-      if ($.verbose && !this._quiet) process.stderr.write(data)
+      $.log('stderr', data, { source: this })
       stderr += data
       combined += data
     }
@@ -273,6 +278,10 @@ export class ProcessPromise extends Promise<ProcessOutput> {
     this._quiet = true
     return this
   }
+
+  isQuiet() {
+    return this._quiet
+  }
 }
 
 export class ProcessOutput extends Error {
@@ -338,16 +347,46 @@ export function within<R>(callback: () => R): R {
   return storage.run({ ...getStore() }, callback)
 }
 
-/**
- *  @deprecated Use $.nothrow() instead.
- */
-export function nothrow(promise: ProcessPromise) {
-  return promise.nothrow()
+export type LogKind = 'cmd' | 'stdout' | 'stderr' | 'cd' | 'fetch'
+export type LogExtra = {
+  source?: ProcessPromise
+  init?: RequestInit
 }
 
-/**
- * @deprecated Use $.quiet() instead.
- */
-export function quiet(promise: ProcessPromise) {
-  return promise.quiet()
+export function log(kind: LogKind, data: string, extra: LogExtra = {}) {
+  if (extra.source?.isQuiet()) return
+  if ($.verbose) {
+    switch (kind) {
+      case 'cmd':
+        process.stderr.write(formatCmd(data))
+        break
+      case 'stdout':
+      case 'stderr':
+        process.stderr.write(data)
+        break
+      case 'cd':
+        process.stderr.write('$ ' + colorize(`cd ${data}`))
+        break
+      case 'fetch':
+        process.stderr.write(
+          '$ ' + colorize(`fetch ${data} `) + inspect(extra.init)
+        )
+        break
+      default:
+        throw new Error(`Unknown log kind "${kind}".`)
+    }
+  }
+}
+
+export function formatCmd(cmd: string) {
+  if (/\n/.test(cmd)) {
+    return (
+      cmd
+        .split('\n')
+        .map((line, i) => `${i == 0 ? '$' : '>'} ${colorize(line)}`)
+        .join('\n') + '\n'
+    )
+  } else {
+    return `$ ${colorize(cmd)}\n`
+  }
 }
src/goods.ts
@@ -14,9 +14,10 @@
 
 import * as globbyModule from 'globby'
 import minimist from 'minimist'
-import { setTimeout as sleep } from 'node:timers/promises'
 import nodeFetch, { RequestInfo, RequestInit } from 'node-fetch'
-import { colorize, isString, stringify } from './util.js'
+import { createInterface } from 'node:readline'
+import { setTimeout as sleep } from 'node:timers/promises'
+import { isString, stringify } from './util.js'
 
 export { default as chalk } from 'chalk'
 export { default as fs } from 'fs-extra'
@@ -38,18 +39,12 @@ globbyModule)
 export const glob = globby
 
 export async function fetch(url: RequestInfo, init?: RequestInit) {
-  if ($.verbose) {
-    if (typeof init !== 'undefined') {
-      console.log('$', colorize(`fetch ${url}`), init)
-    } else {
-      console.log('$', colorize(`fetch ${url}`))
-    }
-  }
+  $.log('fetch', url.toString(), { init })
   return nodeFetch(url, init)
 }
 
 export function cd(dir: string) {
-  if ($.verbose) console.log('$', colorize(`cd ${dir}`))
+  $.log('cd', dir)
   $.cwd = path.resolve($.cwd, dir)
 }
 
@@ -70,6 +65,33 @@ export function echo(pieces: TemplateStringsArray, ...args: any[]) {
   console.log(msg)
 }
 
+export async function question(
+  query?: string,
+  options?: { choices: string[] }
+): Promise<string> {
+  let completer = undefined
+  if (options && Array.isArray(options.choices)) {
+    completer = function completer(line: string) {
+      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)
+    })
+  )
+}
+
 /**
  * Starts a simple CLI spinner.
  * @param title Spinner's title.
src/index.ts
@@ -12,36 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  argv,
-  cd,
-  chalk,
-  echo,
-  fetch,
-  fs,
-  glob,
-  globby,
-  path,
-  sleep,
-  startSpinner,
-  which,
-  YAML,
-  os,
-} from './goods.js'
-import { question } from './question.js'
-import {
+import { ProcessPromise } from './core.js'
+
+export {
   $,
   ProcessPromise,
   ProcessOutput,
-  nothrow,
-  quiet,
   within,
+  log,
+  formatCmd,
+  LogKind,
+  LogExtra,
 } from './core.js'
 
 export {
-  $,
-  ProcessPromise,
-  ProcessOutput,
   argv,
   cd,
   chalk,
@@ -50,14 +34,25 @@ export {
   fs,
   glob,
   globby,
-  nothrow,
   os,
   path,
   question,
-  quiet,
   sleep,
   startSpinner,
   which,
-  within,
   YAML,
+} from './goods.js'
+
+/**
+ *  @deprecated Use $.nothrow() instead.
+ */
+export function nothrow(promise: ProcessPromise) {
+  return promise.nothrow()
+}
+
+/**
+ * @deprecated Use $.quiet() instead.
+ */
+export function quiet(promise: ProcessPromise) {
+  return promise.quiet()
 }
src/print.ts
@@ -1,28 +0,0 @@
-// 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 { colorize } from './util.js'
-
-export function printCmd(cmd: string) {
-  if (/\n/.test(cmd)) {
-    process.stderr.write(
-      cmd
-        .split('\n')
-        .map((line, i) => `${i == 0 ? '$' : '>'} ${colorize(line)}`)
-        .join('\n') + '\n'
-    )
-  } else {
-    process.stderr.write(`$ ${colorize(cmd)}\n`)
-  }
-}
src/question.ts
@@ -11,32 +11,3 @@
 // 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?: string,
-  options?: { choices: string[] }
-): Promise<string> {
-  let completer = undefined
-  if (options && Array.isArray(options.choices)) {
-    completer = function completer(line: string) {
-      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)
-    })
-  )
-}
test/index.test.js
@@ -84,7 +84,7 @@ test('can use array as an argument', async () => {
   assert.is((await $`echo ${args}`).toString(), 'foo')
 })
 
-test('quiet mode is working', async () => {
+test('quiet() mode is working', async () => {
   let stdout = ''
   let log = console.log
   console.log = (...args) => {
@@ -93,6 +93,17 @@ test('quiet mode is working', async () => {
   await $`echo 'test'`.quiet()
   console.log = log
   assert.is(stdout, '')
+  {
+    // Deprecated.
+    let stdout = ''
+    let log = console.log
+    console.log = (...args) => {
+      stdout += args.join(' ')
+    }
+    await quiet($`echo 'test'`)
+    console.log = log
+    assert.is(stdout, '')
+  }
 })
 
 test('pipes are working', async () => {
@@ -211,6 +222,11 @@ test('await $`cmd`.exitCode does not throw', async () => {
 test('nothrow() do not throw', async () => {
   let { exitCode } = await $`exit 42`.nothrow()
   assert.is(exitCode, 42)
+  {
+    // Deprecated.
+    let { exitCode } = await nothrow($`exit 42`)
+    assert.is(exitCode, 42)
+  }
 })
 
 test('globby available', async () => {