uploads.mjs 5.4 KB

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