Skip to content

Commit 52e67f2

Browse files
committed
copypasta over a portion of stderr-filtering to cli, since cli cannot import @packages
1 parent 8ebc620 commit 52e67f2

File tree

5 files changed

+300
-4
lines changed

5 files changed

+300
-4
lines changed

cli/lib/exec/spawn.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ const xvfb = require('./xvfb')
1010
const verify = require('../tasks/verify')
1111
const errors = require('../errors')
1212
const readline = require('readline')
13-
const { filter, DEBUG_PREFIX } = require('@packages/stderr-filtering')
14-
const Debug = require('debug')
13+
const { FilterTaggedContent } = require('../stderr-filtering/FilterTaggedContent')
14+
15+
export const START_TAG = '<<<CYPRESS.STDERR.START>>>'
16+
17+
export const END_TAG = '<<<CYPRESS.STDERR.END>>>'
1518

1619
function isPlatform (platform) {
1720
return os.platform() === platform
@@ -200,7 +203,10 @@ module.exports = {
200203
// to filter out the garbage
201204
if (child.stderr) {
202205
if (process.env.CYPRESS_INTERNAL_E2E_TESTING_SELF_PARENT_PROJECT) {
203-
child.stderr.pipe(filter(process.stderr, Debug('cypress:internal-stderr'), DEBUG_PREFIX))
206+
// strip out tags from stderr output during cy in cy tests
207+
const tagFilter = new FilterTaggedContent(START_TAG, END_TAG, process.stderr)
208+
209+
child.stderr.pipe(tagFilter).pipe(process.stderr)
204210
} else {
205211
debug('piping child STDERR to process STDERR')
206212
child.stderr.on('data', (data) => {
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
'use strict'
2+
let __importDefault = (this && this.__importDefault) || function (mod) {
3+
return (mod && mod.__esModule) ? mod : { 'default': mod }
4+
}
5+
6+
Object.defineProperty(exports, '__esModule', { value: true })
7+
exports.FilterTaggedContent = void 0
8+
9+
const stream_1 = require('stream')
10+
const string_decoder_1 = require('string_decoder')
11+
const LineDecoder_1 = require('./LineDecoder')
12+
const debug_1 = __importDefault(require('debug'))
13+
const writeWithBackpressure_1 = require('./writeWithBackpressure')
14+
const debug = (0, debug_1.default)('cypress:stderr-filtering:FilterTaggedContent')
15+
16+
/**
17+
* Filters content based on start and end tags, supporting multi-line tagged content.
18+
*
19+
* This transform stream processes incoming data and routes content between two output streams
20+
* based on tag detection. Content between start and end tags is sent to the filtered stream,
21+
* while content outside tags is sent to the main output stream. The class handles cases where
22+
* tags span multiple lines by maintaining state across line boundaries.
23+
*
24+
* Example usage:
25+
* ```typescript
26+
* const filter = new FilterTaggedContent('<secret>', '</secret>', filteredStream)
27+
* inputStream.pipe(filter).pipe(outputStream)
28+
* ```
29+
*/
30+
class FilterTaggedContent extends stream_1.Transform {
31+
/**
32+
* Creates a new FilterTaggedContent instance.
33+
*
34+
* @param startTag The string that marks the beginning of content to filter
35+
* @param endTag The string that marks the end of content to filter
36+
* @param filtered The writable stream for filtered content
37+
*/
38+
constructor (startTag, endTag, wasteStream) {
39+
super({
40+
transform: (chunk, encoding, next) => this.transform(chunk, encoding, next),
41+
flush: (callback) => this.flush(callback),
42+
})
43+
44+
this.startTag = startTag
45+
this.endTag = endTag
46+
this.wasteStream = wasteStream
47+
this.inTaggedContent = false
48+
/**
49+
* Processes incoming chunks and routes content based on tag detection.
50+
*
51+
* @param chunk The buffer chunk to process
52+
* @param encoding The encoding of the chunk
53+
* @param next Callback to call when processing is complete
54+
*/
55+
this.transform = async (chunk, encoding, next) => {
56+
let _a; let _b; let _c
57+
58+
try {
59+
this.ensureDecoders(encoding)
60+
const str = (_b = (_a = this.strDecoder) === null || _a === void 0 ? void 0 : _a.write(chunk)) !== null && _b !== void 0 ? _b : '';
61+
62+
(_c = this.lineDecoder) === null || _c === void 0 ? void 0 : _c.write(str)
63+
debug('processing str for tags: "%s"', str)
64+
for (const line of Array.from(this.lineDecoder || [])) {
65+
await this.processLine(line)
66+
}
67+
next()
68+
} catch (err) {
69+
next(err)
70+
}
71+
}
72+
73+
/**
74+
* Flushes any remaining buffered content when the stream ends.
75+
*
76+
* @param callback Callback to call when flushing is complete
77+
*/
78+
this.flush = async (callback) => {
79+
let _a
80+
81+
debug('flushing')
82+
this.ensureDecoders()
83+
try {
84+
for (const line of Array.from(((_a = this.lineDecoder) === null || _a === void 0 ? void 0 : _a.end()) || [])) {
85+
await this.processLine(line)
86+
}
87+
callback()
88+
} catch (err) {
89+
callback(err)
90+
}
91+
}
92+
}
93+
ensureDecoders (encoding) {
94+
let _a
95+
const enc = (_a = (encoding === 'buffer' ? 'utf8' : encoding)) !== null && _a !== void 0 ? _a : 'utf8'
96+
97+
if (!this.lineDecoder) {
98+
this.lineDecoder = new LineDecoder_1.LineDecoder()
99+
}
100+
101+
if (!this.strDecoder) {
102+
this.strDecoder = new string_decoder_1.StringDecoder(enc)
103+
}
104+
}
105+
/**
106+
* Processes a single line and routes content based on tag positions.
107+
*
108+
* This method handles the complex logic of detecting start and end tags within a line,
109+
* maintaining state across lines, and routing content to the appropriate streams.
110+
* It supports cases where both tags appear on the same line, only one tag appears,
111+
* or no tags appear but the line is part of ongoing tagged content.
112+
*
113+
* @param line The line to process
114+
*/
115+
async processLine (line) {
116+
const startPos = line.indexOf(this.startTag)
117+
const endPos = line.lastIndexOf(this.endTag)
118+
119+
if (startPos >= 0 && endPos >= 0) {
120+
// Both tags on same line
121+
if (startPos > 0) {
122+
await this.pass(line.slice(0, startPos))
123+
}
124+
125+
await this.writeToWasteStream(line.slice(startPos + this.startTag.length, endPos))
126+
if (endPos + this.endTag.length < line.length) {
127+
await this.pass(line.slice(endPos + this.endTag.length))
128+
}
129+
} else if (startPos >= 0) {
130+
// Start tag found
131+
if (startPos > 0) {
132+
await this.pass(line.slice(0, startPos))
133+
}
134+
135+
await this.writeToWasteStream(line.slice(startPos + this.startTag.length))
136+
this.inTaggedContent = true
137+
} else if (endPos >= 0) {
138+
// End tag found
139+
await this.writeToWasteStream(line.slice(0, endPos))
140+
if (endPos + this.endTag.length < line.length) {
141+
await this.pass(line.slice(endPos + this.endTag.length))
142+
}
143+
144+
this.inTaggedContent = false
145+
} else if (this.inTaggedContent) {
146+
// Currently in tagged content
147+
await this.writeToWasteStream(line)
148+
} else {
149+
// Not in tagged content
150+
await this.pass(line)
151+
}
152+
}
153+
async writeToWasteStream (line, encoding) {
154+
let _a
155+
156+
debug('writing to waste stream: "%s"', line)
157+
await (0, writeWithBackpressure_1.writeWithBackpressure)(this.wasteStream, Buffer.from(line, (_a = (encoding === 'buffer' ? 'utf8' : encoding)) !== null && _a !== void 0 ? _a : 'utf8'))
158+
}
159+
async pass (line, encoding) {
160+
let _a
161+
162+
debug('passing: "%s"', line)
163+
this.push(Buffer.from(line, (_a = (encoding === 'buffer' ? 'utf8' : encoding)) !== null && _a !== void 0 ? _a : 'utf8'))
164+
}
165+
}
166+
exports.FilterTaggedContent = FilterTaggedContent
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
'use strict'
2+
/**
3+
* Decodes incoming string chunks into complete lines, handling partial lines across chunk boundaries.
4+
*
5+
* This class buffers incoming string data and provides an iterator interface to yield complete
6+
* lines. It handles the case where a line might be split across multiple chunks by maintaining
7+
* an internal buffer. The end() method should be called to flush any remaining buffered content
8+
* when processing is complete.
9+
*/
10+
let __importDefault = (this && this.__importDefault) || function (mod) {
11+
return (mod && mod.__esModule) ? mod : { 'default': mod }
12+
}
13+
14+
Object.defineProperty(exports, '__esModule', { value: true })
15+
exports.LineDecoder = void 0
16+
17+
const debug_1 = __importDefault(require('debug'))
18+
const constants_1 = require('./constants')
19+
const debug = (0, debug_1.default)(`cypress:stderr-filtering:LineDecoder:${process.pid}`)
20+
21+
class LineDecoder {
22+
constructor (overrideToken = constants_1.END_TAG) {
23+
this.overrideToken = overrideToken
24+
this.buffer = ''
25+
}
26+
/**
27+
* Adds a chunk of string data to the internal buffer.
28+
*
29+
* @param chunk The string chunk to add to the buffer
30+
*/
31+
write (chunk) {
32+
debug('writing chunk to line decoder', { chunk })
33+
this.buffer += chunk
34+
}
35+
/**
36+
* Iterates over complete lines in the current buffer.
37+
*
38+
* This generator yields complete lines from the buffer, splitting on newline characters.
39+
* Any incomplete line at the end of the buffer is kept for the next iteration.
40+
* Handles both Windows (\r\n) and Unix (\n) line endings.
41+
*
42+
* @yields Complete lines with newline characters preserved
43+
*/
44+
*[Symbol.iterator] () {
45+
debug('iterating over lines in line decoder')
46+
let nextLine = undefined
47+
48+
do {
49+
nextLine = this.nextLine()
50+
if (nextLine) {
51+
debug('yielding line:', nextLine)
52+
debug('buffer size:', this.buffer.length)
53+
yield nextLine
54+
}
55+
} while (nextLine)
56+
}
57+
/**
58+
* Flushes the remaining buffer content and yields all remaining lines.
59+
*
60+
* This method should be called when processing is complete to ensure all buffered
61+
* content is yielded. It processes any remaining content in the buffer plus an
62+
* optional final chunk. Handles both Windows (\r\n) and Unix (\n) line endings.
63+
*
64+
* @param chunk Optional final chunk to process along with the buffer
65+
* @yields All remaining lines from the buffer and final chunk
66+
*/
67+
*end (chunk) {
68+
this.buffer = `${this.buffer}${(chunk || '')}`
69+
let nextLine = undefined
70+
71+
do {
72+
nextLine = this.nextLine()
73+
if (nextLine) {
74+
yield nextLine
75+
}
76+
} while (nextLine)
77+
}
78+
nextLine () {
79+
const [newlineIndex, length] = [this.buffer.indexOf('\n'), 1]
80+
const endsWithOverrideToken = newlineIndex < 0 ? this.buffer.endsWith(this.overrideToken) : false
81+
82+
if (endsWithOverrideToken) {
83+
debug('ends with override token')
84+
const line = this.buffer
85+
86+
this.buffer = ''
87+
88+
return line
89+
}
90+
91+
if (newlineIndex >= 0) {
92+
debug('contains a newline')
93+
const line = this.buffer.slice(0, newlineIndex + length)
94+
95+
this.buffer = this.buffer.slice(newlineIndex + length)
96+
97+
return line
98+
}
99+
100+
return undefined
101+
}
102+
}
103+
exports.LineDecoder = LineDecoder
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
'use strict'
2+
Object.defineProperty(exports, '__esModule', { value: true })
3+
exports.writeWithBackpressure = writeWithBackpressure
4+
5+
async function writeWithBackpressure (toStream, chunk) {
6+
return new Promise((resolve, reject) => {
7+
try {
8+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk))
9+
const ret = toStream.write(buffer)
10+
11+
if (ret) {
12+
resolve()
13+
} else {
14+
toStream.once('drain', () => {
15+
resolve()
16+
})
17+
}
18+
} catch (err) {
19+
reject(err)
20+
}
21+
})
22+
}

cli/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@
7676
"@cypress/svelte": "0.0.0-development",
7777
"@cypress/vue": "0.0.0-development",
7878
"@packages/root": "0.0.0-development",
79-
"@packages/stderr-filtering": "0.0.0-development",
8079
"@types/bluebird": "3.5.33",
8180
"@types/chai": "4.2.15",
8281
"@types/chai-jquery": "1.1.40",

0 commit comments

Comments
 (0)