Skip to content

Commit 9279b0f

Browse files
authored
fix: use dynamic property access for proces.versions to avoid Edge Runtime warnings (#533)
1 parent 83b6fb8 commit 9279b0f

File tree

2 files changed

+202
-25
lines changed

2 files changed

+202
-25
lines changed

packages/core/realtime-js/src/lib/websocket-factory.ts

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -80,37 +80,41 @@ export class WebSocketFactory {
8080
}
8181
}
8282

83-
if (
84-
typeof process !== 'undefined' &&
85-
process.versions &&
86-
process.versions.node
87-
) {
88-
const nodeVersion = parseInt(process.versions.node.split('.')[0])
89-
90-
// Node.js 22+ should have native WebSocket
91-
if (nodeVersion >= 22) {
92-
// Check if native WebSocket is available (should be in Node.js 22+)
93-
if (typeof globalThis.WebSocket !== 'undefined') {
94-
return { type: 'native', constructor: globalThis.WebSocket }
83+
if (typeof process !== 'undefined') {
84+
// Use dynamic property access to avoid Next.js Edge Runtime static analysis warnings
85+
const processVersions = (process as any)['versions']
86+
if (processVersions && processVersions['node']) {
87+
// Remove 'v' prefix if present and parse the major version
88+
const versionString = processVersions['node']
89+
const nodeVersion = parseInt(
90+
versionString.replace(/^v/, '').split('.')[0]
91+
)
92+
93+
// Node.js 22+ should have native WebSocket
94+
if (nodeVersion >= 22) {
95+
// Check if native WebSocket is available (should be in Node.js 22+)
96+
if (typeof globalThis.WebSocket !== 'undefined') {
97+
return { type: 'native', constructor: globalThis.WebSocket }
98+
}
99+
// If not available, user needs to provide it
100+
return {
101+
type: 'unsupported',
102+
error: `Node.js ${nodeVersion} detected but native WebSocket not found.`,
103+
workaround:
104+
'Provide a WebSocket implementation via the transport option.',
105+
}
95106
}
96-
// If not available, user needs to provide it
107+
108+
// Node.js < 22 doesn't have native WebSocket
97109
return {
98110
type: 'unsupported',
99-
error: `Node.js ${nodeVersion} detected but native WebSocket not found.`,
111+
error: `Node.js ${nodeVersion} detected without native WebSocket support.`,
100112
workaround:
101-
'Provide a WebSocket implementation via the transport option.',
113+
'For Node.js < 22, install "ws" package and provide it via the transport option:\n' +
114+
'import ws from "ws"\n' +
115+
'new RealtimeClient(url, { transport: ws })',
102116
}
103117
}
104-
105-
// Node.js < 22 doesn't have native WebSocket
106-
return {
107-
type: 'unsupported',
108-
error: `Node.js ${nodeVersion} detected without native WebSocket support.`,
109-
workaround:
110-
'For Node.js < 22, install "ws" package and provide it via the transport option:\n' +
111-
'import ws from "ws"\n' +
112-
'new RealtimeClient(url, { transport: ws })',
113-
}
114118
}
115119

116120
return {

packages/core/realtime-js/test/websocket-factory.test.ts

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,179 @@ describe('WebSocketFactory', () => {
107107
})
108108
})
109109

110+
describe('Edge Runtime compatibility - dynamic property access', () => {
111+
// These tests verify that we use dynamic property access to avoid
112+
// Next.js Edge Runtime static analysis warnings
113+
114+
const originalProcess = global.process
115+
116+
afterEach(() => {
117+
global.process = originalProcess
118+
})
119+
120+
test('should handle undefined process.versions', () => {
121+
// Process exists but versions is undefined
122+
global.process = {} as any
123+
Object.defineProperty(global.process, 'versions', {
124+
value: undefined,
125+
configurable: true,
126+
})
127+
delete global.WebSocket
128+
delete (globalThis as any).WebSocket
129+
130+
const env = (WebSocketFactory as any).detectEnvironment()
131+
expect(env.type).toBe('unsupported')
132+
expect(env.error).toContain('Unknown JavaScript runtime')
133+
})
134+
135+
test('should handle null process.versions', () => {
136+
// Process exists but versions is null
137+
global.process = {} as any
138+
Object.defineProperty(global.process, 'versions', {
139+
value: null,
140+
configurable: true,
141+
})
142+
delete global.WebSocket
143+
delete (globalThis as any).WebSocket
144+
145+
const env = (WebSocketFactory as any).detectEnvironment()
146+
expect(env.type).toBe('unsupported')
147+
expect(env.error).toContain('Unknown JavaScript runtime')
148+
})
149+
150+
test('should handle process.versions.node being undefined', () => {
151+
global.process = { versions: {} } as any
152+
delete global.WebSocket
153+
delete (globalThis as any).WebSocket
154+
155+
const env = (WebSocketFactory as any).detectEnvironment()
156+
expect(env.type).toBe('unsupported')
157+
expect(env.error).toContain('Unknown JavaScript runtime')
158+
})
159+
160+
test('should handle process.versions.node being null', () => {
161+
global.process = { versions: { node: null } } as any
162+
delete global.WebSocket
163+
delete (globalThis as any).WebSocket
164+
165+
const env = (WebSocketFactory as any).detectEnvironment()
166+
expect(env.type).toBe('unsupported')
167+
expect(env.error).toContain('Unknown JavaScript runtime')
168+
})
169+
170+
test('should handle invalid Node.js version string', () => {
171+
global.process = { versions: { node: 'invalid-version' } } as any
172+
delete global.WebSocket
173+
delete (globalThis as any).WebSocket
174+
175+
const env = (WebSocketFactory as any).detectEnvironment()
176+
expect(env.type).toBe('unsupported')
177+
// NaN from parseInt('invalid') makes nodeVersion < 22 true
178+
expect(env.error).toContain('detected without native WebSocket support')
179+
})
180+
181+
test('should handle Node.js version without v prefix', () => {
182+
global.process = { versions: { node: '18.0.0' } } as any
183+
delete global.WebSocket
184+
delete (globalThis as any).WebSocket
185+
186+
const env = (WebSocketFactory as any).detectEnvironment()
187+
expect(env.type).toBe('unsupported')
188+
expect(env.error).toContain(
189+
'Node.js 18 detected without native WebSocket support'
190+
)
191+
})
192+
193+
test('should correctly detect Node.js 16', () => {
194+
global.process = { versions: { node: 'v16.14.0' } } as any
195+
delete global.WebSocket
196+
delete (globalThis as any).WebSocket
197+
198+
const env = (WebSocketFactory as any).detectEnvironment()
199+
expect(env.type).toBe('unsupported')
200+
expect(env.error).toContain(
201+
'Node.js 16 detected without native WebSocket support'
202+
)
203+
expect(env.workaround).toContain('ws')
204+
})
205+
206+
test('should correctly detect Node.js 18', () => {
207+
global.process = { versions: { node: 'v18.0.0' } } as any
208+
delete global.WebSocket
209+
delete (globalThis as any).WebSocket
210+
211+
const env = (WebSocketFactory as any).detectEnvironment()
212+
expect(env.type).toBe('unsupported')
213+
expect(env.error).toContain(
214+
'Node.js 18 detected without native WebSocket support'
215+
)
216+
expect(env.workaround).toContain('ws')
217+
})
218+
219+
test('should correctly detect Node.js 20', () => {
220+
global.process = { versions: { node: 'v20.0.0' } } as any
221+
delete global.WebSocket
222+
delete (globalThis as any).WebSocket
223+
224+
const env = (WebSocketFactory as any).detectEnvironment()
225+
expect(env.type).toBe('unsupported')
226+
expect(env.error).toContain(
227+
'Node.js 20 detected without native WebSocket support'
228+
)
229+
expect(env.workaround).toContain('ws')
230+
})
231+
232+
test('should correctly detect Node.js 22 without WebSocket', () => {
233+
global.process = { versions: { node: 'v22.0.0' } } as any
234+
delete global.WebSocket
235+
delete (globalThis as any).WebSocket
236+
237+
const env = (WebSocketFactory as any).detectEnvironment()
238+
expect(env.type).toBe('unsupported')
239+
expect(env.error).toContain(
240+
'Node.js 22 detected but native WebSocket not found'
241+
)
242+
expect(env.workaround).toContain(
243+
'Provide a WebSocket implementation via the transport option'
244+
)
245+
})
246+
247+
test('should correctly detect Node.js 22 with WebSocket', () => {
248+
global.process = { versions: { node: 'v22.0.0' } } as any
249+
delete global.WebSocket
250+
;(globalThis as any).WebSocket = MockWebSocket
251+
252+
const env = (WebSocketFactory as any).detectEnvironment()
253+
expect(env.type).toBe('native')
254+
expect(env.constructor).toBe(MockWebSocket)
255+
256+
delete (globalThis as any).WebSocket
257+
})
258+
259+
test('ensures process.versions access uses bracket notation', () => {
260+
// This test verifies that our implementation doesn't directly access process.versions
261+
// which would trigger Next.js Edge Runtime warnings.
262+
// The actual verification happens through static analysis, but we can test the behavior.
263+
264+
const processProxy = new Proxy({} as any, {
265+
get(target, prop) {
266+
if (prop === 'versions') {
267+
return { node: 'v18.0.0' }
268+
}
269+
return undefined
270+
},
271+
})
272+
273+
global.process = processProxy
274+
delete global.WebSocket
275+
delete (globalThis as any).WebSocket
276+
277+
const env = (WebSocketFactory as any).detectEnvironment()
278+
expect(env.type).toBe('unsupported')
279+
expect(env.error).toContain('Node.js 18')
280+
})
281+
})
282+
110283
describe('Node.js environment', () => {
111284
beforeEach(() => {
112285
delete global.WebSocket

0 commit comments

Comments
 (0)