소스 검색

@uppy/utils: refactor to TS (#4699)


Co-authored-by: Mikael Finstad <finstaden@gmail.com>
Co-authored-by: Nick Rutten <2504906+nickrttn@users.noreply.github.com>
Co-authored-by: Murderlon <merlijn@soverin.net>
Co-authored-by: Artur Paikin <artur@arturpaikin.com>
Antoine du Hamel 1 년 전
부모
커밋
51ecc66e64
99개의 변경된 파일1083개의 추가작업 그리고 703개의 파일을 삭제
  1. 3 0
      packages/@uppy/core/types/index.d.ts
  2. 1 0
      packages/@uppy/utils/package.json
  3. 0 13
      packages/@uppy/utils/src/AbortController.js
  4. 3 3
      packages/@uppy/utils/src/AbortController.test.ts
  5. 22 0
      packages/@uppy/utils/src/AbortController.ts
  6. 0 13
      packages/@uppy/utils/src/ErrorWithCause.js
  7. 0 21
      packages/@uppy/utils/src/ErrorWithCause.test.js
  8. 24 0
      packages/@uppy/utils/src/ErrorWithCause.test.ts
  9. 23 0
      packages/@uppy/utils/src/ErrorWithCause.ts
  10. 0 83
      packages/@uppy/utils/src/EventManager.js
  11. 115 0
      packages/@uppy/utils/src/EventManager.ts
  12. 18 0
      packages/@uppy/utils/src/FileProgress.ts
  13. 0 11
      packages/@uppy/utils/src/NetworkError.js
  14. 19 0
      packages/@uppy/utils/src/NetworkError.ts
  15. 10 7
      packages/@uppy/utils/src/ProgressTimeout.ts
  16. 35 13
      packages/@uppy/utils/src/RateLimitedQueue.test.js
  17. 50 30
      packages/@uppy/utils/src/Translator.test.ts
  18. 49 25
      packages/@uppy/utils/src/Translator.ts
  19. 41 0
      packages/@uppy/utils/src/UppyFile.ts
  20. 5 1
      packages/@uppy/utils/src/canvasToBlob.ts
  21. 1 1
      packages/@uppy/utils/src/dataURItoBlob.test.ts
  22. 8 4
      packages/@uppy/utils/src/dataURItoBlob.ts
  23. 0 5
      packages/@uppy/utils/src/dataURItoFile.js
  24. 1 1
      packages/@uppy/utils/src/dataURItoFile.test.ts
  25. 8 0
      packages/@uppy/utils/src/dataURItoFile.ts
  26. 7 4
      packages/@uppy/utils/src/delay.test.ts
  27. 7 8
      packages/@uppy/utils/src/delay.ts
  28. 0 17
      packages/@uppy/utils/src/emaFilter.js
  29. 4 2
      packages/@uppy/utils/src/emaFilter.test.ts
  30. 22 0
      packages/@uppy/utils/src/emaFilter.ts
  31. 7 1
      packages/@uppy/utils/src/emitSocketProgress.ts
  32. 0 15
      packages/@uppy/utils/src/fetchWithNetworkError.js
  33. 16 0
      packages/@uppy/utils/src/fetchWithNetworkError.ts
  34. 0 10
      packages/@uppy/utils/src/fileFilters.js
  35. 14 0
      packages/@uppy/utils/src/fileFilters.ts
  36. 4 5
      packages/@uppy/utils/src/findAllDOMElements.ts
  37. 5 5
      packages/@uppy/utils/src/findDOMElement.ts
  38. 4 12
      packages/@uppy/utils/src/findIndex.test.js
  39. 1 1
      packages/@uppy/utils/src/generateFileID.test.js
  40. 23 17
      packages/@uppy/utils/src/generateFileID.ts
  41. 0 3
      packages/@uppy/utils/src/getBytesRemaining.js
  42. 5 1
      packages/@uppy/utils/src/getBytesRemaining.test.ts
  43. 5 0
      packages/@uppy/utils/src/getBytesRemaining.ts
  44. 14 7
      packages/@uppy/utils/src/getDroppedFiles/index.ts
  45. 4 2
      packages/@uppy/utils/src/getDroppedFiles/utils/fallbackApi.ts
  46. 13 8
      packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.ts
  47. 0 96
      packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js
  48. 154 0
      packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.ts
  49. 1 1
      packages/@uppy/utils/src/getETA.test.js
  50. 4 3
      packages/@uppy/utils/src/getETA.ts
  51. 1 1
      packages/@uppy/utils/src/getFileNameAndExtension.test.ts
  52. 4 4
      packages/@uppy/utils/src/getFileNameAndExtension.ts
  53. 0 14
      packages/@uppy/utils/src/getFileType.js
  54. 14 10
      packages/@uppy/utils/src/getFileType.test.ts
  55. 17 0
      packages/@uppy/utils/src/getFileType.ts
  56. 1 1
      packages/@uppy/utils/src/getFileTypeExtension.test.ts
  57. 4 3
      packages/@uppy/utils/src/getFileTypeExtension.ts
  58. 0 22
      packages/@uppy/utils/src/getSocketHost.test.js
  59. 22 0
      packages/@uppy/utils/src/getSocketHost.test.ts
  60. 2 2
      packages/@uppy/utils/src/getSocketHost.ts
  61. 6 2
      packages/@uppy/utils/src/getSpeed.test.ts
  62. 3 1
      packages/@uppy/utils/src/getSpeed.ts
  63. 2 5
      packages/@uppy/utils/src/getTextDirection.ts
  64. 2 5
      packages/@uppy/utils/src/getTimeStamp.ts
  65. 0 3
      packages/@uppy/utils/src/hasProperty.js
  66. 6 0
      packages/@uppy/utils/src/hasProperty.ts
  67. 0 8
      packages/@uppy/utils/src/isDOMElement.js
  68. 8 0
      packages/@uppy/utils/src/isDOMElement.ts
  69. 1 3
      packages/@uppy/utils/src/isDragDropSupported.ts
  70. 0 15
      packages/@uppy/utils/src/isMobileDevice.js
  71. 0 30
      packages/@uppy/utils/src/isMobileDevice.test.js
  72. 38 0
      packages/@uppy/utils/src/isMobileDevice.test.ts
  73. 15 0
      packages/@uppy/utils/src/isMobileDevice.ts
  74. 5 5
      packages/@uppy/utils/src/isNetworkError.test.ts
  75. 1 1
      packages/@uppy/utils/src/isNetworkError.ts
  76. 1 1
      packages/@uppy/utils/src/isObjectURL.test.ts
  77. 1 4
      packages/@uppy/utils/src/isObjectURL.ts
  78. 0 12
      packages/@uppy/utils/src/isPreviewSupported.test.js
  79. 22 0
      packages/@uppy/utils/src/isPreviewSupported.test.ts
  80. 1 1
      packages/@uppy/utils/src/isPreviewSupported.ts
  81. 1 1
      packages/@uppy/utils/src/isTouchDevice.test.js
  82. 1 1
      packages/@uppy/utils/src/isTouchDevice.ts
  83. 0 58
      packages/@uppy/utils/src/mimeTypes.js
  84. 59 0
      packages/@uppy/utils/src/mimeTypes.ts
  85. 0 14
      packages/@uppy/utils/src/prettyETA.js
  86. 1 1
      packages/@uppy/utils/src/prettyETA.test.ts
  87. 28 0
      packages/@uppy/utils/src/prettyETA.ts
  88. 0 9
      packages/@uppy/utils/src/remoteFileObjToLocal.js
  89. 19 0
      packages/@uppy/utils/src/remoteFileObjToLocal.ts
  90. 0 0
      packages/@uppy/utils/src/sampleImageDataURI.ts
  91. 1 1
      packages/@uppy/utils/src/secondsToTime.test.ts
  92. 7 1
      packages/@uppy/utils/src/secondsToTime.ts
  93. 2 8
      packages/@uppy/utils/src/toArray.test.ts
  94. 0 0
      packages/@uppy/utils/src/toArray.ts
  95. 1 1
      packages/@uppy/utils/src/truncateString.test.ts
  96. 6 6
      packages/@uppy/utils/src/truncateString.ts
  97. 12 0
      packages/@uppy/utils/tsconfig.build.json
  98. 10 0
      packages/@uppy/utils/tsconfig.json
  99. 8 0
      yarn.lock

+ 3 - 0
packages/@uppy/core/types/index.d.ts

@@ -1,3 +1,6 @@
+// This references the old types on purpose, to make sure to not create breaking changes for TS consumers.
+// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+/// <reference path="../../utils/types/index.d.ts"/>
 import * as UppyUtils from '@uppy/utils'
 
 // Utility types

+ 1 - 0
packages/@uppy/utils/package.json

@@ -72,6 +72,7 @@
     "preact": "^10.5.13"
   },
   "devDependencies": {
+    "@types/lodash": "^4.14.199",
     "vitest": "^0.34.5"
   }
 }

+ 0 - 13
packages/@uppy/utils/src/AbortController.js

@@ -1,13 +0,0 @@
-import hasOwnProperty from './hasProperty.js'
-/**
- * Little AbortController proxy module so we can swap out the implementation easily later.
- */
-export const { AbortController } = globalThis
-export const { AbortSignal } = globalThis
-export const createAbortError = (message = 'Aborted', options) => {
-  const err = new DOMException(message, 'AbortError')
-  if (options != null && hasOwnProperty(options, 'cause')) {
-    Object.defineProperty(err, 'cause', { __proto__: null, configurable: true, writable: true, value: options.cause })
-  }
-  return err
-}

+ 3 - 3
packages/@uppy/utils/src/AbortController.test.js → packages/@uppy/utils/src/AbortController.test.ts

@@ -1,8 +1,8 @@
 import { describe, expect, it, vi } from 'vitest'
-import { AbortController, AbortSignal } from './AbortController.js'
+import { AbortController, AbortSignal } from './AbortController.ts'
 
