Skip to content

Commit 4d0b0ab

Browse files
authored
lint: avoid unintented use of globals in code and tests, improve test for installing/overwriting globals (#4478)
* lint: avoid unintented use of globals in code and tests, improve test for installing/overwriting globals * Apply suggestions from code review
1 parent a07a85e commit 4d0b0ab

10 files changed

Lines changed: 72 additions & 118 deletions

File tree

benchmarks/websocket/messageevent.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ group('MessageEvent instantiation', () => {
1313
})
1414

1515
bench('global - MessageEvent init', () => {
16+
// eslint-disable-next-line no-restricted-globals
1617
return new MessageEvent('event', { data: null, ports: [port1, port2] })
1718
})
1819
})

docs/examples/proxy/fetch.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as http from 'node:http'
22
import { once } from 'node:events'
33
import { createProxy } from 'proxy'
4-
import { ProxyAgent } from '../../../index.js'
4+
import { fetch, ProxyAgent } from '../../../'
55

66
const proxyServer = createProxy(http.createServer())
77
const server = http.createServer((req, res) => {

eslint.config.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict'
22

33
const neo = require('neostandard')
4+
const { installedExports } = require('./lib/global')
45

56
module.exports = [
67
...neo({
@@ -22,7 +23,15 @@ module.exports = [
2223
exports: 'never',
2324
functions: 'never'
2425
}],
25-
'@typescript-eslint/no-redeclare': 'off'
26+
'@typescript-eslint/no-redeclare': 'off',
27+
'no-restricted-globals': ['error',
28+
...installedExports.map(name => {
29+
return {
30+
name,
31+
message: `Use undici-own ${name} instead of the global.`
32+
}
33+
})
34+
]
2635
}
2736
}
2837
]

lib/global.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,25 @@ function getGlobalDispatcher () {
2626
return globalThis[globalDispatcher]
2727
}
2828

29+
// These are the globals that can be installed by undici.install().
30+
// Not exported by index.js to avoid use outside of this module.
31+
const installedExports = /** @type {const} */ (
32+
[
33+
'fetch',
34+
'Headers',
35+
'Response',
36+
'Request',
37+
'FormData',
38+
'WebSocket',
39+
'CloseEvent',
40+
'ErrorEvent',
41+
'MessageEvent',
42+
'EventSource'
43+
]
44+
)
45+
2946
module.exports = {
3047
setGlobalDispatcher,
31-
getGlobalDispatcher
48+
getGlobalDispatcher,
49+
installedExports
3250
}

test/env-http-proxy-agent-nodejs-bundle.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('EnvHttpProxyAgent and setGlobalDispatcher', () => {
2020
process.env = { ...env }
2121
})
2222

23-
test('should work together from the Node.js bundle', async (t) => {
23+
test('should work with global fetch from undici bundled with Node.js', async (t) => {
2424
const { strictEqual } = tspl(t, { plan: 3 })
2525

2626
// Instead of using mocks, start a real server and a minimal proxy server
@@ -69,6 +69,7 @@ describe('EnvHttpProxyAgent and setGlobalDispatcher', () => {
6969
process.env.http_proxy = proxyAddress
7070
setGlobalDispatcher(new EnvHttpProxyAgent())
7171

72+
// eslint-disable-next-line no-restricted-globals
7273
const res = await fetch(serverAddress)
7374
strictEqual(await res.text(), 'Hello world')
7475
})

test/http2.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const { Writable, pipeline, PassThrough, Readable } = require('node:stream')
99

1010
const pem = require('@metcoder95/https-pem')
1111

12-
const { Client, Agent, FormData } = require('..')
12+
const { Client, Agent, FormData, Response } = require('..')
1313

1414
test('Should support H2 connection', async t => {
1515
const body = []

test/install.js

Lines changed: 35 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -2,125 +2,48 @@
22

33
const { test } = require('node:test')
44
const assert = require('node:assert')
5-
const { install } = require('../index')
5+
const undici = require('../index')
6+
const { installedExports } = require('../lib/global')
67

7-
test('install() should add WHATWG fetch classes to globalThis', () => {
8-
// Save original globals to restore later
9-
const originalFetch = globalThis.fetch
10-
const originalHeaders = globalThis.Headers
11-
const originalResponse = globalThis.Response
12-
const originalRequest = globalThis.Request
13-
const originalFormData = globalThis.FormData
14-
const originalWebSocket = globalThis.WebSocket
15-
const originalCloseEvent = globalThis.CloseEvent
16-
const originalErrorEvent = globalThis.ErrorEvent
17-
const originalMessageEvent = globalThis.MessageEvent
18-
const originalEventSource = globalThis.EventSource
8+
test('install() should overwrite only specified elements in globalThis', () => {
9+
const exportsToCheck = new Set(installedExports)
1910

20-
try {
21-
// Remove any existing globals
22-
delete globalThis.fetch
23-
delete globalThis.Headers
24-
delete globalThis.Response
25-
delete globalThis.Request
26-
delete globalThis.FormData
27-
delete globalThis.WebSocket
28-
delete globalThis.CloseEvent
29-
delete globalThis.ErrorEvent
30-
delete globalThis.MessageEvent
31-
delete globalThis.EventSource
11+
// Use a Proxy to verify that only the expected globals are set
12+
// and no other properties are added to globalThis by undici.install().
13+
const proxyGlobalThis = new Proxy(globalThis, {
14+
set (target, prop, value) {
15+
if (exportsToCheck.has(prop)) {
16+
target[prop] = value
17+
exportsToCheck.delete(prop)
18+
return true
19+
}
20+
throw new Error(`Unexpected global set: ${String(prop)}`)
21+
}
22+
})
3223

33-
// Verify they're not defined
34-
assert.strictEqual(globalThis.fetch, undefined)
35-
assert.strictEqual(globalThis.Headers, undefined)
36-
assert.strictEqual(globalThis.Response, undefined)
37-
assert.strictEqual(globalThis.Request, undefined)
38-
assert.strictEqual(globalThis.FormData, undefined)
39-
assert.strictEqual(globalThis.WebSocket, undefined)
40-
assert.strictEqual(globalThis.CloseEvent, undefined)
41-
assert.strictEqual(globalThis.ErrorEvent, undefined)
42-
assert.strictEqual(globalThis.MessageEvent, undefined)
43-
assert.strictEqual(globalThis.EventSource, undefined)
24+
// eslint-disable-next-line no-global-assign
25+
globalThis = proxyGlobalThis
4426

45-
// Call install()
46-
install()
27+
undici.install()
4728

48-
// Verify all classes are now installed
49-
assert.strictEqual(typeof globalThis.fetch, 'function')
50-
assert.strictEqual(typeof globalThis.Headers, 'function')
51-
assert.strictEqual(typeof globalThis.Response, 'function')
52-
assert.strictEqual(typeof globalThis.Request, 'function')
53-
assert.strictEqual(typeof globalThis.FormData, 'function')
54-
assert.strictEqual(typeof globalThis.WebSocket, 'function')
55-
assert.strictEqual(typeof globalThis.CloseEvent, 'function')
56-
assert.strictEqual(typeof globalThis.ErrorEvent, 'function')
57-
assert.strictEqual(typeof globalThis.MessageEvent, 'function')
58-
assert.strictEqual(typeof globalThis.EventSource, 'function')
29+
assert.strictEqual(exportsToCheck.size, 0, `Some expected globals were not set: ${[...exportsToCheck].join(', ')}`)
5930

60-
// Test that the installed classes are functional
61-
const headers = new globalThis.Headers([['content-type', 'application/json']])
62-
assert.strictEqual(headers.get('content-type'), 'application/json')
31+
// Verify that the installed globals match the exports from undici
32+
for (const name of installedExports) {
33+
assert.strictEqual(globalThis[name], undici[name])
34+
}
6335

64-
const request = new globalThis.Request('https://example.com')
65-
assert.strictEqual(request.url, 'https://example.com/')
36+
// Test that the installed classes are functional
37+
const headers = new globalThis.Headers([['content-type', 'application/json']])
38+
assert.strictEqual(headers.get('content-type'), 'application/json')
6639

67-
const response = new globalThis.Response('test body')
68-
assert.strictEqual(response.status, 200)
40+
const request = new globalThis.Request('https://example.com')
41+
assert.strictEqual(request.url, 'https://example.com/')
6942

70-
const formData = new globalThis.FormData()
71-
formData.append('key', 'value')
72-
assert.strictEqual(formData.get('key'), 'value')
73-
} finally {
74-
// Restore original globals
75-
if (originalFetch !== undefined) {
76-
globalThis.fetch = originalFetch
77-
} else {
78-
delete globalThis.fetch
79-
}
80-
if (originalHeaders !== undefined) {
81-
globalThis.Headers = originalHeaders
82-
} else {
83-
delete globalThis.Headers
84-
}
85-
if (originalResponse !== undefined) {
86-
globalThis.Response = originalResponse
87-
} else {
88-
delete globalThis.Response
89-
}
90-
if (originalRequest !== undefined) {
91-
globalThis.Request = originalRequest
92-
} else {
93-
delete globalThis.Request
94-
}
95-
if (originalFormData !== undefined) {
96-
globalThis.FormData = originalFormData
97-
} else {
98-
delete globalThis.FormData
99-
}
100-
if (originalWebSocket !== undefined) {
101-
globalThis.WebSocket = originalWebSocket
102-
} else {
103-
delete globalThis.WebSocket
104-
}
105-
if (originalCloseEvent !== undefined) {
106-
globalThis.CloseEvent = originalCloseEvent
107-
} else {
108-
delete globalThis.CloseEvent
109-
}
110-
if (originalErrorEvent !== undefined) {
111-
globalThis.ErrorEvent = originalErrorEvent
112-
} else {
113-
delete globalThis.ErrorEvent
114-
}
115-
if (originalMessageEvent !== undefined) {
116-
globalThis.MessageEvent = originalMessageEvent
117-
} else {
118-
delete globalThis.MessageEvent
119-
}
120-
if (originalEventSource !== undefined) {
121-
globalThis.EventSource = originalEventSource
122-
} else {
123-
delete globalThis.EventSource
124-
}
125-
}
43+
const response = new globalThis.Response('test body')
44+
assert.strictEqual(response.status, 200)
45+
46+
const formData = new globalThis.FormData()
47+
formData.append('key', 'value')
48+
assert.strictEqual(formData.get('key'), 'value')
12649
})

test/node-fetch/request.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const { describe, it, before, after } = require('node:test')
55
const stream = require('node:stream')
66
const http = require('node:http')
77

8-
const { Request } = require('../../index.js')
8+
const { Request, FormData } = require('../../index.js')
99
const TestServer = require('./utils/server.js')
1010

1111
describe('Request', () => {

test/redirect-request.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@ for (const factory of [
235235

236236
const { statusCode, headers, body: bodyStream } = await request(t, server, undefined, `http://${server}/303`, {
237237
method: 'PATCH',
238+
// eslint-disable-next-line no-restricted-globals
238239
headers: new Headers({
239240
'Content-Encoding': 'gzip',
240241
'X-Foo1': '1',

test/wpt/start-websockets.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { on } from 'events'
77
const { WPT_REPORT } = process.env
88

99
function isGlobalAvailable () {
10+
// eslint-disable-next-line no-restricted-globals
1011
if (typeof WebSocket !== 'undefined') {
1112
return true
1213
}

0 commit comments

Comments
 (0)