131 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			131 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | import { ReadableStreamFrom } from "./shims.mjs"; | ||
|  | export const checkFileSupport = () => { | ||
|  |     if (typeof File === 'undefined') { | ||
|  |         const { process } = globalThis; | ||
|  |         const isOldNode = typeof process?.versions?.node === 'string' && parseInt(process.versions.node.split('.')) < 20; | ||
|  |         throw new Error('`File` is not defined as a global, which is required for file uploads.' + | ||
|  |             (isOldNode ? | ||
|  |                 " Update to Node 20 LTS or newer, or set `globalThis.File` to `import('node:buffer').File`." | ||
|  |                 : '')); | ||
|  |     } | ||
|  | }; | ||
|  | /** | ||
|  |  * Construct a `File` instance. This is used to ensure a helpful error is thrown | ||
|  |  * for environments that don't define a global `File` yet. | ||
|  |  */ | ||
|  | export function makeFile(fileBits, fileName, options) { | ||
|  |     checkFileSupport(); | ||
|  |     return new File(fileBits, fileName ?? 'unknown_file', options); | ||
|  | } | ||
|  | export function getName(value) { | ||
|  |     return (((typeof value === 'object' && | ||
|  |         value !== null && | ||
|  |         (('name' in value && value.name && String(value.name)) || | ||
|  |             ('url' in value && value.url && String(value.url)) || | ||
|  |             ('filename' in value && value.filename && String(value.filename)) || | ||
|  |             ('path' in value && value.path && String(value.path)))) || | ||
|  |         '') | ||
|  |         .split(/[\\/]/) | ||
|  |         .pop() || undefined); | ||
|  | } | ||
|  | export const isAsyncIterable = (value) => value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function'; | ||
|  | /** | ||
|  |  * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value. | ||
|  |  * Otherwise returns the request as is. | ||
|  |  */ | ||
|  | export const maybeMultipartFormRequestOptions = async (opts, fetch) => { | ||
|  |     if (!hasUploadableValue(opts.body)) | ||
|  |         return opts; | ||
|  |     return { ...opts, body: await createForm(opts.body, fetch) }; | ||
|  | }; | ||
|  | export const multipartFormRequestOptions = async (opts, fetch) => { | ||
|  |     return { ...opts, body: await createForm(opts.body, fetch) }; | ||
|  | }; | ||
|  | const supportsFormDataMap = /* @__PURE__ */ new WeakMap(); | ||
|  | /** | ||
|  |  * node-fetch doesn't support the global FormData object in recent node versions. Instead of sending | ||
|  |  * properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]". | ||
|  |  * This function detects if the fetch function provided supports the global FormData object to avoid | ||
|  |  * confusing error messages later on. | ||
|  |  */ | ||
|  | function supportsFormData(fetchObject) { | ||
|  |     const fetch = typeof fetchObject === 'function' ? fetchObject : fetchObject.fetch; | ||
|  |     const cached = supportsFormDataMap.get(fetch); | ||
|  |     if (cached) | ||
|  |         return cached; | ||
|  |     const promise = (async () => { | ||
|  |         try { | ||
|  |             const FetchResponse = ('Response' in fetch ? | ||
|  |                 fetch.Response | ||
|  |                 : (await fetch('data:,')).constructor); | ||
|  |             const data = new FormData(); | ||
|  |             if (data.toString() === (await new FetchResponse(data).text())) { | ||
|  |                 return false; | ||
|  |             } | ||
|  |             return true; | ||
|  |         } | ||
|  |         catch { | ||
|  |             // avoid false negatives
 | ||
|  |             return true; | ||
|  |         } | ||
|  |     })(); | ||
|  |     supportsFormDataMap.set(fetch, promise); | ||
|  |     return promise; | ||
|  | } | ||
|  | export const createForm = async (body, fetch) => { | ||
|  |     if (!(await supportsFormData(fetch))) { | ||
|  |         throw new TypeError('The provided fetch function does not support file uploads with the current global FormData class.'); | ||
|  |     } | ||
|  |     const form = new FormData(); | ||
|  |     await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value))); | ||
|  |     return form; | ||
|  | }; | ||
|  | // We check for Blob not File because Bun.File doesn't inherit from File,
 | ||
|  | // but they both inherit from Blob and have a `name` property at runtime.
 | ||
|  | const isNamedBlob = (value) => value instanceof Blob && 'name' in value; | ||
|  | const isUploadable = (value) => typeof value === 'object' && | ||
|  |     value !== null && | ||
|  |     (value instanceof Response || isAsyncIterable(value) || isNamedBlob(value)); | ||
|  | const hasUploadableValue = (value) => { | ||
|  |     if (isUploadable(value)) | ||
|  |         return true; | ||
|  |     if (Array.isArray(value)) | ||
|  |         return value.some(hasUploadableValue); | ||
|  |     if (value && typeof value === 'object') { | ||
|  |         for (const k in value) { | ||
|  |             if (hasUploadableValue(value[k])) | ||
|  |                 return true; | ||
|  |         } | ||
|  |     } | ||
|  |     return false; | ||
|  | }; | ||
|  | const addFormValue = async (form, key, value) => { | ||
|  |     if (value === undefined) | ||
|  |         return; | ||
|  |     if (value == null) { | ||
|  |         throw new TypeError(`Received null for "${key}"; to pass null in FormData, you must use the string 'null'`); | ||
|  |     } | ||
|  |     // TODO: make nested formats configurable
 | ||
|  |     if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { | ||
|  |         form.append(key, String(value)); | ||
|  |     } | ||
|  |     else if (value instanceof Response) { | ||
|  |         form.append(key, makeFile([await value.blob()], getName(value))); | ||
|  |     } | ||
|  |     else if (isAsyncIterable(value)) { | ||
|  |         form.append(key, makeFile([await new Response(ReadableStreamFrom(value)).blob()], getName(value))); | ||
|  |     } | ||
|  |     else if (isNamedBlob(value)) { | ||
|  |         form.append(key, value, getName(value)); | ||
|  |     } | ||
|  |     else if (Array.isArray(value)) { | ||
|  |         await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry))); | ||
|  |     } | ||
|  |     else if (typeof value === 'object') { | ||
|  |         await Promise.all(Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop))); | ||
|  |     } | ||
|  |     else { | ||
|  |         throw new TypeError(`Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`); | ||
|  |     } | ||
|  | }; | ||
|  | //# sourceMappingURL=uploads.mjs.map
 |