uploads.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.createForm = exports.multipartFormRequestOptions = exports.maybeMultipartFormRequestOptions = exports.isAsyncIterable = exports.checkFileSupport = void 0;
  4. exports.makeFile = makeFile;
  5. exports.getName = getName;
  6. const shims_1 = require("./shims.js");
  7. const checkFileSupport = () => {
  8. if (typeof File === 'undefined') {
  9. const { process } = globalThis;
  10. const isOldNode = typeof process?.versions?.node === 'string' && parseInt(process.versions.node.split('.')) < 20;
  11. throw new Error('`File` is not defined as a global, which is required for file uploads.' +
  12. (isOldNode ?
  13. " Update to Node 20 LTS or newer, or set `globalThis.File` to `import('node:buffer').File`."
  14. : ''));
  15. }
  16. };
  17. exports.checkFileSupport = checkFileSupport;
  18. /**
  19. * Construct a `File` instance. This is used to ensure a helpful error is thrown
  20. * for environments that don't define a global `File` yet.
  21. */
  22. function makeFile(fileBits, fileName, options) {
  23. (0, exports.checkFileSupport)();
  24. return new File(fileBits, fileName ?? 'unknown_file', options);
  25. }
  26. function getName(value) {
  27. return (((typeof value === 'object' &&
  28. value !== null &&
  29. (('name' in value && value.name && String(value.name)) ||
  30. ('url' in value && value.url && String(value.url)) ||
  31. ('filename' in value && value.filename && String(value.filename)) ||
  32. ('path' in value && value.path && String(value.path)))) ||
  33. '')
  34. .split(/[\\/]/)
  35. .pop() || undefined);
  36. }
  37. const isAsyncIterable = (value) => value != null && typeof value === 'object' && typeof value[Symbol.asyncIterator] === 'function';
  38. exports.isAsyncIterable = isAsyncIterable;
  39. /**
  40. * Returns a multipart/form-data request if any part of the given request body contains a File / Blob value.
  41. * Otherwise returns the request as is.
  42. */
  43. const maybeMultipartFormRequestOptions = async (opts, fetch) => {
  44. if (!hasUploadableValue(opts.body))
  45. return opts;
  46. return { ...opts, body: await (0, exports.createForm)(opts.body, fetch) };
  47. };
  48. exports.maybeMultipartFormRequestOptions = maybeMultipartFormRequestOptions;
  49. const multipartFormRequestOptions = async (opts, fetch) => {
  50. return { ...opts, body: await (0, exports.createForm)(opts.body, fetch) };
  51. };
  52. exports.multipartFormRequestOptions = multipartFormRequestOptions;
  53. const supportsFormDataMap = /* @__PURE__ */ new WeakMap();
  54. /**
  55. * node-fetch doesn't support the global FormData object in recent node versions. Instead of sending
  56. * properly-encoded form data, it just stringifies the object, resulting in a request body of "[object FormData]".
  57. * This function detects if the fetch function provided supports the global FormData object to avoid
  58. * confusing error messages later on.
  59. */
  60. function supportsFormData(fetchObject) {
  61. const fetch = typeof fetchObject === 'function' ? fetchObject : fetchObject.fetch;
  62. const cached = supportsFormDataMap.get(fetch);
  63. if (cached)
  64. return cached;
  65. const promise = (async () => {
  66. try {
  67. const FetchResponse = ('Response' in fetch ?
  68. fetch.Response
  69. : (await fetch('data:,')).constructor);
  70. const data = new FormData();
  71. if (data.toString() === (await new FetchResponse(data).text())) {
  72. return false;
  73. }
  74. return true;
  75. }
  76. catch {
  77. // avoid false negatives
  78. return true;
  79. }
  80. })();
  81. supportsFormDataMap.set(fetch, promise);
  82. return promise;
  83. }
  84. const createForm = async (body, fetch) => {
  85. if (!(await supportsFormData(fetch))) {
  86. throw new TypeError('The provided fetch function does not support file uploads with the current global FormData class.');
  87. }
  88. const form = new FormData();
  89. await Promise.all(Object.entries(body || {}).map(([key, value]) => addFormValue(form, key, value)));
  90. return form;
  91. };
  92. exports.createForm = createForm;
  93. // We check for Blob not File because Bun.File doesn't inherit from File,
  94. // but they both inherit from Blob and have a `name` property at runtime.
  95. const isNamedBlob = (value) => value instanceof Blob && 'name' in value;
  96. const isUploadable = (value) => typeof value === 'object' &&
  97. value !== null &&
  98. (value instanceof Response || (0, exports.isAsyncIterable)(value) || isNamedBlob(value));
  99. const hasUploadableValue = (value) => {
  100. if (isUploadable(value))
  101. return true;
  102. if (Array.isArray(value))
  103. return value.some(hasUploadableValue);
  104. if (value && typeof value === 'object') {
  105. for (const k in value) {
  106. if (hasUploadableValue(value[k]))
  107. return true;
  108. }
  109. }
  110. return false;
  111. };
  112. const addFormValue = async (form, key, value) => {
  113. if (value === undefined)
  114. return;
  115. if (value == null) {
  116. throw new TypeError(`Received null for "${key}"; to pass null in FormData, you must use the string 'null'`);
  117. }
  118. // TODO: make nested formats configurable
  119. if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
  120. form.append(key, String(value));
  121. }
  122. else if (value instanceof Response) {
  123. form.append(key, makeFile([await value.blob()], getName(value)));
  124. }
  125. else if ((0, exports.isAsyncIterable)(value)) {
  126. form.append(key, makeFile([await new Response((0, shims_1.ReadableStreamFrom)(value)).blob()], getName(value)));
  127. }
  128. else if (isNamedBlob(value)) {
  129. form.append(key, value, getName(value));
  130. }
  131. else if (Array.isArray(value)) {
  132. await Promise.all(value.map((entry) => addFormValue(form, key + '[]', entry)));
  133. }
  134. else if (typeof value === 'object') {
  135. await Promise.all(Object.entries(value).map(([name, prop]) => addFormValue(form, `${key}[${name}]`, prop)));
  136. }
  137. else {
  138. throw new TypeError(`Invalid value given to form, expected a string, number, boolean, object, Array, File or Blob but got ${value} instead`);
  139. }
  140. };
  141. //# sourceMappingURL=uploads.js.map