From 305e55a3b32d80647b88c45b0719c0540bd441b8 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 22 Aug 2025 10:06:45 -0400 Subject: [PATCH 01/12] chore: convert server/lib/plugins files from JS to TS --- .../data-context/src/data/ProjectConfigIpc.ts | 4 +- .../src/data/ProjectConfigManager.ts | 4 +- .../data-context/src/util/pluginHandlers.ts | 6 +- packages/server/lib/controllers/spec.ts | 3 +- packages/server/lib/plugins/dev-server.js | 37 ---- packages/server/lib/plugins/dev-server.ts | 51 +++++ packages/server/lib/plugins/preprocessor.js | 149 -------------- packages/server/lib/plugins/preprocessor.ts | 174 ++++++++++++++++ packages/server/lib/plugins/run_events.js | 17 -- packages/server/lib/plugins/run_events.ts | 22 ++ packages/server/lib/plugins/util.js | 91 --------- packages/server/lib/plugins/util.ts | 190 ++++++++++++++++++ packages/types/src/server.ts | 22 ++ 13 files changed, 468 insertions(+), 302 deletions(-) delete mode 100644 packages/server/lib/plugins/dev-server.js create mode 100644 packages/server/lib/plugins/dev-server.ts delete mode 100644 packages/server/lib/plugins/preprocessor.js create mode 100644 packages/server/lib/plugins/preprocessor.ts delete mode 100644 packages/server/lib/plugins/run_events.js create mode 100644 packages/server/lib/plugins/run_events.ts delete mode 100644 packages/server/lib/plugins/util.js create mode 100644 packages/server/lib/plugins/util.ts diff --git a/packages/data-context/src/data/ProjectConfigIpc.ts b/packages/data-context/src/data/ProjectConfigIpc.ts index ed6b8471eed0..9edbee6bd44c 100644 --- a/packages/data-context/src/data/ProjectConfigIpc.ts +++ b/packages/data-context/src/data/ProjectConfigIpc.ts @@ -24,7 +24,7 @@ import { TagStream } from '@packages/stderr-filtering' // NOTE: need the file:// prefix to avoid https://nodejs.org/api/errors.html#err_unsupported_esm_url_scheme on windows const tsx = os.platform() === 'win32' ? `file://${toPosix(require.resolve('tsx'))}` : toPosix(require.resolve('tsx')) -export type IpcHandler = (ipc: ProjectConfigIpc) => void +export type PluginIpcHandler = (ipc: ProjectConfigIpc) => void /** * If running as root on Linux, no-sandbox must be passed or Chrome will not start @@ -194,7 +194,7 @@ export class ProjectConfigIpc extends EventEmitter { }) } - async callSetupNodeEventsWithConfig (testingType: TestingType, config: FullConfig, handlers: IpcHandler[]): Promise { + async callSetupNodeEventsWithConfig (testingType: TestingType, config: FullConfig, handlers: PluginIpcHandler[]): Promise { for (const handler of handlers) { handler(this) } diff --git a/packages/data-context/src/data/ProjectConfigManager.ts b/packages/data-context/src/data/ProjectConfigManager.ts index 248d960876c7..a6d492c2b060 100644 --- a/packages/data-context/src/data/ProjectConfigManager.ts +++ b/packages/data-context/src/data/ProjectConfigManager.ts @@ -1,5 +1,5 @@ import { CypressError, getError } from '@packages/errors' -import { DebugData, IpcHandler, LoadConfigReply, ProjectConfigIpc, SetupNodeEventsReply } from './ProjectConfigIpc' +import { DebugData, PluginIpcHandler, LoadConfigReply, ProjectConfigIpc, SetupNodeEventsReply } from './ProjectConfigIpc' import assert from 'assert' import type { AllModeOptions, FullConfig, TestingType } from '@packages/types' import debugLib from 'debug' @@ -34,7 +34,7 @@ type ProjectConfigManagerOptions = { ctx: DataContext configFile: string | false projectRoot: string - handlers: IpcHandler[] + handlers: PluginIpcHandler[] hasCypressEnvFile: boolean eventRegistrar: EventRegistrar onError: (cypressError: CypressError) => void diff --git a/packages/data-context/src/util/pluginHandlers.ts b/packages/data-context/src/util/pluginHandlers.ts index 1a1ecdb9625e..d0883f79a0d4 100644 --- a/packages/data-context/src/util/pluginHandlers.ts +++ b/packages/data-context/src/util/pluginHandlers.ts @@ -1,12 +1,12 @@ -import type { IpcHandler } from '../data' +import type { PluginIpcHandler } from '../data' -let pluginHandlers: IpcHandler[] = [] +let pluginHandlers: PluginIpcHandler[] = [] export const getServerPluginHandlers = () => { return pluginHandlers } -export const registerServerPluginHandler = (handler: IpcHandler) => { +export const registerServerPluginHandler = (handler: PluginIpcHandler) => { pluginHandlers.push(handler) } diff --git a/packages/server/lib/controllers/spec.ts b/packages/server/lib/controllers/spec.ts index e2f9c25678d2..47748e81ec95 100644 --- a/packages/server/lib/controllers/spec.ts +++ b/packages/server/lib/controllers/spec.ts @@ -4,6 +4,7 @@ import { get as errorsGet } from '../errors' import preprocessor from '../plugins/preprocessor' import type { Cfg } from '../project-base' import type { Request, Response } from 'express' +import type { PreprocessorError } from '@packages/types' const debug = Debug('cypress:server:controllers:spec') @@ -49,7 +50,7 @@ export = { }) .catch({ code: 'ECONNABORTED' }, ignoreECONNABORTED) .catch({ code: 'EPIPE' }, ignoreEPIPE) - .catch((err: any) => { + .catch((err: PreprocessorError) => { debug(`preprocessor error for spec '%s': %s`, spec, err.stack) if (!config.isTextTerminal) { diff --git a/packages/server/lib/plugins/dev-server.js b/packages/server/lib/plugins/dev-server.js deleted file mode 100644 index 1194b86b8b02..000000000000 --- a/packages/server/lib/plugins/dev-server.js +++ /dev/null @@ -1,37 +0,0 @@ -require('../cwd') - -const EE = require('events') -const debug = require('debug')('cypress:ct:dev-server') -const plugins = require('../plugins') - -const baseEmitter = new EE() - -plugins.registerHandler((ipc) => { - baseEmitter.on('dev-server:specs:changed', (specsAndOptions) => { - ipc.send('dev-server:specs:changed', specsAndOptions) - }) - - ipc.on('dev-server:compile:success', ({ specFile } = {}) => { - baseEmitter.emit('dev-server:compile:success', { specFile }) - }) -}) - -// for simpler stubbing from unit tests -const API = { - emitter: baseEmitter, - - start ({ specs, config }) { - return plugins.execute('dev-server:start', { specs, config }) - }, - - updateSpecs (specs, options) { - baseEmitter.emit('dev-server:specs:changed', { specs, options }) - }, - - close () { - debug('close dev-server') - baseEmitter.removeAllListeners() - }, -} - -module.exports = API diff --git a/packages/server/lib/plugins/dev-server.ts b/packages/server/lib/plugins/dev-server.ts new file mode 100644 index 000000000000..e339dd7f124c --- /dev/null +++ b/packages/server/lib/plugins/dev-server.ts @@ -0,0 +1,51 @@ +import '../cwd' + +import { EventEmitter } from 'events' +import debug from 'debug' +import * as plugins from '../plugins' +import type { FullConfig, SpecWithRelativeRoot, PluginIpcHandler } from '@packages/types' + +const debugFn = debug('cypress:ct:dev-server') + +const baseEmitter = new EventEmitter() + +interface SpecsChangedOptions { + neededForJustInTimeCompile?: boolean +} + +plugins.registerHandler((ipc: PluginIpcHandler) => { + baseEmitter.on('dev-server:specs:changed', (specsAndOptions: { specs: SpecWithRelativeRoot[], options?: SpecsChangedOptions }) => { + ipc.send('dev-server:specs:changed', specsAndOptions) + }) + + ipc.on('dev-server:compile:success', ({ specFile }: { specFile?: string } = {}) => { + baseEmitter.emit('dev-server:compile:success', { specFile }) + }) +}) + +// for simpler stubbing from unit tests +interface DevServerAPI { + emitter: EventEmitter + start: (options: { specs: Cypress.Spec[], config: FullConfig }) => Promise + updateSpecs: (specs: SpecWithRelativeRoot[], options?: SpecsChangedOptions) => void + close: () => void +} + +const API: DevServerAPI = { + emitter: baseEmitter, + + start ({ specs, config }: { specs: Cypress.Spec[], config: FullConfig }) { + return plugins.execute('dev-server:start', { specs, config }) + }, + + updateSpecs (specs: SpecWithRelativeRoot[], options?: SpecsChangedOptions) { + baseEmitter.emit('dev-server:specs:changed', { specs, options }) + }, + + close () { + debugFn('close dev-server') + baseEmitter.removeAllListeners() + }, +} + +export default API diff --git a/packages/server/lib/plugins/preprocessor.js b/packages/server/lib/plugins/preprocessor.js deleted file mode 100644 index 6f85100befaf..000000000000 --- a/packages/server/lib/plugins/preprocessor.js +++ /dev/null @@ -1,149 +0,0 @@ -require('../cwd') - -const _ = require('lodash') -const EE = require('events') -const path = require('path') -const debug = require('debug')('cypress:server:preprocessor') -const Promise = require('bluebird') -const appData = require('../util/app_data') -const plugins = require('../plugins') -const { telemetry } = require('@packages/telemetry') - -const errorMessage = function (err = {}) { - return err.stack || err.annotated || err.message || err.toString() -} - -const clientSideError = function (err) { - // eslint-disable-next-line no-console - console.log(err.message) - - err = errorMessage(err) - - return `\ -(function () { - Cypress.action("spec:script:error", { - type: "BUNDLE_ERROR", - error: ${JSON.stringify(err)} - }) -}())\ -` -} - -const baseEmitter = new EE() -let fileObjects = {} -let fileProcessors = {} - -plugins.registerHandler((ipc) => { - ipc.on('preprocessor:rerun', (filePath) => { - debug('ipc preprocessor:rerun event') - - baseEmitter.emit('file:updated', filePath) - }) - - baseEmitter.on('close', (filePath) => { - debug('base emitter plugin close event') - - ipc.send('preprocessor:close', filePath) - }) -}) - -// for simpler stubbing from unit tests -const API = { - errorMessage, - - clientSideError, - - emitter: baseEmitter, - - getFile (filePath, config) { - let fileObject; let fileProcessor - - debug(`getting file ${filePath}`) - filePath = path.resolve(config.projectRoot, filePath) - - debug(`getFile ${filePath}`) - - if (!(fileObject = fileObjects[filePath])) { - // we should be watching the file if we are NOT - // in a text terminal aka cypress run - // TODO: rename this to config.isRunMode - // vs config.isInteractiveMode - const shouldWatch = !config.isTextTerminal || Boolean(process.env.CYPRESS_INTERNAL_FORCE_FILEWATCH) - - const baseFilePath = filePath.replace(config.projectRoot, '') - - fileObject = (fileObjects[filePath] = _.extend(new EE(), { - filePath, - shouldWatch, - outputPath: appData.getBundledFilePath(config.projectRoot, baseFilePath), - })) - - fileObject.on('rerun', () => { - debug('file object rerun event') - - return baseEmitter.emit('file:updated', filePath) - }) - - baseEmitter.once('close', () => { - debug('base emitter native close event') - - return fileObject.emit('close') - }) - } - - if (config.isTextTerminal && (fileProcessor = fileProcessors[filePath])) { - debug('headless and already processed') - - return fileProcessor - } - - const preprocessor = (fileProcessors[filePath] = Promise.try(() => { - const span = telemetry.startSpan({ name: 'file:preprocessor' }) - - return plugins.execute('file:preprocessor', fileObject).then((arg) => { - span?.setAttribute('file', arg) - span?.end() - - return arg - }) - })) - - return preprocessor - }, - - removeFile (filePath, config) { - let fileObject - - filePath = path.resolve(config.projectRoot, filePath) - - if (!fileProcessors[filePath]) { - return - } - - debug(`removeFile ${filePath}`) - - baseEmitter.emit('close', filePath) - - fileObject = fileObjects[filePath] - - if (fileObject) { - fileObject.emit('close') - } - - delete fileObjects[filePath] - - delete fileProcessors[filePath] - }, - - close () { - debug('close preprocessor') - - fileObjects = {} - fileProcessors = {} - baseEmitter.emit('close') - - baseEmitter.removeAllListeners() - }, -} - -module.exports = API diff --git a/packages/server/lib/plugins/preprocessor.ts b/packages/server/lib/plugins/preprocessor.ts new file mode 100644 index 000000000000..51c158a4ff00 --- /dev/null +++ b/packages/server/lib/plugins/preprocessor.ts @@ -0,0 +1,174 @@ +import '../cwd' + +import _ from 'lodash' +import { EventEmitter } from 'events' +import path from 'path' +import debug from 'debug' +import Promise from 'bluebird' +import appData from '../util/app_data' +import * as plugins from '../plugins' +import { telemetry } from '@packages/telemetry' +import type { PluginIpcHandler, PreprocessorError } from '@packages/types' +import type { Cfg } from '../project-base' + +const debugFn = debug('cypress:server:preprocessor') + +// Interface for file objects that extend EventEmitter with additional properties +interface FileObject extends EventEmitter { + filePath: string + shouldWatch: boolean + outputPath: string +} + +const errorMessage = function (err: PreprocessorError = {} as PreprocessorError) { + return err.stack || err.annotated || err.message || err.toString() +} + +const clientSideError = function (err: PreprocessorError) { + // eslint-disable-next-line no-console + console.log(err.message) + + const errorString = errorMessage(err) + + return `\ +(function () { + Cypress.action("spec:script:error", { + type: "BUNDLE_ERROR", + error: ${JSON.stringify(errorString)} + }) +}())\ +` +} + +const baseEmitter = new EventEmitter() +let fileObjects: Record = {} +let fileProcessors: Record> = {} +let processedFiles: Record = {} + +plugins.registerHandler((ipc: PluginIpcHandler) => { + ipc.on('preprocessor:rerun', (filePath: string) => { + debugFn('ipc preprocessor:rerun event') + + baseEmitter.emit('file:updated', filePath) + }) + + baseEmitter.on('close', (filePath: string) => { + debugFn('base emitter plugin close event') + + ipc.send('preprocessor:close', filePath) + }) +}) + +// for simpler stubbing from unit tests +interface PreprocessorAPI { + errorMessage: (err?: PreprocessorError) => string + clientSideError: (err: PreprocessorError) => string + emitter: EventEmitter + getFile: (filePath: string, config: Cfg) => Promise + removeFile: (filePath: string, config: Cfg) => void + close: () => void +} + +const API: PreprocessorAPI = { + errorMessage, + + clientSideError, + + emitter: baseEmitter, + + getFile (filePath: string, config: Cfg) { + let fileObject: FileObject + + debugFn(`getting file ${filePath}`) + filePath = path.resolve(config.projectRoot, filePath) + + debugFn(`getFile ${filePath}`) + + if (!(fileObject = fileObjects[filePath])) { + // we should be watching the file if we are NOT + // in a text terminal aka cypress run + // TODO: rename this to config.isRunMode + // vs config.isInteractiveMode + const shouldWatch = !config.isTextTerminal || Boolean(process.env.CYPRESS_INTERNAL_FORCE_FILEWATCH) + + const baseFilePath = filePath.replace(config.projectRoot, '') + + fileObject = (fileObjects[filePath] = _.extend(new EventEmitter(), { + filePath, + shouldWatch, + outputPath: appData.getBundledFilePath(config.projectRoot, baseFilePath), + })) + + fileObject.on('rerun', () => { + debugFn('file object rerun event') + + return baseEmitter.emit('file:updated', filePath) + }) + + baseEmitter.once('close', () => { + debugFn('base emitter native close event') + + return fileObject.emit('close') + }) + } + + // Check if we already have a processor for this file in headless mode + if (config.isTextTerminal && processedFiles[filePath]) { + debugFn('headless and already processed') + + return fileProcessors[filePath] + } + + const preprocessor = (fileProcessors[filePath] = Promise.try(() => { + const span = telemetry.startSpan({ name: 'file:preprocessor' }) + + return plugins.execute('file:preprocessor', fileObject).then((arg: string) => { + span?.setAttribute('file', arg) + span?.end() + processedFiles[filePath] = true + + return arg + }) + })) + + return preprocessor + }, + + removeFile (filePath: string, config: Cfg) { + let fileObject: FileObject + + filePath = path.resolve(config.projectRoot, filePath) + + if (!fileProcessors[filePath]) { + return + } + + debugFn(`removeFile ${filePath}`) + + baseEmitter.emit('close', filePath) + + fileObject = fileObjects[filePath] + + if (fileObject) { + fileObject.emit('close') + } + + delete fileObjects[filePath] + + delete fileProcessors[filePath] + delete processedFiles[filePath] + }, + + close () { + debugFn('close preprocessor') + + fileObjects = {} + fileProcessors = {} + processedFiles = {} + baseEmitter.emit('close') + + baseEmitter.removeAllListeners() + }, +} + +export default API diff --git a/packages/server/lib/plugins/run_events.js b/packages/server/lib/plugins/run_events.js deleted file mode 100644 index 83e2ae0b5021..000000000000 --- a/packages/server/lib/plugins/run_events.js +++ /dev/null @@ -1,17 +0,0 @@ -const Promise = require('bluebird') - -const errors = require('../errors') -const plugins = require('../plugins') - -module.exports = { - execute: Promise.method((eventName, ...args) => { - if (!plugins.has(eventName)) return - - return plugins.execute(eventName, ...args) - .catch((err) => { - err = err || {} - - errors.throwErr('PLUGINS_RUN_EVENT_ERROR', eventName, err) - }) - }), -} diff --git a/packages/server/lib/plugins/run_events.ts b/packages/server/lib/plugins/run_events.ts new file mode 100644 index 000000000000..64b794e09c05 --- /dev/null +++ b/packages/server/lib/plugins/run_events.ts @@ -0,0 +1,22 @@ +import Promise from 'bluebird' +import * as errors from '../errors' +import * as plugins from '../plugins' + +interface RunEventsAPI { + execute: (eventName: string, ...args: any[]) => Promise +} + +const API: RunEventsAPI = { + execute: Promise.method((eventName: string, ...args: any[]) => { + if (!plugins.has(eventName)) return + + return plugins.execute(eventName, ...args) + .catch((err: any) => { + err = err || {} + + errors.throwErr('PLUGINS_RUN_EVENT_ERROR', eventName, err) + }) + }), +} + +export default API diff --git a/packages/server/lib/plugins/util.js b/packages/server/lib/plugins/util.js deleted file mode 100644 index 2bcb6921d0ee..000000000000 --- a/packages/server/lib/plugins/util.js +++ /dev/null @@ -1,91 +0,0 @@ -const _ = require('lodash') -const EE = require('events') -const Promise = require('bluebird') -const path = require('path') -const UNDEFINED_SERIALIZED = '__cypress_undefined__' - -const buildErrorLocationFromTransformError = (err, projectRoot) => { - const cleanMessage = err.message - // replace the first line with better text (remove potentially misleading word TypeScript for example) - .replace(/^.*\n/g, 'Error compiling file\n') - - // Regex to pull out the error from the message body of a tsx TransformError. It displays the relative path to a file - const transformErrorRegex = /\n(.*?):(\d+):(\d+):/g - const failurePath = transformErrorRegex.exec(cleanMessage) - - return { - compilerErrorLocation: failurePath ? { filePath: path.relative(projectRoot, failurePath[1]), line: Number(failurePath[2]), column: Number(failurePath[3]) } : null, - originalMessage: err.message, - message: cleanMessage, - } -} - -const serializeError = (err) => { - const obj = _.pick(err, - 'name', 'message', 'stack', 'code', 'annotated', 'type', - 'details', 'isCypressErr', 'messageMarkdown', - 'originalError', - // Location of the error when a TransformError or a esbuild error occurs (parse error from ts-node or esbuild) - 'compilerErrorLocation') - - if (obj.originalError) { - obj.originalError = serializeError(obj.originalError) - } - - return obj -} - -module.exports = { - buildErrorLocationFromTransformError, - - serializeError, - - nonNodeRequires () { - return Object.keys(require.cache).filter((c) => !c.includes('/node_modules/')) - }, - - wrapIpc (aProcess) { - const emitter = new EE() - - aProcess.on('message', (message) => { - return emitter.emit(message.event, ...message.args) - }) - - // prevent max listeners warning on ipc - // @see https://github.com/cypress-io/cypress/issues/1305#issuecomment-780895569 - emitter.setMaxListeners(Infinity) - - return { - send (event, ...args) { - if (aProcess.killed || !aProcess.connected) { - return - } - - return aProcess.send({ - event, - args, - }) - }, - - on: emitter.on.bind(emitter), - removeListener: emitter.removeListener.bind(emitter), - } - }, - - wrapChildPromise (ipc, invoke, ids, args = []) { - return Promise.try(() => { - return invoke(ids.eventId, args) - }) - .then((value) => { - // undefined is coerced into null when sent over ipc, but we need - // to differentiate between them for 'task' event - if (value === undefined) { - value = UNDEFINED_SERIALIZED - } - - return ipc.send(`promise:fulfilled:${ids.invocationId}`, null, value) - }).catch((err) => { - return ipc.send(`promise:fulfilled:${ids.invocationId}`, serializeError(err)) - }) - }, -} diff --git a/packages/server/lib/plugins/util.ts b/packages/server/lib/plugins/util.ts new file mode 100644 index 000000000000..6bd6a37007ff --- /dev/null +++ b/packages/server/lib/plugins/util.ts @@ -0,0 +1,190 @@ +import _ from 'lodash' +import { EventEmitter } from 'events' +import Promise from 'bluebird' +import path from 'path' + +const UNDEFINED_SERIALIZED = '__cypress_undefined__' + +interface CompilerErrorLocation { + filePath: string + line: number + column: number +} + +interface ErrorLocationResult { + compilerErrorLocation: CompilerErrorLocation | null + originalMessage: string + message: string +} + +interface SerializedError { + name?: string + message?: string + stack?: string + code?: string + annotated?: string + type?: string + details?: string | Record + isCypressErr?: boolean + messageMarkdown?: string + originalError?: SerializedError + compilerErrorLocation?: CompilerErrorLocation +} + +/** + * Interface for wrapping child processes with EventEmitter functionality + * Used by wrapIpc() to create a communication layer between parent and child processes + * Provides send/receive capabilities while maintaining EventEmitter event handling + */ +interface ProcessIpcWrapper { + send: (event: string, ...args: any[]) => void + on: (event: string, listener: (...args: any[]) => void) => EventEmitter + removeListener: (event: string, listener: (...args: any[]) => void) => EventEmitter +} + +interface InvokeIds { + eventId: string + invocationId: string +} + +/** + * Interface for errors that can occur during file transformation/compilation + * Covers TransformError (tsx) and esbuild errors with location information + */ +interface TransformError extends Error { + name: string + message: string + errors?: Array<{ + location?: { + file: string + line: number + column: number + } + }> +} + +/** + * Interface for any object that can be serialized as an error + * Covers Node.js errors, Cypress errors, and custom error objects + */ +interface ErrorLike { + name?: string + message?: string + stack?: string + code?: string + annotated?: string + type?: string + details?: string | Record + isCypressErr?: boolean + messageMarkdown?: string + originalError?: ErrorLike + compilerErrorLocation?: CompilerErrorLocation + [key: string]: any // Allow additional properties +} + +/** + * Interface for process-like objects that can communicate via IPC + * Covers Node.js process, ChildProcess objects, and test mocks + */ +interface ProcessLike { + on(event: 'message', listener: (message: { event: string, args: any[] }) => void): void + killed?: boolean + connected?: boolean + send(message: { event: string, args: any[] }): boolean | void +} + +const buildErrorLocationFromTransformError = (err: TransformError, projectRoot: string): ErrorLocationResult => { + const cleanMessage = err.message + // replace the first line with better text (remove potentially misleading word TypeScript for example) + .replace(/^.*\n/g, 'Error compiling file\n') + + // Regex to pull out the error from the message body of a tsx TransformError. It displays the relative path to a file + const transformErrorRegex = /\n(.*?):(\d+):(\d+):/g + const failurePath = transformErrorRegex.exec(cleanMessage) + + return { + compilerErrorLocation: failurePath ? { filePath: path.relative(projectRoot, failurePath[1]), line: Number(failurePath[2]), column: Number(failurePath[3]) } : null, + originalMessage: err.message, + message: cleanMessage, + } +} + +const serializeError = (err: ErrorLike): SerializedError => { + const obj = _.pick(err, + 'name', 'message', 'stack', 'code', 'annotated', 'type', + 'details', 'isCypressErr', 'messageMarkdown', + 'originalError', + // Location of the error when a TransformError or a esbuild error occurs (parse error from ts-node or esbuild) + 'compilerErrorLocation') + + if (obj.originalError) { + obj.originalError = serializeError(obj.originalError) + } + + return obj +} + +interface UtilAPI { + buildErrorLocationFromTransformError: (err: TransformError, projectRoot: string) => ErrorLocationResult + serializeError: (err: ErrorLike) => SerializedError + nonNodeRequires: () => string[] + wrapIpc: (aProcess: ProcessLike) => ProcessIpcWrapper + wrapChildPromise: (ipc: ProcessIpcWrapper, invoke: (eventId: string, args: any[]) => any, ids: InvokeIds, args?: any[]) => Promise +} + +const API: UtilAPI = { + buildErrorLocationFromTransformError, + + serializeError, + + nonNodeRequires () { + return Object.keys(require.cache).filter((c) => !c.includes('/node_modules/')) + }, + + wrapIpc (aProcess: ProcessLike) { + const emitter = new EventEmitter() + + aProcess.on('message', (message: { event: string, args: any[] }) => { + return emitter.emit(message.event, ...message.args) + }) + + // prevent max listeners warning on ipc + // @see https://github.com/cypress-io/cypress/issues/1305#issuecomment-780895569 + emitter.setMaxListeners(Infinity) + + return { + send (event: string, ...args: any[]) { + if (aProcess.killed || !aProcess.connected) { + return + } + + return aProcess.send({ + event, + args, + }) + }, + + on: emitter.on.bind(emitter), + removeListener: emitter.removeListener.bind(emitter), + } + }, + + wrapChildPromise (ipc: ProcessIpcWrapper, invoke: (eventId: string, args: any[]) => any, ids: InvokeIds, args: any[] = []) { + return Promise.try(() => { + return invoke(ids.eventId, args) + }) + .then((value) => { + // undefined is coerced into null when sent over ipc, but we need + // to differentiate between them for 'task' event + if (value === undefined) { + value = UNDEFINED_SERIALIZED + } + + return ipc.send(`promise:fulfilled:${ids.invocationId}`, null, value) + }).catch((err) => { + return ipc.send(`promise:fulfilled:${ids.invocationId}`, serializeError(err)) + }) + }, +} + +export default API diff --git a/packages/types/src/server.ts b/packages/types/src/server.ts index 3bcdbb39d0db..f7ba0343a082 100644 --- a/packages/types/src/server.ts +++ b/packages/types/src/server.ts @@ -5,6 +5,28 @@ import type { RunModeVideoApi } from './video' import type { ProtocolManagerShape } from './protocol' import type Protocol from 'devtools-protocol' +/** + * Interface for errors that can occur during file preprocessing + * These are typically compilation errors, file system errors, or plugin execution errors + * Used across preprocessor packages and error handling systems + */ +export interface PreprocessorError extends Error { + stack?: string + annotated?: string + message: string + filePath?: string + originalStack?: string +} + +/** + * Interface for IPC handlers that can send and receive messages + * Used by plugin handlers to communicate with the main process + */ +export interface PluginIpcHandler { + send: (event: string, ...args: any[]) => boolean + on: (event: string, listener: (...args: any[]) => void) => this +} + export type OpenProjectLaunchOpts = { projectRoot: string shouldLaunchNewTab: boolean From b51f17aacd65666917420b351a41ef0084728300 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 22 Aug 2025 10:13:02 -0400 Subject: [PATCH 02/12] consolidate error types --- packages/server/lib/plugins/util.ts | 68 +---------------------------- packages/types/src/server.ts | 37 ++++++++++++++++ 2 files changed, 39 insertions(+), 66 deletions(-) diff --git a/packages/server/lib/plugins/util.ts b/packages/server/lib/plugins/util.ts index 6bd6a37007ff..36cafa9c969b 100644 --- a/packages/server/lib/plugins/util.ts +++ b/packages/server/lib/plugins/util.ts @@ -2,86 +2,22 @@ import _ from 'lodash' import { EventEmitter } from 'events' import Promise from 'bluebird' import path from 'path' +import type { CompilerErrorLocation, ProcessIpcWrapper, TransformError } from '@packages/types' +import type { SerializedError, ErrorLike } from '@packages/errors' const UNDEFINED_SERIALIZED = '__cypress_undefined__' -interface CompilerErrorLocation { - filePath: string - line: number - column: number -} - interface ErrorLocationResult { compilerErrorLocation: CompilerErrorLocation | null originalMessage: string message: string } -interface SerializedError { - name?: string - message?: string - stack?: string - code?: string - annotated?: string - type?: string - details?: string | Record - isCypressErr?: boolean - messageMarkdown?: string - originalError?: SerializedError - compilerErrorLocation?: CompilerErrorLocation -} - -/** - * Interface for wrapping child processes with EventEmitter functionality - * Used by wrapIpc() to create a communication layer between parent and child processes - * Provides send/receive capabilities while maintaining EventEmitter event handling - */ -interface ProcessIpcWrapper { - send: (event: string, ...args: any[]) => void - on: (event: string, listener: (...args: any[]) => void) => EventEmitter - removeListener: (event: string, listener: (...args: any[]) => void) => EventEmitter -} - interface InvokeIds { eventId: string invocationId: string } -/** - * Interface for errors that can occur during file transformation/compilation - * Covers TransformError (tsx) and esbuild errors with location information - */ -interface TransformError extends Error { - name: string - message: string - errors?: Array<{ - location?: { - file: string - line: number - column: number - } - }> -} - -/** - * Interface for any object that can be serialized as an error - * Covers Node.js errors, Cypress errors, and custom error objects - */ -interface ErrorLike { - name?: string - message?: string - stack?: string - code?: string - annotated?: string - type?: string - details?: string | Record - isCypressErr?: boolean - messageMarkdown?: string - originalError?: ErrorLike - compilerErrorLocation?: CompilerErrorLocation - [key: string]: any // Allow additional properties -} - /** * Interface for process-like objects that can communicate via IPC * Covers Node.js process, ChildProcess objects, and test mocks diff --git a/packages/types/src/server.ts b/packages/types/src/server.ts index f7ba0343a082..f1340a690885 100644 --- a/packages/types/src/server.ts +++ b/packages/types/src/server.ts @@ -5,6 +5,43 @@ import type { RunModeVideoApi } from './video' import type { ProtocolManagerShape } from './protocol' import type Protocol from 'devtools-protocol' +/** + * Interface for compiler error location information + * Used across error handling systems to provide file, line, and column details + */ +export interface CompilerErrorLocation { + filePath: string + line: number + column: number +} + +/** + * Interface for wrapping child processes with EventEmitter functionality + * Used by wrapIpc() to create a communication layer between parent and child processes + * Provides send/receive capabilities while maintaining EventEmitter event handling + */ +export interface ProcessIpcWrapper { + send: (event: string, ...args: any[]) => void + on: (event: string, listener: (...args: any[]) => void) => any + removeListener: (event: string, listener: (...args: any[]) => void) => any +} + +/** + * Interface for errors that can occur during file transformation/compilation + * Covers TransformError (tsx) and esbuild errors with location information + */ +export interface TransformError extends Error { + name: string + message: string + errors?: Array<{ + location?: { + file: string + line: number + column: number + } + }> +} + /** * Interface for errors that can occur during file preprocessing * These are typically compilation errors, file system errors, or plugin execution errors From f45d895e692f1955acdf5c3cda3cdee14e42f67c Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 22 Aug 2025 10:22:20 -0400 Subject: [PATCH 03/12] update anys to unknowns where we don't know the args --- packages/server/lib/plugins/dev-server.ts | 13 +++++++++++-- packages/server/lib/plugins/preprocessor.ts | 9 ++++++--- packages/server/lib/plugins/run_events.ts | 11 ++++++----- packages/server/lib/plugins/util.ts | 19 +++++++++++-------- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/packages/server/lib/plugins/dev-server.ts b/packages/server/lib/plugins/dev-server.ts index e339dd7f124c..ef98e2713db6 100644 --- a/packages/server/lib/plugins/dev-server.ts +++ b/packages/server/lib/plugins/dev-server.ts @@ -13,12 +13,21 @@ interface SpecsChangedOptions { neededForJustInTimeCompile?: boolean } +interface SpecsChangedData { + specs: SpecWithRelativeRoot[] + options?: SpecsChangedOptions +} + +interface CompileSuccessData { + specFile?: string +} + plugins.registerHandler((ipc: PluginIpcHandler) => { - baseEmitter.on('dev-server:specs:changed', (specsAndOptions: { specs: SpecWithRelativeRoot[], options?: SpecsChangedOptions }) => { + baseEmitter.on('dev-server:specs:changed', (specsAndOptions: SpecsChangedData) => { ipc.send('dev-server:specs:changed', specsAndOptions) }) - ipc.on('dev-server:compile:success', ({ specFile }: { specFile?: string } = {}) => { + ipc.on('dev-server:compile:success', ({ specFile }: CompileSuccessData = {}) => { baseEmitter.emit('dev-server:compile:success', { specFile }) }) }) diff --git a/packages/server/lib/plugins/preprocessor.ts b/packages/server/lib/plugins/preprocessor.ts index 51c158a4ff00..233712aa2f3e 100644 --- a/packages/server/lib/plugins/preprocessor.ts +++ b/packages/server/lib/plugins/preprocessor.ts @@ -18,13 +18,17 @@ interface FileObject extends EventEmitter { filePath: string shouldWatch: boolean outputPath: string + on(event: 'rerun', listener: () => void): this + on(event: 'close', listener: () => void): this + emit(event: 'rerun'): boolean + emit(event: 'close'): boolean } -const errorMessage = function (err: PreprocessorError = {} as PreprocessorError) { +const errorMessage = function (err: PreprocessorError = {} as PreprocessorError): string { return err.stack || err.annotated || err.message || err.toString() } -const clientSideError = function (err: PreprocessorError) { +const clientSideError = function (err: PreprocessorError): string { // eslint-disable-next-line no-console console.log(err.message) @@ -54,7 +58,6 @@ plugins.registerHandler((ipc: PluginIpcHandler) => { baseEmitter.on('close', (filePath: string) => { debugFn('base emitter plugin close event') - ipc.send('preprocessor:close', filePath) }) }) diff --git a/packages/server/lib/plugins/run_events.ts b/packages/server/lib/plugins/run_events.ts index 64b794e09c05..44d1328bd6d5 100644 --- a/packages/server/lib/plugins/run_events.ts +++ b/packages/server/lib/plugins/run_events.ts @@ -3,18 +3,19 @@ import * as errors from '../errors' import * as plugins from '../plugins' interface RunEventsAPI { - execute: (eventName: string, ...args: any[]) => Promise + execute: (eventName: string, ...args: unknown[]) => Promise } const API: RunEventsAPI = { - execute: Promise.method((eventName: string, ...args: any[]) => { + execute: Promise.method((eventName: string, ...args: unknown[]) => { if (!plugins.has(eventName)) return return plugins.execute(eventName, ...args) - .catch((err: any) => { - err = err || {} + .catch((err: unknown) => { + // Ensure we have a valid error object for throwErr + const error = err instanceof Error ? err : new Error(String(err)) - errors.throwErr('PLUGINS_RUN_EVENT_ERROR', eventName, err) + errors.throwErr('PLUGINS_RUN_EVENT_ERROR', eventName, error) }) }), } diff --git a/packages/server/lib/plugins/util.ts b/packages/server/lib/plugins/util.ts index 36cafa9c969b..f4c6378ed388 100644 --- a/packages/server/lib/plugins/util.ts +++ b/packages/server/lib/plugins/util.ts @@ -23,10 +23,10 @@ interface InvokeIds { * Covers Node.js process, ChildProcess objects, and test mocks */ interface ProcessLike { - on(event: 'message', listener: (message: { event: string, args: any[] }) => void): void + on(event: 'message', listener: (message: { event: string, args: unknown[] }) => void): void killed?: boolean connected?: boolean - send(message: { event: string, args: any[] }): boolean | void + send(message: { event: string, args: unknown[] }): boolean | void } const buildErrorLocationFromTransformError = (err: TransformError, projectRoot: string): ErrorLocationResult => { @@ -65,7 +65,7 @@ interface UtilAPI { serializeError: (err: ErrorLike) => SerializedError nonNodeRequires: () => string[] wrapIpc: (aProcess: ProcessLike) => ProcessIpcWrapper - wrapChildPromise: (ipc: ProcessIpcWrapper, invoke: (eventId: string, args: any[]) => any, ids: InvokeIds, args?: any[]) => Promise + wrapChildPromise: (ipc: ProcessIpcWrapper, invoke: (eventId: string, args: unknown[]) => unknown, ids: InvokeIds, args?: unknown[]) => Promise } const API: UtilAPI = { @@ -80,7 +80,7 @@ const API: UtilAPI = { wrapIpc (aProcess: ProcessLike) { const emitter = new EventEmitter() - aProcess.on('message', (message: { event: string, args: any[] }) => { + aProcess.on('message', (message: { event: string, args: unknown[] }) => { return emitter.emit(message.event, ...message.args) }) @@ -89,7 +89,7 @@ const API: UtilAPI = { emitter.setMaxListeners(Infinity) return { - send (event: string, ...args: any[]) { + send (event: string, ...args: unknown[]) { if (aProcess.killed || !aProcess.connected) { return } @@ -105,7 +105,7 @@ const API: UtilAPI = { } }, - wrapChildPromise (ipc: ProcessIpcWrapper, invoke: (eventId: string, args: any[]) => any, ids: InvokeIds, args: any[] = []) { + wrapChildPromise (ipc: ProcessIpcWrapper, invoke: (eventId: string, args: unknown[]) => unknown, ids: InvokeIds, args: unknown[] = []) { return Promise.try(() => { return invoke(ids.eventId, args) }) @@ -117,8 +117,11 @@ const API: UtilAPI = { } return ipc.send(`promise:fulfilled:${ids.invocationId}`, null, value) - }).catch((err) => { - return ipc.send(`promise:fulfilled:${ids.invocationId}`, serializeError(err)) + }).catch((err: unknown) => { + // Ensure we have a valid error object for serializeError + const error = err instanceof Error ? err : new Error(String(err)) + + return ipc.send(`promise:fulfilled:${ids.invocationId}`, serializeError(error as ErrorLike)) }) }, } From 77f36e179cc86e9f5574f54a14abfc11a5c239d2 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Fri, 22 Aug 2025 12:37:05 -0400 Subject: [PATCH 04/12] add module esports --- packages/server/lib/plugins/dev-server.ts | 2 ++ packages/server/lib/plugins/preprocessor.ts | 2 ++ packages/server/lib/plugins/run_events.ts | 6 ++++-- packages/server/lib/plugins/util.ts | 2 ++ 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/server/lib/plugins/dev-server.ts b/packages/server/lib/plugins/dev-server.ts index ef98e2713db6..a4cfc3eaeded 100644 --- a/packages/server/lib/plugins/dev-server.ts +++ b/packages/server/lib/plugins/dev-server.ts @@ -58,3 +58,5 @@ const API: DevServerAPI = { } export default API + +module.exports = API diff --git a/packages/server/lib/plugins/preprocessor.ts b/packages/server/lib/plugins/preprocessor.ts index 233712aa2f3e..8e724cc29afb 100644 --- a/packages/server/lib/plugins/preprocessor.ts +++ b/packages/server/lib/plugins/preprocessor.ts @@ -175,3 +175,5 @@ const API: PreprocessorAPI = { } export default API + +module.exports = API diff --git a/packages/server/lib/plugins/run_events.ts b/packages/server/lib/plugins/run_events.ts index 44d1328bd6d5..60bca1f3df0c 100644 --- a/packages/server/lib/plugins/run_events.ts +++ b/packages/server/lib/plugins/run_events.ts @@ -12,8 +12,8 @@ const API: RunEventsAPI = { return plugins.execute(eventName, ...args) .catch((err: unknown) => { - // Ensure we have a valid error object for throwErr - const error = err instanceof Error ? err : new Error(String(err)) + // Match original JavaScript behavior: err = err || {} + const error = (err || {}) as any errors.throwErr('PLUGINS_RUN_EVENT_ERROR', eventName, error) }) @@ -21,3 +21,5 @@ const API: RunEventsAPI = { } export default API + +module.exports = API diff --git a/packages/server/lib/plugins/util.ts b/packages/server/lib/plugins/util.ts index f4c6378ed388..cc702815247b 100644 --- a/packages/server/lib/plugins/util.ts +++ b/packages/server/lib/plugins/util.ts @@ -127,3 +127,5 @@ const API: UtilAPI = { } export default API + +module.exports = API From dc933313d54c2b95df2641db77403425013a5428 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Mon, 25 Aug 2025 10:01:42 -0400 Subject: [PATCH 05/12] forgot to push this I guess --- packages/server/lib/plugins/preprocessor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/lib/plugins/preprocessor.ts b/packages/server/lib/plugins/preprocessor.ts index 8e724cc29afb..cbcdfaf4d255 100644 --- a/packages/server/lib/plugins/preprocessor.ts +++ b/packages/server/lib/plugins/preprocessor.ts @@ -116,7 +116,7 @@ const API: PreprocessorAPI = { } // Check if we already have a processor for this file in headless mode - if (config.isTextTerminal && processedFiles[filePath]) { + if (config.isTextTerminal && fileProcessors[filePath]) { debugFn('headless and already processed') return fileProcessors[filePath] From 086e3c8dc33a4419c68ca81f4fdea8607e6e7021 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Mon, 25 Aug 2025 11:23:24 -0400 Subject: [PATCH 06/12] Update so this isn't always true --- packages/server/lib/plugins/preprocessor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/lib/plugins/preprocessor.ts b/packages/server/lib/plugins/preprocessor.ts index cbcdfaf4d255..62883d26a77d 100644 --- a/packages/server/lib/plugins/preprocessor.ts +++ b/packages/server/lib/plugins/preprocessor.ts @@ -116,7 +116,7 @@ const API: PreprocessorAPI = { } // Check if we already have a processor for this file in headless mode - if (config.isTextTerminal && fileProcessors[filePath]) { + if (config.isTextTerminal && filePath in fileProcessors) { debugFn('headless and already processed') return fileProcessors[filePath] @@ -142,7 +142,7 @@ const API: PreprocessorAPI = { filePath = path.resolve(config.projectRoot, filePath) - if (!fileProcessors[filePath]) { + if (!(filePath in fileProcessors)) { return } From b89ab397c1e9395963646e1647d5234fe3eb8d06 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Mon, 25 Aug 2025 13:35:48 -0400 Subject: [PATCH 07/12] add some utils to a JS file --- packages/server/lib/plugins/util.js | 105 ++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 packages/server/lib/plugins/util.js diff --git a/packages/server/lib/plugins/util.js b/packages/server/lib/plugins/util.js new file mode 100644 index 000000000000..ad4c9a4842d1 --- /dev/null +++ b/packages/server/lib/plugins/util.js @@ -0,0 +1,105 @@ +const { EventEmitter } = require('events') + +// Minimal util functions needed by child processes +const wrapIpc = (aProcess) => { + const emitter = new EventEmitter() + + aProcess.on('message', (message) => { + return emitter.emit(message.event, ...message.args) + }) + + // prevent max listeners warning on ipc + emitter.setMaxListeners(Infinity) + + return { + send (event, ...args) { + if (aProcess.killed || !aProcess.connected) { + return + } + + return aProcess.send({ + event, + args, + }) + }, + + on: emitter.on.bind(emitter), + removeListener: emitter.removeListener.bind(emitter), + } +} + +const wrapChildPromise = (ipc, invoke, ids, args = []) => { + return require('bluebird').try(() => { + return invoke(ids.eventId, args) + }) + .then((value) => { + // undefined is coerced into null when sent over ipc, but we need + // to differentiate between them for 'task' event + if (value === undefined) { + value = '__cypress_undefined__' + } + + return ipc.send(`promise:fulfilled:${ids.invocationId}`, null, value) + }).catch((err) => { + return ipc.send(`promise:fulfilled:${ids.invocationId}`, serializeError(err)) + }) +} + +const serializeError = (err) => { + const obj = {} + + if (err.name) obj.name = err.name + + if (err.message) obj.message = err.message + + if (err.stack) obj.stack = err.stack + + if (err.code) obj.code = err.code + + if (err.annotated) obj.annotated = err.annotated + + if (err.type) obj.type = err.type + + if (err.details) obj.details = err.details + + if (err.isCypressErr) obj.isCypressErr = err.isCypressErr + + if (err.messageMarkdown) obj.messageMarkdown = err.messageMarkdown + + if (err.originalError) obj.originalError = serializeError(err.originalError) + + if (err.compilerErrorLocation) obj.compilerErrorLocation = err.compilerErrorLocation + + return obj +} + +const nonNodeRequires = () => { + return Object.keys(require.cache).filter((c) => !c.includes('/node_modules/')) +} + +const buildErrorLocationFromTransformError = (err, projectRoot) => { + const cleanMessage = err.message + .replace(/^.*\n/g, 'Error compiling file\n') + + // Regex to pull out the error from the message body of a tsx TransformError + const transformErrorRegex = /\n(.*?):(\d+):(\d+):/g + const failurePath = transformErrorRegex.exec(cleanMessage) + + return { + compilerErrorLocation: failurePath ? { + filePath: require('path').relative(projectRoot, failurePath[1]), + line: Number(failurePath[2]), + column: Number(failurePath[3]), + } : null, + originalMessage: err.message, + message: cleanMessage, + } +} + +module.exports = { + wrapIpc, + wrapChildPromise, + serializeError, + nonNodeRequires, + buildErrorLocationFromTransformError, +} From 32bf1a0a99d2a70bba22ce0e7c767085c961d5cc Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Mon, 25 Aug 2025 13:39:47 -0400 Subject: [PATCH 08/12] remove duplicate ts file --- packages/server/lib/plugins/util.js | 156 +++++++++++++--------------- packages/server/lib/plugins/util.ts | 131 ----------------------- 2 files changed, 71 insertions(+), 216 deletions(-) delete mode 100644 packages/server/lib/plugins/util.ts diff --git a/packages/server/lib/plugins/util.js b/packages/server/lib/plugins/util.js index ad4c9a4842d1..2bcb6921d0ee 100644 --- a/packages/server/lib/plugins/util.js +++ b/packages/server/lib/plugins/util.js @@ -1,105 +1,91 @@ -const { EventEmitter } = require('events') +const _ = require('lodash') +const EE = require('events') +const Promise = require('bluebird') +const path = require('path') +const UNDEFINED_SERIALIZED = '__cypress_undefined__' -// Minimal util functions needed by child processes -const wrapIpc = (aProcess) => { - const emitter = new EventEmitter() - - aProcess.on('message', (message) => { - return emitter.emit(message.event, ...message.args) - }) +const buildErrorLocationFromTransformError = (err, projectRoot) => { + const cleanMessage = err.message + // replace the first line with better text (remove potentially misleading word TypeScript for example) + .replace(/^.*\n/g, 'Error compiling file\n') - // prevent max listeners warning on ipc - emitter.setMaxListeners(Infinity) + // Regex to pull out the error from the message body of a tsx TransformError. It displays the relative path to a file + const transformErrorRegex = /\n(.*?):(\d+):(\d+):/g + const failurePath = transformErrorRegex.exec(cleanMessage) return { - send (event, ...args) { - if (aProcess.killed || !aProcess.connected) { - return - } - - return aProcess.send({ - event, - args, - }) - }, - - on: emitter.on.bind(emitter), - removeListener: emitter.removeListener.bind(emitter), + compilerErrorLocation: failurePath ? { filePath: path.relative(projectRoot, failurePath[1]), line: Number(failurePath[2]), column: Number(failurePath[3]) } : null, + originalMessage: err.message, + message: cleanMessage, } } -const wrapChildPromise = (ipc, invoke, ids, args = []) => { - return require('bluebird').try(() => { - return invoke(ids.eventId, args) - }) - .then((value) => { - // undefined is coerced into null when sent over ipc, but we need - // to differentiate between them for 'task' event - if (value === undefined) { - value = '__cypress_undefined__' - } - - return ipc.send(`promise:fulfilled:${ids.invocationId}`, null, value) - }).catch((err) => { - return ipc.send(`promise:fulfilled:${ids.invocationId}`, serializeError(err)) - }) -} - const serializeError = (err) => { - const obj = {} - - if (err.name) obj.name = err.name - - if (err.message) obj.message = err.message - - if (err.stack) obj.stack = err.stack - - if (err.code) obj.code = err.code - - if (err.annotated) obj.annotated = err.annotated - - if (err.type) obj.type = err.type + const obj = _.pick(err, + 'name', 'message', 'stack', 'code', 'annotated', 'type', + 'details', 'isCypressErr', 'messageMarkdown', + 'originalError', + // Location of the error when a TransformError or a esbuild error occurs (parse error from ts-node or esbuild) + 'compilerErrorLocation') + + if (obj.originalError) { + obj.originalError = serializeError(obj.originalError) + } - if (err.details) obj.details = err.details + return obj +} - if (err.isCypressErr) obj.isCypressErr = err.isCypressErr +module.exports = { + buildErrorLocationFromTransformError, - if (err.messageMarkdown) obj.messageMarkdown = err.messageMarkdown + serializeError, - if (err.originalError) obj.originalError = serializeError(err.originalError) + nonNodeRequires () { + return Object.keys(require.cache).filter((c) => !c.includes('/node_modules/')) + }, - if (err.compilerErrorLocation) obj.compilerErrorLocation = err.compilerErrorLocation + wrapIpc (aProcess) { + const emitter = new EE() - return obj -} + aProcess.on('message', (message) => { + return emitter.emit(message.event, ...message.args) + }) -const nonNodeRequires = () => { - return Object.keys(require.cache).filter((c) => !c.includes('/node_modules/')) -} + // prevent max listeners warning on ipc + // @see https://github.com/cypress-io/cypress/issues/1305#issuecomment-780895569 + emitter.setMaxListeners(Infinity) -const buildErrorLocationFromTransformError = (err, projectRoot) => { - const cleanMessage = err.message - .replace(/^.*\n/g, 'Error compiling file\n') + return { + send (event, ...args) { + if (aProcess.killed || !aProcess.connected) { + return + } - // Regex to pull out the error from the message body of a tsx TransformError - const transformErrorRegex = /\n(.*?):(\d+):(\d+):/g - const failurePath = transformErrorRegex.exec(cleanMessage) + return aProcess.send({ + event, + args, + }) + }, - return { - compilerErrorLocation: failurePath ? { - filePath: require('path').relative(projectRoot, failurePath[1]), - line: Number(failurePath[2]), - column: Number(failurePath[3]), - } : null, - originalMessage: err.message, - message: cleanMessage, - } -} + on: emitter.on.bind(emitter), + removeListener: emitter.removeListener.bind(emitter), + } + }, + + wrapChildPromise (ipc, invoke, ids, args = []) { + return Promise.try(() => { + return invoke(ids.eventId, args) + }) + .then((value) => { + // undefined is coerced into null when sent over ipc, but we need + // to differentiate between them for 'task' event + if (value === undefined) { + value = UNDEFINED_SERIALIZED + } -module.exports = { - wrapIpc, - wrapChildPromise, - serializeError, - nonNodeRequires, - buildErrorLocationFromTransformError, + return ipc.send(`promise:fulfilled:${ids.invocationId}`, null, value) + }).catch((err) => { + return ipc.send(`promise:fulfilled:${ids.invocationId}`, serializeError(err)) + }) + }, } diff --git a/packages/server/lib/plugins/util.ts b/packages/server/lib/plugins/util.ts deleted file mode 100644 index cc702815247b..000000000000 --- a/packages/server/lib/plugins/util.ts +++ /dev/null @@ -1,131 +0,0 @@ -import _ from 'lodash' -import { EventEmitter } from 'events' -import Promise from 'bluebird' -import path from 'path' -import type { CompilerErrorLocation, ProcessIpcWrapper, TransformError } from '@packages/types' -import type { SerializedError, ErrorLike } from '@packages/errors' - -const UNDEFINED_SERIALIZED = '__cypress_undefined__' - -interface ErrorLocationResult { - compilerErrorLocation: CompilerErrorLocation | null - originalMessage: string - message: string -} - -interface InvokeIds { - eventId: string - invocationId: string -} - -/** - * Interface for process-like objects that can communicate via IPC - * Covers Node.js process, ChildProcess objects, and test mocks - */ -interface ProcessLike { - on(event: 'message', listener: (message: { event: string, args: unknown[] }) => void): void - killed?: boolean - connected?: boolean - send(message: { event: string, args: unknown[] }): boolean | void -} - -const buildErrorLocationFromTransformError = (err: TransformError, projectRoot: string): ErrorLocationResult => { - const cleanMessage = err.message - // replace the first line with better text (remove potentially misleading word TypeScript for example) - .replace(/^.*\n/g, 'Error compiling file\n') - - // Regex to pull out the error from the message body of a tsx TransformError. It displays the relative path to a file - const transformErrorRegex = /\n(.*?):(\d+):(\d+):/g - const failurePath = transformErrorRegex.exec(cleanMessage) - - return { - compilerErrorLocation: failurePath ? { filePath: path.relative(projectRoot, failurePath[1]), line: Number(failurePath[2]), column: Number(failurePath[3]) } : null, - originalMessage: err.message, - message: cleanMessage, - } -} - -const serializeError = (err: ErrorLike): SerializedError => { - const obj = _.pick(err, - 'name', 'message', 'stack', 'code', 'annotated', 'type', - 'details', 'isCypressErr', 'messageMarkdown', - 'originalError', - // Location of the error when a TransformError or a esbuild error occurs (parse error from ts-node or esbuild) - 'compilerErrorLocation') - - if (obj.originalError) { - obj.originalError = serializeError(obj.originalError) - } - - return obj -} - -interface UtilAPI { - buildErrorLocationFromTransformError: (err: TransformError, projectRoot: string) => ErrorLocationResult - serializeError: (err: ErrorLike) => SerializedError - nonNodeRequires: () => string[] - wrapIpc: (aProcess: ProcessLike) => ProcessIpcWrapper - wrapChildPromise: (ipc: ProcessIpcWrapper, invoke: (eventId: string, args: unknown[]) => unknown, ids: InvokeIds, args?: unknown[]) => Promise -} - -const API: UtilAPI = { - buildErrorLocationFromTransformError, - - serializeError, - - nonNodeRequires () { - return Object.keys(require.cache).filter((c) => !c.includes('/node_modules/')) - }, - - wrapIpc (aProcess: ProcessLike) { - const emitter = new EventEmitter() - - aProcess.on('message', (message: { event: string, args: unknown[] }) => { - return emitter.emit(message.event, ...message.args) - }) - - // prevent max listeners warning on ipc - // @see https://github.com/cypress-io/cypress/issues/1305#issuecomment-780895569 - emitter.setMaxListeners(Infinity) - - return { - send (event: string, ...args: unknown[]) { - if (aProcess.killed || !aProcess.connected) { - return - } - - return aProcess.send({ - event, - args, - }) - }, - - on: emitter.on.bind(emitter), - removeListener: emitter.removeListener.bind(emitter), - } - }, - - wrapChildPromise (ipc: ProcessIpcWrapper, invoke: (eventId: string, args: unknown[]) => unknown, ids: InvokeIds, args: unknown[] = []) { - return Promise.try(() => { - return invoke(ids.eventId, args) - }) - .then((value) => { - // undefined is coerced into null when sent over ipc, but we need - // to differentiate between them for 'task' event - if (value === undefined) { - value = UNDEFINED_SERIALIZED - } - - return ipc.send(`promise:fulfilled:${ids.invocationId}`, null, value) - }).catch((err: unknown) => { - // Ensure we have a valid error object for serializeError - const error = err instanceof Error ? err : new Error(String(err)) - - return ipc.send(`promise:fulfilled:${ids.invocationId}`, serializeError(error as ErrorLike)) - }) - }, -} - -export default API - -module.exports = API From 4d81beaa9ae5243c647185f0dffcc691a835d9e5 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Mon, 25 Aug 2025 15:06:24 -0400 Subject: [PATCH 09/12] remove dead code --- packages/server/lib/plugins/preprocessor.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/server/lib/plugins/preprocessor.ts b/packages/server/lib/plugins/preprocessor.ts index 62883d26a77d..7491c80d0209 100644 --- a/packages/server/lib/plugins/preprocessor.ts +++ b/packages/server/lib/plugins/preprocessor.ts @@ -47,7 +47,6 @@ const clientSideError = function (err: PreprocessorError): string { const baseEmitter = new EventEmitter() let fileObjects: Record = {} let fileProcessors: Record> = {} -let processedFiles: Record = {} plugins.registerHandler((ipc: PluginIpcHandler) => { ipc.on('preprocessor:rerun', (filePath: string) => { @@ -128,7 +127,6 @@ const API: PreprocessorAPI = { return plugins.execute('file:preprocessor', fileObject).then((arg: string) => { span?.setAttribute('file', arg) span?.end() - processedFiles[filePath] = true return arg }) @@ -159,7 +157,6 @@ const API: PreprocessorAPI = { delete fileObjects[filePath] delete fileProcessors[filePath] - delete processedFiles[filePath] }, close () { @@ -167,7 +164,6 @@ const API: PreprocessorAPI = { fileObjects = {} fileProcessors = {} - processedFiles = {} baseEmitter.emit('close') baseEmitter.removeAllListeners() From c61416bc4b89c751f74a7b42130083f28fb59dd5 Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Tue, 2 Sep 2025 10:25:41 -0400 Subject: [PATCH 10/12] remove module.exports = API --- packages/server/lib/plugins/dev-server.ts | 2 -- packages/server/lib/plugins/preprocessor.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/server/lib/plugins/dev-server.ts b/packages/server/lib/plugins/dev-server.ts index a4cfc3eaeded..ef98e2713db6 100644 --- a/packages/server/lib/plugins/dev-server.ts +++ b/packages/server/lib/plugins/dev-server.ts @@ -58,5 +58,3 @@ const API: DevServerAPI = { } export default API - -module.exports = API diff --git a/packages/server/lib/plugins/preprocessor.ts b/packages/server/lib/plugins/preprocessor.ts index 7491c80d0209..3336d82787c0 100644 --- a/packages/server/lib/plugins/preprocessor.ts +++ b/packages/server/lib/plugins/preprocessor.ts @@ -171,5 +171,3 @@ const API: PreprocessorAPI = { } export default API - -module.exports = API From 09cb9b1587579b71bef5e7f60675519ffb81803a Mon Sep 17 00:00:00 2001 From: Bill Glesias Date: Tue, 2 Sep 2025 12:06:35 -0400 Subject: [PATCH 11/12] chore: fix exports of run events and adapt CJS tests importing TS modules to use the default export --- packages/server/lib/plugins/run_events.ts | 2 -- packages/server/test/unit/open_project_spec.js | 4 ++-- packages/server/test/unit/plugins/preprocessor_spec.js | 2 +- packages/server/test/unit/plugins/run_events_spec.js | 2 +- packages/server/test/unit/project_spec.js | 2 +- packages/server/test/unit/socket_spec.js | 2 +- packages/server/test/unit/spec_spec.js | 2 +- 7 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/server/lib/plugins/run_events.ts b/packages/server/lib/plugins/run_events.ts index 60bca1f3df0c..9f5aff011f15 100644 --- a/packages/server/lib/plugins/run_events.ts +++ b/packages/server/lib/plugins/run_events.ts @@ -21,5 +21,3 @@ const API: RunEventsAPI = { } export default API - -module.exports = API diff --git a/packages/server/test/unit/open_project_spec.js b/packages/server/test/unit/open_project_spec.js index ad0ed0f067fb..bfd8376e96f6 100644 --- a/packages/server/test/unit/open_project_spec.js +++ b/packages/server/test/unit/open_project_spec.js @@ -4,8 +4,8 @@ const Bluebird = require('bluebird') const browsers = require(`../../lib/browsers`) const ProjectBase = require(`../../lib/project-base`).ProjectBase const { openProject } = require('../../lib/open_project') -const preprocessor = require(`../../lib/plugins/preprocessor`) -const runEvents = require(`../../lib/plugins/run_events`) +const preprocessor = require(`../../lib/plugins/preprocessor`).default +const runEvents = require(`../../lib/plugins/run_events`).default const Fixtures = require('@tooling/system-tests') const delay = require('lodash/delay') diff --git a/packages/server/test/unit/plugins/preprocessor_spec.js b/packages/server/test/unit/plugins/preprocessor_spec.js index 693df788f98f..5b9a73464269 100644 --- a/packages/server/test/unit/plugins/preprocessor_spec.js +++ b/packages/server/test/unit/plugins/preprocessor_spec.js @@ -5,7 +5,7 @@ const path = require('path') const appData = require(`../../../lib/util/app_data`) const plugins = require(`../../../lib/plugins`) -const preprocessor = require(`../../../lib/plugins/preprocessor`) +const preprocessor = require(`../../../lib/plugins/preprocessor`).default describe('lib/plugins/preprocessor', () => { beforeEach(function () { diff --git a/packages/server/test/unit/plugins/run_events_spec.js b/packages/server/test/unit/plugins/run_events_spec.js index ae47983807b1..98a435886109 100644 --- a/packages/server/test/unit/plugins/run_events_spec.js +++ b/packages/server/test/unit/plugins/run_events_spec.js @@ -2,7 +2,7 @@ require('../../spec_helper') const errors = require(`../../../lib/errors`) const plugins = require(`../../../lib/plugins`) -const runEvents = require(`../../../lib/plugins/run_events`) +const runEvents = require(`../../../lib/plugins/run_events`).default describe('lib/plugins/run_events', () => { context('#execute', () => { diff --git a/packages/server/test/unit/project_spec.js b/packages/server/test/unit/project_spec.js index 49971f864c9d..c61a3a692222 100644 --- a/packages/server/test/unit/project_spec.js +++ b/packages/server/test/unit/project_spec.js @@ -11,7 +11,7 @@ const { ServerBase } = require(`../../lib/server-base`) const { ProjectBase } = require(`../../lib/project-base`) const { Automation } = require(`../../lib/automation`) const savedState = require(`../../lib/saved_state`) -const runEvents = require(`../../lib/plugins/run_events`) +const runEvents = require(`../../lib/plugins/run_events`).default const system = require(`../../lib/util/system`) const { getCtx } = require(`../../lib/makeDataContext`) const browsers = require('../../lib/browsers') diff --git a/packages/server/test/unit/socket_spec.js b/packages/server/test/unit/socket_spec.js index bbf838a9f260..0f659ce822e6 100644 --- a/packages/server/test/unit/socket_spec.js +++ b/packages/server/test/unit/socket_spec.js @@ -10,7 +10,7 @@ const errors = require('../../lib/errors') const { SocketE2E } = require('../../lib/socket-e2e') const { ServerBase } = require('../../lib/server-base') const { Automation } = require('../../lib/automation') -const preprocessor = require('../../lib/plugins/preprocessor') +const preprocessor = require('../../lib/plugins/preprocessor').default const { fs } = require('../../lib/util/fs') const session = require('../../lib/session') diff --git a/packages/server/test/unit/spec_spec.js b/packages/server/test/unit/spec_spec.js index 9fb2ff2c0fd8..c2621ee56843 100644 --- a/packages/server/test/unit/spec_spec.js +++ b/packages/server/test/unit/spec_spec.js @@ -1,7 +1,7 @@ require('../spec_helper') const spec = require(`../../lib/controllers/spec`) -const preprocessor = require(`../../lib/plugins/preprocessor`) +const preprocessor = require(`../../lib/plugins/preprocessor`).default describe('lib/controllers/spec', () => { const specName = 'sample.js' From a64c21d59c48945f2094c368b8f53939a90ead7c Mon Sep 17 00:00:00 2001 From: Jennifer Shehane Date: Tue, 2 Sep 2025 13:43:31 -0400 Subject: [PATCH 12/12] fix another preprocessor default call --- packages/server/test/integration/http_requests_spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/test/integration/http_requests_spec.js b/packages/server/test/integration/http_requests_spec.js index e593f29d550b..b80d9947925a 100644 --- a/packages/server/test/integration/http_requests_spec.js +++ b/packages/server/test/integration/http_requests_spec.js @@ -21,7 +21,7 @@ const { setupFullConfigWithDefaults } = require('@packages/config') const config = require(`../../lib/config`) const { ServerBase } = require(`../../lib/server-base`) const pluginsModule = require(`../../lib/plugins`) -const preprocessor = require(`../../lib/plugins/preprocessor`) +const preprocessor = require(`../../lib/plugins/preprocessor`).default const resolve = require(`../../lib/util/resolve`) const { fs } = require(`../../lib/util/fs`) const CacheBuster = require(`../../lib/util/cache_buster`)