11import type { DOMWindow } from 'jsdom'
22import type { Environment } from '../../types/environment'
33import type { JSDOMOptions } from '../../types/jsdom-options'
4+ import { URL as NodeURL } from 'node:url'
45import { populateGlobal } from './utils'
56
67function catchWindowErrors ( window : DOMWindow ) {
@@ -35,14 +36,18 @@ function catchWindowErrors(window: DOMWindow) {
3536 }
3637}
3738
38- let _FormData ! : typeof FormData
39+ let NodeFormData_ ! : typeof FormData
40+ let NodeBlob_ ! : typeof Blob
41+ let NodeRequest_ ! : typeof Request
3942
4043export default < Environment > {
4144 name : 'jsdom' ,
4245 viteEnvironment : 'client' ,
4346 async setupVM ( { jsdom = { } } ) {
4447 // delay initialization because it takes ~1s
45- _FormData = globalThis . FormData
48+ NodeFormData_ = globalThis . FormData
49+ NodeBlob_ = globalThis . Blob
50+ NodeRequest_ = globalThis . Request
4651
4752 const { CookieJar, JSDOM , ResourceLoader, VirtualConsole } = await import (
4853 'jsdom' ,
@@ -82,12 +87,15 @@ export default <Environment>{
8287
8388 const clearWindowErrors = catchWindowErrors ( dom . window )
8489
90+ const utils = createCompatUtils ( dom . window )
91+
8592 // TODO: browser doesn't expose Buffer, but a lot of dependencies use it
8693 dom . window . Buffer = Buffer
8794 dom . window . jsdom = dom
88- dom . window . FormData = createFormData ( dom . window )
95+ dom . window . Request = createCompatRequest ( utils )
96+ dom . window . URL = createJSDOMCompatURL ( utils )
8997
90- // inject web globals if they missing in JSDOM but otherwise available in Nodejs
98+ // inject web globals if they are missing in JSDOM but otherwise available in Nodejs
9199 // https://nodejs.org/dist/latest/docs/api/globals.html
92100 const globalNames = [
93101 'structuredClone' ,
@@ -111,13 +119,14 @@ export default <Environment>{
111119 // we also should override other APIs they use
112120 const overrideGlobals = [
113121 'fetch' ,
114- 'Request' ,
115122 'Response' ,
116123 'Headers' ,
117124 'AbortController' ,
118125 'AbortSignal' ,
119- 'URL' ,
120126 'URLSearchParams' ,
127+ // URL and Request is overriden with a compat one
128+ // 'URL',
129+ // 'Request',
121130 ] as const
122131 for ( const name of overrideGlobals ) {
123132 const value = globalThis [ name ]
@@ -140,7 +149,9 @@ export default <Environment>{
140149 } ,
141150 async setup ( global , { jsdom = { } } ) {
142151 // delay initialization because it takes ~1s
143- _FormData = globalThis . FormData
152+ NodeFormData_ = globalThis . FormData
153+ NodeBlob_ = globalThis . Blob
154+ NodeRequest_ = globalThis . Request
144155
145156 const { CookieJar, JSDOM , ResourceLoader, VirtualConsole } = await import (
146157 'jsdom' ,
@@ -183,9 +194,11 @@ export default <Environment>{
183194 } )
184195
185196 const clearWindowErrors = catchWindowErrors ( global )
197+ const utils = createCompatUtils ( dom . window )
186198
187199 global . jsdom = dom
188- global . FormData = createFormData ( dom . window )
200+ global . Request = createCompatRequest ( utils )
201+ global . URL = createJSDOMCompatURL ( utils )
189202
190203 return {
191204 teardown ( global ) {
@@ -200,26 +213,71 @@ export default <Environment>{
200213 } ,
201214}
202215
203- // Node.js 24 has a global FormData that Request accepts
204- // FormData is not used anywhere else in JSDOM, so we can safely
205- // override it with Node.js implementation, but keep the DOM behaviour
206- // this is required because Request (and other fetch API)
207- // are not implemented by JSDOM
208- function createFormData ( window : DOMWindow ) {
209- const JSDOMFormData = window . FormData
210- if ( ! _FormData ) {
211- return JSDOMFormData
216+ function createCompatRequest ( utils : CompatUtils ) {
217+ return class Request extends NodeRequest_ {
218+ constructor ( ...args : [ input : RequestInfo , init ?: RequestInit ] ) {
219+ const [ input , init ] = args
220+ if ( init ?. body != null ) {
221+ const compatInit = { ...init }
222+ if ( init . body instanceof utils . window . Blob ) {
223+ compatInit . body = utils . makeCompatBlob ( init . body as any ) as any
224+ }
225+ if ( init . body instanceof utils . window . FormData ) {
226+ compatInit . body = utils . makeCompatFormData ( init . body )
227+ }
228+ super ( input , compatInit )
229+ }
230+ else {
231+ super ( ...args )
232+ }
233+ }
212234 }
235+ }
236+
237+ function createJSDOMCompatURL ( utils : CompatUtils ) : typeof URL {
238+ return class URL extends NodeURL {
239+ static createObjectURL ( blob : any ) : string {
240+ if ( blob instanceof utils . window . Blob ) {
241+ const compatBlob = utils . makeCompatBlob ( blob )
242+ return NodeURL . createObjectURL ( compatBlob as any )
243+ }
244+ return NodeURL . createObjectURL ( blob )
245+ }
246+ } as typeof URL
247+ }
213248
214- return class FormData extends _FormData {
215- constructor ( ...args : any [ ] ) {
216- super ( )
217- const formData = new JSDOMFormData ( ...args )
249+ interface CompatUtils {
250+ window : DOMWindow
251+ makeCompatBlob : ( blob : Blob ) => Blob
252+ makeCompatFormData : ( formData : FormData ) => FormData
253+ }
254+
255+ function createCompatUtils ( window : DOMWindow ) : CompatUtils {
256+ // this returns a hidden Symbol(impl)
257+ // this is cursed, and jsdom should just implement fetch API itself
258+ const implSymbol = Object . getOwnPropertySymbols (
259+ Object . getOwnPropertyDescriptors ( new window . Blob ( ) ) ,
260+ ) [ 0 ]
261+ const utils = {
262+ window,
263+ makeCompatFormData ( formData : FormData ) {
264+ const nodeFormData = new NodeFormData_ ( )
218265 formData . forEach ( ( value , key ) => {
219- this . append ( key , value )
266+ if ( value instanceof window . Blob ) {
267+ nodeFormData . append ( key , utils . makeCompatBlob ( value as any ) as any )
268+ }
269+ else {
270+ nodeFormData . append ( key , value )
271+ }
220272 } )
221- }
273+ return nodeFormData
274+ } ,
275+ makeCompatBlob ( blob : Blob ) {
276+ const buffer = ( blob as any ) [ implSymbol ] . _buffer
277+ return new NodeBlob_ ( [ buffer ] , { type : blob . type } )
278+ } ,
222279 }
280+ return utils
223281}
224282
225283function patchAddEventListener ( window : DOMWindow ) {
0 commit comments