-function flushInstantTimeouts () {
-  return new Promise(resolve => setTimeout(resolve, 0))
+function flushInstantTimeouts() {
+  return new Promise((resolve) => setTimeout(resolve, 0))
 }
 
 describe('AbortController', () => {

+ 22 - 0
packages/@uppy/utils/src/AbortController.ts

@@ -0,0 +1,22 @@
+import hasOwnProperty from './hasProperty.ts'
+/**
+ * Little AbortController proxy module so we can swap out the implementation easily later.
+ */
+export const { AbortController } = globalThis
+export const { AbortSignal } = globalThis
+export const createAbortError = (
+  message = 'Aborted',
+  options?: Parameters<typeof Error>[1],
+): DOMException => {
+  const err = new DOMException(message, 'AbortError')
+  if (options != null && hasOwnProperty(options, 'cause')) {
+    Object.defineProperty(err, 'cause', {
+      // @ts-expect-error TS is drunk
+      __proto__: null,
+      configurable: true,
+      writable: true,
+      value: options.cause,
+    })
+  }
+  return err
+}

+ 0 - 13
packages/@uppy/utils/src/ErrorWithCause.js

@@ -1,13 +0,0 @@
-import hasProperty from './hasProperty.js'
-
-class ErrorWithCause extends Error {
-  constructor (message, options = {}) {
-    super(message)
-    this.cause = options.cause
-    if (this.cause && hasProperty(this.cause, 'isNetworkError')) {
-      this.isNetworkError = this.cause.isNetworkError
-    }
-  }
-}
-
-export default ErrorWithCause

+ 0 - 21
packages/@uppy/utils/src/ErrorWithCause.test.js

@@ -1,21 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import ErrorWithCause from './ErrorWithCause.js'
-import NetworkError from './NetworkError.js'
-import isNetworkError from './isNetworkError.js'
-
-describe('ErrorWithCause', () => {
-  it('should support a `{ cause }` option', () => {
-    const cause = new Error('cause')
-    expect(new ErrorWithCause('message').cause).toEqual(undefined)
-    expect(new ErrorWithCause('message', {}).cause).toEqual(undefined)
-    expect(new ErrorWithCause('message', { cause }).cause).toEqual(cause)
-  })
-  it('should propagate isNetworkError', () => {
-    const regularError = new Error('cause')
-    const networkError = new NetworkError('cause')
-    expect(isNetworkError(new ErrorWithCause('message', { cause: networkError }).isNetworkError)).toEqual(true)
-    expect(isNetworkError(new ErrorWithCause('message', { cause: regularError }).isNetworkError)).toEqual(false)
-    expect(isNetworkError(new ErrorWithCause('message', {}).isNetworkError)).toEqual(false)
-    expect(isNetworkError(new ErrorWithCause('message').isNetworkError)).toEqual(false)
-  })
-})

+ 24 - 0
packages/@uppy/utils/src/ErrorWithCause.test.ts

@@ -0,0 +1,24 @@
+import { describe, expect, it } from 'vitest'
+import ErrorWithCause from './ErrorWithCause.ts'
+import NetworkError from './NetworkError.ts'
+
+describe('ErrorWithCause', () => {
+  it('should support a `{ cause }` option', () => {
+    const cause = new Error('cause')
+    expect(new ErrorWithCause('message').cause).toEqual(undefined)
+    expect(new ErrorWithCause('message', {}).cause).toEqual(undefined)
+    expect(new ErrorWithCause('message', { cause }).cause).toEqual(cause)
+  })
+  it('should propagate isNetworkError', () => {
+    const regularError = new Error('cause')
+    const networkError = new NetworkError('cause')
+    expect(
+      new ErrorWithCause('message', { cause: networkError }).isNetworkError,
+    ).toEqual(true)
+    expect(
+      new ErrorWithCause('message', { cause: regularError }).isNetworkError,
+    ).toEqual(false)
+    expect(new ErrorWithCause('message', {}).isNetworkError).toEqual(false)
+    expect(new ErrorWithCause('message').isNetworkError).toEqual(false)
+  })
+})

+ 23 - 0
packages/@uppy/utils/src/ErrorWithCause.ts

@@ -0,0 +1,23 @@
+import type NetworkError from './NetworkError.ts'
+import hasProperty from './hasProperty.ts'
+
+class ErrorWithCause extends Error {
+  public isNetworkError: boolean
+
+  public cause: Error['cause']
+
+  constructor(
+    message?: ConstructorParameters<ErrorConstructor>[0],
+    options?: ConstructorParameters<ErrorConstructor>[1],
+  ) {
+    super(message)
+    this.cause = options?.cause
+    if (this.cause && hasProperty(this.cause, 'isNetworkError')) {
+      this.isNetworkError = (this.cause as NetworkError).isNetworkError
+    } else {
+      this.isNetworkError = false
+    }
+  }
+}
+
+export default ErrorWithCause

+ 0 - 83
packages/@uppy/utils/src/EventManager.js

@@ -1,83 +0,0 @@
-/**
- * Create a wrapper around an event emitter with a `remove` method to remove
- * all events that were added using the wrapped emitter.
- */
-export default class EventManager {
-  #uppy
-
-  #events = []
-
-  constructor (uppy) {
-    this.#uppy = uppy
-  }
-
-  on (event, fn) {
-    this.#events.push([event, fn])
-    return this.#uppy.on(event, fn)
-  }
-
-  remove () {
-    for (const [event, fn] of this.#events.splice(0)) {
-      this.#uppy.off(event, fn)
-    }
-  }
-
-  onFilePause (fileID, cb) {
-    this.on('upload-pause', (targetFileID, isPaused) => {
-      if (fileID === targetFileID) {
-        cb(isPaused)
-      }
-    })
-  }
-
-  onFileRemove (fileID, cb) {
-    this.on('file-removed', (file) => {
-      if (fileID === file.id) cb(file.id)
-    })
-  }
-
-  onPause (fileID, cb) {
-    this.on('upload-pause', (targetFileID, isPaused) => {
-      if (fileID === targetFileID) {
-        // const isPaused = this.#uppy.pauseResume(fileID)
-        cb(isPaused)
-      }
-    })
-  }
-
-  onRetry (fileID, cb) {
-    this.on('upload-retry', (targetFileID) => {
-      if (fileID === targetFileID) {
-        cb()
-      }
-    })
-  }
-
-  onRetryAll (fileID, cb) {
-    this.on('retry-all', () => {
-      if (!this.#uppy.getFile(fileID)) return
-      cb()
-    })
-  }
-
-  onPauseAll (fileID, cb) {
-    this.on('pause-all', () => {
-      if (!this.#uppy.getFile(fileID)) return
-      cb()
-    })
-  }
-
-  onCancelAll (fileID, eventHandler) {
-    this.on('cancel-all', (...args) => {
-      if (!this.#uppy.getFile(fileID)) return
-      eventHandler(...args)
-    })
-  }
-
-  onResumeAll (fileID, cb) {
-    this.on('resume-all', () => {
-      if (!this.#uppy.getFile(fileID)) return
-      cb()
-    })
-  }
-}

+ 115 - 0
packages/@uppy/utils/src/EventManager.ts

@@ -0,0 +1,115 @@
+import type {
+  Uppy,
+  UploadPauseCallback,
+  FileRemovedCallback,
+  UploadRetryCallback,
+  GenericEventCallback,
+} from '@uppy/core'
+import type { UppyFile } from './UppyFile'
+/**
+ * Create a wrapper around an event emitter with a `remove` method to remove
+ * all events that were added using the wrapped emitter.
+ */
+export default class EventManager {
+  #uppy: Uppy
+
+  #events: Array<Parameters<typeof Uppy.prototype.on>> = []
+
+  constructor(uppy: Uppy) {
+    this.#uppy = uppy
+  }
+
+  on(
+    event: Parameters<typeof Uppy.prototype.on>[0],
+    fn: Parameters<typeof Uppy.prototype.on>[1],
+  ): Uppy {
+    this.#events.push([event, fn])
+    return this.#uppy.on(event, fn)
+  }
+
+  remove(): void {
+    for (const [event, fn] of this.#events.splice(0)) {
+      this.#uppy.off(event, fn)
+    }
+  }
+
+  onFilePause(fileID: UppyFile['id'], cb: (isPaused: boolean) => void): void {
+    this.on(
+      'upload-pause',
+      (
+        targetFileID: Parameters<UploadPauseCallback>[0],
+        isPaused: Parameters<UploadPauseCallback>[1],
+      ) => {
+        if (fileID === targetFileID) {
+          cb(isPaused)
+        }
+      },
+    )
+  }
+
+  onFileRemove(
+    fileID: UppyFile['id'],
+    cb: (isPaused: UppyFile['id']) => void,
+  ): void {
+    this.on('file-removed', (file: Parameters<FileRemovedCallback<any>>[0]) => {
+      if (fileID === file.id) cb(file.id)
+    })
+  }
+
+  onPause(fileID: UppyFile['id'], cb: (isPaused: boolean) => void): void {
+    this.on(
+      'upload-pause',
+      (
+        targetFileID: Parameters<UploadPauseCallback>[0],
+        isPaused: Parameters<UploadPauseCallback>[1],
+      ) => {
+        if (fileID === targetFileID) {
+          // const isPaused = this.#uppy.pauseResume(fileID)
+          cb(isPaused)
+        }
+      },
+    )
+  }
+
+  onRetry(fileID: UppyFile['id'], cb: () => void): void {
+    this.on(
+      'upload-retry',
+      (targetFileID: Parameters<UploadRetryCallback>[0]) => {
+        if (fileID === targetFileID) {
+          cb()
+        }
+      },
+    )
+  }
+
+  onRetryAll(fileID: UppyFile['id'], cb: () => void): void {
+    this.on('retry-all', () => {
+      if (!this.#uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+
+  onPauseAll(fileID: UppyFile['id'], cb: () => void): void {
+    this.on('pause-all', () => {
+      if (!this.#uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+
+  onCancelAll(
+    fileID: UppyFile['id'],
+    eventHandler: GenericEventCallback,
+  ): void {
+    this.on('cancel-all', (...args: Parameters<GenericEventCallback>) => {
+      if (!this.#uppy.getFile(fileID)) return
+      eventHandler(...args)
+    })
+  }
+
+  onResumeAll(fileID: UppyFile['id'], cb: () => void): void {
+    this.on('resume-all', () => {
+      if (!this.#uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+}

+ 18 - 0
packages/@uppy/utils/src/FileProgress.ts

@@ -0,0 +1,18 @@
+interface FileProgressBase {
+  progress: number
+  uploadComplete: boolean
+  percentage: number
+  bytesTotal: number
+}
+
+// FileProgress is either started or not started. We want to make sure TS doesn't
+// let us mix the two cases, and for that effect, we have one type for each case:
+export type FileProgressStarted = FileProgressBase & {
+  uploadStarted: number
+  bytesUploaded: number
+}
+export type FileProgressNotStarted = FileProgressBase & {
+  uploadStarted: null
+  bytesUploaded: false
+}
+export type FileProgress = FileProgressStarted | FileProgressNotStarted

+ 0 - 11
packages/@uppy/utils/src/NetworkError.js

@@ -1,11 +0,0 @@
-class NetworkError extends Error {
-  constructor (error, xhr = null) {
-    super(`This looks like a network error, the endpoint might be blocked by an internet provider or a firewall.`)
-
-    this.cause = error
-    this.isNetworkError = true
-    this.request = xhr
-  }
-}
-
-export default NetworkError

+ 19 - 0
packages/@uppy/utils/src/NetworkError.ts

@@ -0,0 +1,19 @@
+class NetworkError extends Error {
+  public cause: unknown
+
+  public isNetworkError: true
+
+  public request: null | XMLHttpRequest
+
+  constructor(error: unknown, xhr: null | XMLHttpRequest = null) {
+    super(
+      `This looks like a network error, the endpoint might be blocked by an internet provider or a firewall.`,
+    )
+
+    this.cause = error
+    this.isNetworkError = true
+    this.request = xhr
+  }
+}
+
+export default NetworkError

+ 10 - 7
packages/@uppy/utils/src/ProgressTimeout.js → packages/@uppy/utils/src/ProgressTimeout.ts

@@ -5,20 +5,23 @@
  * Call `timer.done()` when the upload has completed.
  */
 class ProgressTimeout {
-  #aliveTimer
+  #aliveTimer?: ReturnType<typeof setTimeout>
 
   #isDone = false
 
-  #onTimedOut
+  #onTimedOut: Parameters<typeof setTimeout>[0]
 
-  #timeout
+  #timeout: number
 
-  constructor (timeout, timeoutHandler) {
+  constructor(
+    timeout: number,
+    timeoutHandler: Parameters<typeof setTimeout>[0],
+  ) {
     this.#timeout = timeout
     this.#onTimedOut = timeoutHandler
   }
 
-  progress () {
+  progress(): void {
     // Some browsers fire another progress event when the upload is
     // cancelled, so we have to ignore progress after the timer was
     // told to stop.
@@ -30,10 +33,10 @@ class ProgressTimeout {
     }
   }
 
-  done () {
+  done(): void {
     if (!this.#isDone) {
       clearTimeout(this.#aliveTimer)
-      this.#aliveTimer = null
+      this.#aliveTimer = undefined
       this.#isDone = true
     }
   }

+ 35 - 13
packages/@uppy/utils/src/RateLimitedQueue.test.js

@@ -1,11 +1,10 @@
 import { describe, expect, it } from 'vitest'
 import { RateLimitedQueue } from './RateLimitedQueue.js'
-
-const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
+import delay from './delay.ts'
 
 describe('RateLimitedQueue', () => {
   let pending = 0
-  function fn () {
+  function fn() {
     pending++
     return delay(15).then(() => pending--)
   }
@@ -15,9 +14,16 @@ describe('RateLimitedQueue', () => {
     const fn2 = queue.wrapPromiseFunction(fn)
 
     const result = Promise.all([
-      fn2(), fn2(), fn2(), fn2(),
-      fn2(), fn2(), fn2(), fn2(),
-      fn2(), fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
     ])
 
     expect(pending).toBe(4)
@@ -34,9 +40,16 @@ describe('RateLimitedQueue', () => {
     const fn2 = queue.wrapPromiseFunction(fn)
 
     const result = Promise.all([
-      fn2(), fn2(), fn2(), fn2(),
-      fn2(), fn2(), fn2(), fn2(),
-      fn2(), fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
     ])
 
     expect(pending).toBe(10)
@@ -48,13 +61,22 @@ describe('RateLimitedQueue', () => {
 
   it('should accept non-promise function in wrapPromiseFunction()', () => {
     const queue = new RateLimitedQueue(1)
-    function syncFn () { return 1 }
+    function syncFn() {
+      return 1
+    }
     const fn2 = queue.wrapPromiseFunction(syncFn)
 
     return Promise.all([
-      fn2(), fn2(), fn2(), fn2(),
-      fn2(), fn2(), fn2(), fn2(),
-      fn2(), fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
+      fn2(),
     ])
   })
 })

+ 50 - 30
packages/@uppy/utils/src/Translator.test.js → packages/@uppy/utils/src/Translator.test.ts

@@ -1,7 +1,7 @@
 import { describe, expect, it } from 'vitest'
-import Translator from './Translator.js'
+import Translator, { type Locale } from './Translator.ts'
 
-const english = {
+const english: Locale<0 | 1> = {
   strings: {
     chooseFile: 'Choose a file',
     youHaveChosen: 'You have chosen: %{fileName}',
@@ -9,12 +9,12 @@ const english = {
       0: '%{smart_count} file selected',
       1: '%{smart_count} files selected',
     },
-    pluralize (n) {
-      if (n === 1) {
-        return 0
-      }
-      return 1
-    },
+  },
+  pluralize(n) {
+    if (n === 1) {
+      return 0
+    }
+    return 1
   },
 }
 
@@ -28,7 +28,7 @@ const russian = {
       2: 'Выбрано %{smart_count} файлов',
     },
   },
-  pluralize (n) {
+  pluralize(n: number) {
     if (n % 10 === 1 && n % 100 !== 11) {
       return 0
     }
@@ -50,41 +50,58 @@ describe('Translator', () => {
 
     it('should translate a string with non-string elements', () => {
       const translator = new Translator({
+        pluralize: english.pluralize,
         strings: {
           test: 'Hello %{who}!',
           test2: 'Hello %{who}',
         },
       })
 
-      const who = Symbol('who')
-      expect(translator.translateArray('test', { who })).toEqual(['Hello ', who, '!'])
+      const who = Symbol('who') as any as string
+      expect(translator.translateArray('test', { who })).toEqual([
+        'Hello ',
+        who,
+        '!',
+      ])
       // No empty string at the end.
-      expect(translator.translateArray('test2', { who })).toEqual(['Hello ', who])
+      expect(translator.translateArray('test2', { who })).toEqual([
+        'Hello ',
+        who,
+      ])
     })
   })
 
   describe('translation strings inheritance / overriding', () => {
     const launguagePackLoadedInCore = english
     const defaultStrings = {
+      pluralize: english.pluralize,
       strings: {
         youHaveChosen: 'You have chosen 123: %{fileName}',
       },
     }
     const userSuppliedStrings = {
+      pluralize: english.pluralize,
       strings: {
         youHaveChosen: 'Beep boop: %{fileName}',
       },
     }
 
     it('should prioritize language pack strings from Core over default', () => {
-      const translator = new Translator([defaultStrings, launguagePackLoadedInCore])
+      const translator = new Translator([
+        defaultStrings,
+        launguagePackLoadedInCore,
+      ])
       expect(
         translator.translate('youHaveChosen', { fileName: 'img.jpg' }),
       ).toEqual('You have chosen: img.jpg')
     })
 
     it('should prioritize user-supplied strings over language pack from Core', () => {
-      const translator = new Translator([defaultStrings, launguagePackLoadedInCore, userSuppliedStrings])
+      const translator = new Translator([
+        defaultStrings,
+        launguagePackLoadedInCore,
+        userSuppliedStrings,
+      ])
       expect(
         translator.translate('youHaveChosen', { fileName: 'img.jpg' }),
       ).toEqual('Beep boop: img.jpg')
@@ -103,17 +120,17 @@ describe('Translator', () => {
   describe('pluralization', () => {
     it('should translate a string', () => {
       const translator = new Translator(russian)
-      expect(
-        translator.translate('filesChosen', { smart_count: 18 }),
-      ).toEqual('Выбрано 18 файлов')
+      expect(translator.translate('filesChosen', { smart_count: 18 })).toEqual(
+        'Выбрано 18 файлов',
+      )
 
-      expect(
-        translator.translate('filesChosen', { smart_count: 1 }),
-      ).toEqual('Выбран 1 файл')
+      expect(translator.translate('filesChosen', { smart_count: 1 })).toEqual(
+        'Выбран 1 файл',
+      )
 
-      expect(
-        translator.translate('filesChosen', { smart_count: 0 }),
-      ).toEqual('Выбрано 0 файлов')
+      expect(translator.translate('filesChosen', { smart_count: 0 })).toEqual(
+        'Выбрано 0 файлов',
+      )
     })
 
     it('should support strings without plural forms', () => {
@@ -124,12 +141,12 @@ describe('Translator', () => {
         pluralize: () => 0,
       })
 
-      expect(
-        translator.translate('theAmount', { smart_count: 0 }),
-      ).toEqual('het aantal is 0')
-      expect(
-        translator.translate('theAmount', { smart_count: 1 }),
-      ).toEqual('het aantal is 1')
+      expect(translator.translate('theAmount', { smart_count: 0 })).toEqual(
+        'het aantal is 0',
+      )
+      expect(translator.translate('theAmount', { smart_count: 1 })).toEqual(
+        'het aantal is 1',
+      )
       expect(
         translator.translate('theAmount', { smart_count: 1202530 }),
       ).toEqual('het aantal is 1202530')
@@ -143,11 +160,14 @@ describe('Translator', () => {
             1: '%{smart_count} tests',
           },
         },
+        pluralize: () => 1,
       })
 
       expect(() => {
         translator.translate('test')
-      }).toThrow('Attempted to use a string with plural forms, but no value was given for %{smart_count}')
+      }).toThrow(
+        'Attempted to use a string with plural forms, but no value was given for %{smart_count}',
+      )
     })
   })
 })

+ 49 - 25
packages/@uppy/utils/src/Translator.js → packages/@uppy/utils/src/Translator.ts

@@ -1,7 +1,23 @@
-import has from './hasProperty.js'
+import has from './hasProperty.ts'
 
-function insertReplacement (source, rx, replacement) {
-  const newParts = []
+// We're using a generic because languages have different plural rules.
+export interface Locale<T extends number = number> {
+  strings: Record<string, string | Record<T, string>>
+  pluralize: (n: number) => T
+}
+
+type Options = {
+  smart_count?: number
+} & {
+  [key: string]: string | number
+}
+
+function insertReplacement(
+  source: Array<string | unknown>,
+  rx: RegExp,
+  replacement: string,
+): Array<string | unknown> {
+  const newParts: Array<string | unknown> = []
   source.forEach((chunk) => {
     // When the source contains multiple placeholders for interpolation,
     // we should ignore chunks that are not strings, because those
@@ -32,14 +48,16 @@ function insertReplacement (source, rx, replacement) {
  * @license https://github.com/airbnb/polyglot.js/blob/master/LICENSE
  * taken from https://github.com/airbnb/polyglot.js/blob/master/lib/polyglot.js#L299
  *
- * @param {string} phrase that needs interpolation, with placeholders
- * @param {object} options with values that will be used to replace placeholders
- * @returns {any[]} interpolated
+ * @param phrase that needs interpolation, with placeholders
+ * @param options with values that will be used to replace placeholders
  */
-function interpolate (phrase, options) {
+function interpolate(
+  phrase: string,
+  options?: Options,
+): Array<string | unknown> {
   const dollarRegex = /\$/g
   const dollarBillsYall = '$$$$'
-  let interpolated = [phrase]
+  let interpolated: Array<string | unknown> = [phrase]
 
   if (options == null) return interpolated
 
@@ -55,7 +73,11 @@ function interpolate (phrase, options) {
       // We create a new `RegExp` each time instead of using a more-efficient
       // string replace so that the same argument can be replaced multiple times
       // in the same phrase.
-      interpolated = insertReplacement(interpolated, new RegExp(`%\\{${arg}\\}`, 'g'), replacement)
+      interpolated = insertReplacement(
+        interpolated,
+        new RegExp(`%\\{${arg}\\}`, 'g'),
+        replacement as string,
+      )
     }
   }
 
@@ -74,13 +96,12 @@ function interpolate (phrase, options) {
  * Usage example: `translator.translate('files_chosen', {smart_count: 3})`
  */
 export default class Translator {
-  /**
-   * @param {object|Array<object>} locales - locale or list of locales.
-   */
-  constructor (locales) {
+  protected locale: Locale
+
+  constructor(locales: Locale | Locale[]) {
     this.locale = {
       strings: {},
-      pluralize (n) {
+      pluralize(n: number): 0 | 1 {
         if (n === 1) {
           return 0
         }
@@ -95,35 +116,36 @@ export default class Translator {
     }
   }
 
-  #apply (locale) {
+  #apply(locale?: Locale): void {
     if (!locale?.strings) {
       return
     }
 
     const prevLocale = this.locale
-    this.locale = { ...prevLocale, strings: { ...prevLocale.strings, ...locale.strings } }
+    this.locale = {
+      ...prevLocale,
+      strings: { ...prevLocale.strings, ...locale.strings },
+    } as any
     this.locale.pluralize = locale.pluralize || prevLocale.pluralize
   }
 
   /**
    * Public translate method
    *
-   * @param {string} key
-   * @param {object} options with values that will be used later to replace placeholders in string
-   * @returns {string} translated (and interpolated)
+   * @param key
+   * @param options with values that will be used later to replace placeholders in string
+   * @returns string translated (and interpolated)
    */
-  translate (key, options) {
+  translate(key: string, options?: Options): string {
     return this.translateArray(key, options).join('')
   }
 
   /**
    * Get a translation and return the translated and interpolated parts as an array.
    *
-   * @param {string} key
-   * @param {object} options with values that will be used to replace placeholders
-   * @returns {Array} The translated and interpolated parts, in order.
+   * @returns The translated and interpolated parts, in order.
    */
-  translateArray (key, options) {
+  translateArray(key: string, options?: Options): Array<string | unknown> {
     if (!has(this.locale.strings, key)) {
       throw new Error(`missing string: ${key}`)
     }
@@ -136,7 +158,9 @@ export default class Translator {
         const plural = this.locale.pluralize(options.smart_count)
         return interpolate(string[plural], options)
       }
-      throw new Error('Attempted to use a string with plural forms, but no value was given for %{smart_count}')
+      throw new Error(
+        'Attempted to use a string with plural forms, but no value was given for %{smart_count}',
+      )
     }
 
     return interpolate(string, options)

+ 41 - 0
packages/@uppy/utils/src/UppyFile.ts

@@ -0,0 +1,41 @@
+import type { FileProgress } from './FileProgress'
+
+interface IndexedObject<T> {
+  [key: string]: T
+  [key: number]: T
+}
+
+export type InternalMetadata = { name: string; type?: string }
+
+export interface UppyFile<
+  TMeta = IndexedObject<any>,
+  TBody = IndexedObject<any>,
+> {
+  data: Blob | File
+  error?: Error
+  extension: string
+  id: string
+  isPaused?: boolean
+  isRestored?: boolean
+  isRemote: boolean
+  meta: InternalMetadata & TMeta
+  name: string
+  preview?: string
+  progress?: FileProgress
+  remote?: {
+    host: string
+    url: string
+    body?: Record<string, unknown>
+    provider?: string
+    companionUrl: string
+  }
+  serverToken: string
+  size: number
+  source?: string
+  type?: string
+  response?: {
+    body: TBody
+    status: number
+    uploadURL: string | undefined
+  }
+}

+ 5 - 1
packages/@uppy/utils/src/canvasToBlob.js → packages/@uppy/utils/src/canvasToBlob.ts

@@ -4,7 +4,11 @@
  * @param {HTMLCanvasElement} canvas
  * @returns {Promise}
  */
-export default function canvasToBlob (canvas, type, quality) {
+export default function canvasToBlob(
+  canvas: HTMLCanvasElement,
+  type: string,
+  quality?: number,
+): Promise<Blob | null> {
   return new Promise((resolve) => {
     canvas.toBlob(resolve, type, quality)
   })

+ 1 - 1
packages/@uppy/utils/src/dataURItoBlob.test.js → packages/@uppy/utils/src/dataURItoBlob.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import dataURItoBlob from './dataURItoBlob.js'
+import dataURItoBlob from './dataURItoBlob.ts'
 import sampleImageDataURI from './sampleImageDataURI.js'
 
 describe('dataURItoBlob', () => {

+ 8 - 4
packages/@uppy/utils/src/dataURItoBlob.js → packages/@uppy/utils/src/dataURItoBlob.ts

@@ -1,21 +1,25 @@
 const DATA_URL_PATTERN = /^data:([^/]+\/[^,;]+(?:[^,]*?))(;base64)?,([\s\S]*)$/
 
-export default function dataURItoBlob (dataURI, opts, toFile) {
+export default function dataURItoBlob(
+  dataURI: string,
+  opts: { mimeType?: string; name?: string },
+  toFile?: boolean,
+): Blob | File {
   // get the base64 data
   const dataURIData = DATA_URL_PATTERN.exec(dataURI)
 
   // user may provide mime type, if not get it from data URI
   const mimeType = opts.mimeType ?? dataURIData?.[1] ?? 'plain/text'
 
-  let data
-  if (dataURIData[2] != null) {
+  let data!: BlobPart[] // We add `!` to tell TS we're OK with `data` being not defined when the dataURI is invalid.
+  if (dataURIData?.[2] != null) {
     const binary = atob(decodeURIComponent(dataURIData[3]))
     const bytes = new Uint8Array(binary.length)
     for (let i = 0; i < binary.length; i++) {
       bytes[i] = binary.charCodeAt(i)
     }
     data = [bytes]
-  } else {
+  } else if (dataURIData?.[3] != null) {
     data = [decodeURIComponent(dataURIData[3])]
   }
 

+ 0 - 5
packages/@uppy/utils/src/dataURItoFile.js

@@ -1,5 +0,0 @@
-import dataURItoBlob from './dataURItoBlob.js'
-
-export default function dataURItoFile (dataURI, opts) {
-  return dataURItoBlob(dataURI, opts, true)
-}

+ 1 - 1
packages/@uppy/utils/src/dataURItoFile.test.js → packages/@uppy/utils/src/dataURItoFile.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import dataURItoFile from './dataURItoFile.js'
+import dataURItoFile from './dataURItoFile.ts'
 import sampleImageDataURI from './sampleImageDataURI.js'
 
 describe('dataURItoFile', () => {

+ 8 - 0
packages/@uppy/utils/src/dataURItoFile.ts

@@ -0,0 +1,8 @@
+import dataURItoBlob from './dataURItoBlob.ts'
+
+export default function dataURItoFile(
+  dataURI: string,
+  opts: { mimeType?: string; name?: string },
+): File | Blob {
+  return dataURItoBlob(dataURI, opts, true)
+}

+ 7 - 4
packages/@uppy/utils/src/delay.test.js → packages/@uppy/utils/src/delay.test.ts

@@ -1,6 +1,6 @@
 import { describe, expect, it } from 'vitest'
-import { AbortController } from './AbortController.js'
-import delay from './delay.js'
+import { AbortController } from './AbortController.ts'
+import delay from './delay.ts'
 
 describe('delay', () => {
   it('should wait for the specified time', async () => {
@@ -12,9 +12,12 @@ describe('delay', () => {
   })
 
   it('should reject if signal is already aborted', async () => {
-    const signal = { aborted: true }
+    const signal = { aborted: true } as any as AbortSignal
     const start = Date.now()
-    await expect(delay(100, { signal })).rejects.toHaveProperty('name', 'AbortError')
+    await expect(delay(100, { signal })).rejects.toHaveProperty(
+      'name',
+      'AbortError',
+    )
     // should really be instant but using a very large range in case CI decides
     // to be super busy and block the event loop for a while.
     expect(Date.now() - start).toBeLessThan(50)

+ 7 - 8
packages/@uppy/utils/src/delay.js → packages/@uppy/utils/src/delay.ts

@@ -1,13 +1,12 @@
-import { createAbortError } from './AbortController.js'
+import { createAbortError } from './AbortController.ts'
 
 /**
  * Return a Promise that resolves after `ms` milliseconds.
- *
- * @param {number} ms - Number of milliseconds to wait.
- * @param {{ signal?: AbortSignal }} [opts] - An abort signal that can be used to cancel the delay early.
- * @returns {Promise<void>} A Promise that resolves after the given amount of `ms`.
  */
-export default function delay (ms, opts) {
+export default function delay(
+  ms: number,
+  opts?: { signal: AbortSignal },
+): Promise<void> {
   return new Promise((resolve, reject) => {
     if (opts?.signal?.aborted) {
       return reject(createAbortError())
@@ -18,13 +17,13 @@ export default function delay (ms, opts) {
       resolve()
     }, ms)
 
-    function onabort () {
+    function onabort(): void {
       clearTimeout(timeout)
       cleanup() // eslint-disable-line no-use-before-define
       reject(createAbortError())
     }
     opts?.signal?.addEventListener('abort', onabort)
-    function cleanup () {
+    function cleanup(): void {
       opts?.signal?.removeEventListener('abort', onabort)
     }
     return undefined

+ 0 - 17
packages/@uppy/utils/src/emaFilter.js

@@ -1,17 +0,0 @@
-/**
- * Low-pass filter using Exponential Moving Averages (aka exponential smoothing)
- * Filters a sequence of values by updating the mixing the previous output value
- * with the new input using the exponential window function
- *
- * @param {*} newValue the n-th value of the sequence
- * @param {*} previousSmoothedValue the exponential average of the first n-1 values
- * @param {*} halfLife value of `dt` to move the smoothed value halfway between `previousFilteredValue` and `newValue`
- * @param {*} dt time elapsed between adding the (n-1)th and the n-th values
- * @returns the exponential average of the first n values
- */
-export default function emaFilter (newValue, previousSmoothedValue, halfLife, dt) {
-  if (halfLife === 0 || newValue === previousSmoothedValue) return newValue
-  if (dt === 0) return previousSmoothedValue
-
-  return newValue + (previousSmoothedValue - newValue) * (2 ** (-dt / halfLife))
-}

+ 4 - 2
packages/@uppy/utils/src/emaFilter.test.js → packages/@uppy/utils/src/emaFilter.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import emaFilter from './emaFilter.js'
+import emaFilter from './emaFilter.ts'
 
 describe('emaFilter', () => {
   it('should calculate the exponential average', () => {
@@ -25,7 +25,9 @@ describe('emaFilter', () => {
     let lastFilteredValue = firstValue
     for (let i = 0; i < 10; ++i) {
       lastFilteredValue = emaFilter(newValue, lastFilteredValue, halfLife, step)
-      expect(lastFilteredValue).toBeCloseTo(emaFilter(newValue, firstValue, halfLife, step * (i + 1)))
+      expect(lastFilteredValue).toBeCloseTo(
+        emaFilter(newValue, firstValue, halfLife, step * (i + 1)),
+      )
     }
   })
 })

+ 22 - 0
packages/@uppy/utils/src/emaFilter.ts

@@ -0,0 +1,22 @@
+/**
+ * Low-pass filter using Exponential Moving Averages (aka exponential smoothing)
+ * Filters a sequence of values by updating the mixing the previous output value
+ * with the new input using the exponential window function
+ *
+ * @param newValue the n-th value of the sequence
+ * @param previousSmoothedValue the exponential average of the first n-1 values
+ * @param halfLife value of `dt` to move the smoothed value halfway between `previousFilteredValue` and `newValue`
+ * @param dt time elapsed between adding the (n-1)th and the n-th values
+ * @returns the exponential average of the first n values
+ */
+export default function emaFilter(
+  newValue: number,
+  previousSmoothedValue: number,
+  halfLife: number,
+  dt: number,
+): number {
+  if (halfLife === 0 || newValue === previousSmoothedValue) return newValue
+  if (dt === 0) return previousSmoothedValue
+
+  return newValue + (previousSmoothedValue - newValue) * 2 ** (-dt / halfLife)
+}

+ 7 - 1
packages/@uppy/utils/src/emitSocketProgress.js → packages/@uppy/utils/src/emitSocketProgress.ts

@@ -1,6 +1,12 @@
 import throttle from 'lodash/throttle.js'
+import type { UppyFile } from './UppyFile'
+import type { FileProgress } from './FileProgress'
 
-function emitSocketProgress (uploader, progressData, file) {
+function emitSocketProgress(
+  uploader: any,
+  progressData: FileProgress,
+  file: UppyFile,
+): void {
   const { progress, bytesUploaded, bytesTotal } = progressData
   if (progress) {
     uploader.uppy.log(`Upload progress: ${progress}`)

+ 0 - 15
packages/@uppy/utils/src/fetchWithNetworkError.js

@@ -1,15 +0,0 @@
-import NetworkError from './NetworkError.js'
-
-/**
- * Wrapper around window.fetch that throws a NetworkError when appropriate
- */
-export default function fetchWithNetworkError (...options) {
-  return fetch(...options)
-    .catch((err) => {
-      if (err.name === 'AbortError') { // todo maybe instead see npm package is-network-error
-        throw err
-      } else {
-        throw new NetworkError(err)
-      }
-    })
-}

+ 16 - 0
packages/@uppy/utils/src/fetchWithNetworkError.ts

@@ -0,0 +1,16 @@
+import NetworkError from './NetworkError.ts'
+
+/**
+ * Wrapper around window.fetch that throws a NetworkError when appropriate
+ */
+export default function fetchWithNetworkError(
+  ...options: Parameters<typeof globalThis.fetch>
+): ReturnType<typeof globalThis.fetch> {
+  return fetch(...options).catch((err) => {
+    if (err.name === 'AbortError') {
+      throw err
+    } else {
+      throw new NetworkError(err)
+    }
+  })
+}

+ 0 - 10
packages/@uppy/utils/src/fileFilters.js

@@ -1,10 +0,0 @@
-export function filterNonFailedFiles (files) {
-  const hasError = (file) => 'error' in file && file.error
-
-  return files.filter((file) => !hasError(file))
-}
-
-// Don't double-emit upload-started for Golden Retriever-restored files that were already started
-export function filterFilesToEmitUploadStarted (files) {
-  return files.filter((file) => !file.progress.uploadStarted || !file.isRestored)
-}

+ 14 - 0
packages/@uppy/utils/src/fileFilters.ts

@@ -0,0 +1,14 @@
+import type { UppyFile } from './UppyFile'
+
+export function filterNonFailedFiles(files: UppyFile[]): UppyFile[] {
+  const hasError = (file: UppyFile): boolean => 'error' in file && !!file.error
+
+  return files.filter((file) => !hasError(file))
+}
+
+// Don't double-emit upload-started for Golden Retriever-restored files that were already started
+export function filterFilesToEmitUploadStarted(files: UppyFile[]): UppyFile[] {
+  return files.filter(
+    (file) => !file.progress?.uploadStarted || !file.isRestored,
+  )
+}

+ 4 - 5
packages/@uppy/utils/src/findAllDOMElements.js → packages/@uppy/utils/src/findAllDOMElements.ts

@@ -1,12 +1,11 @@
-import isDOMElement from './isDOMElement.js'
+import isDOMElement from './isDOMElement.ts'
 
 /**
  * Find one or more DOM elements.
- *
- * @param {string|Node} element
- * @returns {Node[]|null}
  */
-export default function findAllDOMElements (element) {
+export default function findAllDOMElements(
+  element: string | Node,
+): Node[] | null {
   if (typeof element === 'string') {
     const elements = document.querySelectorAll(element)
     return elements.length === 0 ? null : Array.from(elements)

+ 5 - 5
packages/@uppy/utils/src/findDOMElement.js → packages/@uppy/utils/src/findDOMElement.ts

@@ -1,12 +1,12 @@
-import isDOMElement from './isDOMElement.js'
+import isDOMElement from './isDOMElement.ts'
 
 /**
  * Find a DOM element.
- *
- * @param {Node|string} element
- * @returns {Node|null}
  */
-export default function findDOMElement (element, context = document) {
+export default function findDOMElement(
+  element: Node | string,
+  context = document,
+): Node | null {
   if (typeof element === 'string') {
     return context.querySelector(element)
   }

+ 4 - 12
packages/@uppy/utils/src/findIndex.test.js

@@ -3,22 +3,14 @@ import findIndex from './findIndex.js'
 
 describe('findIndex', () => {
   it('should return index of an object in an array, that matches a predicate', () => {
-    const arr = [
-      { name: 'foo' },
-      { name: 'bar' },
-      { name: '123' },
-    ]
-    const index = findIndex(arr, item => item.name === 'bar')
+    const arr = [{ name: 'foo' }, { name: 'bar' }, { name: '123' }]
+    const index = findIndex(arr, (item) => item.name === 'bar')
     expect(index).toEqual(1)
   })
 
   it('should return -1 when no object in an array matches a predicate', () => {
-    const arr = [
-      { name: 'foo' },
-      { name: 'bar' },
-      { name: '123' },
-    ]
-    const index = findIndex(arr, item => item.name === 'hello')
+    const arr = [{ name: 'foo' }, { name: 'bar' }, { name: '123' }]
+    const index = findIndex(arr, (item) => item.name === 'hello')
     expect(index).toEqual(-1)
   })
 })

+ 1 - 1
packages/@uppy/utils/src/generateFileID.test.js

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import generateFileID from './generateFileID.js'
+import generateFileID from './generateFileID.ts'
 
 describe('generateFileID', () => {
   it('should take the filename object and produce a lowercase file id made up of uppy- prefix, file name (cleaned up to be lowercase, letters and numbers only), type, relative path (folder) from file.meta.relativePath, size and lastModified date', () => {

+ 23 - 17
packages/@uppy/utils/src/generateFileID.js → packages/@uppy/utils/src/generateFileID.ts

@@ -1,25 +1,25 @@
-import getFileType from './getFileType.js'
+import type { UppyFile } from './UppyFile'
+import getFileType from './getFileType.ts'
 
-function encodeCharacter (character) {
+function encodeCharacter(character: string): string {
   return character.charCodeAt(0).toString(32)
 }
 
-function encodeFilename (name) {
+function encodeFilename(name: string): string {
   let suffix = ''
-  return name.replace(/[^A-Z0-9]/ig, (character) => {
-    suffix += `-${encodeCharacter(character)}`
-    return '/'
-  }) + suffix
+  return (
+    name.replace(/[^A-Z0-9]/gi, (character) => {
+      suffix += `-${encodeCharacter(character)}`
+      return '/'
+    }) + suffix
+  )
 }
 
 /**
  * Takes a file object and turns it into fileID, by converting file.name to lowercase,
  * removing extra characters and adding type, size and lastModified
- *
- * @param {object} file
- * @returns {string} the fileID
  */
-export default function generateFileID (file) {
+export default function generateFileID(file: UppyFile): string {
   // It's tempting to do `[items].filter(Boolean).join('-')` here, but that
   // is slower! simple string concatenation is fast
 
@@ -39,8 +39,8 @@ export default function generateFileID (file) {
   if (file.data.size !== undefined) {
     id += `-${file.data.size}`
   }
-  if (file.data.lastModified !== undefined) {
-    id += `-${file.data.lastModified}`
+  if ((file.data as File).lastModified !== undefined) {
+    id += `-${(file.data as File).lastModified}`
   }
 
   return id
@@ -48,14 +48,20 @@ export default function generateFileID (file) {
 
 // If the provider has a stable, unique ID, then we can use that to identify the file.
 // Then we don't have to generate our own ID, and we can add the same file many times if needed (different path)
-function hasFileStableId (file) {
+function hasFileStableId(file: UppyFile): boolean {
   if (!file.isRemote || !file.remote) return false
   // These are the providers that it seems like have stable IDs for their files. The other's I haven't checked yet.
-  const stableIdProviders = new Set(['box', 'dropbox', 'drive', 'facebook', 'unsplash'])
-  return stableIdProviders.has(file.remote.provider)
+  const stableIdProviders = new Set([
+    'box',
+    'dropbox',
+    'drive',
+    'facebook',
+    'unsplash',
+  ])
+  return stableIdProviders.has(file.remote.provider as any)
 }
 
-export function getSafeFileId (file) {
+export function getSafeFileId(file: UppyFile): string {
   if (hasFileStableId(file)) return file.id
 
   const fileType = getFileType(file)

+ 0 - 3
packages/@uppy/utils/src/getBytesRemaining.js

@@ -1,3 +0,0 @@
-export default function getBytesRemaining (fileProgress) {
-  return fileProgress.bytesTotal - fileProgress.bytesUploaded
-}

+ 5 - 1
packages/@uppy/utils/src/getBytesRemaining.test.js → packages/@uppy/utils/src/getBytesRemaining.test.ts

@@ -1,11 +1,15 @@
 import { describe, expect, it } from 'vitest'
-import getBytesRemaining from './getBytesRemaining.js'
+import getBytesRemaining from './getBytesRemaining.ts'
 
 describe('getBytesRemaining', () => {
   it('should calculate the bytes remaining given a fileProgress object', () => {
     const fileProgress = {
       bytesUploaded: 1024,
       bytesTotal: 3096,
+      progress: 0,
+      uploadComplete: false,
+      percentage: 0,
+      uploadStarted: 0,
     }
     expect(getBytesRemaining(fileProgress)).toEqual(2072)
   })

+ 5 - 0
packages/@uppy/utils/src/getBytesRemaining.ts

@@ -0,0 +1,5 @@
+import type { FileProgress } from './FileProgress'
+
+export default function getBytesRemaining(fileProgress: FileProgress): number {
+  return fileProgress.bytesTotal - (fileProgress.bytesUploaded as number)
+}

+ 14 - 7
packages/@uppy/utils/src/getDroppedFiles/index.js → packages/@uppy/utils/src/getDroppedFiles/index.ts

@@ -1,5 +1,5 @@
-import webkitGetAsEntryApi from './utils/webkitGetAsEntryApi/index.js'
-import fallbackApi from './utils/fallbackApi.js'
+import webkitGetAsEntryApi from './utils/webkitGetAsEntryApi/index.ts'
+import fallbackApi from './utils/fallbackApi.ts'
 
 /**
  * Returns a promise that resolves to the array of dropped files (if a folder is
@@ -8,22 +8,29 @@ import fallbackApi from './utils/fallbackApi.js'
  * Each file has .relativePath prop appended to it (e.g. "/docs/Prague/ticket_from_prague_to_ufa.pdf")
  * if browser supports it. Otherwise it's undefined.
  *
- * @param {DataTransfer} dataTransfer
- * @param {Function} logDropError - a function that's called every time some
+ * @param dataTransfer
+ * @param options
+ * @param options.logDropError - a function that's called every time some
  * folder or some file error out (e.g. because of the folder name being too long
  * on Windows). Notice that resulting promise will always be resolved anyway.
  *
  * @returns {Promise} - Array<File>
  */
-export default async function getDroppedFiles (dataTransfer, { logDropError = () => {} } = {}) {
+export default async function getDroppedFiles(
+  dataTransfer: DataTransfer,
+  options?: {
+    logDropError?: any
+  },
+): Promise<File[]> {
   // Get all files from all subdirs. Works (at least) in Chrome, Mozilla, and Safari
+  const logDropError = options?.logDropError ?? Function.prototype
   try {
     const accumulator = []
     for await (const file of webkitGetAsEntryApi(dataTransfer, logDropError)) {
-      accumulator.push(file)
+      accumulator.push(file as File)
     }
     return accumulator
-  // Otherwise just return all first-order files
+    // Otherwise just return all first-order files
   } catch {
     return fallbackApi(dataTransfer)
   }

+ 4 - 2
packages/@uppy/utils/src/getDroppedFiles/utils/fallbackApi.js → packages/@uppy/utils/src/getDroppedFiles/utils/fallbackApi.ts

@@ -1,7 +1,9 @@
-import toArray from '../../toArray.js'
+import toArray from '../../toArray.ts'
 
 // .files fallback, should be implemented in any browser
-export default function fallbackApi (dataTransfer) {
+export default function fallbackApi(
+  dataTransfer: DataTransfer,
+): Promise<File[]> {
   const files = toArray(dataTransfer.files)
   return Promise.resolve(files)
 }

+ 13 - 8
packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.js → packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.ts

@@ -1,12 +1,12 @@
 /**
  * Recursive function, calls the original callback() when the directory is entirely parsed.
- *
- * @param {FileSystemDirectoryReader} directoryReader
- * @param {Array} oldEntries
- * @param {Function} logDropError
- * @param {Function} callback - called with ([ all files and directories in that directoryReader ])
  */
-export default function getFilesAndDirectoriesFromDirectory (directoryReader, oldEntries, logDropError, { onSuccess }) {
+export default function getFilesAndDirectoriesFromDirectory(
+  directoryReader: FileSystemDirectoryReader,
+  oldEntries: FileSystemEntry[],
+  logDropError: (error?: unknown) => void,
+  { onSuccess }: { onSuccess: (newEntries: FileSystemEntry[]) => void },
+): void {
   directoryReader.readEntries(
     (entries) => {
       const newEntries = [...oldEntries, ...entries]
@@ -14,9 +14,14 @@ export default function getFilesAndDirectoriesFromDirectory (directoryReader, ol
       // must be called until it calls the onSuccess with an empty array.
       if (entries.length) {
         queueMicrotask(() => {
-          getFilesAndDirectoriesFromDirectory(directoryReader, newEntries, logDropError, { onSuccess })
+          getFilesAndDirectoriesFromDirectory(
+            directoryReader,
+            newEntries,
+            logDropError,
+            { onSuccess },
+          )
         })
-      // Done iterating this particular directory
+        // Done iterating this particular directory
       } else {
         onSuccess(newEntries)
       }

+ 0 - 96
packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js

@@ -1,96 +0,0 @@
-import getFilesAndDirectoriesFromDirectory from './getFilesAndDirectoriesFromDirectory.js'
-
-/**
- * Polyfill for the new (experimental) getAsFileSystemHandle API (using the popular webkitGetAsEntry behind the scenes)
- * so that we can switch to the getAsFileSystemHandle API once it (hopefully) becomes standard
- */
-function getAsFileSystemHandleFromEntry (entry, logDropError) {
-  if (entry == null) return entry
-  return {
-    // eslint-disable-next-line no-nested-ternary
-    kind: entry.isFile ? 'file' : entry.isDirectory ? 'directory' : undefined,
-    name: entry.name,
-    getFile () {
-      return new Promise((resolve, reject) => entry.file(resolve, reject))
-    },
-    async* values () {
-      // If the file is a directory.
-      const directoryReader = entry.createReader()
-      const entries = await new Promise(resolve => {
-        getFilesAndDirectoriesFromDirectory(directoryReader, [], logDropError, {
-          onSuccess: (dirEntries) => resolve(dirEntries.map(file => getAsFileSystemHandleFromEntry(file, logDropError))),
-        })
-      })
-      yield* entries
-    },
-  }
-}
-
-async function* createPromiseToAddFileOrParseDirectory (entry, relativePath, lastResortFile = undefined) {
-  const getNextRelativePath = () => `${relativePath}/${entry.name}`
-
-  // For each dropped item, - make sure it's a file/directory, and start deepening in!
-  if (entry.kind === 'file') {
-    const file = await entry.getFile()
-    if (file != null) {
-      file.relativePath = relativePath ? getNextRelativePath() : null
-      yield file
-    } else if (lastResortFile != null) yield lastResortFile
-  } else if (entry.kind === 'directory') {
-    for await (const handle of entry.values()) {
-      // Recurse on the directory, appending the dir name to the relative path
-      yield* createPromiseToAddFileOrParseDirectory(handle, relativePath ? getNextRelativePath() : entry.name)
-    }
-  } else if (lastResortFile != null) yield lastResortFile
-}
-
-/**
- * Load all files from data transfer, and recursively read any directories.
- * Note that IE is not supported for drag-drop, because IE doesn't support Data Transfers
- *
- * @param {DataTransfer} dataTransfer
- * @param {*} logDropError on error
- */
-export default async function* getFilesFromDataTransfer (dataTransfer, logDropError) {
-  // Retrieving the dropped items must happen synchronously
-  // otherwise only the first item gets treated and the other ones are garbage collected.
-  // https://github.com/transloadit/uppy/pull/3998
-  const fileSystemHandles = await Promise.all(Array.from(dataTransfer.items, async item => {
-    let fileSystemHandle
-
-    // TODO enable getAsFileSystemHandle API once we can get it working with subdirectories
-    // IMPORTANT: Need to check isSecureContext *before* calling getAsFileSystemHandle
-    // or else Chrome will crash when running in HTTP: https://github.com/transloadit/uppy/issues/4133
-    // if (window.isSecureContext && item.getAsFileSystemHandle != null) entry = await item.getAsFileSystemHandle()
-
-    // `webkitGetAsEntry` exists in all popular browsers (including non-WebKit browsers),
-    // however it may be renamed to getAsEntry() in the future, so you should code defensively, looking for both.
-    // from https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
-    const getAsEntry = () => (typeof item.getAsEntry === 'function' ? item.getAsEntry() : item.webkitGetAsEntry())
-    // eslint-disable-next-line prefer-const
-    fileSystemHandle ??= getAsFileSystemHandleFromEntry(getAsEntry(), logDropError)
-
-    return {
-      fileSystemHandle,
-      lastResortFile: item.getAsFile(), // can be used as a fallback in case other methods fail
-    }
-  }))
-
-  for (const { lastResortFile, fileSystemHandle } of fileSystemHandles) {
-    // fileSystemHandle and lastResortFile can be null when we drop an url.
-    if (fileSystemHandle != null) {
-      try {
-        yield* createPromiseToAddFileOrParseDirectory(fileSystemHandle, '', lastResortFile)
-      } catch (err) {
-        // Example: If dropping a symbolic link, Chromium will throw:
-        // "DOMException: A requested file or directory could not be found at the time an operation was processed.",
-        // So we will use lastResortFile instead. See https://github.com/transloadit/uppy/issues/3505.
-        if (lastResortFile != null) {
-          yield lastResortFile
-        } else {
-          logDropError(err)
-        }
-      }
-    } else if (lastResortFile != null) yield lastResortFile
-  }
-}

+ 154 - 0
packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.ts

@@ -0,0 +1,154 @@
+import getFilesAndDirectoriesFromDirectory from './getFilesAndDirectoriesFromDirectory.ts'
+
+interface FileSystemFileHandle extends FileSystemHandle {
+  getFile(): Promise<File>
+}
+interface FileSystemDirectoryHandle extends FileSystemHandle {
+  values(): AsyncGenerator<
+    FileSystemDirectoryHandle | FileSystemFileHandle,
+    void,
+    undefined
+  >
+}
+
+/**
+ * Polyfill for the new (experimental) getAsFileSystemHandle API (using the popular webkitGetAsEntry behind the scenes)
+ * so that we can switch to the getAsFileSystemHandle API once it (hopefully) becomes standard
+ */
+function getAsFileSystemHandleFromEntry(
+  entry: FileSystemEntry | null | undefined,
+  logDropError: Parameters<typeof getFilesAndDirectoriesFromDirectory>[2],
+): FileSystemFileHandle | FileSystemDirectoryHandle | null | undefined {
+  if (entry == null) return entry
+  return {
+    // eslint-disable-next-line no-nested-ternary
+    kind: entry.isFile
+      ? 'file'
+      : entry.isDirectory
+      ? 'directory'
+      : (undefined as never),
+    name: entry.name,
+    getFile(): ReturnType<FileSystemFileHandle['getFile']> {
+      return new Promise((resolve, reject) =>
+        (entry as FileSystemFileEntry).file(resolve, reject),
+      )
+    },
+    async *values(): ReturnType<FileSystemDirectoryHandle['values']> {
+      // If the file is a directory.
+      const directoryReader = (entry as FileSystemDirectoryEntry).createReader()
+      const entries = await new Promise<
+        Array<NonNullable<ReturnType<typeof getAsFileSystemHandleFromEntry>>>
+      >((resolve) => {
+        getFilesAndDirectoriesFromDirectory(directoryReader, [], logDropError, {
+          onSuccess: (dirEntries) =>
+            resolve(
+              dirEntries.map(
+                // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+                (file) => getAsFileSystemHandleFromEntry(file, logDropError)!,
+              ),
+            ),
+        })
+      })
+      yield* entries
+    },
+    isSameEntry: undefined as any as FileSystemDirectoryHandle['isSameEntry'],
+  }
+}
+
+async function* createPromiseToAddFileOrParseDirectory(
+  entry: FileSystemFileHandle | FileSystemDirectoryHandle,
+  relativePath: string,
+  lastResortFile: File | null | undefined = undefined,
+): AsyncGenerator<File> {
+  const getNextRelativePath = (): string => `${relativePath}/${entry.name}`
+
+  // For each dropped item, - make sure it's a file/directory, and start deepening in!
+  if (entry.kind === 'file') {
+    const file = await (entry as FileSystemFileHandle).getFile()
+    if (file != null) {
+      ;(file as any).relativePath = relativePath ? getNextRelativePath() : null
+      yield file
+    } else if (lastResortFile != null) yield lastResortFile
+  } else if (entry.kind === 'directory') {
+    for await (const handle of (entry as FileSystemDirectoryHandle).values()) {
+      // Recurse on the directory, appending the dir name to the relative path
+      yield* createPromiseToAddFileOrParseDirectory(
+        handle,
+        relativePath ? getNextRelativePath() : entry.name,
+      )
+    }
+  } else if (lastResortFile != null) yield lastResortFile
+}
+
+/**
+ * Load all files from data transfer, and recursively read any directories.
+ * Note that IE is not supported for drag-drop, because IE doesn't support Data Transfers
+ *
+ * @param {DataTransfer} dataTransfer
+ * @param {*} logDropError on error
+ */
+export default async function* getFilesFromDataTransfer(
+  dataTransfer: DataTransfer,
+  logDropError: Parameters<typeof getFilesAndDirectoriesFromDirectory>[2],
+): ReturnType<typeof createPromiseToAddFileOrParseDirectory> {
+  // Retrieving the dropped items must happen synchronously
+  // otherwise only the first item gets treated and the other ones are garbage collected.
+  // https://github.com/transloadit/uppy/pull/3998
+  const fileSystemHandles = await Promise.all(
+    Array.from(dataTransfer.items, async (item) => {
+      let fileSystemHandle:
+        | FileSystemFileHandle
+        | FileSystemDirectoryHandle
+        | null
+        | undefined
+
+      // TODO enable getAsFileSystemHandle API once we can get it working with subdirectories
+      // IMPORTANT: Need to check isSecureContext *before* calling getAsFileSystemHandle
+      // or else Chrome will crash when running in HTTP: https://github.com/transloadit/uppy/issues/4133
+      // if (window.isSecureContext && item.getAsFileSystemHandle != null)
+      // fileSystemHandle = await item.getAsFileSystemHandle()
+
+      // `webkitGetAsEntry` exists in all popular browsers (including non-WebKit browsers),
+      // however it may be renamed to getAsEntry() in the future, so you should code defensively, looking for both.
+      // from https://developer.mozilla.org/en-US/docs/Web/API/DataTransferItem/webkitGetAsEntry
+      const getAsEntry = (): ReturnType<
+        DataTransferItem['webkitGetAsEntry']
+      > =>
+        typeof (item as any).getAsEntry === 'function'
+          ? (item as any).getAsEntry()
+          : item.webkitGetAsEntry()
+      // eslint-disable-next-line prefer-const
+      fileSystemHandle ??= getAsFileSystemHandleFromEntry(
+        getAsEntry(),
+        logDropError,
+      )
+
+      return {
+        fileSystemHandle,
+        lastResortFile: item.getAsFile(), // can be used as a fallback in case other methods fail
+      }
+    }),
+  )
+
+  for (const { lastResortFile, fileSystemHandle } of fileSystemHandles) {
+    // fileSystemHandle and lastResortFile can be null when we drop an url.
+    if (fileSystemHandle != null) {
+      try {
+        yield* createPromiseToAddFileOrParseDirectory(
+          fileSystemHandle,
+          '',
+          lastResortFile,
+        )
+      } catch (err) {
+        // Example: If dropping a symbolic link, Chromium will throw:
+        // "DOMException: A requested file or directory could not be found at the time an operation was processed.",
+        // So we will use lastResortFile instead. See https://github.com/transloadit/uppy/issues/3505.
+        if (lastResortFile != null) {
+          yield lastResortFile
+        } else {
+          logDropError(err)
+        }
+      }
+    } else if (lastResortFile != null) yield lastResortFile
+  }
+}

+ 1 - 1
packages/@uppy/utils/src/getETA.test.js

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import getETA from './getETA.js'
+import getETA from './getETA.ts'
 
 describe('getETA', () => {
   it('should get the ETA remaining based on a fileProgress object', () => {

+ 4 - 3
packages/@uppy/utils/src/getETA.js → packages/@uppy/utils/src/getETA.ts

@@ -1,7 +1,8 @@
-import getSpeed from './getSpeed.js'
-import getBytesRemaining from './getBytesRemaining.js'
+import getSpeed from './getSpeed.ts'
+import getBytesRemaining from './getBytesRemaining.ts'
+import type { FileProgress } from './FileProgress.ts'
 
-export default function getETA (fileProgress) {
+export default function getETA(fileProgress: FileProgress): number {
   if (!fileProgress.bytesUploaded) return 0
 
   const uploadSpeed = getSpeed(fileProgress)

+ 1 - 1
packages/@uppy/utils/src/getFileNameAndExtension.test.js → packages/@uppy/utils/src/getFileNameAndExtension.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import getFileNameAndExtension from './getFileNameAndExtension.js'
+import getFileNameAndExtension from './getFileNameAndExtension.ts'
 
 describe('getFileNameAndExtension', () => {
   it('should return the filename and extension as an array', () => {

+ 4 - 4
packages/@uppy/utils/src/getFileNameAndExtension.js → packages/@uppy/utils/src/getFileNameAndExtension.ts

@@ -1,10 +1,10 @@
 /**
  * Takes a full filename string and returns an object {name, extension}
- *
- * @param {string} fullFileName
- * @returns {object} {name, extension}
  */
-export default function getFileNameAndExtension (fullFileName) {
+export default function getFileNameAndExtension(fullFileName: string): {
+  name: string
+  extension: string | undefined
+} {
   const lastDot = fullFileName.lastIndexOf('.')
   // these count as no extension: "no-dot", "trailing-dot."
   if (lastDot === -1 || lastDot === fullFileName.length - 1) {

+ 0 - 14
packages/@uppy/utils/src/getFileType.js

@@ -1,14 +0,0 @@
-import getFileNameAndExtension from './getFileNameAndExtension.js'
-import mimeTypes from './mimeTypes.js'
-
-export default function getFileType (file) {
-  if (file.type) return file.type
-
-  const fileExtension = file.name ? getFileNameAndExtension(file.name).extension?.toLowerCase() : null
-  if (fileExtension && fileExtension in mimeTypes) {
-    // else, see if we can map extension to a mime type
-    return mimeTypes[fileExtension]
-  }
-  // if all fails, fall back to a generic byte stream type
-  return 'application/octet-stream'
-}

+ 14 - 10
packages/@uppy/utils/src/getFileType.test.js → packages/@uppy/utils/src/getFileType.test.ts

@@ -1,5 +1,6 @@
 import { describe, expect, it } from 'vitest'
-import getFileType from './getFileType.js'
+import getFileType from './getFileType.ts'
+import type { UppyFile } from './UppyFile.ts'
 
 describe('getFileType', () => {
   it('should trust the filetype if the file comes from a remote source', () => {
@@ -7,7 +8,7 @@ describe('getFileType', () => {
       isRemote: true,
       type: 'audio/webm',
       name: 'foo.webm',
-    }
+    } as any as UppyFile
     expect(getFileType(file)).toEqual('audio/webm')
   })
 
@@ -16,7 +17,7 @@ describe('getFileType', () => {
       type: 'audio/webm',
       name: 'foo.webm',
       data: 'sdfsdfhq9efbicw',
-    }
+    } as any as UppyFile
     expect(getFileType(file)).toEqual('audio/webm')
   })
 
@@ -24,24 +25,27 @@ describe('getFileType', () => {
     const fileMP3 = {
       name: 'foo.mp3',
       data: 'sdfsfhfh329fhwihs',
-    }
+    } as any as UppyFile
     const fileYAML = {
       name: 'bar.yaml',
       data: 'sdfsfhfh329fhwihs',
-    }
+    } as any as UppyFile
     const fileMKV = {
       name: 'bar.mkv',
       data: 'sdfsfhfh329fhwihs',
-    }
+    } as any as UppyFile
     const fileDicom = {
       name: 'bar.dicom',
       data: 'sdfsfhfh329fhwihs',
-    }
+    } as any as UppyFile
     const fileWebp = {
       name: 'bar.webp',
       data: 'sdfsfhfh329fhwihs',
-    }
-    const toUpper = (file) => ({ ...file, name: file.name.toUpperCase() })
+    } as any as UppyFile
+    const toUpper = (file: UppyFile) => ({
+      ...file,
+      name: file.name.toUpperCase(),
+    })
     expect(getFileType(fileMP3)).toEqual('audio/mp3')
     expect(getFileType(toUpper(fileMP3))).toEqual('audio/mp3')
     expect(getFileType(fileYAML)).toEqual('text/yaml')
@@ -58,7 +62,7 @@ describe('getFileType', () => {
     const file = {
       name: 'foobar',
       data: 'sdfsfhfh329fhwihs',
-    }
+    } as any as UppyFile
     expect(getFileType(file)).toEqual('application/octet-stream')
   })
 })

+ 17 - 0
packages/@uppy/utils/src/getFileType.ts

@@ -0,0 +1,17 @@
+import type { UppyFile } from './UppyFile'
+import getFileNameAndExtension from './getFileNameAndExtension.ts'
+import mimeTypes from './mimeTypes.ts'
+
+export default function getFileType(file: UppyFile): string {
+  if (file.type) return file.type
+
+  const fileExtension = file.name
+    ? getFileNameAndExtension(file.name).extension?.toLowerCase()
+    : null
+  if (fileExtension && fileExtension in mimeTypes) {
+    // else, see if we can map extension to a mime type
+    return mimeTypes[fileExtension as keyof typeof mimeTypes]
+  }
+  // if all fails, fall back to a generic byte stream type
+  return 'application/octet-stream'
+}

+ 1 - 1
packages/@uppy/utils/src/getFileTypeExtension.test.js → packages/@uppy/utils/src/getFileTypeExtension.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import getFileTypeExtension from './getFileTypeExtension.js'
+import getFileTypeExtension from './getFileTypeExtension.ts'
 
 describe('getFileTypeExtension', () => {
   it('should return the filetype based on the specified mime type', () => {

+ 4 - 3
packages/@uppy/utils/src/getFileTypeExtension.js → packages/@uppy/utils/src/getFileTypeExtension.ts

@@ -1,4 +1,5 @@
 const mimeToExtensions = {
+  __proto__: null,
   'audio/mp3': 'mp3',
   'audio/mp4': 'mp4',
   'audio/ogg': 'ogg',
@@ -15,11 +16,11 @@ const mimeToExtensions = {
   'video/webm': 'webm',
   'video/x-matroska': 'mkv',
   'video/x-msvideo': 'avi',
-}
+} as unknown as Record<string, string>
 
-export default function getFileTypeExtension (mimeType) {
+export default function getFileTypeExtension(mimeType: string): string | null {
   // Remove the ; bit in 'video/x-matroska;codecs=avc1'
   // eslint-disable-next-line no-param-reassign
-  [mimeType] = mimeType.split(';', 1)
+  ;[mimeType] = mimeType.split(';', 1)
   return mimeToExtensions[mimeType] || null
 }

+ 0 - 22
packages/@uppy/utils/src/getSocketHost.test.js

@@ -1,22 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import getSocketHost from './getSocketHost.js'
-
-describe('getSocketHost', () => {
-  it('should get the host from the specified url', () => {
-    expect(
-      getSocketHost('https://foo.bar/a/b/cd?e=fghi&l=k&m=n'),
-    ).toEqual('wss://foo.bar/a/b/cd?e=fghi&l=k&m=n')
-
-    expect(
-      getSocketHost('Https://foo.bar/a/b/cd?e=fghi&l=k&m=n'),
-    ).toEqual('wss://foo.bar/a/b/cd?e=fghi&l=k&m=n')
-
-    expect(
-      getSocketHost('foo.bar/a/b/cd?e=fghi&l=k&m=n'),
-    ).toEqual('wss://foo.bar/a/b/cd?e=fghi&l=k&m=n')
-
-    expect(
-      getSocketHost('http://foo.bar/a/b/cd?e=fghi&l=k&m=n'),
-    ).toEqual('ws://foo.bar/a/b/cd?e=fghi&l=k&m=n')
-  })
-})

+ 22 - 0
packages/@uppy/utils/src/getSocketHost.test.ts

@@ -0,0 +1,22 @@
+import { describe, expect, it } from 'vitest'
+import getSocketHost from './getSocketHost.ts'
+
+describe('getSocketHost', () => {
+  it('should get the host from the specified url', () => {
+    expect(getSocketHost('https://foo.bar/a/b/cd?e=fghi&l=k&m=n')).toEqual(
+      'wss://foo.bar/a/b/cd?e=fghi&l=k&m=n',
+    )
+
+    expect(getSocketHost('Https://foo.bar/a/b/cd?e=fghi&l=k&m=n')).toEqual(
+      'wss://foo.bar/a/b/cd?e=fghi&l=k&m=n',
+    )
+
+    expect(getSocketHost('foo.bar/a/b/cd?e=fghi&l=k&m=n')).toEqual(
+      'wss://foo.bar/a/b/cd?e=fghi&l=k&m=n',
+    )
+
+    expect(getSocketHost('http://foo.bar/a/b/cd?e=fghi&l=k&m=n')).toEqual(
+      'ws://foo.bar/a/b/cd?e=fghi&l=k&m=n',
+    )
+  })
+})

+ 2 - 2
packages/@uppy/utils/src/getSocketHost.js → packages/@uppy/utils/src/getSocketHost.ts

@@ -1,7 +1,7 @@
-export default function getSocketHost (url) {
+export default function getSocketHost(url: string): string {
   // get the host domain
   const regex = /^(?:https?:\/\/|\/\/)?(?:[^@\n]+@)?(?:www\.)?([^\n]+)/i
-  const host = regex.exec(url)[1]
+  const host = regex.exec(url)?.[1]
   const socketProtocol = /^http:\/\//i.test(url) ? 'ws' : 'wss'
 
   return `${socketProtocol}://${host}`

+ 6 - 2
packages/@uppy/utils/src/getSpeed.test.js → packages/@uppy/utils/src/getSpeed.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import getSpeed from './getSpeed.js'
+import getSpeed from './getSpeed.ts'
 
 describe('getSpeed', () => {
   it('should calculate the speed given a fileProgress object', () => {
@@ -7,7 +7,11 @@ describe('getSpeed', () => {
     const date5SecondsAgo = new Date(dateNow.getTime() - 5 * 1000)
     const fileProgress = {
       bytesUploaded: 1024,
-      uploadStarted: date5SecondsAgo,
+      uploadStarted: date5SecondsAgo.getTime(),
+      progress: 0,
+      uploadComplete: false,
+      percentage: 0,
+      bytesTotal: 0,
     }
     expect(Math.round(getSpeed(fileProgress))).toEqual(Math.round(205))
   })

+ 3 - 1
packages/@uppy/utils/src/getSpeed.js → packages/@uppy/utils/src/getSpeed.ts

@@ -1,4 +1,6 @@
-export default function getSpeed (fileProgress) {
+import type { FileProgress } from './FileProgress'
+
+export default function getSpeed(fileProgress: FileProgress): number {
   if (!fileProgress.bytesUploaded) return 0
 
   const timeElapsed = Date.now() - fileProgress.uploadStarted

+ 2 - 5
packages/@uppy/utils/src/getTextDirection.js → packages/@uppy/utils/src/getTextDirection.ts

@@ -1,11 +1,8 @@
 /**
  * Get the declared text direction for an element.
- *
- * @param {Node} element
- * @returns {string|undefined}
  */
 
-function getTextDirection (element) {
+function getTextDirection(element?: HTMLElement): string | undefined {
   // There is another way to determine text direction using getComputedStyle(), as done here:
   // https://github.com/pencil-js/text-direction/blob/2a235ce95089b3185acec3b51313cbba921b3811/text-direction.js
   //
@@ -14,7 +11,7 @@ function getTextDirection (element) {
   // bidirectional CSS style sheets work.
   while (element && !element.dir) {
     // eslint-disable-next-line no-param-reassign
-    element = element.parentNode
+    element = element.parentNode as HTMLElement
   }
   return element?.dir
 }

+ 2 - 5
packages/@uppy/utils/src/getTimeStamp.js → packages/@uppy/utils/src/getTimeStamp.ts

@@ -1,17 +1,14 @@
 /**
  * Adds zero to strings shorter than two characters.
- *
- * @param {number} number
- * @returns {string}
  */
-function pad (number) {
+function pad(number: number): string {
   return number < 10 ? `0${number}` : number.toString()
 }
 
 /**
  * Returns a timestamp in the format of `hours:minutes:seconds`
  */
-export default function getTimeStamp () {
+export default function getTimeStamp(): string {
   const date = new Date()
   const hours = pad(date.getHours())
   const minutes = pad(date.getMinutes())

+ 0 - 3
packages/@uppy/utils/src/hasProperty.js

@@ -1,3 +0,0 @@
-export default function has (object, key) {
-  return Object.prototype.hasOwnProperty.call(object, key)
-}

+ 6 - 0
packages/@uppy/utils/src/hasProperty.ts

@@ -0,0 +1,6 @@
+export default function has(
+  object: Parameters<typeof Object.hasOwn>[0],
+  key: Parameters<typeof Object.hasOwn>[1],
+): ReturnType<typeof Object.hasOwn> {
+  return Object.prototype.hasOwnProperty.call(object, key)
+}

+ 0 - 8
packages/@uppy/utils/src/isDOMElement.js

@@ -1,8 +0,0 @@
-/**
- * Check if an object is a DOM element. Duck-typing based on `nodeType`.
- *
- * @param {*} obj
- */
-export default function isDOMElement (obj) {
-  return obj?.nodeType === Node.ELEMENT_NODE
-}

+ 8 - 0
packages/@uppy/utils/src/isDOMElement.ts

@@ -0,0 +1,8 @@
+/**
+ * Check if an object is a DOM element. Duck-typing based on `nodeType`.
+ */
+export default function isDOMElement(obj: unknown): obj is Element {
+  if (typeof obj !== 'object' || obj === null) return false
+  if (!('nodeType' in obj)) return false
+  return obj.nodeType === Node.ELEMENT_NODE
+}

+ 1 - 3
packages/@uppy/utils/src/isDragDropSupported.js → packages/@uppy/utils/src/isDragDropSupported.ts

@@ -1,9 +1,7 @@
 /**
  * Checks if the browser supports Drag & Drop (not supported on mobile devices, for example).
- *
- * @returns {boolean}
  */
-export default function isDragDropSupported () {
+export default function isDragDropSupported(): boolean {
   const div = document.body
 
   if (!('draggable' in div) || !('ondragstart' in div && 'ondrop' in div)) {

+ 0 - 15
packages/@uppy/utils/src/isMobileDevice.js

@@ -1,15 +0,0 @@
-/**
- * Checks if current device reports itself as “mobile”.
- * Very simple, not very reliable.
- *
- * @returns {boolean}
- */
-export default function isMobileDevice () {
-  if (typeof window !== 'undefined'
-      && window.navigator
-      && window.navigator.userAgent
-      && window.navigator.userAgent.match(/Mobi/)) {
-    return true
-  }
-  return false
-}

+ 0 - 30
packages/@uppy/utils/src/isMobileDevice.test.js

@@ -1,30 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import isMobileDevice from './isMobileDevice.js'
-
-let fakeUserAgent = null
-
-Object.defineProperty(globalThis.navigator, 'userAgent', {
-  get () {
-    return fakeUserAgent
-  },
-})
-
-function setUserAgent (userAgent) {
-  fakeUserAgent = userAgent
-}
-
-describe('isMobileDevice', () => {
-  it('should return true if the specified user agent is mobile', () => {
-    setUserAgent('Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1')
-    expect(isMobileDevice()).toEqual(true)
-    setUserAgent('Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/192.0.0.34.85;]')
-    expect(isMobileDevice()).toEqual(true)
-  })
-
-  it('should return false if the user agent is not mobile', () => {
-    setUserAgent('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko)')
-    expect(isMobileDevice()).toEqual(false)
-    setUserAgent('Mozilla/5.0 (SMART-TV; Linux; Tizen 2.4.0) AppleWebkit/538.1 (KHTML, like Gecko) SamsungBrowser/1.1 TV Safari/538.1')
-    expect(isMobileDevice()).toEqual(false)
-  })
-})

+ 38 - 0
packages/@uppy/utils/src/isMobileDevice.test.ts

@@ -0,0 +1,38 @@
+import { describe, expect, it } from 'vitest'
+import isMobileDevice from './isMobileDevice.ts'
+
+let fakeUserAgent: string | null = null
+
+Object.defineProperty(globalThis.navigator, 'userAgent', {
+  get() {
+    return fakeUserAgent
+  },
+})
+
+function setUserAgent(userAgent: string | null) {
+  fakeUserAgent = userAgent
+}
+
+describe('isMobileDevice', () => {
+  it('should return true if the specified user agent is mobile', () => {
+    setUserAgent(
+      'Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1',
+    )
+    expect(isMobileDevice()).toEqual(true)
+    setUserAgent(
+      'Mozilla/5.0 (Linux; Android 7.0; SM-G570M Build/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/69.0.3497.100 Mobile Safari/537.36 [FB_IAB/FB4A;FBAV/192.0.0.34.85;]',
+    )
+    expect(isMobileDevice()).toEqual(true)
+  })
+
+  it('should return false if the user agent is not mobile', () => {
+    setUserAgent(
+      'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_5) AppleWebKit/605.1.15 (KHTML, like Gecko)',
+    )
+    expect(isMobileDevice()).toEqual(false)
+    setUserAgent(
+      'Mozilla/5.0 (SMART-TV; Linux; Tizen 2.4.0) AppleWebkit/538.1 (KHTML, like Gecko) SamsungBrowser/1.1 TV Safari/538.1',
+    )
+    expect(isMobileDevice()).toEqual(false)
+  })
+})

+ 15 - 0
packages/@uppy/utils/src/isMobileDevice.ts

@@ -0,0 +1,15 @@
+/**
+ * Checks if current device reports itself as “mobile”.
+ * Very simple, not very reliable.
+ */
+export default function isMobileDevice(): boolean {
+  if (
+    typeof window !== 'undefined' &&
+    window.navigator &&
+    window.navigator.userAgent &&
+    window.navigator.userAgent.match(/Mobi/)
+  ) {
+    return true
+  }
+  return false
+}

+ 5 - 5
packages/@uppy/utils/src/isNetworkError.test.js → packages/@uppy/utils/src/isNetworkError.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import isNetworkError from './isNetworkError.js'
+import isNetworkError from './isNetworkError.ts'
 
 describe('isNetworkError', () => {
   it('should return true if the specified xhr object contains a network error', () => {
@@ -7,25 +7,25 @@ describe('isNetworkError', () => {
       readyState: 4,
       responseText: '',
       status: 0,
-    }
+    } as any as XMLHttpRequest
 
     const xhrNetworkError2Mock = {
       readyState: 2,
       responseText: '',
       status: 300,
-    }
+    } as any as XMLHttpRequest
 
     const xhrRegularErrorMock = {
       readyState: 4,
       responseText: 'Failed',
       status: 400,
-    }
+    } as any as XMLHttpRequest
 
     const xhrNetworkSuccessMock = {
       readyState: 4,
       responseText: 'Success',
       status: 200,
-    }
+    } as any as XMLHttpRequest
 
     expect(isNetworkError(xhrNetworkErrorMock)).toEqual(true)
     expect(isNetworkError(xhrNetworkError2Mock)).toEqual(true)

+ 1 - 1
packages/@uppy/utils/src/isNetworkError.js → packages/@uppy/utils/src/isNetworkError.ts

@@ -1,4 +1,4 @@
-function isNetworkError (xhr) {
+function isNetworkError(xhr?: XMLHttpRequest): boolean {
   if (!xhr) {
     return false
   }

+ 1 - 1
packages/@uppy/utils/src/isObjectURL.test.js → packages/@uppy/utils/src/isObjectURL.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import isObjectURL from './isObjectURL.js'
+import isObjectURL from './isObjectURL.ts'
 
 describe('isObjectURL', () => {
   it('should return true if the specified url is an object url', () => {

+ 1 - 4
packages/@uppy/utils/src/isObjectURL.js → packages/@uppy/utils/src/isObjectURL.ts

@@ -1,9 +1,6 @@
 /**
  * Check if a URL string is an object URL from `URL.createObjectURL`.
- *
- * @param {string} url
- * @returns {boolean}
  */
-export default function isObjectURL (url) {
+export default function isObjectURL(url: string): boolean {
   return url.startsWith('blob:')
 }

+ 0 - 12
packages/@uppy/utils/src/isPreviewSupported.test.js

@@ -1,12 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import isPreviewSupported from './isPreviewSupported.js'
-
-describe('isPreviewSupported', () => {
-  it('should return true for any filetypes that browsers can preview', () => {
-    const supported = ['image/jpeg', 'image/gif', 'image/png', 'image/svg', 'image/svg+xml', 'image/bmp', 'image/jpg', 'image/webp', 'image/avif']
-    supported.forEach(ext => {
-      expect(isPreviewSupported(ext)).toEqual(true)
-    })
-    expect(isPreviewSupported('foo')).toEqual(false)
-  })
-})

+ 22 - 0
packages/@uppy/utils/src/isPreviewSupported.test.ts

@@ -0,0 +1,22 @@
+import { describe, expect, it } from 'vitest'
+import isPreviewSupported from './isPreviewSupported.ts'
+
+describe('isPreviewSupported', () => {
+  it('should return true for any filetypes that browsers can preview', () => {
+    const supported = [
+      'image/jpeg',
+      'image/gif',
+      'image/png',
+      'image/svg',
+      'image/svg+xml',
+      'image/bmp',
+      'image/jpg',
+      'image/webp',
+      'image/avif',
+    ]
+    supported.forEach((ext) => {
+      expect(isPreviewSupported(ext)).toEqual(true)
+    })
+    expect(isPreviewSupported('foo')).toEqual(false)
+  })
+})

+ 1 - 1
packages/@uppy/utils/src/isPreviewSupported.js → packages/@uppy/utils/src/isPreviewSupported.ts

@@ -1,4 +1,4 @@
-export default function isPreviewSupported (fileType) {
+export default function isPreviewSupported(fileType: string): boolean {
   if (!fileType) return false
   // list of images that browsers can preview
   return /^[^/]+\/(jpe?g|gif|png|svg|svg\+xml|bmp|webp|avif)$/.test(fileType)

+ 1 - 1
packages/@uppy/utils/src/isTouchDevice.test.js

@@ -1,5 +1,5 @@
 import { afterEach, beforeEach, describe, expect, it } from 'vitest'
-import isTouchDevice from './isTouchDevice.js'
+import isTouchDevice from './isTouchDevice.ts'
 
 describe('isTouchDevice', () => {
   const RealTouchStart = globalThis.window.ontouchstart

+ 1 - 1
packages/@uppy/utils/src/isTouchDevice.js → packages/@uppy/utils/src/isTouchDevice.ts

@@ -1,3 +1,3 @@
-export default function isTouchDevice () {
+export default function isTouchDevice(): boolean {
   return 'ontouchstart' in window || 'maxTouchPoints' in navigator
 }

+ 0 - 58
packages/@uppy/utils/src/mimeTypes.js

@@ -1,58 +0,0 @@
-// ___Why not add the mime-types package?
-//    It's 19.7kB gzipped, and we only need mime types for well-known extensions (for file previews).
-// ___Where to take new extensions from?
-//    https://github.com/jshttp/mime-db/blob/master/db.json
-
-export default {
-  md: 'text/markdown',
-  markdown: 'text/markdown',
-  mp4: 'video/mp4',
-  mp3: 'audio/mp3',
-  svg: 'image/svg+xml',
-  jpg: 'image/jpeg',
-  png: 'image/png',
-  webp: 'image/webp',
-  gif: 'image/gif',
-  heic: 'image/heic',
-  heif: 'image/heif',
-  yaml: 'text/yaml',
-  yml: 'text/yaml',
-  csv: 'text/csv',
-  tsv: 'text/tab-separated-values',
-  tab: 'text/tab-separated-values',
-  avi: 'video/x-msvideo',
-  mks: 'video/x-matroska',
-  mkv: 'video/x-matroska',
-  mov: 'video/quicktime',
-  dicom: 'application/dicom',
-  doc: 'application/msword',
-  docm: 'application/vnd.ms-word.document.macroenabled.12',
-  docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-  dot: 'application/msword',
-  dotm: 'application/vnd.ms-word.template.macroenabled.12',
-  dotx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
-  xla: 'application/vnd.ms-excel',
-  xlam: 'application/vnd.ms-excel.addin.macroenabled.12',
-  xlc: 'application/vnd.ms-excel',
-  xlf: 'application/x-xliff+xml',
-  xlm: 'application/vnd.ms-excel',
-  xls: 'application/vnd.ms-excel',
-  xlsb: 'application/vnd.ms-excel.sheet.binary.macroenabled.12',
-  xlsm: 'application/vnd.ms-excel.sheet.macroenabled.12',
-  xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
-  xlt: 'application/vnd.ms-excel',
-  xltm: 'application/vnd.ms-excel.template.macroenabled.12',
-  xltx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
-  xlw: 'application/vnd.ms-excel',
-  txt: 'text/plain',
-  text: 'text/plain',
-  conf: 'text/plain',
-  log: 'text/plain',
-  pdf: 'application/pdf',
-  zip: 'application/zip',
-  '7z': 'application/x-7z-compressed',
-  rar: 'application/x-rar-compressed',
-  tar: 'application/x-tar',
-  gz: 'application/gzip',
-  dmg: 'application/x-apple-diskimage',
-}

+ 59 - 0
packages/@uppy/utils/src/mimeTypes.ts

@@ -0,0 +1,59 @@
+// ___Why not add the mime-types package?
+//    It's 19.7kB gzipped, and we only need mime types for well-known extensions (for file previews).
+// ___Where to take new extensions from?
+//    https://github.com/jshttp/mime-db/blob/master/db.json
+
+export default {
+  __proto__: null as never,
+  md: 'text/markdown' as const,
+  markdown: 'text/markdown' as const,
+  mp4: 'video/mp4' as const,
+  mp3: 'audio/mp3' as const,
+  svg: 'image/svg+xml' as const,
+  jpg: 'image/jpeg' as const,
+  png: 'image/png' as const,
+  webp: 'image/webp' as const,
+  gif: 'image/gif' as const,
+  heic: 'image/heic' as const,
+  heif: 'image/heif' as const,
+  yaml: 'text/yaml' as const,
+  yml: 'text/yaml' as const,
+  csv: 'text/csv' as const,
+  tsv: 'text/tab-separated-values' as const,
+  tab: 'text/tab-separated-values' as const,
+  avi: 'video/x-msvideo' as const,
+  mks: 'video/x-matroska' as const,
+  mkv: 'video/x-matroska' as const,
+  mov: 'video/quicktime' as const,
+  dicom: 'application/dicom' as const,
+  doc: 'application/msword' as const,
+  docm: 'application/vnd.ms-word.document.macroenabled.12' as const,
+  docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' as const,
+  dot: 'application/msword' as const,
+  dotm: 'application/vnd.ms-word.template.macroenabled.12' as const,
+  dotx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' as const,
+  xla: 'application/vnd.ms-excel' as const,
+  xlam: 'application/vnd.ms-excel.addin.macroenabled.12' as const,
+  xlc: 'application/vnd.ms-excel' as const,
+  xlf: 'application/x-xliff+xml' as const,
+  xlm: 'application/vnd.ms-excel' as const,
+  xls: 'application/vnd.ms-excel' as const,
+  xlsb: 'application/vnd.ms-excel.sheet.binary.macroenabled.12' as const,
+  xlsm: 'application/vnd.ms-excel.sheet.macroenabled.12' as const,
+  xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' as const,
+  xlt: 'application/vnd.ms-excel' as const,
+  xltm: 'application/vnd.ms-excel.template.macroenabled.12' as const,
+  xltx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.template' as const,
+  xlw: 'application/vnd.ms-excel' as const,
+  txt: 'text/plain' as const,
+  text: 'text/plain' as const,
+  conf: 'text/plain' as const,
+  log: 'text/plain' as const,
+  pdf: 'application/pdf' as const,
+  zip: 'application/zip' as const,
+  '7z': 'application/x-7z-compressed' as const,
+  rar: 'application/x-rar-compressed' as const,
+  tar: 'application/x-tar' as const,
+  gz: 'application/gzip' as const,
+  dmg: 'application/x-apple-diskimage' as const,
+}

+ 0 - 14
packages/@uppy/utils/src/prettyETA.js

@@ -1,14 +0,0 @@
-import secondsToTime from './secondsToTime.js'
-
-export default function prettyETA (seconds) {
-  const time = secondsToTime(seconds)
-
-  // Only display hours and minutes if they are greater than 0 but always
-  // display minutes if hours is being displayed
-  // Display a leading zero if the there is a preceding unit: 1m 05s, but 5s
-  const hoursStr = time.hours === 0 ? '' : `${time.hours}h`
-  const minutesStr = time.minutes === 0 ? '' : `${time.hours === 0 ? time.minutes : ` ${time.minutes.toString(10).padStart(2, '0')}`}m`
-  const secondsStr = time.hours !== 0 ? '' : `${time.minutes === 0 ? time.seconds : ` ${time.seconds.toString(10).padStart(2, '0')}`}s`
-
-  return `${hoursStr}${minutesStr}${secondsStr}`
-}

+ 1 - 1
packages/@uppy/utils/src/prettyETA.test.js → packages/@uppy/utils/src/prettyETA.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import prettyETA from './prettyETA.js'
+import prettyETA from './prettyETA.ts'
 
 describe('prettyETA', () => {
   it('should convert the specified number of seconds to a pretty ETA', () => {

+ 28 - 0
packages/@uppy/utils/src/prettyETA.ts

@@ -0,0 +1,28 @@
+import secondsToTime from './secondsToTime.ts'
+
+export default function prettyETA(seconds: number): string {
+  const time = secondsToTime(seconds)
+
+  // Only display hours and minutes if they are greater than 0 but always
+  // display minutes if hours is being displayed
+  // Display a leading zero if the there is a preceding unit: 1m 05s, but 5s
+  const hoursStr = time.hours === 0 ? '' : `${time.hours}h`
+  const minutesStr =
+    time.minutes === 0
+      ? ''
+      : `${
+          time.hours === 0
+            ? time.minutes
+            : ` ${time.minutes.toString(10).padStart(2, '0')}`
+        }m`
+  const secondsStr =
+    time.hours !== 0
+      ? ''
+      : `${
+          time.minutes === 0
+            ? time.seconds
+            : ` ${time.seconds.toString(10).padStart(2, '0')}`
+        }s`
+
+  return `${hoursStr}${minutesStr}${secondsStr}`
+}

+ 0 - 9
packages/@uppy/utils/src/remoteFileObjToLocal.js

@@ -1,9 +0,0 @@
-import getFileNameAndExtension from './getFileNameAndExtension.js'
-
-export default function remoteFileObjToLocal (file) {
-  return {
-    ...file,
-    type: file.mimeType,
-    extension: file.name ? getFileNameAndExtension(file.name).extension : null,
-  }
-}

+ 19 - 0
packages/@uppy/utils/src/remoteFileObjToLocal.ts

@@ -0,0 +1,19 @@
+import getFileNameAndExtension from './getFileNameAndExtension.ts'
+
+interface ObjectWithMIMEAndName {
+  name?: string
+  mimeType: unknown
+}
+
+export default function remoteFileObjToLocal<T extends ObjectWithMIMEAndName>(
+  file: T,
+): T & {
+  type: T['mimeType']
+  extension: string | undefined | null
+} {
+  return {
+    ...file,
+    type: file.mimeType,
+    extension: file.name ? getFileNameAndExtension(file.name).extension : null,
+  }
+}

+ 0 - 0
packages/@uppy/utils/src/sampleImageDataURI.js → packages/@uppy/utils/src/sampleImageDataURI.ts


+ 1 - 1
packages/@uppy/utils/src/secondsToTime.test.js → packages/@uppy/utils/src/secondsToTime.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import secondsToTime from './secondsToTime.js'
+import secondsToTime from './secondsToTime.ts'
 
 describe('secondsToTime', () => {
   it('converts seconds to an { hours, minutes, seconds } object', () => {

+ 7 - 1
packages/@uppy/utils/src/secondsToTime.js → packages/@uppy/utils/src/secondsToTime.ts

@@ -1,4 +1,10 @@
-export default function secondsToTime (rawSeconds) {
+interface Time {
+  hours: number
+  minutes: number
+  seconds: number
+}
+
+export default function secondsToTime(rawSeconds: number): Time {
   const hours = Math.floor(rawSeconds / 3600) % 24
   const minutes = Math.floor(rawSeconds / 60) % 60
   const seconds = Math.floor(rawSeconds % 60)

+ 2 - 8
packages/@uppy/utils/src/toArray.test.js → packages/@uppy/utils/src/toArray.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import toArray from './toArray.js'
+import toArray from './toArray.ts'
 
 describe('toArray', () => {
   it('should convert a array-like object into an array', () => {
@@ -12,12 +12,6 @@ describe('toArray', () => {
       length: 5,
     }
 
-    expect(toArray(obj)).toEqual([
-      'zero',
-      'one',
-      'two',
-      'three',
-      'four',
-    ])
+    expect(toArray(obj)).toEqual(['zero', 'one', 'two', 'three', 'four'])
   })
 })

+ 0 - 0
packages/@uppy/utils/src/toArray.js → packages/@uppy/utils/src/toArray.ts


+ 1 - 1
packages/@uppy/utils/src/truncateString.test.js → packages/@uppy/utils/src/truncateString.test.ts

@@ -1,5 +1,5 @@
 import { describe, expect, it } from 'vitest'
-import truncateString from './truncateString.js'
+import truncateString from './truncateString.ts'
 
 describe('truncateString', () => {
   it('should truncate the string to the length', () => {

+ 6 - 6
packages/@uppy/utils/src/truncateString.js → packages/@uppy/utils/src/truncateString.ts

@@ -1,19 +1,19 @@
 /**
  * Truncates a string to the given number of chars (maxLength) by inserting '...' in the middle of that string.
  * Partially taken from https://stackoverflow.com/a/5723274/3192470.
- *
- * @param {string} string - string to be truncated
- * @param {number} maxLength - maximum size of the resulting string
- * @returns {string}
  */
 const separator = '...'
-export default function truncateString (string, maxLength) {
+export default function truncateString(
+  string: string,
+  maxLength: number,
+): string {
   // Return the empty string if maxLength is zero
   if (maxLength === 0) return ''
   // Return original string if it's already shorter than maxLength
   if (string.length <= maxLength) return string
   // Return truncated substring appended of the ellipsis char if string can't be meaningfully truncated
-  if (maxLength <= separator.length + 1) return `${string.slice(0, maxLength - 1)}…`
+  if (maxLength <= separator.length + 1)
+    return `${string.slice(0, maxLength - 1)}…`
 
   const charsToShow = maxLength - separator.length
   const frontChars = Math.ceil(charsToShow / 2)

+ 12 - 0
packages/@uppy/utils/tsconfig.build.json

@@ -0,0 +1,12 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "outDir": "./lib",
+    "rootDir": "./src",
+    "allowJs": true,
+    "skipLibCheck": true
+  },
+  "include": ["./src/**/*.*"],
+  "exclude": ["./src/**/*.test.ts"],
+  "references": []
+}

+ 10 - 0
packages/@uppy/utils/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "allowJs": true,
+    "emitDeclarationOnly": false,
+    "noEmit": true
+  },
+  "include": ["src/*.ts"],
+  "references": []
+}

+ 8 - 0
yarn.lock

@@ -8197,6 +8197,13 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@types/lodash@npm:^4.14.199":
+  version: 4.14.199
+  resolution: "@types/lodash@npm:4.14.199"
+  checksum: e68d1fcbbfce953ed87b296a628573f62939227bcda0c934954e862b421e8a34c5e71cad6fea27b9980567909e6a4698f09025692958e36d64ea9ed99ec6fb2e
+  languageName: node
+  linkType: hard
+
 "@types/mdast@npm:^3.0.0":
   version: 3.0.10
   resolution: "@types/mdast@npm:3.0.10"
@@ -9805,6 +9812,7 @@ __metadata:
   version: 0.0.0-use.local
   resolution: "@uppy/utils@workspace:packages/@uppy/utils"
   dependencies:
+    "@types/lodash": ^4.14.199
     lodash: ^4.17.21
     preact: ^10.5.13
     vitest: ^0.34.5