Prechádzať zdrojové kódy

@uppy/tus: migrate to TS (#4899)

Co-authored-by: Mikael Finstad <finstaden@gmail.com>
Merlijn Vos 1 rok pred
rodič
commit
4d03bba8b2

+ 0 - 39
packages/@uppy/tus/src/getFingerprint.js

@@ -1,39 +0,0 @@
-import * as tus from 'tus-js-client'
-
-function isCordova () {
-  return typeof window !== 'undefined' && (
-    typeof window.PhoneGap !== 'undefined'
-    || typeof window.Cordova !== 'undefined'
-    || typeof window.cordova !== 'undefined'
-  )
-}
-
-function isReactNative () {
-  return typeof navigator !== 'undefined'
-    && typeof navigator.product === 'string'
-    && navigator.product.toLowerCase() === 'reactnative'
-}
-
-// We override tus fingerprint to uppy’s `file.id`, since the `file.id`
-// now also includes `relativePath` for files added from folders.
-// This means you can add 2 identical files, if one is in folder a,
-// the other in folder b — `a/file.jpg` and `b/file.jpg`, when added
-// together with a folder, will be treated as 2 separate files.
-//
-// For React Native and Cordova, we let tus-js-client’s default
-// fingerprint handling take charge.
-export default function getFingerprint (uppyFileObj) {
-  return (file, options) => {
-    if (isCordova() || isReactNative()) {
-      return tus.defaultOptions.fingerprint(file, options)
-    }
-
-    const uppyFingerprint = [
-      'tus',
-      uppyFileObj.id,
-      options.endpoint,
-    ].join('-')
-
-    return Promise.resolve(uppyFingerprint)
-  }
-}

+ 44 - 0
packages/@uppy/tus/src/getFingerprint.ts

@@ -0,0 +1,44 @@
+import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
+import * as tus from 'tus-js-client'
+
+function isCordova() {
+  return (
+    typeof window !== 'undefined' &&
+    // @ts-expect-error may exist
+    (typeof window.PhoneGap !== 'undefined' ||
+      // @ts-expect-error may exist
+      typeof window.Cordova !== 'undefined' ||
+      // @ts-expect-error may exist
+      typeof window.cordova !== 'undefined')
+  )
+}
+
+function isReactNative() {
+  return (
+    typeof navigator !== 'undefined' &&
+    typeof navigator.product === 'string' &&
+    navigator.product.toLowerCase() === 'reactnative'
+  )
+}
+
+// We override tus fingerprint to uppy’s `file.id`, since the `file.id`
+// now also includes `relativePath` for files added from folders.
+// This means you can add 2 identical files, if one is in folder a,
+// the other in folder b — `a/file.jpg` and `b/file.jpg`, when added
+// together with a folder, will be treated as 2 separate files.
+//
+// For React Native and Cordova, we let tus-js-client’s default
+// fingerprint handling take charge.
+export default function getFingerprint<M extends Meta, B extends Body>(
+  uppyFile: UppyFile<M, B>,
+): tus.UploadOptions['fingerprint'] {
+  return (file, options) => {
+    if (isCordova() || isReactNative()) {
+      return tus.defaultOptions.fingerprint!(file, options)
+    }
+
+    const uppyFingerprint = ['tus', uppyFile.id, options!.endpoint].join('-')
+
+    return Promise.resolve(uppyFingerprint)
+  }
+}

+ 13 - 4
packages/@uppy/tus/src/index.test.js → packages/@uppy/tus/src/index.test.ts

@@ -1,29 +1,38 @@
 import { describe, expect, it } from 'vitest'
 import Core from '@uppy/core'
-import Tus from './index.js'
+import Tus from './index.ts'
 
 describe('Tus', () => {
   it('Throws errors if autoRetry option is true', () => {
     const uppy = new Core()
 
     expect(() => {
+      // @ts-expect-error removed
       uppy.use(Tus, { autoRetry: true })
-    }).toThrowError(/The `autoRetry` option was deprecated and has been removed/)
+    }).toThrowError(
+      /The `autoRetry` option was deprecated and has been removed/,
+    )
   })
 
   it('Throws errors if autoRetry option is false', () => {
     const uppy = new Core()
 
     expect(() => {
+      // @ts-expect-error removed
       uppy.use(Tus, { autoRetry: false })
-    }).toThrowError(/The `autoRetry` option was deprecated and has been removed/)
+    }).toThrowError(
+      /The `autoRetry` option was deprecated and has been removed/,
+    )
   })
 
   it('Throws errors if autoRetry option is `undefined`', () => {
     const uppy = new Core()
 
     expect(() => {
+      // @ts-expect-error removed
       uppy.use(Tus, { autoRetry: undefined })
-    }).toThrowError(/The `autoRetry` option was deprecated and has been removed/)
+    }).toThrowError(
+      /The `autoRetry` option was deprecated and has been removed/,
+    )
   })
 })

