From f6f4714cf752bb3239da820b2710fde3af59f953 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Tue, 6 Oct 2026 11:06:21 +0000 Subject: [PATCH] fix(dev): disable dev channels when `BroadcastChannel` is unavailable --- packages/nuxt-cli/src/dev/broadcast.ts | 16 +++++++++++++ packages/nuxt-cli/src/dev/error-channel.ts | 14 ++++------- packages/nuxt-cli/src/dev/log-channel.ts | 10 +++----- packages/nuxt-cli/src/dev/span-channel.ts | 10 +++----- packages/nuxt-cli/test/unit/broadcast.spec.ts | 24 +++++++++++++++++++ 5 files changed, 51 insertions(+), 23 deletions(-) create mode 100644 packages/nuxt-cli/src/dev/broadcast.ts create mode 100644 packages/nuxt-cli/test/unit/broadcast.spec.ts diff --git a/packages/nuxt-cli/src/dev/broadcast.ts b/packages/nuxt-cli/src/dev/broadcast.ts new file mode 100644 index 000000000..3559b1b44 --- /dev/null +++ b/packages/nuxt-cli/src/dev/broadcast.ts @@ -0,0 +1,16 @@ +import { BroadcastChannel } from 'node:worker_threads' + +import { debug } from '../utils/logger' + +/** Listen on the channel called `name`, or return nothing where the runtime cannot open one. */ +export function openBroadcast(name: string, onMessage: (data: T) => void): BroadcastChannel | undefined { + try { + const channel = new BroadcastChannel(name) + channel.unref() + channel.onmessage = (event: { data: T }) => onMessage(event.data) + return channel + } + catch (error) { + debug(`Could not open the \`${name}\` channel:`, error) + } +} diff --git a/packages/nuxt-cli/src/dev/error-channel.ts b/packages/nuxt-cli/src/dev/error-channel.ts index f953bb817..d53fba8f6 100644 --- a/packages/nuxt-cli/src/dev/error-channel.ts +++ b/packages/nuxt-cli/src/dev/error-channel.ts @@ -6,11 +6,10 @@ import type { ProgressSnapshot } from '../utils/progress-snapshot' import process from 'node:process' -import { BroadcastChannel } from 'node:worker_threads' - import { isAbsolute, join, relative } from 'pathe' import { debug } from '../utils/logger' +import { openBroadcast } from './broadcast' import { isLoopbackAddress } from './host-check' import { DEV_INTERNAL_PREFIX } from './progress' @@ -332,11 +331,7 @@ export interface ErrorBridgeHandlers { * until the returned function is called. */ export function openErrorBridge(handlers: ErrorBridgeHandlers = {}, options: ErrorChannelOptions = {}): () => void { - const broadcast = new BroadcastChannel(ERROR_BROADCAST_CHANNEL) - broadcast.unref() - broadcast.postMessage(SYNC_MESSAGE) - broadcast.onmessage = (event: { data: unknown }) => { - const message = event.data + const broadcast = openBroadcast(ERROR_BROADCAST_CHANNEL, (message: unknown) => { if (!isDevErrorMessage(message)) { return } @@ -368,8 +363,9 @@ export function openErrorBridge(handlers: ErrorBridgeHandlers = {}, options: Err } } }).catch(error => debug('Could not handle a forwarded error report:', error)) - } - return () => broadcast.close() + }) + broadcast?.postMessage(SYNC_MESSAGE) + return () => broadcast?.close() } export async function closeErrorChannel(): Promise { diff --git a/packages/nuxt-cli/src/dev/log-channel.ts b/packages/nuxt-cli/src/dev/log-channel.ts index 5f19553ac..e46bb6ab0 100644 --- a/packages/nuxt-cli/src/dev/log-channel.ts +++ b/packages/nuxt-cli/src/dev/log-channel.ts @@ -1,4 +1,4 @@ -import { BroadcastChannel } from 'node:worker_threads' +import { openBroadcast } from './broadcast' export interface ServerLogEvent { level: number @@ -26,10 +26,6 @@ export const DEV_LOG_CHANNEL = 'nuxt:dev:log' /** Receive the app's logs until the returned function is called. */ export function openDevLogChannel(sink: (log: ServerLogEvent) => void): () => void { - const channel = new BroadcastChannel(DEV_LOG_CHANNEL) - channel.unref() - channel.onmessage = (event: { data: ServerLogEvent }) => { - sink(event.data) - } - return () => channel.close() + const channel = openBroadcast(DEV_LOG_CHANNEL, sink) + return () => channel?.close() } diff --git a/packages/nuxt-cli/src/dev/span-channel.ts b/packages/nuxt-cli/src/dev/span-channel.ts index 0f0ae7721..9faed9537 100644 --- a/packages/nuxt-cli/src/dev/span-channel.ts +++ b/packages/nuxt-cli/src/dev/span-channel.ts @@ -1,4 +1,4 @@ -import { BroadcastChannel } from 'node:worker_threads' +import { openBroadcast } from './broadcast' /** A timed piece of work the app did while serving a request. */ export interface DevRequestSpan { @@ -29,10 +29,6 @@ const DEV_SPAN_CHANNEL = 'nuxt:dev:span' /** Receive the app's spans until the returned function is called. */ export function openDevSpanChannel(sink: (span: DevRequestSpan) => void): () => void { - const channel = new BroadcastChannel(DEV_SPAN_CHANNEL) - channel.unref() - channel.onmessage = (event: { data: DevRequestSpan }) => { - sink(event.data) - } - return () => channel.close() + const channel = openBroadcast(DEV_SPAN_CHANNEL, sink) + return () => channel?.close() } diff --git a/packages/nuxt-cli/test/unit/broadcast.spec.ts b/packages/nuxt-cli/test/unit/broadcast.spec.ts new file mode 100644 index 000000000..4e9d2c088 --- /dev/null +++ b/packages/nuxt-cli/test/unit/broadcast.spec.ts @@ -0,0 +1,24 @@ +import { describe, expect, it, vi } from 'vitest' + +import { openErrorBridge } from '../../src/dev/error-channel' +import { openDevLogChannel } from '../../src/dev/log-channel' +import { openDevSpanChannel } from '../../src/dev/span-channel' + +vi.mock('node:worker_threads', async importOriginal => ({ + ...await importOriginal(), + BroadcastChannel: class { + constructor() { + throw new TypeError('Cannot read properties of undefined (reading \'on\')') + } + }, +})) + +describe('dev channels', () => { + it.each([ + ['log', () => openDevLogChannel(() => {})], + ['span', () => openDevSpanChannel(() => {})], + ['error', () => openErrorBridge()], + ])('should open and close the %s channel when `BroadcastChannel` cannot be constructed', (_, open) => { + expect(() => open()()).not.toThrow() + }) +})