From 51ecc66e64bb5e38b6ff47d3ead30bc3add6b95e Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Mon, 6 Nov 2023 15:01:50 +0100 Subject: [PATCH] @uppy/utils: refactor to TS (#4699) Co-authored-by: Mikael Finstad Co-authored-by: Nick Rutten <2504906+nickrttn@users.noreply.github.com> Co-authored-by: Murderlon Co-authored-by: Artur Paikin --- packages/@uppy/core/types/index.d.ts | 3 + packages/@uppy/utils/package.json | 1 + packages/@uppy/utils/src/AbortController.js | 13 -- ...roller.test.js => AbortController.test.ts} | 6 +- packages/@uppy/utils/src/AbortController.ts | 22 +++ packages/@uppy/utils/src/ErrorWithCause.js | 13 -- .../@uppy/utils/src/ErrorWithCause.test.js | 21 --- .../@uppy/utils/src/ErrorWithCause.test.ts | 24 +++ packages/@uppy/utils/src/ErrorWithCause.ts | 23 +++ packages/@uppy/utils/src/EventManager.js | 83 ---------- packages/@uppy/utils/src/EventManager.ts | 115 +++++++++++++ packages/@uppy/utils/src/FileProgress.ts | 18 ++ packages/@uppy/utils/src/NetworkError.js | 11 -- packages/@uppy/utils/src/NetworkError.ts | 19 +++ ...{ProgressTimeout.js => ProgressTimeout.ts} | 17 +- .../@uppy/utils/src/RateLimitedQueue.test.js | 48 ++++-- ...{Translator.test.js => Translator.test.ts} | 80 +++++---- .../src/{Translator.js => Translator.ts} | 74 ++++++--- packages/@uppy/utils/src/UppyFile.ts | 41 +++++ .../src/{canvasToBlob.js => canvasToBlob.ts} | 6 +- ...RItoBlob.test.js => dataURItoBlob.test.ts} | 2 +- .../{dataURItoBlob.js => dataURItoBlob.ts} | 12 +- packages/@uppy/utils/src/dataURItoFile.js | 5 - ...RItoFile.test.js => dataURItoFile.test.ts} | 2 +- packages/@uppy/utils/src/dataURItoFile.ts | 8 + .../src/{delay.test.js => delay.test.ts} | 11 +- .../@uppy/utils/src/{delay.js => delay.ts} | 15 +- packages/@uppy/utils/src/emaFilter.js | 17 -- .../{emaFilter.test.js => emaFilter.test.ts} | 6 +- packages/@uppy/utils/src/emaFilter.ts | 22 +++ ...ocketProgress.js => emitSocketProgress.ts} | 8 +- .../@uppy/utils/src/fetchWithNetworkError.js | 15 -- .../@uppy/utils/src/fetchWithNetworkError.ts | 16 ++ packages/@uppy/utils/src/fileFilters.js | 10 -- packages/@uppy/utils/src/fileFilters.ts | 14 ++ ...llDOMElements.js => findAllDOMElements.ts} | 9 +- .../{findDOMElement.js => findDOMElement.ts} | 10 +- packages/@uppy/utils/src/findIndex.test.js | 16 +- .../@uppy/utils/src/generateFileID.test.js | 2 +- .../{generateFileID.js => generateFileID.ts} | 40 +++-- packages/@uppy/utils/src/getBytesRemaining.js | 3 - ...ning.test.js => getBytesRemaining.test.ts} | 6 +- packages/@uppy/utils/src/getBytesRemaining.ts | 5 + .../getDroppedFiles/{index.js => index.ts} | 21 ++- .../utils/{fallbackApi.js => fallbackApi.ts} | 6 +- ...=> getFilesAndDirectoriesFromDirectory.ts} | 21 ++- .../utils/webkitGetAsEntryApi/index.js | 96 ----------- .../utils/webkitGetAsEntryApi/index.ts | 154 ++++++++++++++++++ packages/@uppy/utils/src/getETA.test.js | 2 +- .../@uppy/utils/src/{getETA.js => getETA.ts} | 7 +- ...est.js => getFileNameAndExtension.test.ts} | 2 +- ...xtension.js => getFileNameAndExtension.ts} | 8 +- packages/@uppy/utils/src/getFileType.js | 14 -- ...etFileType.test.js => getFileType.test.ts} | 24 +-- packages/@uppy/utils/src/getFileType.ts | 17 ++ ...n.test.js => getFileTypeExtension.test.ts} | 2 +- ...peExtension.js => getFileTypeExtension.ts} | 7 +- .../@uppy/utils/src/getSocketHost.test.js | 22 --- .../@uppy/utils/src/getSocketHost.test.ts | 22 +++ .../{getSocketHost.js => getSocketHost.ts} | 4 +- .../{getSpeed.test.js => getSpeed.test.ts} | 8 +- .../utils/src/{getSpeed.js => getSpeed.ts} | 4 +- ...etTextDirection.js => getTextDirection.ts} | 7 +- .../src/{getTimeStamp.js => getTimeStamp.ts} | 7 +- packages/@uppy/utils/src/hasProperty.js | 3 - packages/@uppy/utils/src/hasProperty.ts | 6 + packages/@uppy/utils/src/isDOMElement.js | 8 - packages/@uppy/utils/src/isDOMElement.ts | 8 + ...ropSupported.js => isDragDropSupported.ts} | 4 +- packages/@uppy/utils/src/isMobileDevice.js | 15 -- .../@uppy/utils/src/isMobileDevice.test.js | 30 ---- .../@uppy/utils/src/isMobileDevice.test.ts | 38 +++++ packages/@uppy/utils/src/isMobileDevice.ts | 15 ++ ...rkError.test.js => isNetworkError.test.ts} | 10 +- .../{isNetworkError.js => isNetworkError.ts} | 2 +- ...sObjectURL.test.js => isObjectURL.test.ts} | 2 +- .../src/{isObjectURL.js => isObjectURL.ts} | 5 +- .../utils/src/isPreviewSupported.test.js | 12 -- .../utils/src/isPreviewSupported.test.ts | 22 +++ ...viewSupported.js => isPreviewSupported.ts} | 2 +- .../@uppy/utils/src/isTouchDevice.test.js | 2 +- .../{isTouchDevice.js => isTouchDevice.ts} | 2 +- packages/@uppy/utils/src/mimeTypes.js | 58 ------- packages/@uppy/utils/src/mimeTypes.ts | 59 +++++++ packages/@uppy/utils/src/prettyETA.js | 14 -- .../{prettyETA.test.js => prettyETA.test.ts} | 2 +- packages/@uppy/utils/src/prettyETA.ts | 28 ++++ .../@uppy/utils/src/remoteFileObjToLocal.js | 9 - .../@uppy/utils/src/remoteFileObjToLocal.ts | 19 +++ ...eImageDataURI.js => sampleImageDataURI.ts} | 0 ...dsToTime.test.js => secondsToTime.test.ts} | 2 +- .../{secondsToTime.js => secondsToTime.ts} | 8 +- .../src/{toArray.test.js => toArray.test.ts} | 10 +- .../utils/src/{toArray.js => toArray.ts} | 0 ...eString.test.js => truncateString.test.ts} | 2 +- .../{truncateString.js => truncateString.ts} | 12 +- packages/@uppy/utils/tsconfig.build.json | 12 ++ packages/@uppy/utils/tsconfig.json | 10 ++ yarn.lock | 8 + 99 files changed, 1083 insertions(+), 703 deletions(-) delete mode 100644 packages/@uppy/utils/src/AbortController.js rename packages/@uppy/utils/src/{AbortController.test.js => AbortController.test.ts} (93%) create mode 100644 packages/@uppy/utils/src/AbortController.ts delete mode 100644 packages/@uppy/utils/src/ErrorWithCause.js delete mode 100644 packages/@uppy/utils/src/ErrorWithCause.test.js create mode 100644 packages/@uppy/utils/src/ErrorWithCause.test.ts create mode 100644 packages/@uppy/utils/src/ErrorWithCause.ts delete mode 100644 packages/@uppy/utils/src/EventManager.js create mode 100644 packages/@uppy/utils/src/EventManager.ts create mode 100644 packages/@uppy/utils/src/FileProgress.ts delete mode 100644 packages/@uppy/utils/src/NetworkError.js create mode 100644 packages/@uppy/utils/src/NetworkError.ts rename packages/@uppy/utils/src/{ProgressTimeout.js => ProgressTimeout.ts} (75%) rename packages/@uppy/utils/src/{Translator.test.js => Translator.test.ts} (69%) rename packages/@uppy/utils/src/{Translator.js => Translator.ts} (67%) create mode 100644 packages/@uppy/utils/src/UppyFile.ts rename packages/@uppy/utils/src/{canvasToBlob.js => canvasToBlob.ts} (61%) rename packages/@uppy/utils/src/{dataURItoBlob.test.js => dataURItoBlob.test.ts} (88%) rename packages/@uppy/utils/src/{dataURItoBlob.js => dataURItoBlob.ts} (68%) delete mode 100644 packages/@uppy/utils/src/dataURItoFile.js rename packages/@uppy/utils/src/{dataURItoFile.test.js => dataURItoFile.test.ts} (90%) create mode 100644 packages/@uppy/utils/src/dataURItoFile.ts rename packages/@uppy/utils/src/{delay.test.js => delay.test.ts} (86%) rename packages/@uppy/utils/src/{delay.js => delay.ts} (59%) delete mode 100644 packages/@uppy/utils/src/emaFilter.js rename packages/@uppy/utils/src/{emaFilter.test.js => emaFilter.test.ts} (86%) create mode 100644 packages/@uppy/utils/src/emaFilter.ts rename packages/@uppy/utils/src/{emitSocketProgress.js => emitSocketProgress.ts} (66%) delete mode 100644 packages/@uppy/utils/src/fetchWithNetworkError.js create mode 100644 packages/@uppy/utils/src/fetchWithNetworkError.ts delete mode 100644 packages/@uppy/utils/src/fileFilters.js create mode 100644 packages/@uppy/utils/src/fileFilters.ts rename packages/@uppy/utils/src/{findAllDOMElements.js => findAllDOMElements.ts} (65%) rename packages/@uppy/utils/src/{findDOMElement.js => findDOMElement.ts} (51%) rename packages/@uppy/utils/src/{generateFileID.js => generateFileID.ts} (62%) delete mode 100644 packages/@uppy/utils/src/getBytesRemaining.js rename packages/@uppy/utils/src/{getBytesRemaining.test.js => getBytesRemaining.test.ts} (68%) create mode 100644 packages/@uppy/utils/src/getBytesRemaining.ts rename packages/@uppy/utils/src/getDroppedFiles/{index.js => index.ts} (66%) rename packages/@uppy/utils/src/getDroppedFiles/utils/{fallbackApi.js => fallbackApi.ts} (51%) rename packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/{getFilesAndDirectoriesFromDirectory.js => getFilesAndDirectoriesFromDirectory.ts} (57%) delete mode 100644 packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js create mode 100644 packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.ts rename packages/@uppy/utils/src/{getETA.js => getETA.ts} (54%) rename packages/@uppy/utils/src/{getFileNameAndExtension.test.js => getFileNameAndExtension.test.ts} (88%) rename packages/@uppy/utils/src/{getFileNameAndExtension.js => getFileNameAndExtension.ts} (75%) delete mode 100644 packages/@uppy/utils/src/getFileType.js rename packages/@uppy/utils/src/{getFileType.test.js => getFileType.test.ts} (82%) create mode 100644 packages/@uppy/utils/src/getFileType.ts rename packages/@uppy/utils/src/{getFileTypeExtension.test.js => getFileTypeExtension.test.ts} (93%) rename packages/@uppy/utils/src/{getFileTypeExtension.js => getFileTypeExtension.ts} (76%) delete mode 100644 packages/@uppy/utils/src/getSocketHost.test.js create mode 100644 packages/@uppy/utils/src/getSocketHost.test.ts rename packages/@uppy/utils/src/{getSocketHost.js => getSocketHost.ts} (67%) rename packages/@uppy/utils/src/{getSpeed.test.js => getSpeed.test.ts} (68%) rename packages/@uppy/utils/src/{getSpeed.js => getSpeed.ts} (62%) rename packages/@uppy/utils/src/{getTextDirection.js => getTextDirection.ts} (83%) rename packages/@uppy/utils/src/{getTimeStamp.js => getTimeStamp.ts} (76%) delete mode 100644 packages/@uppy/utils/src/hasProperty.js create mode 100644 packages/@uppy/utils/src/hasProperty.ts delete mode 100644 packages/@uppy/utils/src/isDOMElement.js create mode 100644 packages/@uppy/utils/src/isDOMElement.ts rename packages/@uppy/utils/src/{isDragDropSupported.js => isDragDropSupported.ts} (82%) delete mode 100644 packages/@uppy/utils/src/isMobileDevice.js delete mode 100644 packages/@uppy/utils/src/isMobileDevice.test.js create mode 100644 packages/@uppy/utils/src/isMobileDevice.test.ts create mode 100644 packages/@uppy/utils/src/isMobileDevice.ts rename packages/@uppy/utils/src/{isNetworkError.test.js => isNetworkError.test.ts} (82%) rename packages/@uppy/utils/src/{isNetworkError.js => isNetworkError.ts} (71%) rename packages/@uppy/utils/src/{isObjectURL.test.js => isObjectURL.test.ts} (89%) rename packages/@uppy/utils/src/{isObjectURL.js => isObjectURL.ts} (55%) delete mode 100644 packages/@uppy/utils/src/isPreviewSupported.test.js create mode 100644 packages/@uppy/utils/src/isPreviewSupported.test.ts rename packages/@uppy/utils/src/{isPreviewSupported.js => isPreviewSupported.ts} (68%) rename packages/@uppy/utils/src/{isTouchDevice.js => isTouchDevice.ts} (57%) delete mode 100644 packages/@uppy/utils/src/mimeTypes.js create mode 100644 packages/@uppy/utils/src/mimeTypes.ts delete mode 100644 packages/@uppy/utils/src/prettyETA.js rename packages/@uppy/utils/src/{prettyETA.test.js => prettyETA.test.ts} (93%) create mode 100644 packages/@uppy/utils/src/prettyETA.ts delete mode 100644 packages/@uppy/utils/src/remoteFileObjToLocal.js create mode 100644 packages/@uppy/utils/src/remoteFileObjToLocal.ts rename packages/@uppy/utils/src/{sampleImageDataURI.js => sampleImageDataURI.ts} (100%) rename packages/@uppy/utils/src/{secondsToTime.test.js => secondsToTime.test.ts} (92%) rename packages/@uppy/utils/src/{secondsToTime.js => secondsToTime.ts} (57%) rename packages/@uppy/utils/src/{toArray.test.js => toArray.test.ts} (65%) rename packages/@uppy/utils/src/{toArray.js => toArray.ts} (100%) rename packages/@uppy/utils/src/{truncateString.test.js => truncateString.test.ts} (96%) rename packages/@uppy/utils/src/{truncateString.js => truncateString.ts} (71%) create mode 100644 packages/@uppy/utils/tsconfig.build.json create mode 100644 packages/@uppy/utils/tsconfig.json diff --git a/packages/@uppy/core/types/index.d.ts b/packages/@uppy/core/types/index.d.ts index e9f323283..7c0b39ea3 100644 --- a/packages/@uppy/core/types/index.d.ts +++ b/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 +/// import * as UppyUtils from '@uppy/utils' // Utility types diff --git a/packages/@uppy/utils/package.json b/packages/@uppy/utils/package.json index 4b0fb4416..6e8c64085 100644 --- a/packages/@uppy/utils/package.json +++ b/packages/@uppy/utils/package.json @@ -72,6 +72,7 @@ "preact": "^10.5.13" }, "devDependencies": { + "@types/lodash": "^4.14.199", "vitest": "^0.34.5" } } diff --git a/packages/@uppy/utils/src/AbortController.js b/packages/@uppy/utils/src/AbortController.js deleted file mode 100644 index 366cb2c82..000000000 --- a/packages/@uppy/utils/src/AbortController.js +++ /dev/null @@ -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 -} diff --git a/packages/@uppy/utils/src/AbortController.test.js b/packages/@uppy/utils/src/AbortController.test.ts similarity index 93% rename from packages/@uppy/utils/src/AbortController.test.js rename to packages/@uppy/utils/src/AbortController.test.ts index b5f323532..f807ee27b 100644 --- a/packages/@uppy/utils/src/AbortController.test.js +++ b/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', () => { diff --git a/packages/@uppy/utils/src/AbortController.ts b/packages/@uppy/utils/src/AbortController.ts new file mode 100644 index 000000000..361c2bfba --- /dev/null +++ b/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[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 +} diff --git a/packages/@uppy/utils/src/ErrorWithCause.js b/packages/@uppy/utils/src/ErrorWithCause.js deleted file mode 100644 index 63a34c6f0..000000000 --- a/packages/@uppy/utils/src/ErrorWithCause.js +++ /dev/null @@ -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 diff --git a/packages/@uppy/utils/src/ErrorWithCause.test.js b/packages/@uppy/utils/src/ErrorWithCause.test.js deleted file mode 100644 index 9176bb65f..000000000 --- a/packages/@uppy/utils/src/ErrorWithCause.test.js +++ /dev/null @@ -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) - }) -}) diff --git a/packages/@uppy/utils/src/ErrorWithCause.test.ts b/packages/@uppy/utils/src/ErrorWithCause.test.ts new file mode 100644 index 000000000..4a64fadf4 --- /dev/null +++ b/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) + }) +}) diff --git a/packages/@uppy/utils/src/ErrorWithCause.ts b/packages/@uppy/utils/src/ErrorWithCause.ts new file mode 100644 index 000000000..31a005b12 --- /dev/null +++ b/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[0], + options?: ConstructorParameters[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 diff --git a/packages/@uppy/utils/src/EventManager.js b/packages/@uppy/utils/src/EventManager.js deleted file mode 100644 index b675443d1..000000000 --- a/packages/@uppy/utils/src/EventManager.js +++ /dev/null @@ -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() - }) - } -} diff --git a/packages/@uppy/utils/src/EventManager.ts b/packages/@uppy/utils/src/EventManager.ts new file mode 100644 index 000000000..df9a717d7 --- /dev/null +++ b/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> = [] + + constructor(uppy: Uppy) { + this.#uppy = uppy + } + + on( + event: Parameters[0], + fn: Parameters[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[0], + isPaused: Parameters[1], + ) => { + if (fileID === targetFileID) { + cb(isPaused) + } + }, + ) + } + + onFileRemove( + fileID: UppyFile['id'], + cb: (isPaused: UppyFile['id']) => void, + ): void { + this.on('file-removed', (file: Parameters>[0]) => { + if (fileID === file.id) cb(file.id) + }) + } + + onPause(fileID: UppyFile['id'], cb: (isPaused: boolean) => void): void { + this.on( + 'upload-pause', + ( + targetFileID: Parameters[0], + isPaused: Parameters[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[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) => { + 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() + }) + } +} diff --git a/packages/@uppy/utils/src/FileProgress.ts b/packages/@uppy/utils/src/FileProgress.ts new file mode 100644 index 000000000..1e52271b2 --- /dev/null +++ b/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 diff --git a/packages/@uppy/utils/src/NetworkError.js b/packages/@uppy/utils/src/NetworkError.js deleted file mode 100644 index 979c293df..000000000 --- a/packages/@uppy/utils/src/NetworkError.js +++ /dev/null @@ -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 diff --git a/packages/@uppy/utils/src/NetworkError.ts b/packages/@uppy/utils/src/NetworkError.ts new file mode 100644 index 000000000..c96490727 --- /dev/null +++ b/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 diff --git a/packages/@uppy/utils/src/ProgressTimeout.js b/packages/@uppy/utils/src/ProgressTimeout.ts similarity index 75% rename from packages/@uppy/utils/src/ProgressTimeout.js rename to packages/@uppy/utils/src/ProgressTimeout.ts index 103993531..8e6617fd4 100644 --- a/packages/@uppy/utils/src/ProgressTimeout.js +++ b/packages/@uppy/utils/src/ProgressTimeout.ts @@ -5,20 +5,23 @@ * Call `timer.done()` when the upload has completed. */ class ProgressTimeout { - #aliveTimer + #aliveTimer?: ReturnType #isDone = false - #onTimedOut + #onTimedOut: Parameters[0] - #timeout + #timeout: number - constructor (timeout, timeoutHandler) { + constructor( + timeout: number, + timeoutHandler: Parameters[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 } } diff --git a/packages/@uppy/utils/src/RateLimitedQueue.test.js b/packages/@uppy/utils/src/RateLimitedQueue.test.js index 32700af09..da2b000cc 100644 --- a/packages/@uppy/utils/src/RateLimitedQueue.test.js +++ b/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(), ]) }) }) diff --git a/packages/@uppy/utils/src/Translator.test.js b/packages/@uppy/utils/src/Translator.test.ts similarity index 69% rename from packages/@uppy/utils/src/Translator.test.js rename to packages/@uppy/utils/src/Translator.test.ts index dc8d62e95..e3bb67239 100644 --- a/packages/@uppy/utils/src/Translator.test.js +++ b/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}', + ) }) }) }) diff --git a/packages/@uppy/utils/src/Translator.js b/packages/@uppy/utils/src/Translator.ts similarity index 67% rename from packages/@uppy/utils/src/Translator.js rename to packages/@uppy/utils/src/Translator.ts index 8acecb8b3..34a4f202a 100644 --- a/packages/@uppy/utils/src/Translator.js +++ b/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 { + strings: Record> + pluralize: (n: number) => T +} + +type Options = { + smart_count?: number +} & { + [key: string]: string | number +} + +function insertReplacement( + source: Array, + rx: RegExp, + replacement: string, +): Array { + const newParts: Array = [] 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 { const dollarRegex = /\$/g const dollarBillsYall = '$$$$' - let interpolated = [phrase] + let interpolated: Array = [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} 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 { 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) diff --git a/packages/@uppy/utils/src/UppyFile.ts b/packages/@uppy/utils/src/UppyFile.ts new file mode 100644 index 000000000..c5bdd1970 --- /dev/null +++ b/packages/@uppy/utils/src/UppyFile.ts @@ -0,0 +1,41 @@ +import type { FileProgress } from './FileProgress' + +interface IndexedObject { + [key: string]: T + [key: number]: T +} + +export type InternalMetadata = { name: string; type?: string } + +export interface UppyFile< + TMeta = IndexedObject, + TBody = IndexedObject, +> { + 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 + provider?: string + companionUrl: string + } + serverToken: string + size: number + source?: string + type?: string + response?: { + body: TBody + status: number + uploadURL: string | undefined + } +} diff --git a/packages/@uppy/utils/src/canvasToBlob.js b/packages/@uppy/utils/src/canvasToBlob.ts similarity index 61% rename from packages/@uppy/utils/src/canvasToBlob.js rename to packages/@uppy/utils/src/canvasToBlob.ts index d8818e370..cc58a76ea 100644 --- a/packages/@uppy/utils/src/canvasToBlob.js +++ b/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 { return new Promise((resolve) => { canvas.toBlob(resolve, type, quality) }) diff --git a/packages/@uppy/utils/src/dataURItoBlob.test.js b/packages/@uppy/utils/src/dataURItoBlob.test.ts similarity index 88% rename from packages/@uppy/utils/src/dataURItoBlob.test.js rename to packages/@uppy/utils/src/dataURItoBlob.test.ts index 95145d136..a8539f970 100644 --- a/packages/@uppy/utils/src/dataURItoBlob.test.js +++ b/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', () => { diff --git a/packages/@uppy/utils/src/dataURItoBlob.js b/packages/@uppy/utils/src/dataURItoBlob.ts similarity index 68% rename from packages/@uppy/utils/src/dataURItoBlob.js rename to packages/@uppy/utils/src/dataURItoBlob.ts index 98aa1bd84..38aac9d0d 100644 --- a/packages/@uppy/utils/src/dataURItoBlob.js +++ b/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])] } diff --git a/packages/@uppy/utils/src/dataURItoFile.js b/packages/@uppy/utils/src/dataURItoFile.js deleted file mode 100644 index fd612dfe9..000000000 --- a/packages/@uppy/utils/src/dataURItoFile.js +++ /dev/null @@ -1,5 +0,0 @@ -import dataURItoBlob from './dataURItoBlob.js' - -export default function dataURItoFile (dataURI, opts) { - return dataURItoBlob(dataURI, opts, true) -} diff --git a/packages/@uppy/utils/src/dataURItoFile.test.js b/packages/@uppy/utils/src/dataURItoFile.test.ts similarity index 90% rename from packages/@uppy/utils/src/dataURItoFile.test.js rename to packages/@uppy/utils/src/dataURItoFile.test.ts index 412026750..f402c9189 100644 --- a/packages/@uppy/utils/src/dataURItoFile.test.js +++ b/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', () => { diff --git a/packages/@uppy/utils/src/dataURItoFile.ts b/packages/@uppy/utils/src/dataURItoFile.ts new file mode 100644 index 000000000..a34d0c101 --- /dev/null +++ b/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) +} diff --git a/packages/@uppy/utils/src/delay.test.js b/packages/@uppy/utils/src/delay.test.ts similarity index 86% rename from packages/@uppy/utils/src/delay.test.js rename to packages/@uppy/utils/src/delay.test.ts index 6027bf7fd..54cc3cd63 100644 --- a/packages/@uppy/utils/src/delay.test.js +++ b/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) diff --git a/packages/@uppy/utils/src/delay.js b/packages/@uppy/utils/src/delay.ts similarity index 59% rename from packages/@uppy/utils/src/delay.js rename to packages/@uppy/utils/src/delay.ts index d0432b688..6e35ee932 100644 --- a/packages/@uppy/utils/src/delay.js +++ b/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} 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 { 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 diff --git a/packages/@uppy/utils/src/emaFilter.js b/packages/@uppy/utils/src/emaFilter.js deleted file mode 100644 index b04483b67..000000000 --- a/packages/@uppy/utils/src/emaFilter.js +++ /dev/null @@ -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)) -} diff --git a/packages/@uppy/utils/src/emaFilter.test.js b/packages/@uppy/utils/src/emaFilter.test.ts similarity index 86% rename from packages/@uppy/utils/src/emaFilter.test.js rename to packages/@uppy/utils/src/emaFilter.test.ts index eba103bd0..f9abc631b 100644 --- a/packages/@uppy/utils/src/emaFilter.test.js +++ b/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)), + ) } }) }) diff --git a/packages/@uppy/utils/src/emaFilter.ts b/packages/@uppy/utils/src/emaFilter.ts new file mode 100644 index 000000000..dec5641db --- /dev/null +++ b/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) +} diff --git a/packages/@uppy/utils/src/emitSocketProgress.js b/packages/@uppy/utils/src/emitSocketProgress.ts similarity index 66% rename from packages/@uppy/utils/src/emitSocketProgress.js rename to packages/@uppy/utils/src/emitSocketProgress.ts index 19e91598d..744372922 100644 --- a/packages/@uppy/utils/src/emitSocketProgress.js +++ b/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}`) diff --git a/packages/@uppy/utils/src/fetchWithNetworkError.js b/packages/@uppy/utils/src/fetchWithNetworkError.js deleted file mode 100644 index a978d3286..000000000 --- a/packages/@uppy/utils/src/fetchWithNetworkError.js +++ /dev/null @@ -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) - } - }) -} diff --git a/packages/@uppy/utils/src/fetchWithNetworkError.ts b/packages/@uppy/utils/src/fetchWithNetworkError.ts new file mode 100644 index 000000000..bf392ac61 --- /dev/null +++ b/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 +): ReturnType { + return fetch(...options).catch((err) => { + if (err.name === 'AbortError') { + throw err + } else { + throw new NetworkError(err) + } + }) +} diff --git a/packages/@uppy/utils/src/fileFilters.js b/packages/@uppy/utils/src/fileFilters.js deleted file mode 100644 index f4164cc6f..000000000 --- a/packages/@uppy/utils/src/fileFilters.js +++ /dev/null @@ -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) -} diff --git a/packages/@uppy/utils/src/fileFilters.ts b/packages/@uppy/utils/src/fileFilters.ts new file mode 100644 index 000000000..255e23b8a --- /dev/null +++ b/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, + ) +} diff --git a/packages/@uppy/utils/src/findAllDOMElements.js b/packages/@uppy/utils/src/findAllDOMElements.ts similarity index 65% rename from packages/@uppy/utils/src/findAllDOMElements.js rename to packages/@uppy/utils/src/findAllDOMElements.ts index f434be7ab..1df2feb3f 100644 --- a/packages/@uppy/utils/src/findAllDOMElements.js +++ b/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) diff --git a/packages/@uppy/utils/src/findDOMElement.js b/packages/@uppy/utils/src/findDOMElement.ts similarity index 51% rename from packages/@uppy/utils/src/findDOMElement.js rename to packages/@uppy/utils/src/findDOMElement.ts index 5c321a7ce..3e0513dec 100644 --- a/packages/@uppy/utils/src/findDOMElement.js +++ b/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) } diff --git a/packages/@uppy/utils/src/findIndex.test.js b/packages/@uppy/utils/src/findIndex.test.js index 1dc97307a..5ba4d6208 100644 --- a/packages/@uppy/utils/src/findIndex.test.js +++ b/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) }) }) diff --git a/packages/@uppy/utils/src/generateFileID.test.js b/packages/@uppy/utils/src/generateFileID.test.js index a7a89059c..ed0deda5b 100644 --- a/packages/@uppy/utils/src/generateFileID.test.js +++ b/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', () => { diff --git a/packages/@uppy/utils/src/generateFileID.js b/packages/@uppy/utils/src/generateFileID.ts similarity index 62% rename from packages/@uppy/utils/src/generateFileID.js rename to packages/@uppy/utils/src/generateFileID.ts index bdb636765..74c53a75d 100644 --- a/packages/@uppy/utils/src/generateFileID.js +++ b/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) diff --git a/packages/@uppy/utils/src/getBytesRemaining.js b/packages/@uppy/utils/src/getBytesRemaining.js deleted file mode 100644 index c87438eb0..000000000 --- a/packages/@uppy/utils/src/getBytesRemaining.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function getBytesRemaining (fileProgress) { - return fileProgress.bytesTotal - fileProgress.bytesUploaded -} diff --git a/packages/@uppy/utils/src/getBytesRemaining.test.js b/packages/@uppy/utils/src/getBytesRemaining.test.ts similarity index 68% rename from packages/@uppy/utils/src/getBytesRemaining.test.js rename to packages/@uppy/utils/src/getBytesRemaining.test.ts index 0a95c53aa..f08c56091 100644 --- a/packages/@uppy/utils/src/getBytesRemaining.test.js +++ b/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) }) diff --git a/packages/@uppy/utils/src/getBytesRemaining.ts b/packages/@uppy/utils/src/getBytesRemaining.ts new file mode 100644 index 000000000..94675ae4c --- /dev/null +++ b/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) +} diff --git a/packages/@uppy/utils/src/getDroppedFiles/index.js b/packages/@uppy/utils/src/getDroppedFiles/index.ts similarity index 66% rename from packages/@uppy/utils/src/getDroppedFiles/index.js rename to packages/@uppy/utils/src/getDroppedFiles/index.ts index 8758db6ab..f3839dd4f 100644 --- a/packages/@uppy/utils/src/getDroppedFiles/index.js +++ b/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 */ -export default async function getDroppedFiles (dataTransfer, { logDropError = () => {} } = {}) { +export default async function getDroppedFiles( + dataTransfer: DataTransfer, + options?: { + logDropError?: any + }, +): Promise { // 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) } diff --git a/packages/@uppy/utils/src/getDroppedFiles/utils/fallbackApi.js b/packages/@uppy/utils/src/getDroppedFiles/utils/fallbackApi.ts similarity index 51% rename from packages/@uppy/utils/src/getDroppedFiles/utils/fallbackApi.js rename to packages/@uppy/utils/src/getDroppedFiles/utils/fallbackApi.ts index 22d9b4d6d..ebbba9d6b 100644 --- a/packages/@uppy/utils/src/getDroppedFiles/utils/fallbackApi.js +++ b/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 { const files = toArray(dataTransfer.files) return Promise.resolve(files) } diff --git a/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.js b/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.ts similarity index 57% rename from packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.js rename to packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.ts index 65cbc174b..93bfd2229 100644 --- a/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/getFilesAndDirectoriesFromDirectory.js +++ b/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) } diff --git a/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js b/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js deleted file mode 100644 index c84d62e39..000000000 --- a/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.js +++ /dev/null @@ -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 - } -} diff --git a/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.ts b/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.ts new file mode 100644 index 000000000..3ba195634 --- /dev/null +++ b/packages/@uppy/utils/src/getDroppedFiles/utils/webkitGetAsEntryApi/index.ts @@ -0,0 +1,154 @@ +import getFilesAndDirectoriesFromDirectory from './getFilesAndDirectoriesFromDirectory.ts' + +interface FileSystemFileHandle extends FileSystemHandle { + getFile(): Promise +} +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[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 { + return new Promise((resolve, reject) => + (entry as FileSystemFileEntry).file(resolve, reject), + ) + }, + async *values(): ReturnType { + // If the file is a directory. + const directoryReader = (entry as FileSystemDirectoryEntry).createReader() + const entries = await new Promise< + Array>> + >((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 { + 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[2], +): ReturnType { + // 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 + } +} diff --git a/packages/@uppy/utils/src/getETA.test.js b/packages/@uppy/utils/src/getETA.test.js index a72e37871..cc8f49256 100644 --- a/packages/@uppy/utils/src/getETA.test.js +++ b/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', () => { diff --git a/packages/@uppy/utils/src/getETA.js b/packages/@uppy/utils/src/getETA.ts similarity index 54% rename from packages/@uppy/utils/src/getETA.js rename to packages/@uppy/utils/src/getETA.ts index cfb8eb364..e85416aee 100644 --- a/packages/@uppy/utils/src/getETA.js +++ b/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) diff --git a/packages/@uppy/utils/src/getFileNameAndExtension.test.js b/packages/@uppy/utils/src/getFileNameAndExtension.test.ts similarity index 88% rename from packages/@uppy/utils/src/getFileNameAndExtension.test.js rename to packages/@uppy/utils/src/getFileNameAndExtension.test.ts index 6821892be..d87a0315f 100644 --- a/packages/@uppy/utils/src/getFileNameAndExtension.test.js +++ b/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', () => { diff --git a/packages/@uppy/utils/src/getFileNameAndExtension.js b/packages/@uppy/utils/src/getFileNameAndExtension.ts similarity index 75% rename from packages/@uppy/utils/src/getFileNameAndExtension.js rename to packages/@uppy/utils/src/getFileNameAndExtension.ts index 576c7068a..65aec8df3 100644 --- a/packages/@uppy/utils/src/getFileNameAndExtension.js +++ b/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) { diff --git a/packages/@uppy/utils/src/getFileType.js b/packages/@uppy/utils/src/getFileType.js deleted file mode 100644 index dacf9626e..000000000 --- a/packages/@uppy/utils/src/getFileType.js +++ /dev/null @@ -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' -} diff --git a/packages/@uppy/utils/src/getFileType.test.js b/packages/@uppy/utils/src/getFileType.test.ts similarity index 82% rename from packages/@uppy/utils/src/getFileType.test.js rename to packages/@uppy/utils/src/getFileType.test.ts index f920b70de..e120ef073 100644 --- a/packages/@uppy/utils/src/getFileType.test.js +++ b/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') }) }) diff --git a/packages/@uppy/utils/src/getFileType.ts b/packages/@uppy/utils/src/getFileType.ts new file mode 100644 index 000000000..73718026f --- /dev/null +++ b/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' +} diff --git a/packages/@uppy/utils/src/getFileTypeExtension.test.js b/packages/@uppy/utils/src/getFileTypeExtension.test.ts similarity index 93% rename from packages/@uppy/utils/src/getFileTypeExtension.test.js rename to packages/@uppy/utils/src/getFileTypeExtension.test.ts index 4f44a9131..6326d4a19 100644 --- a/packages/@uppy/utils/src/getFileTypeExtension.test.js +++ b/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', () => { diff --git a/packages/@uppy/utils/src/getFileTypeExtension.js b/packages/@uppy/utils/src/getFileTypeExtension.ts similarity index 76% rename from packages/@uppy/utils/src/getFileTypeExtension.js rename to packages/@uppy/utils/src/getFileTypeExtension.ts index df42f2de3..6f9385a96 100644 --- a/packages/@uppy/utils/src/getFileTypeExtension.js +++ b/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 -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 } diff --git a/packages/@uppy/utils/src/getSocketHost.test.js b/packages/@uppy/utils/src/getSocketHost.test.js deleted file mode 100644 index 068437005..000000000 --- a/packages/@uppy/utils/src/getSocketHost.test.js +++ /dev/null @@ -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') - }) -}) diff --git a/packages/@uppy/utils/src/getSocketHost.test.ts b/packages/@uppy/utils/src/getSocketHost.test.ts new file mode 100644 index 000000000..f0293977b --- /dev/null +++ b/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', + ) + }) +}) diff --git a/packages/@uppy/utils/src/getSocketHost.js b/packages/@uppy/utils/src/getSocketHost.ts similarity index 67% rename from packages/@uppy/utils/src/getSocketHost.js rename to packages/@uppy/utils/src/getSocketHost.ts index d0d914c60..12c389c5a 100644 --- a/packages/@uppy/utils/src/getSocketHost.js +++ b/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}` diff --git a/packages/@uppy/utils/src/getSpeed.test.js b/packages/@uppy/utils/src/getSpeed.test.ts similarity index 68% rename from packages/@uppy/utils/src/getSpeed.test.js rename to packages/@uppy/utils/src/getSpeed.test.ts index 124cc4c1e..fed19d507 100644 --- a/packages/@uppy/utils/src/getSpeed.test.js +++ b/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)) }) diff --git a/packages/@uppy/utils/src/getSpeed.js b/packages/@uppy/utils/src/getSpeed.ts similarity index 62% rename from packages/@uppy/utils/src/getSpeed.js rename to packages/@uppy/utils/src/getSpeed.ts index bf7c2135a..1d12c27b3 100644 --- a/packages/@uppy/utils/src/getSpeed.js +++ b/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 diff --git a/packages/@uppy/utils/src/getTextDirection.js b/packages/@uppy/utils/src/getTextDirection.ts similarity index 83% rename from packages/@uppy/utils/src/getTextDirection.js rename to packages/@uppy/utils/src/getTextDirection.ts index ba84db17e..7f3369914 100644 --- a/packages/@uppy/utils/src/getTextDirection.js +++ b/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 } diff --git a/packages/@uppy/utils/src/getTimeStamp.js b/packages/@uppy/utils/src/getTimeStamp.ts similarity index 76% rename from packages/@uppy/utils/src/getTimeStamp.js rename to packages/@uppy/utils/src/getTimeStamp.ts index 2e323c44b..d461a439f 100644 --- a/packages/@uppy/utils/src/getTimeStamp.js +++ b/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()) diff --git a/packages/@uppy/utils/src/hasProperty.js b/packages/@uppy/utils/src/hasProperty.js deleted file mode 100644 index cb5f70403..000000000 --- a/packages/@uppy/utils/src/hasProperty.js +++ /dev/null @@ -1,3 +0,0 @@ -export default function has (object, key) { - return Object.prototype.hasOwnProperty.call(object, key) -} diff --git a/packages/@uppy/utils/src/hasProperty.ts b/packages/@uppy/utils/src/hasProperty.ts new file mode 100644 index 000000000..f9c0e3ca2 --- /dev/null +++ b/packages/@uppy/utils/src/hasProperty.ts @@ -0,0 +1,6 @@ +export default function has( + object: Parameters[0], + key: Parameters[1], +): ReturnType { + return Object.prototype.hasOwnProperty.call(object, key) +} diff --git a/packages/@uppy/utils/src/isDOMElement.js b/packages/@uppy/utils/src/isDOMElement.js deleted file mode 100644 index bdb6f2e23..000000000 --- a/packages/@uppy/utils/src/isDOMElement.js +++ /dev/null @@ -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 -} diff --git a/packages/@uppy/utils/src/isDOMElement.ts b/packages/@uppy/utils/src/isDOMElement.ts new file mode 100644 index 000000000..4e644cd17 --- /dev/null +++ b/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 +} diff --git a/packages/@uppy/utils/src/isDragDropSupported.js b/packages/@uppy/utils/src/isDragDropSupported.ts similarity index 82% rename from packages/@uppy/utils/src/isDragDropSupported.js rename to packages/@uppy/utils/src/isDragDropSupported.ts index 7c3d38811..ba6133479 100644 --- a/packages/@uppy/utils/src/isDragDropSupported.js +++ b/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)) { diff --git a/packages/@uppy/utils/src/isMobileDevice.js b/packages/@uppy/utils/src/isMobileDevice.js deleted file mode 100644 index 1497dea81..000000000 --- a/packages/@uppy/utils/src/isMobileDevice.js +++ /dev/null @@ -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 -} diff --git a/packages/@uppy/utils/src/isMobileDevice.test.js b/packages/@uppy/utils/src/isMobileDevice.test.js deleted file mode 100644 index a10dffbcb..000000000 --- a/packages/@uppy/utils/src/isMobileDevice.test.js +++ /dev/null @@ -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) - }) -}) diff --git a/packages/@uppy/utils/src/isMobileDevice.test.ts b/packages/@uppy/utils/src/isMobileDevice.test.ts new file mode 100644 index 000000000..d6ee41914 --- /dev/null +++ b/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) + }) +}) diff --git a/packages/@uppy/utils/src/isMobileDevice.ts b/packages/@uppy/utils/src/isMobileDevice.ts new file mode 100644 index 000000000..97a2dfcfc --- /dev/null +++ b/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 +} diff --git a/packages/@uppy/utils/src/isNetworkError.test.js b/packages/@uppy/utils/src/isNetworkError.test.ts similarity index 82% rename from packages/@uppy/utils/src/isNetworkError.test.js rename to packages/@uppy/utils/src/isNetworkError.test.ts index 00473d4b7..c7715cffd 100644 --- a/packages/@uppy/utils/src/isNetworkError.test.js +++ b/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) diff --git a/packages/@uppy/utils/src/isNetworkError.js b/packages/@uppy/utils/src/isNetworkError.ts similarity index 71% rename from packages/@uppy/utils/src/isNetworkError.js rename to packages/@uppy/utils/src/isNetworkError.ts index adbbe5819..bc806c203 100644 --- a/packages/@uppy/utils/src/isNetworkError.js +++ b/packages/@uppy/utils/src/isNetworkError.ts @@ -1,4 +1,4 @@ -function isNetworkError (xhr) { +function isNetworkError(xhr?: XMLHttpRequest): boolean { if (!xhr) { return false } diff --git a/packages/@uppy/utils/src/isObjectURL.test.js b/packages/@uppy/utils/src/isObjectURL.test.ts similarity index 89% rename from packages/@uppy/utils/src/isObjectURL.test.js rename to packages/@uppy/utils/src/isObjectURL.test.ts index e48f8bab3..a911892f8 100644 --- a/packages/@uppy/utils/src/isObjectURL.test.js +++ b/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', () => { diff --git a/packages/@uppy/utils/src/isObjectURL.js b/packages/@uppy/utils/src/isObjectURL.ts similarity index 55% rename from packages/@uppy/utils/src/isObjectURL.js rename to packages/@uppy/utils/src/isObjectURL.ts index c87213573..ab8788d0c 100644 --- a/packages/@uppy/utils/src/isObjectURL.js +++ b/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:') } diff --git a/packages/@uppy/utils/src/isPreviewSupported.test.js b/packages/@uppy/utils/src/isPreviewSupported.test.js deleted file mode 100644 index 8b1ce35e6..000000000 --- a/packages/@uppy/utils/src/isPreviewSupported.test.js +++ /dev/null @@ -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) - }) -}) diff --git a/packages/@uppy/utils/src/isPreviewSupported.test.ts b/packages/@uppy/utils/src/isPreviewSupported.test.ts new file mode 100644 index 000000000..c2c8ee699 --- /dev/null +++ b/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) + }) +}) diff --git a/packages/@uppy/utils/src/isPreviewSupported.js b/packages/@uppy/utils/src/isPreviewSupported.ts similarity index 68% rename from packages/@uppy/utils/src/isPreviewSupported.js rename to packages/@uppy/utils/src/isPreviewSupported.ts index 85e8b67be..c62b1c790 100644 --- a/packages/@uppy/utils/src/isPreviewSupported.js +++ b/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) diff --git a/packages/@uppy/utils/src/isTouchDevice.test.js b/packages/@uppy/utils/src/isTouchDevice.test.js index c2558f27c..6d0625f5d 100644 --- a/packages/@uppy/utils/src/isTouchDevice.test.js +++ b/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 diff --git a/packages/@uppy/utils/src/isTouchDevice.js b/packages/@uppy/utils/src/isTouchDevice.ts similarity index 57% rename from packages/@uppy/utils/src/isTouchDevice.js rename to packages/@uppy/utils/src/isTouchDevice.ts index 983639bce..bf97b3623 100644 --- a/packages/@uppy/utils/src/isTouchDevice.js +++ b/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 } diff --git a/packages/@uppy/utils/src/mimeTypes.js b/packages/@uppy/utils/src/mimeTypes.js deleted file mode 100644 index ab02877b6..000000000 --- a/packages/@uppy/utils/src/mimeTypes.js +++ /dev/null @@ -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', -} diff --git a/packages/@uppy/utils/src/mimeTypes.ts b/packages/@uppy/utils/src/mimeTypes.ts new file mode 100644 index 000000000..73e3c910e --- /dev/null +++ b/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, +} diff --git a/packages/@uppy/utils/src/prettyETA.js b/packages/@uppy/utils/src/prettyETA.js deleted file mode 100644 index 2d5bc3ddb..000000000 --- a/packages/@uppy/utils/src/prettyETA.js +++ /dev/null @@ -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}` -} diff --git a/packages/@uppy/utils/src/prettyETA.test.js b/packages/@uppy/utils/src/prettyETA.test.ts similarity index 93% rename from packages/@uppy/utils/src/prettyETA.test.js rename to packages/@uppy/utils/src/prettyETA.test.ts index be99fb26a..a1b829442 100644 --- a/packages/@uppy/utils/src/prettyETA.test.js +++ b/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', () => { diff --git a/packages/@uppy/utils/src/prettyETA.ts b/packages/@uppy/utils/src/prettyETA.ts new file mode 100644 index 000000000..f93b2226d --- /dev/null +++ b/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}` +} diff --git a/packages/@uppy/utils/src/remoteFileObjToLocal.js b/packages/@uppy/utils/src/remoteFileObjToLocal.js deleted file mode 100644 index ca90397ae..000000000 --- a/packages/@uppy/utils/src/remoteFileObjToLocal.js +++ /dev/null @@ -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, - } -} diff --git a/packages/@uppy/utils/src/remoteFileObjToLocal.ts b/packages/@uppy/utils/src/remoteFileObjToLocal.ts new file mode 100644 index 000000000..fbc03f6c3 --- /dev/null +++ b/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( + file: T, +): T & { + type: T['mimeType'] + extension: string | undefined | null +} { + return { + ...file, + type: file.mimeType, + extension: file.name ? getFileNameAndExtension(file.name).extension : null, + } +} diff --git a/packages/@uppy/utils/src/sampleImageDataURI.js b/packages/@uppy/utils/src/sampleImageDataURI.ts similarity index 100% rename from packages/@uppy/utils/src/sampleImageDataURI.js rename to packages/@uppy/utils/src/sampleImageDataURI.ts diff --git a/packages/@uppy/utils/src/secondsToTime.test.js b/packages/@uppy/utils/src/secondsToTime.test.ts similarity index 92% rename from packages/@uppy/utils/src/secondsToTime.test.js rename to packages/@uppy/utils/src/secondsToTime.test.ts index 7bc7fa53d..44e21b4b4 100644 --- a/packages/@uppy/utils/src/secondsToTime.test.js +++ b/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', () => { diff --git a/packages/@uppy/utils/src/secondsToTime.js b/packages/@uppy/utils/src/secondsToTime.ts similarity index 57% rename from packages/@uppy/utils/src/secondsToTime.js rename to packages/@uppy/utils/src/secondsToTime.ts index 444a245f0..dcead9744 100644 --- a/packages/@uppy/utils/src/secondsToTime.js +++ b/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) diff --git a/packages/@uppy/utils/src/toArray.test.js b/packages/@uppy/utils/src/toArray.test.ts similarity index 65% rename from packages/@uppy/utils/src/toArray.test.js rename to packages/@uppy/utils/src/toArray.test.ts index 9bd96cac8..541b0fb5e 100644 --- a/packages/@uppy/utils/src/toArray.test.js +++ b/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']) }) }) diff --git a/packages/@uppy/utils/src/toArray.js b/packages/@uppy/utils/src/toArray.ts similarity index 100% rename from packages/@uppy/utils/src/toArray.js rename to packages/@uppy/utils/src/toArray.ts diff --git a/packages/@uppy/utils/src/truncateString.test.js b/packages/@uppy/utils/src/truncateString.test.ts similarity index 96% rename from packages/@uppy/utils/src/truncateString.test.js rename to packages/@uppy/utils/src/truncateString.test.ts index 24f283986..5b881f97a 100644 --- a/packages/@uppy/utils/src/truncateString.test.js +++ b/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', () => { diff --git a/packages/@uppy/utils/src/truncateString.js b/packages/@uppy/utils/src/truncateString.ts similarity index 71% rename from packages/@uppy/utils/src/truncateString.js rename to packages/@uppy/utils/src/truncateString.ts index 32b7e7388..6f40afecc 100644 --- a/packages/@uppy/utils/src/truncateString.js +++ b/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) diff --git a/packages/@uppy/utils/tsconfig.build.json b/packages/@uppy/utils/tsconfig.build.json new file mode 100644 index 000000000..761856045 --- /dev/null +++ b/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": [] +} diff --git a/packages/@uppy/utils/tsconfig.json b/packages/@uppy/utils/tsconfig.json new file mode 100644 index 000000000..4311b0a60 --- /dev/null +++ b/packages/@uppy/utils/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "allowJs": true, + "emitDeclarationOnly": false, + "noEmit": true + }, + "include": ["src/*.ts"], + "references": [] +} diff --git a/yarn.lock b/yarn.lock index 942e6db72..64538f6ee 100644 --- a/yarn.lock +++ b/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