Skip to content

Commit

Permalink
feat: make ProcessOutput iterable (#1101)
Browse files Browse the repository at this point in the history
* feat: make `ProcessOutput` iterable

closes #1053
closes #1027

superseds #1088
relates #984

* refactor: reuse iterators logic
  • Loading branch information
antongolub authored Feb 17, 2025
1 parent ea5f5c0 commit 1fc9d0b
Show file tree
Hide file tree
Showing 5 changed files with 43 additions and 15 deletions.
4 changes: 2 additions & 2 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
{
"name": "zx/core",
"path": ["build/core.cjs", "build/util.cjs", "build/vendor-core.cjs"],
"limit": "77 kB",
"limit": "77.5 kB",
"brotli": false,
"gzip": false
},
Expand Down Expand Up @@ -30,7 +30,7 @@
{
"name": "all",
"path": "build/*",
"limit": "849 kB",
"limit": "850 kB",
"brotli": false,
"gzip": false
}
Expand Down
29 changes: 16 additions & 13 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ import {
log,
isString,
isStringLiteral,
bufToString,
getLast,
getLines,
noop,
once,
parseBool,
Expand Down Expand Up @@ -601,26 +601,19 @@ export class ProcessPromise extends Promise<ProcessOutput> {

// Async iterator API
async *[Symbol.asyncIterator](): AsyncIterator<string> {
let last: string | undefined
const getLines = (chunk: Buffer | string) => {
const lines = ((last || '') + bufToString(chunk)).split('\n')
last = lines.pop()
return lines
}
const memo: (string | undefined)[] = []

for (const chunk of this._zurk!.store.stdout) {
const lines = getLines(chunk)
for (const line of lines) yield line
yield* getLines(chunk, memo)
}

for await (const chunk of this.stdout[Symbol.asyncIterator]
? this.stdout
: VoidStream.from(this.stdout)) {
const lines = getLines(chunk)
for (const line of lines) yield line
yield* getLines(chunk, memo)
}

if (last) yield last
if (memo[0]) yield memo[0]

if ((await this.exitCode) !== 0) throw this._output
}
Expand Down Expand Up @@ -758,13 +751,23 @@ export class ProcessOutput extends Error {
}

lines(): string[] {
return this.valueOf().split(/\r?\n/)
return [...this]
}

valueOf(): string {
return this.stdall.trim()
}

*[Symbol.iterator](): Iterator<string> {
const memo: (string | undefined)[] = []

for (const chunk of this._dto.store.stdall) {
yield* getLines(chunk, memo)
}

if (memo[0]) yield memo[0]
}

static getExitMessage = formatExitMessage

static getErrorMessage = formatErrorMessage;
Expand Down
9 changes: 9 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -383,3 +383,12 @@ export const toCamelCase = (str: string) =>

export const parseBool = (v: string): boolean | string =>
({ true: true, false: false })[v] ?? v

export const getLines = (
chunk: Buffer | string,
next: (string | undefined)[]
) => {
const lines = ((next.pop() || '') + bufToString(chunk)).split(/\r?\n/)
next.push(lines.pop())
return lines
}
15 changes: 15 additions & 0 deletions test/core.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,21 @@ describe('core', () => {
globalThis.Blob = Blob
})

test('[Symbol.Iterator]', () => {
const o = new ProcessOutput({
store: {
stdall: ['foo\nba', 'r\nbaz'],
},
})
const lines = []
const expected = ['foo', 'bar', 'baz']
for (const line of o) {
lines.push(line)
}
assert.deepEqual(lines, expected)
assert.deepEqual(o.lines(), expected)
})

describe('static', () => {
test('getExitMessage()', () => {
assert.match(
Expand Down
1 change: 1 addition & 0 deletions test/smoke/node.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import 'zx/globals'
{
const p = await $`echo foo`
assert.match(p.stdout, /foo/)
assert.deepEqual(p.lines(), ['foo'])
}

// captures err stack
Expand Down

0 comments on commit 1fc9d0b

Please sign in to comment.