+ 216 - 126
packages/@uppy/tus/src/index.js → packages/@uppy/tus/src/index.ts

@@ -1,26 +1,66 @@
-import BasePlugin from '@uppy/core/lib/BasePlugin.js'
+import BasePlugin, {
+  type DefinePluginOpts,
+  type PluginOpts,
+} from '@uppy/core/lib/BasePlugin.js'
 import * as tus from 'tus-js-client'
-import EventManager from '@uppy/utils/lib/EventManager'
+import EventManager from '@uppy/core/lib/EventManager.js'
 import NetworkError from '@uppy/utils/lib/NetworkError'
 import isNetworkError from '@uppy/utils/lib/isNetworkError'
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore untyped
 import { RateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue'
 import hasProperty from '@uppy/utils/lib/hasProperty'
-import { filterNonFailedFiles, filterFilesToEmitUploadStarted } from '@uppy/utils/lib/fileFilters'
-import getFingerprint from './getFingerprint.js'
-
+import {
+  filterNonFailedFiles,
+  filterFilesToEmitUploadStarted,
+} from '@uppy/utils/lib/fileFilters'
+import type { Meta, Body, UppyFile } from '@uppy/utils/lib/UppyFile'
+import type { Uppy } from '@uppy/core'
+import type { RequestClient } from '@uppy/companion-client'
+import getFingerprint from './getFingerprint.ts'
+
+// eslint-disable-next-line @typescript-eslint/ban-ts-comment
+// @ts-ignore We don't want TS to generate types for the package.json
 import packageJson from '../package.json'
 
-/** @typedef {import('..').TusOptions} TusOptions */
-/** @typedef {import('tus-js-client').UploadOptions} RawTusOptions */
-/** @typedef {import('@uppy/core').Uppy} Uppy */
-/** @typedef {import('@uppy/core').UppyFile} UppyFile */
-/** @typedef {import('@uppy/core').FailedUppyFile<{}>} FailedUppyFile */
+declare module '@uppy/utils/lib/UppyFile' {
+  // eslint-disable-next-line no-shadow, @typescript-eslint/no-unused-vars
+  export interface UppyFile<M extends Meta, B extends Body> {
+    // TODO: figure out what else is in this type
+    tus?: { uploadUrl?: string | null }
+  }
+}
+
+type RestTusUploadOptions = Omit<
+  tus.UploadOptions,
+  'onShouldRetry' | 'onBeforeRequest' | 'headers'
+>
+
+export interface TusOpts<M extends Meta, B extends Body>
+  extends PluginOpts,
+    RestTusUploadOptions {
+  endpoint: string
+  headers?:
+    | Record<string, string>
+    | ((file: UppyFile<M, B>) => Record<string, string>)
+  limit?: number
+  chunkSize?: number
+  onBeforeRequest?: (req: tus.HttpRequest, file: UppyFile<M, B>) => void
+  onShouldRetry?: (
+    err: tus.DetailedError,
+    retryAttempt: number,
+    options: TusOpts<M, B>,
+    next: (e: tus.DetailedError) => void,
+  ) => boolean
+  retryDelays?: number[]
+  withCredentials?: boolean
+  allowedMetaFields?: string[]
+  rateLimitedQueue?: RateLimitedQueue
+}
 
 /**
  * Extracted from https://github.com/tus/tus-js-client/blob/master/lib/upload.js#L13
  * excepted we removed 'fingerprint' key to avoid adding more dependencies
- *
- * @type {RawTusOptions}
  */
 const tusDefaultOptions = {
   endpoint: '',
@@ -44,43 +84,52 @@ const tusDefaultOptions = {
   removeFingerprintOnSuccess: false,
   uploadLengthDeferred: false,
   uploadDataDuringCreation: false,
-}
+} satisfies tus.UploadOptions
+
+const defaultOptions = {
+  limit: 20,
+  retryDelays: tusDefaultOptions.retryDelays,
+  withCredentials: false,
+} satisfies Partial<TusOpts<any, any>>
+
+type Opts<M extends Meta, B extends Body> = DefinePluginOpts<
+  TusOpts<M, B>,
+  keyof typeof defaultOptions
+>
 
 /**
  * Tus resumable file uploader
  */
-export default class Tus extends BasePlugin {
+export default class Tus<M extends Meta, B extends Body> extends BasePlugin<
+  Opts<M, B>,
+  M,
+  B
+> {
   static VERSION = packageJson.version
 
   #retryDelayIterator
 
-  /**
-   * @param {Uppy} uppy
-   * @param {TusOptions} opts
-   */
-  constructor (uppy, opts) {
-    super(uppy, opts)
-    this.type = 'uploader'
-    this.id = this.opts.id || 'Tus'
-    this.title = 'Tus'
+  requests: RateLimitedQueue
 
-    // set default options
-    const defaultOptions = {
-      limit: 20,
-      retryDelays: tusDefaultOptions.retryDelays,
-      withCredentials: false,
-    }
+  uploaders: Record<string, tus.Upload | null>
+
+  uploaderEvents: Record<string, EventManager<M, B> | null>
 
-    // merge default options with the ones set by user
-    /** @type {import("..").TusOptions} */
-    this.opts = { ...defaultOptions, ...opts }
+  constructor(uppy: Uppy<M, B>, opts: TusOpts<M, B>) {
+    super(uppy, { ...defaultOptions, ...opts })
+    this.type = 'uploader'
+    this.id = this.opts.id || 'Tus'
 
     if (opts?.allowedMetaFields === undefined && 'metaFields' in this.opts) {
-      throw new Error('The `metaFields` option has been renamed to `allowedMetaFields`.')
+      throw new Error(
+        'The `metaFields` option has been renamed to `allowedMetaFields`.',
+      )
     }
 
     if ('autoRetry' in opts) {
-      throw new Error('The `autoRetry` option was deprecated and has been removed.')
+      throw new Error(
+        'The `autoRetry` option was deprecated and has been removed.',
+      )
     }
 
     /**
@@ -88,7 +137,8 @@ export default class Tus extends BasePlugin {
      *
      * @type {RateLimitedQueue}
      */
-    this.requests = this.opts.rateLimitedQueue ?? new RateLimitedQueue(this.opts.limit)
+    this.requests =
+      this.opts.rateLimitedQueue ?? new RateLimitedQueue(this.opts.limit)
     this.#retryDelayIterator = this.opts.retryDelays?.values()
 
     this.uploaders = Object.create(null)
@@ -97,11 +147,11 @@ export default class Tus extends BasePlugin {
     this.handleResetProgress = this.handleResetProgress.bind(this)
   }
 
-  handleResetProgress () {
+  handleResetProgress(): void {
     const files = { ...this.uppy.getState().files }
     Object.keys(files).forEach((fileID) => {
       // Only clone the file object if it has a Tus `uploadUrl` attached.
-      if (files[fileID].tus && files[fileID].tus.uploadUrl) {
+      if (files[fileID]?.tus?.uploadUrl) {
         const tusState = { ...files[fileID].tus }
         delete tusState.uploadUrl
         files[fileID] = { ...files[fileID], tus: tusState }
@@ -114,23 +164,21 @@ export default class Tus extends BasePlugin {
   /**
    * Clean up all references for a file's upload: the tus.Upload instance,
    * any events related to the file, and the Companion WebSocket connection.
-   *
-   * @param {string} fileID
    */
-  resetUploaderReferences (fileID, opts = {}) {
+  resetUploaderReferences(fileID: string, opts?: { abort: boolean }): void {
     if (this.uploaders[fileID]) {
       const uploader = this.uploaders[fileID]
 
-      uploader.abort()
+      uploader!.abort()
 
-      if (opts.abort) {
-        uploader.abort(true)
+      if (opts?.abort) {
+        uploader!.abort(true)
       }
 
       this.uploaders[fileID] = null
     }
     if (this.uploaderEvents[fileID]) {
-      this.uploaderEvents[fileID].remove()
+      this.uploaderEvents[fileID]!.remove()
       this.uploaderEvents[fileID] = null
     }
   }
@@ -167,17 +215,15 @@ export default class Tus extends BasePlugin {
    *  - Before replacing the `queuedRequest` variable, the previous `queuedRequest` must be aborted, else it will keep taking
    *    up a spot in the queue.
    *
-   * @param {UppyFile} file for use with upload
-   * @returns {Promise<void>}
    */
-  #uploadLocalFile (file) {
+  #uploadLocalFile(file: UppyFile<M, B>): Promise<tus.Upload | string> {
     this.resetUploaderReferences(file.id)
 
     // Create a new tus upload
-    return new Promise((resolve, reject) => {
-      let queuedRequest
-      let qRequest
-      let upload
+    return new Promise<tus.Upload | string>((resolve, reject) => {
+      let queuedRequest: RateLimitedQueue.QueueEntry
+      let qRequest: () => void
+      let upload: tus.Upload
 
       const opts = {
         ...this.opts,
@@ -188,10 +234,11 @@ export default class Tus extends BasePlugin {
         opts.headers = opts.headers(file)
       }
 
-      /** @type {RawTusOptions} */
-      const uploadOptions = {
+      const { onShouldRetry, onBeforeRequest, ...commonOpts } = opts
+
+      const uploadOptions: tus.UploadOptions = {
         ...tusDefaultOptions,
-        ...opts,
+        ...commonOpts,
       }
 
       // We override tus fingerprint to uppy’s `file.id`, since the `file.id`
@@ -200,19 +247,21 @@ export default class Tus extends BasePlugin {
       // the other in folder b.
       uploadOptions.fingerprint = getFingerprint(file)
 
-      uploadOptions.onBeforeRequest = (req) => {
+      uploadOptions.onBeforeRequest = async (req) => {
         const xhr = req.getUnderlyingObject()
         xhr.withCredentials = !!opts.withCredentials
 
         let userProvidedPromise
-        if (typeof opts.onBeforeRequest === 'function') {
-          userProvidedPromise = opts.onBeforeRequest(req, file)
+        if (typeof onBeforeRequest === 'function') {
+          userProvidedPromise = onBeforeRequest(req, file)
         }
 
         if (hasProperty(queuedRequest, 'shouldBeRequeued')) {
           if (!queuedRequest.shouldBeRequeued) return Promise.reject()
-          let done
-          const p = new Promise((res) => { // eslint-disable-line promise/param-names
+          // TODO: switch to `Promise.withResolvers` on the next major if available.
+          let done: () => void
+          // eslint-disable-next-line promise/param-names
+          const p = new Promise<void>((res) => {
             done = res
           })
           queuedRequest = this.requests.run(() => {
@@ -230,7 +279,8 @@ export default class Tus extends BasePlugin {
           // This means we can hold the Tus retry here with a `Promise.all`,
           // together with the returned value of the user provided
           // `onBeforeRequest` option callback (in case it returns a promise).
-          return Promise.all([p, userProvidedPromise])
+          await Promise.all([p, userProvidedPromise])
+          return undefined
         }
         return userProvidedPromise
       }
@@ -238,7 +288,10 @@ export default class Tus extends BasePlugin {
       uploadOptions.onError = (err) => {
         this.uppy.log(err)
 
-        const xhr = err.originalRequest ? err.originalRequest.getUnderlyingObject() : null
+        const xhr =
+          (err as tus.DetailedError).originalRequest != null ?
+            (err as tus.DetailedError).originalRequest.getUnderlyingObject()
+          : null
         if (isNetworkError(xhr)) {
           // eslint-disable-next-line no-param-reassign
           err = new NetworkError(err, xhr)
@@ -260,6 +313,8 @@ export default class Tus extends BasePlugin {
           opts.onProgress(bytesUploaded, bytesTotal)
         }
         this.uppy.emit('upload-progress', file, {
+          // TODO: remove `uploader` in next major
+          // @ts-expect-error untyped
           uploader: this,
           bytesUploaded,
           bytesTotal,
@@ -268,7 +323,9 @@ export default class Tus extends BasePlugin {
 
       uploadOptions.onSuccess = () => {
         const uploadResp = {
-          uploadURL: upload.url,
+          uploadURL: upload.url ?? undefined,
+          status: 200,
+          body: {} as B,
         }
 
         this.resetUploaderReferences(file.id)
@@ -277,7 +334,9 @@ export default class Tus extends BasePlugin {
         this.uppy.emit('upload-success', file, uploadResp)
 
         if (upload.url) {
-          this.uppy.log(`Download ${upload.file.name} from ${upload.url}`)
+          // @ts-expect-error not typed in tus-js-client
+          const { name } = upload.file
+          this.uppy.log(`Download ${name} from ${upload.url}`)
         }
         if (typeof opts.onSuccess === 'function') {
           opts.onSuccess()
@@ -286,7 +345,7 @@ export default class Tus extends BasePlugin {
         resolve(upload)
       }
 
-      const defaultOnShouldRetry = (err) => {
+      const defaultOnShouldRetry = (err: tus.DetailedError) => {
         const status = err?.originalResponse?.getStatus()
 
         if (status === 429) {
@@ -298,57 +357,86 @@ export default class Tus extends BasePlugin {
             }
             this.requests.rateLimit(next.value)
           }
-        } else if (status > 400 && status < 500 && status !== 409 && status !== 423) {
+        } else if (
+          status != null &&
+          status > 400 &&
+          status < 500 &&
+          status !== 409 &&
+          status !== 423
+        ) {
           // HTTP 4xx, the server won't send anything, it's doesn't make sense to retry
           // HTTP 409 Conflict (happens if the Upload-Offset header does not match the one on the server)
           // HTTP 423 Locked (happens when a paused download is resumed too quickly)
           return false
-        } else if (typeof navigator !== 'undefined' && navigator.onLine === false) {
+        } else if (
+          typeof navigator !== 'undefined' &&
+          navigator.onLine === false
+        ) {
           // The navigator is offline, let's wait for it to come back online.
           if (!this.requests.isPaused) {
             this.requests.pause()
-            window.addEventListener('online', () => {
-              this.requests.resume()
-            }, { once: true })
+            window.addEventListener(
+              'online',
+              () => {
+                this.requests.resume()
+              },
+              { once: true },
+            )
           }
         }
         queuedRequest.abort()
         queuedRequest = {
           shouldBeRequeued: true,
-          abort () {
+          abort() {
             this.shouldBeRequeued = false
           },
-          done () {
-            throw new Error('Cannot mark a queued request as done: this indicates a bug')
+          done() {
+            throw new Error(
+              'Cannot mark a queued request as done: this indicates a bug',
+            )
           },
-          fn () {
+          fn() {
             throw new Error('Cannot run a queued request: this indicates a bug')
           },
         }
         return true
       }
 
-      if (opts.onShouldRetry != null) {
-        uploadOptions.onShouldRetry = (...args) => opts.onShouldRetry(...args, defaultOnShouldRetry)
+      if (onShouldRetry != null) {
+        uploadOptions.onShouldRetry = (
+          error: tus.DetailedError,
+          retryAttempt: number,
+        ) => onShouldRetry(error, retryAttempt, opts, defaultOnShouldRetry)
       } else {
         uploadOptions.onShouldRetry = defaultOnShouldRetry
       }
 
-      const copyProp = (obj, srcProp, destProp) => {
+      const copyProp = (
+        obj: Record<string, unknown>,
+        srcProp: string,
+        destProp: string,
+      ) => {
         if (hasProperty(obj, srcProp) && !hasProperty(obj, destProp)) {
           // eslint-disable-next-line no-param-reassign
           obj[destProp] = obj[srcProp]
         }
       }
 
-      /** @type {Record<string, string>} */
-      const meta = {}
-      const allowedMetaFields = Array.isArray(opts.allowedMetaFields)
-        ? opts.allowedMetaFields
-        // Send along all fields by default.
+      // We can't use `allowedMetaFields` to index generic M
+      // and we also don't care about the type specifically here,
+      // we just want to pass the meta fields along.
+      const meta: Record<string, string> = {}
+      const allowedMetaFields =
+        Array.isArray(opts.allowedMetaFields) ?
+          opts.allowedMetaFields
+          // Send along all fields by default.
         : Object.keys(file.meta)
       allowedMetaFields.forEach((item) => {
-        meta[item] = file.meta[item]
+        // tus type definition for metadata only accepts `Record<string, string>`
+        // but in reality (at runtime) it accepts `Record<string, unknown>`
+        // tus internally converts everything into a string, but let's do it here instead to be explicit.
+        // because Uppy can have anything inside meta values, (for example relativePath: null is often sent by uppy)
+        meta[item] = String(file.meta[item])
       })
 
       // tusd uses metadata fields 'filetype' and 'filename'
@@ -379,7 +467,9 @@ export default class Tus extends BasePlugin {
       upload.findPreviousUploads().then((previousUploads) => {
         const previousUpload = previousUploads[0]
         if (previousUpload) {
-          this.uppy.log(`[Tus] Resuming upload of ${file.id} started at ${previousUpload.creationTime}`)
+          this.uppy.log(
+            `[Tus] Resuming upload of ${file.id} started at ${previousUpload.creationTime}`,
+          )
           upload.resumeFromPreviousUpload(previousUpload)
         }
       })
@@ -433,11 +523,8 @@ export default class Tus extends BasePlugin {
   /**
    * Store the uploadUrl on the file options, so that when Golden Retriever
    * restores state, we will continue uploading to the correct URL.
-   *
-   * @param {UppyFile} file
-   * @param {string} uploadURL
    */
-  onReceiveUploadUrl (file, uploadURL) {
+  onReceiveUploadUrl(file: UppyFile<M, B>, uploadURL: string | null): void {
     const currentFile = this.uppy.getFile(file.id)
     if (!currentFile) return
     // Only do the update if we didn't have an upload URL yet.
@@ -449,7 +536,7 @@ export default class Tus extends BasePlugin {
     }
   }
 
-  #getCompanionClientArgs (file) {
+  #getCompanionClientArgs(file: UppyFile<M, B>) {
     const opts = { ...this.opts }
 
     if (file.tus) {
@@ -458,7 +545,7 @@ export default class Tus extends BasePlugin {
     }
 
     return {
-      ...file.remote.body,
+      ...file.remote?.body,
       endpoint: opts.endpoint,
       uploadUrl: opts.uploadUrl,
       protocol: 'tus',
@@ -468,48 +555,45 @@ export default class Tus extends BasePlugin {
     }
   }
 
-  /**
-   * @param {(UppyFile | FailedUppyFile)[]} files
-   */
-  async #uploadFiles (files) {
+  async #uploadFiles(files: UppyFile<M, B>[]) {
     const filesFiltered = filterNonFailedFiles(files)
     const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered)
     this.uppy.emit('upload-start', filesToEmit)
 
-    await Promise.allSettled(filesFiltered.map((file, i) => {
-      const current = i + 1
-      const total = files.length
-
-      if (file.isRemote) {
-        const getQueue = () => this.requests
-        const controller = new AbortController()
+    await Promise.allSettled(
+      filesFiltered.map((file) => {
+        if (file.isRemote) {
+          const getQueue = () => this.requests
+          const controller = new AbortController()
 
-        const removedHandler = (removedFile) => {
-          if (removedFile.id === file.id) controller.abort()
+          const removedHandler = (removedFile: UppyFile<M, B>) => {
+            if (removedFile.id === file.id) controller.abort()
+          }
+          this.uppy.on('file-removed', removedHandler)
+
+          const uploadPromise = this.uppy
+            .getRequestClientForFile<RequestClient<M, B>>(file)
+            .uploadRemoteFile(file, this.#getCompanionClientArgs(file), {
+              signal: controller.signal,
+              getQueue,
+            })
+
+          this.requests.wrapSyncFunction(
+            () => {
+              this.uppy.off('file-removed', removedHandler)
+            },
+            { priority: -1 },
+          )()
+
+          return uploadPromise
         }
-        this.uppy.on('file-removed', removedHandler)
-
-        const uploadPromise = this.uppy.getRequestClientForFile(file).uploadRemoteFile(
-          file,
-          this.#getCompanionClientArgs(file),
-          { signal: controller.signal, getQueue },
-        )
 
-        this.requests.wrapSyncFunction(() => {
-          this.uppy.off('file-removed', removedHandler)
-        }, { priority: -1 })()
-
-        return uploadPromise
-      }
-
-      return this.#uploadLocalFile(file, current, total)
-    }))
+        return this.#uploadLocalFile(file)
+      }),
+    )
   }
 
-  /**
-   * @param {string[]} fileIDs
-   */
-  #handleUpload = async (fileIDs) => {
+  #handleUpload = async (fileIDs: string[]) => {
     if (fileIDs.length === 0) {
       this.uppy.log('[Tus] No files to upload')
       return
@@ -528,18 +612,24 @@ export default class Tus extends BasePlugin {
     await this.#uploadFiles(filesToUpload)
   }
 
-  install () {
+  install(): void {
     this.uppy.setState({
-      capabilities: { ...this.uppy.getState().capabilities, resumableUploads: true },
+      capabilities: {
+        ...this.uppy.getState().capabilities,
+        resumableUploads: true,
+      },
     })
     this.uppy.addUploader(this.#handleUpload)
 
     this.uppy.on('reset-progress', this.handleResetProgress)
   }
 
-  uninstall () {
+  uninstall(): void {
     this.uppy.setState({
-      capabilities: { ...this.uppy.getState().capabilities, resumableUploads: false },
+      capabilities: {
+        ...this.uppy.getState().capabilities,
+        resumableUploads: false,
+      },
     })
     this.uppy.removeUploader(this.#handleUpload)
   }

+ 30 - 0
packages/@uppy/tus/tsconfig.build.json

@@ -0,0 +1,30 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "noImplicitAny": false,
+    "outDir": "./lib",
+    "paths": {
+      "@uppy/companion-client": ["../companion-client/src/index.js"],
+      "@uppy/companion-client/lib/*": ["../companion-client/src/*"],
+      "@uppy/utils/lib/*": ["../utils/src/*"],
+      "@uppy/core": ["../core/src/index.js"],
+      "@uppy/core/lib/*": ["../core/src/*"]
+    },
+    "resolveJsonModule": false,
+    "rootDir": "./src",
+    "skipLibCheck": true
+  },
+  "include": ["./src/**/*.*"],
+  "exclude": ["./src/**/*.test.ts"],
+  "references": [
+    {
+      "path": "../companion-client/tsconfig.build.json"
+    },
+    {
+      "path": "../utils/tsconfig.build.json"
+    },
+    {
+      "path": "../core/tsconfig.build.json"
+    }
+  ]
+}

+ 26 - 0
packages/@uppy/tus/tsconfig.json

@@ -0,0 +1,26 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "emitDeclarationOnly": false,
+    "noEmit": true,
+    "paths": {
+      "@uppy/companion-client": ["../companion-client/src/index.js"],
+      "@uppy/companion-client/lib/*": ["../companion-client/src/*"],
+      "@uppy/utils/lib/*": ["../utils/src/*"],
+      "@uppy/core": ["../core/src/index.js"],
+      "@uppy/core/lib/*": ["../core/src/*"],
+    },
+  },
+  "include": ["./package.json", "./src/**/*.*"],
+  "references": [
+    {
+      "path": "../companion-client/tsconfig.build.json",
+    },
+    {
+      "path": "../utils/tsconfig.build.json",
+    },
+    {
+      "path": "../core/tsconfig.build.json",
+    },
+  ],
+}