UNPKG

formdata-node

Version:

Spec-compliant FormData implementation for Node.js

572 lines (561 loc) 21.9 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); var __accessCheck = (obj, member, msg) => { if (!member.has(obj)) throw TypeError("Cannot " + msg); }; var __privateGet = (obj, member, getter) => { __accessCheck(obj, member, "read from private field"); return getter ? getter.call(obj) : member.get(obj); }; var __privateAdd = (obj, member, value) => { if (member.has(obj)) throw TypeError("Cannot add the same private member more than once"); member instanceof WeakSet ? member.add(obj) : member.set(obj, value); }; var __privateSet = (obj, member, value, setter) => { __accessCheck(obj, member, "write to private field"); setter ? setter.call(obj, value) : member.set(obj, value); return value; }; var __privateMethod = (obj, member, method) => { __accessCheck(obj, member, "access private method"); return method; }; // src/index.ts var src_exports = {}; __export(src_exports, { Blob: () => Blob, File: () => File, FormData: () => FormData }); module.exports = __toCommonJS(src_exports); // src/isFunction.ts var isFunction = (value) => typeof value === "function"; // src/isObject.ts var isObject = (value) => typeof value === "object" && value != null && !Array.isArray(value); // src/isAsyncIterable.ts var isAsyncIterable = (value) => isObject(value) && isFunction(value[Symbol.asyncIterator]); // src/blobHelpers.ts var MAX_CHUNK_SIZE = 65536; async function* clonePart(value) { if (value.byteLength <= MAX_CHUNK_SIZE) { yield value; return; } let offset = 0; while (offset < value.byteLength) { const size = Math.min(value.byteLength - offset, MAX_CHUNK_SIZE); const buffer = value.buffer.slice(offset, offset + size); offset += buffer.byteLength; yield new Uint8Array(buffer); } } async function* readStream(readable) { const reader = readable.getReader(); while (true) { const { done, value } = await reader.read(); if (done) { break; } yield value; } } async function* chunkStream(stream) { for await (const value of stream) { yield* clonePart(value); } } var getStreamIterator = (source) => { if (isAsyncIterable(source)) { return chunkStream(source); } if (isFunction(source.getReader)) { return chunkStream(readStream(source)); } throw new TypeError( "Unsupported data source: Expected either ReadableStream or async iterable." ); }; async function* consumeNodeBlob(blob) { let position = 0; while (position !== blob.size) { const chunk = blob.slice( position, Math.min(blob.size, position + MAX_CHUNK_SIZE) ); const buffer = await chunk.arrayBuffer(); position += buffer.byteLength; yield new Uint8Array(buffer); } } async function* consumeBlobParts(parts, clone = false) { for (const part of parts) { if (ArrayBuffer.isView(part)) { if (clone) { yield* clonePart(part); } else { yield part; } } else if (isFunction(part.stream)) { yield* getStreamIterator(part.stream()); } else { yield* consumeNodeBlob(part); } } } function* sliceBlob(blobParts, blobSize, start = 0, end) { end ??= blobSize; let relativeStart = start < 0 ? Math.max(blobSize + start, 0) : Math.min(start, blobSize); let relativeEnd = end < 0 ? Math.max(blobSize + end, 0) : Math.min(end, blobSize); const span = Math.max(relativeEnd - relativeStart, 0); let added = 0; for (const part of blobParts) { if (added >= span) { break; } const partSize = ArrayBuffer.isView(part) ? part.byteLength : part.size; if (relativeStart && partSize <= relativeStart) { relativeStart -= partSize; relativeEnd -= partSize; } else { let chunk; if (ArrayBuffer.isView(part)) { chunk = part.subarray(relativeStart, Math.min(partSize, relativeEnd)); added += chunk.byteLength; } else { chunk = part.slice(relativeStart, Math.min(partSize, relativeEnd)); added += chunk.size; } relativeEnd -= partSize; relativeStart = 0; yield chunk; } } } // src/Blob.ts var _parts, _type, _size; var _Blob = class _Blob { /** * Returns a new [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) object. * The content of the blob consists of the concatenation of the values given in the parameter array. * * @param blobParts An `Array` strings, or [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), [`ArrayBufferView`](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView), [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects, or a mix of any of such objects, that will be put inside the [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). * @param options An optional object of type `BlobPropertyBag`. */ constructor(blobParts = [], options = {}) { /** * An `Array` of [`ArrayBufferView`](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView) or [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects, or a mix of any of such objects, that will be put inside the [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). */ __privateAdd(this, _parts, []); /** * Returns the [`MIME type`](https://developer.mozilla.org/en-US/docs/Glossary/MIME_type) of the [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). */ __privateAdd(this, _type, ""); /** * Returns the size of the [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) in bytes. */ __privateAdd(this, _size, 0); options ??= {}; if (typeof blobParts !== "object" || blobParts === null) { throw new TypeError( "Failed to construct 'Blob': The provided value cannot be converted to a sequence." ); } if (!isFunction(blobParts[Symbol.iterator])) { throw new TypeError( "Failed to construct 'Blob': The object must have a callable @@iterator property." ); } if (typeof options !== "object" && !isFunction(options)) { throw new TypeError( "Failed to construct 'Blob': parameter 2 cannot convert to dictionary." ); } const encoder = new TextEncoder(); for (const raw of blobParts) { let part; if (ArrayBuffer.isView(raw)) { part = new Uint8Array(raw.buffer.slice( raw.byteOffset, raw.byteOffset + raw.byteLength )); } else if (raw instanceof ArrayBuffer) { part = new Uint8Array(raw.slice(0)); } else if (raw instanceof _Blob) { part = raw; } else { part = encoder.encode(String(raw)); } __privateSet(this, _size, __privateGet(this, _size) + (ArrayBuffer.isView(part) ? part.byteLength : part.size)); __privateGet(this, _parts).push(part); } const type = options.type === void 0 ? "" : String(options.type); __privateSet(this, _type, /^[\x20-\x7E]*$/.test(type) ? type : ""); } static [Symbol.hasInstance](value) { return Boolean( value && typeof value === "object" && isFunction(value.constructor) && (isFunction(value.stream) || isFunction(value.arrayBuffer)) && /^(Blob|File)$/.test(value[Symbol.toStringTag]) ); } /** * Returns the [`MIME type`](https://developer.mozilla.org/en-US/docs/Glossary/MIME_type) of the [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). */ get type() { return __privateGet(this, _type); } /** * Returns the size of the [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File) in bytes. */ get size() { return __privateGet(this, _size); } /** * Creates and returns a new [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) object which contains data from a subset of the blob on which it's called. * * @param start An index into the Blob indicating the first byte to include in the new Blob. If you specify a negative value, it's treated as an offset from the end of the Blob toward the beginning. For example, -10 would be the 10th from last byte in the Blob. The default value is 0. If you specify a value for start that is larger than the size of the source Blob, the returned Blob has size 0 and contains no data. * @param end An index into the Blob indicating the first byte that will *not* be included in the new Blob (i.e. the byte exactly at this index is not included). If you specify a negative value, it's treated as an offset from the end of the Blob toward the beginning. For example, -10 would be the 10th from last byte in the Blob. The default value is size. * @param contentType The content type to assign to the new Blob; this will be the value of its type property. The default value is an empty string. */ slice(start, end, contentType) { return new _Blob(sliceBlob(__privateGet(this, _parts), this.size, start, end), { type: contentType }); } /** * Returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that resolves with a string containing the contents of the blob, interpreted as UTF-8. */ async text() { const decoder = new TextDecoder(); let result = ""; for await (const chunk of consumeBlobParts(__privateGet(this, _parts))) { result += decoder.decode(chunk, { stream: true }); } result += decoder.decode(); return result; } /** * Returns a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) that resolves with the contents of the blob as binary data contained in an [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). */ async arrayBuffer() { const view = new Uint8Array(this.size); let offset = 0; for await (const chunk of consumeBlobParts(__privateGet(this, _parts))) { view.set(chunk, offset); offset += chunk.length; } return view.buffer; } /** * Returns a [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) which upon reading returns the data contained within the [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob). */ stream() { const iterator = consumeBlobParts(__privateGet(this, _parts), true); return new ReadableStream({ async pull(controller) { const { value, done } = await iterator.next(); if (done) { return queueMicrotask(() => controller.close()); } controller.enqueue(value); }, async cancel() { await iterator.return(); } }); } get [Symbol.toStringTag]() { return "Blob"; } }; _parts = new WeakMap(); _type = new WeakMap(); _size = new WeakMap(); var Blob = _Blob; Object.defineProperties(Blob.prototype, { type: { enumerable: true }, size: { enumerable: true }, slice: { enumerable: true }, stream: { enumerable: true }, text: { enumerable: true }, arrayBuffer: { enumerable: true } }); // src/isBlob.ts var isBlob = (value) => value instanceof Blob; // src/File.ts var _name, _lastModified; var File = class extends Blob { /** * Creates a new File instance. * * @param fileBits An `Array` strings, or [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer), [`ArrayBufferView`](https://developer.mozilla.org/en-US/docs/Web/API/ArrayBufferView), [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects, or a mix of any of such objects, that will be put inside the [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). * @param name The name of the file. * @param options An options object containing optional attributes for the file. */ constructor(fileBits, name, options = {}) { super(fileBits, options); /** * Returns the name of the file referenced by the File object. */ __privateAdd(this, _name, void 0); /** * The last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight). Files without a known last modified date return the current date. */ __privateAdd(this, _lastModified, 0); if (arguments.length < 2) { throw new TypeError( `Failed to construct 'File': 2 arguments required, but only ${arguments.length} present.` ); } __privateSet(this, _name, String(name)); const lastModified = options.lastModified === void 0 ? Date.now() : Number(options.lastModified); if (!Number.isNaN(lastModified)) { __privateSet(this, _lastModified, lastModified); } } static [Symbol.hasInstance](value) { return value instanceof Blob && value[Symbol.toStringTag] === "File" && typeof value.name === "string"; } /** * Name of the file referenced by the File object. */ get name() { return __privateGet(this, _name); } /* c8 ignore next 3 */ get webkitRelativePath() { return ""; } /** * The last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight). Files without a known last modified date return the current date. */ get lastModified() { return __privateGet(this, _lastModified); } get [Symbol.toStringTag]() { return "File"; } }; _name = new WeakMap(); _lastModified = new WeakMap(); // src/isFile.ts var isFile = (value) => value instanceof File; // src/FormData.ts var _entries, _setEntry, setEntry_fn; var FormData = class { constructor() { __privateAdd(this, _setEntry); /** * Stores internal data for every entry */ __privateAdd(this, _entries, /* @__PURE__ */ new Map()); } static [Symbol.hasInstance](value) { if (!value) { return false; } const val = value; return Boolean( isFunction(val.constructor) && val[Symbol.toStringTag] === "FormData" && isFunction(val.append) && isFunction(val.set) && isFunction(val.get) && isFunction(val.getAll) && isFunction(val.has) && isFunction(val.delete) && isFunction(val.entries) && isFunction(val.values) && isFunction(val.keys) && isFunction(val[Symbol.iterator]) && isFunction(val.forEach) ); } /** * Appends a new value onto an existing key inside a FormData object, * or adds the key if it does not already exist. * * The difference between `set()` and `append()` is that if the specified key already exists, `set()` will overwrite all existing values with the new one, whereas `append()` will append the new value onto the end of the existing set of values. * * @param name The name of the field whose data is contained in `value`. * @param value The field's value. This can be [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). If none of these are specified the value is converted to a string. * @param fileName The filename reported to the server, when a Blob or File is passed as the second parameter. The default filename for Blob objects is "blob". The default filename for File objects is the file's filename. */ append(name, value, fileName) { __privateMethod(this, _setEntry, setEntry_fn).call(this, { name, fileName, append: true, rawValue: value, argsLength: arguments.length }); } /** * Set a new value for an existing key inside FormData, * or add the new field if it does not already exist. * * @param name The name of the field whose data is contained in `value`. * @param value The field's value. This can be [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) or [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File). If none of these are specified the value is converted to a string. * @param fileName The filename reported to the server, when a Blob or File is passed as the second parameter. The default filename for Blob objects is "blob". The default filename for File objects is the file's filename. * */ set(name, value, fileName) { __privateMethod(this, _setEntry, setEntry_fn).call(this, { name, fileName, append: false, rawValue: value, argsLength: arguments.length }); } /** * Returns the first value associated with a given key from within a `FormData` object. * If you expect multiple values and want all of them, use the `getAll()` method instead. * * @param {string} name A name of the value you want to retrieve. * * @returns A `FormDataEntryValue` containing the value. If the key doesn't exist, the method returns null. */ get(name) { const field = __privateGet(this, _entries).get(String(name)); if (!field) { return null; } return field[0]; } /** * Returns all the values associated with a given key from within a `FormData` object. * * @param {string} name A name of the value you want to retrieve. * * @returns An array of `FormDataEntryValue` whose key matches the value passed in the `name` parameter. If the key doesn't exist, the method returns an empty list. */ getAll(name) { const field = __privateGet(this, _entries).get(String(name)); if (!field) { return []; } return field.slice(); } /** * Returns a boolean stating whether a `FormData` object contains a certain key. * * @param name A string representing the name of the key you want to test for. * * @return A boolean value. */ has(name) { return __privateGet(this, _entries).has(String(name)); } /** * Deletes a key and its value(s) from a `FormData` object. * * @param name The name of the key you want to delete. */ delete(name) { __privateGet(this, _entries).delete(String(name)); } /** * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all keys contained in this `FormData` object. * Each key is a `string`. */ *keys() { for (const key of __privateGet(this, _entries).keys()) { yield key; } } /** * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through the `FormData` key/value pairs. * The key of each pair is a string; the value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue). */ *entries() { for (const name of this.keys()) { const values = this.getAll(name); for (const value of values) { yield [name, value]; } } } /** * Returns an [`iterator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) allowing to go through all values contained in this object `FormData` object. * Each value is a [`FormDataValue`](https://developer.mozilla.org/en-US/docs/Web/API/FormDataEntryValue). */ *values() { for (const [, value] of this) { yield value; } } /** * An alias for FormData#entries() */ [Symbol.iterator]() { return this.entries(); } /** * Executes given callback function for each field of the FormData instance */ forEach(callback, thisArg) { for (const [name, value] of this) { callback.call(thisArg, value, name, this); } } get [Symbol.toStringTag]() { return "FormData"; } }; _entries = new WeakMap(); _setEntry = new WeakSet(); setEntry_fn = function({ name, rawValue, append, fileName, argsLength }) { const methodName = append ? "append" : "set"; if (argsLength < 2) { throw new TypeError( `Failed to execute '${methodName}' on 'FormData': 2 arguments required, but only ${argsLength} present.` ); } name = String(name); let value; if (isFile(rawValue)) { value = fileName === void 0 ? rawValue : new File([rawValue], fileName, { // otherwise, create new File with given fileName type: rawValue.type, lastModified: rawValue.lastModified }); } else if (isBlob(rawValue)) { value = new File([rawValue], fileName === void 0 ? "blob" : fileName, { type: rawValue.type }); } else if (fileName) { throw new TypeError( `Failed to execute '${methodName}' on 'FormData': parameter 2 is not of type 'Blob'.` ); } else { value = String(rawValue); } const values = __privateGet(this, _entries).get(name); if (!values) { return void __privateGet(this, _entries).set(name, [value]); } if (!append) { return void __privateGet(this, _entries).set(name, [value]); } values.push(value); }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Blob, File, FormData }); /*! Based on fetch-blob. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> & David Frank */