Commit bfff0ef

Anton Golub <antongolub@antongolub.com>
2024-09-19 12:19:10
fix: detect Duplex on pipe (#905)
* fix: detect Duplex on pipe closes #904 * test: add isStringLiteral test
1 parent 01f744b
src/core.ts
@@ -30,7 +30,6 @@ import {
   chalk,
   which,
   ps,
-  isStringLiteral,
   type ChalkInstance,
   type RequestInfo,
   type RequestInit,
@@ -43,6 +42,7 @@ import {
   formatCmd,
   getCallerLocation,
   isString,
+  isStringLiteral,
   noop,
   parseDuration,
   preferLocalBin,
@@ -325,7 +325,6 @@ export class ProcessPromise extends Promise<ProcessOutput> {
           c
         ) => {
           self._resolved = true
-
           // Ensures EOL
           if (stderr && !stderr.endsWith('\n')) c.on.stderr?.(eol, c)
           if (stdout && !stdout.endsWith('\n')) c.on.stdout?.(eol, c)
@@ -466,7 +465,7 @@ export class ProcessPromise extends Promise<ProcessOutput> {
     dest: Writable | ProcessPromise | TemplateStringsArray,
     ...args: any[]
   ): ProcessPromise {
-    if (isStringLiteral(dest))
+    if (isStringLiteral(dest, ...args))
       return this.pipe($(dest as TemplateStringsArray, ...args))
     if (isString(dest))
       throw new Error('The pipe() method does not take strings. Forgot $?')
src/util.ts
@@ -17,8 +17,6 @@ import path from 'node:path'
 import fs from 'node:fs'
 import { chalk } from './vendor-core.js'
 
-export { isStringLiteral } from './vendor-core.js'
-
 export function tempdir(prefix = `zx-${randomId()}`) {
   const dirpath = path.join(os.tmpdir(), prefix)
   fs.mkdirSync(dirpath, { recursive: true })
@@ -47,6 +45,18 @@ export function isString(obj: any) {
   return typeof obj === 'string'
 }
 
+export const isStringLiteral = (
+  pieces: any,
+  ...rest: any[]
+): pieces is TemplateStringsArray => {
+  return (
+    pieces?.length > 0 &&
+    pieces.raw?.length === pieces.length &&
+    Object.isFrozen(pieces) &&
+    rest.length + 1 === pieces.length
+  )
+}
+
 const pad = (v: string) => (v === ' ' ? ' ' : '')
 
 export function preferLocalBin(
src/vendor-core.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-export { exec, buildCmd, isStringLiteral, type TSpawnStore } from 'zurk/spawn'
+export { exec, buildCmd, type TSpawnStore } from 'zurk/spawn'
 
 export type RequestInfo = Parameters<typeof globalThis.fetch>[0]
 export type RequestInit = Parameters<typeof globalThis.fetch>[1]
test/core.test.js
@@ -358,6 +358,13 @@ describe('core', () => {
         assert.equal(p.stdout.trim(), 'foo')
       })
 
+      test('accepts stdout', async () => {
+        const p1 = $`echo pipe-to-stdout`
+        const p2 = p1.pipe(process.stdout)
+        assert.equal(p1, p2)
+        assert.equal((await p1).stdout.trim(), 'pipe-to-stdout')
+      })
+
       test('checks argument type', async () => {
         let err
         try {
test/util.test.js
@@ -20,6 +20,7 @@ import {
   errnoMessage,
   formatCmd,
   isString,
+  isStringLiteral,
   noop,
   parseDuration,
   quote,
@@ -59,6 +60,17 @@ describe('util', () => {
     assert.ok(!isString(1))
   })
 
+  test('isStringLiteral()', () => {
+    const bar = 'baz'
+    assert.ok(isStringLiteral``)
+    assert.ok(isStringLiteral`foo`)
+    assert.ok(isStringLiteral`foo ${bar}`)
+
+    assert.ok(!isStringLiteral(''))
+    assert.ok(!isStringLiteral('foo'))
+    assert.ok(!isStringLiteral(['foo']))
+  })
+
   test('quote()', () => {
     assert.ok(quote('string') === 'string')
     assert.ok(quote(`'\f\n\r\t\v\0`) === `$'\\'\\f\\n\\r\\t\\v\\0'`)