Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 80 additions & 2 deletions packages/vite/src/shared/__tests__/forwardConsole.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { describe, expect, test } from 'vitest'
import { formatConsoleArgs } from '../forwardConsole'
import { setTimeout } from 'node:timers/promises'
import { describe, expect, test, vi } from 'vitest'
import {
formatConsoleArgs,
setupForwardConsoleHandler,
} from '../forwardConsole'
import {
type NormalizedModuleRunnerTransport,
SendBeforeConnectError,
} from '../moduleRunnerTransport'

describe('formatConsoleArgs', () => {
test('formats placeholders', () => {
Expand Down Expand Up @@ -59,3 +67,73 @@ describe('formatConsoleArgs', () => {
)
})
})

describe('setupForwardConsoleHandler', () => {
function createMockConsole() {
return {
error: vi.fn(),
warn: vi.fn(),
info: vi.fn(),
log: vi.fn(),
debug: vi.fn(),
} as unknown as Console
}

function createMockTransport(
send: (...args: any[]) => Promise<void>,
): NormalizedModuleRunnerTransport {
return {
connect: () => Promise.resolve(),
disconnect: () => Promise.resolve(),
send,
invoke: () => Promise.resolve({ result: undefined } as any),
}
}

test('ignore SendBeforeConnectError from transport.send', async () => {
const transport = createMockTransport(() =>
Promise.reject(new SendBeforeConnectError('not connected yet')),
)
const console = createMockConsole()

setupForwardConsoleHandler(
transport,
{
enabled: true,
unhandledErrors: false,
logLevels: ['log'],
},
console,
)

console.log('hi')
await setTimeout(50)

expect(console.error).not.toHaveBeenCalled()
})

test('log errors from transport.send', async () => {
const transport = createMockTransport(() =>
Promise.reject(new Error('other error')),
)
const console = createMockConsole()

setupForwardConsoleHandler(
transport,
{
enabled: true,
unhandledErrors: false,
logLevels: ['log'],
},
console,
)

console.log('hi')
await setTimeout(50)

expect(console.error).toHaveBeenCalledWith(
'Failed to send error to Vite server:',
new Error('other error'),
)
})
})
64 changes: 46 additions & 18 deletions packages/vite/src/shared/forwardConsole.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { ForwardConsolePayload } from '#types/customEvent'
import type { NormalizedModuleRunnerTransport } from './moduleRunnerTransport'
import {
type NormalizedModuleRunnerTransport,
SendBeforeConnectError,
} from './moduleRunnerTransport'

export type ForwardConsoleLogLevel =
| 'error'
Expand All @@ -23,13 +26,14 @@ export interface ResolvedForwardConsoleOptions {
export function setupForwardConsoleHandler(
transport: NormalizedModuleRunnerTransport,
options: ResolvedForwardConsoleOptions,
console: Console = globalThis.console,
): void {
if (!options.enabled) {
return
}

function sendError(type: 'error' | 'unhandled-rejection', error: any) {
transport.send({
async function sendError(type: 'error' | 'unhandled-rejection', error: any) {
await transport.send({
type: 'custom',
event: 'vite:forward-console',
data: {
Expand All @@ -43,20 +47,32 @@ export function setupForwardConsoleHandler(
})
}

function sendLog(level: ForwardConsoleLogLevel, args: unknown[]) {
transport.send({
type: 'custom',
event: 'vite:forward-console',
data: {
type: 'log',
async function sendLog(level: ForwardConsoleLogLevel, args: unknown[]) {
try {
await transport.send({
type: 'custom',
event: 'vite:forward-console',
data: {
level,
message: formatConsoleArgs(args),
},
} satisfies ForwardConsolePayload,
})
type: 'log',
data: {
level,
message: formatConsoleArgs(args),
},
} satisfies ForwardConsolePayload,
})
} catch (err) {
try {
await sendError('unhandled-rejection', err)
} catch (err) {
if (!(err instanceof SendBeforeConnectError)) {
originalConsoleError('Failed to send error to Vite server:', err)
}
}
}
}

const originalConsoleError = console.error

for (const level of options.logLevels) {
const original = (console as any)[level]
if (typeof original !== 'function') {
Expand All @@ -69,18 +85,30 @@ export function setupForwardConsoleHandler(
}

if (options.unhandledErrors && typeof window !== 'undefined') {
window.addEventListener('error', (event) => {
window.addEventListener('error', async (event) => {
// `ErrorEvent` doesn't necessarily have `ErrorEvent.error`.
// Use `ErrorEvent.message` as fallback e.g. for ResizeObserver error.
// https://developer.mozilla.org/en-US/docs/Web/API/ErrorEvent/error
// https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver#observation_errors
const error =
event.error ?? (event.message ? new Error(event.message) : event)
sendError('error', error)
try {
await sendError('error', error)
} catch (err) {
if (!(err instanceof SendBeforeConnectError)) {
originalConsoleError('Failed to send error to Vite server:', err)
}
}
})

window.addEventListener('unhandledrejection', (event) => {
sendError('unhandled-rejection', event.reason)
window.addEventListener('unhandledrejection', async (event) => {
try {
await sendError('unhandled-rejection', event.reason)
} catch (err) {
if (!(err instanceof SendBeforeConnectError)) {
originalConsoleError('Failed to send error to Vite server:', err)
}
}
})
}
}
Expand Down
11 changes: 9 additions & 2 deletions packages/vite/src/shared/moduleRunnerTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ export const normalizeModuleRunnerTransport = (
if (connectingPromise) {
await connectingPromise
} else {
throw new Error('send was called before connect')
throw new SendBeforeConnectError('send was called before connect')
}
}
await invokeableTransport.send(data)
Expand All @@ -247,14 +247,21 @@ export const normalizeModuleRunnerTransport = (
if (connectingPromise) {
await connectingPromise
} else {
throw new Error('invoke was called before connect')
throw new SendBeforeConnectError('invoke was called before connect')
}
}
return invokeableTransport.invoke(name, data)
},
}
}

export class SendBeforeConnectError extends Error {
constructor(message: string) {
super(message)
this.name = 'SendBeforeConnectError'
}
}

export const createWebSocketModuleRunnerTransport = (options: {
// eslint-disable-next-line n/no-unsupported-features/node-builtins
createConnection: () => WebSocket
Expand Down
Loading