diff --git a/packages/@uppy/audio/.npmignore b/packages/@uppy/audio/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/audio/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/companion-client/.npmignore b/packages/@uppy/companion-client/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/companion-client/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/compressor/.npmignore b/packages/@uppy/compressor/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/compressor/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/compressor/src/index.test.js b/packages/@uppy/compressor/src/index.test.ts similarity index 67% rename from packages/@uppy/compressor/src/index.test.js rename to packages/@uppy/compressor/src/index.test.ts index 6a67049933..f225125d92 100644 --- a/packages/@uppy/compressor/src/index.test.js +++ b/packages/@uppy/compressor/src/index.test.ts @@ -3,10 +3,11 @@ import Core from '@uppy/core' import getFileNameAndExtension from '@uppy/utils/lib/getFileNameAndExtension' import fs from 'node:fs' import path from 'node:path' -import CompressorPlugin from './index.js' +import CompressorPlugin from './index.ts' // Compressor uses browser canvas API, so need to mock compress() -CompressorPlugin.prototype.compress = (blob) => { +// @ts-expect-error mocked +CompressorPlugin.prototype.compress = async (blob: Blob) => { return { name: `${getFileNameAndExtension(blob.name).name}.webp`, type: 'image/webp', @@ -16,11 +17,28 @@ CompressorPlugin.prototype.compress = (blob) => { } // eslint-disable-next-line no-restricted-globals -const sampleImage = fs.readFileSync(path.join(__dirname, '../../../../e2e/cypress/fixtures/images/image.jpg')) +const sampleImage = fs.readFileSync( + path.join(__dirname, '../../../../e2e/cypress/fixtures/images/image.jpg'), +) -const file1 = { source: 'jest', name: 'image-1.jpeg', type: 'image/jpeg', data: new File([sampleImage], 'image-1.jpeg', { type: 'image/jpeg' }) } -const file2 = { source: 'jest', name: 'yolo', type: 'image/jpeg', data: new File([sampleImage], 'yolo', { type: 'image/jpeg' }) } -const file3 = { source: 'jest', name: 'my.file.is.weird.png', type: 'image/png', data: new File([sampleImage], 'my.file.is.weird.png', { type: 'image/png' }) } +const file1 = { + source: 'jest', + name: 'image-1.jpeg', + type: 'image/jpeg', + data: new File([sampleImage], 'image-1.jpeg', { type: 'image/jpeg' }), +} +const file2 = { + source: 'jest', + name: 'yolo', + type: 'image/jpeg', + data: new File([sampleImage], 'yolo', { type: 'image/jpeg' }), +} +const file3 = { + source: 'jest', + name: 'my.file.is.weird.png', + type: 'image/png', + data: new File([sampleImage], 'my.file.is.weird.png', { type: 'image/png' }), +} describe('CompressorPlugin', () => { it('should change update extension in file.name and file.meta.name', () => { diff --git a/packages/@uppy/compressor/src/index.js b/packages/@uppy/compressor/src/index.ts similarity index 72% rename from packages/@uppy/compressor/src/index.js rename to packages/@uppy/compressor/src/index.ts index db6f1d9434..c9f6eaaab4 100644 --- a/packages/@uppy/compressor/src/index.js +++ b/packages/@uppy/compressor/src/index.ts @@ -1,14 +1,34 @@ -import { BasePlugin } from '@uppy/core' +import { BasePlugin, Uppy } from '@uppy/core' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore import { RateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue' import getFileNameAndExtension from '@uppy/utils/lib/getFileNameAndExtension' import prettierBytes from '@transloadit/prettier-bytes' import CompressorJS from 'compressorjs' -import locale from './locale.js' -export default class Compressor extends BasePlugin { +import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile' +import type { PluginOpts } from '@uppy/core/lib/BasePlugin.ts' + +import locale from './locale.ts' + +declare module '@uppy/core' { + export interface UppyEventMap { + 'compressor:complete': (file: UppyFile[]) => void + } +} + +export interface CompressorOpts extends PluginOpts, CompressorJS.Options { + quality: number + limit?: number +} + +export default class Compressor< + M extends Meta, + B extends Body, +> extends BasePlugin { #RateLimitedQueue - constructor (uppy, opts) { + constructor(uppy: Uppy, opts: CompressorOpts) { super(uppy, opts) this.id = this.opts.id || 'Compressor' this.type = 'modifier' @@ -30,7 +50,7 @@ export default class Compressor extends BasePlugin { this.compress = this.compress.bind(this) } - compress (blob) { + compress(blob: Blob): Promise { return new Promise((resolve, reject) => { /* eslint-disable no-new */ new CompressorJS(blob, { @@ -41,15 +61,17 @@ export default class Compressor extends BasePlugin { }) } - async prepareUpload (fileIDs) { + async prepareUpload(fileIDs: string[]): Promise { let totalCompressedSize = 0 - const compressedFiles = [] + const compressedFiles: UppyFile[] = [] const compressAndApplyResult = this.#RateLimitedQueue.wrapPromiseFunction( - async (file) => { + async (file: UppyFile) => { try { const compressedBlob = await this.compress(file.data) const compressedSavingsSize = file.data.size - compressedBlob.size - this.uppy.log(`[Image Compressor] Image ${file.id} compressed by ${prettierBytes(compressedSavingsSize)}`) + this.uppy.log( + `[Image Compressor] Image ${file.id} compressed by ${prettierBytes(compressedSavingsSize)}`, + ) totalCompressedSize += compressedSavingsSize const { name, type, size } = compressedBlob @@ -61,7 +83,9 @@ export default class Compressor extends BasePlugin { this.uppy.setFileState(file.id, { ...(name && { name }), - ...(compressedFileName.extension && { extension: compressedFileName.extension }), + ...(compressedFileName.extension && { + extension: compressedFileName.extension, + }), ...(type && { type }), ...(size && { size }), data: compressedBlob, @@ -73,7 +97,10 @@ export default class Compressor extends BasePlugin { }) compressedFiles.push(file) } catch (err) { - this.uppy.log(`[Image Compressor] Failed to compress ${file.id}:`, 'warning') + this.uppy.log( + `[Image Compressor] Failed to compress ${file.id}:`, + 'warning', + ) this.uppy.log(err, 'warning') } }, @@ -97,7 +124,7 @@ export default class Compressor extends BasePlugin { file.data = file.data.slice(0, file.data.size, file.type) } - if (!file.type.startsWith('image/')) { + if (!file.type?.startsWith('image/')) { return Promise.resolve() } @@ -128,11 +155,11 @@ export default class Compressor extends BasePlugin { } } - install () { + install(): void { this.uppy.addPreProcessor(this.prepareUpload) } - uninstall () { + uninstall(): void { this.uppy.removePreProcessor(this.prepareUpload) } } diff --git a/packages/@uppy/compressor/src/locale.js b/packages/@uppy/compressor/src/locale.ts similarity index 100% rename from packages/@uppy/compressor/src/locale.js rename to packages/@uppy/compressor/src/locale.ts diff --git a/packages/@uppy/compressor/tsconfig.build.json b/packages/@uppy/compressor/tsconfig.build.json new file mode 100644 index 0000000000..1b0ca41093 --- /dev/null +++ b/packages/@uppy/compressor/tsconfig.build.json @@ -0,0 +1,25 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "noImplicitAny": false, + "outDir": "./lib", + "paths": { + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"] + }, + "resolveJsonModule": false, + "rootDir": "./src", + "skipLibCheck": true + }, + "include": ["./src/**/*.*"], + "exclude": ["./src/**/*.test.ts"], + "references": [ + { + "path": "../utils/tsconfig.build.json" + }, + { + "path": "../core/tsconfig.build.json" + } + ] +} diff --git a/packages/@uppy/compressor/tsconfig.json b/packages/@uppy/compressor/tsconfig.json new file mode 100644 index 0000000000..a76c3b714a --- /dev/null +++ b/packages/@uppy/compressor/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "emitDeclarationOnly": false, + "noEmit": true, + "paths": { + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"], + }, + }, + "include": ["./package.json", "./src/**/*.*"], + "references": [ + { + "path": "../utils/tsconfig.build.json", + }, + { + "path": "../core/tsconfig.build.json", + }, + ], +} diff --git a/packages/@uppy/core/.npmignore b/packages/@uppy/core/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/core/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/drop-target/.npmignore b/packages/@uppy/drop-target/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/drop-target/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/image-editor/.npmignore b/packages/@uppy/image-editor/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/image-editor/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/image-editor/src/Editor.tsx b/packages/@uppy/image-editor/src/Editor.tsx index f8e5bea967..90f52a1e42 100644 --- a/packages/@uppy/image-editor/src/Editor.tsx +++ b/packages/@uppy/image-editor/src/Editor.tsx @@ -81,7 +81,7 @@ export default class Editor extends Component< prevCropboxData, ) if (newCropboxData) this.cropper.setCropBoxData(newCropboxData) - // When we stretch the cropbox by one of its sides + // 2. When we stretch the cropbox by one of its sides } else { const newCropboxData = limitCropboxMovementOnResize( canvasData, @@ -120,7 +120,7 @@ export default class Editor extends Component< } onRotateGranular = (ev: ChangeEvent): void => { - // 1. Set state + // 1. Set state const newGranularAngle = Number(ev.target.value) this.setState({ angleGranular: newGranularAngle }) diff --git a/packages/@uppy/image-editor/src/ImageEditor.tsx b/packages/@uppy/image-editor/src/ImageEditor.tsx index da9d1a8493..d66db50410 100644 --- a/packages/@uppy/image-editor/src/ImageEditor.tsx +++ b/packages/@uppy/image-editor/src/ImageEditor.tsx @@ -156,7 +156,8 @@ export default class ImageEditor< const { currentImage } = this.getPluginState() this.uppy.setFileState(currentImage!.id, { - data: blob!, + // Reinserting image's name and type, because .toBlob loses both. + data: new File([blob!], currentImage!.name, { type: blob!.type }), size: blob!.size, preview: undefined, }) diff --git a/packages/@uppy/locales/.npmignore b/packages/@uppy/locales/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/locales/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss index 95a4eb2c23..29d12d67a3 100644 --- a/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss +++ b/packages/@uppy/provider-views/src/style/uppy-ProviderBrowser-viewType--list.scss @@ -69,6 +69,7 @@ .uppy-ProviderBrowserItem-inner { display: flex; align-items: center; + color: inherit; // For better outline padding: 2px; diff --git a/packages/@uppy/status-bar/.npmignore b/packages/@uppy/status-bar/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/status-bar/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/store-default/.npmignore b/packages/@uppy/store-default/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/store-default/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/tus/.npmignore b/packages/@uppy/tus/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/tus/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/tus/src/getFingerprint.js b/packages/@uppy/tus/src/getFingerprint.js deleted file mode 100644 index 47eecbb03d..0000000000 --- a/packages/@uppy/tus/src/getFingerprint.js +++ /dev/null @@ -1,39 +0,0 @@ -import * as tus from 'tus-js-client' - -function isCordova () { - return typeof window !== 'undefined' && ( - typeof window.PhoneGap !== 'undefined' - || typeof window.Cordova !== 'undefined' - || typeof window.cordova !== 'undefined' - ) -} - -function isReactNative () { - return typeof navigator !== 'undefined' - && typeof navigator.product === 'string' - && navigator.product.toLowerCase() === 'reactnative' -} - -// We override tus fingerprint to uppy’s `file.id`, since the `file.id` -// now also includes `relativePath` for files added from folders. -// This means you can add 2 identical files, if one is in folder a, -// the other in folder b — `a/file.jpg` and `b/file.jpg`, when added -// together with a folder, will be treated as 2 separate files. -// -// For React Native and Cordova, we let tus-js-client’s default -// fingerprint handling take charge. -export default function getFingerprint (uppyFileObj) { - return (file, options) => { - if (isCordova() || isReactNative()) { - return tus.defaultOptions.fingerprint(file, options) - } - - const uppyFingerprint = [ - 'tus', - uppyFileObj.id, - options.endpoint, - ].join('-') - - return Promise.resolve(uppyFingerprint) - } -} diff --git a/packages/@uppy/tus/src/getFingerprint.ts b/packages/@uppy/tus/src/getFingerprint.ts new file mode 100644 index 0000000000..aee8d2d3ea --- /dev/null +++ b/packages/@uppy/tus/src/getFingerprint.ts @@ -0,0 +1,44 @@ +import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile' +import * as tus from 'tus-js-client' + +function isCordova() { + return ( + typeof window !== 'undefined' && + // @ts-expect-error may exist + (typeof window.PhoneGap !== 'undefined' || + // @ts-expect-error may exist + typeof window.Cordova !== 'undefined' || + // @ts-expect-error may exist + typeof window.cordova !== 'undefined') + ) +} + +function isReactNative() { + return ( + typeof navigator !== 'undefined' && + typeof navigator.product === 'string' && + navigator.product.toLowerCase() === 'reactnative' + ) +} + +// We override tus fingerprint to uppy’s `file.id`, since the `file.id` +// now also includes `relativePath` for files added from folders. +// This means you can add 2 identical files, if one is in folder a, +// the other in folder b — `a/file.jpg` and `b/file.jpg`, when added +// together with a folder, will be treated as 2 separate files. +// +// For React Native and Cordova, we let tus-js-client’s default +// fingerprint handling take charge. +export default function getFingerprint( + uppyFile: UppyFile, +): tus.UploadOptions['fingerprint'] { + return (file, options) => { + if (isCordova() || isReactNative()) { + return tus.defaultOptions.fingerprint!(file, options) + } + + const uppyFingerprint = ['tus', uppyFile.id, options!.endpoint].join('-') + + return Promise.resolve(uppyFingerprint) + } +} diff --git a/packages/@uppy/tus/src/index.test.js b/packages/@uppy/tus/src/index.test.ts similarity index 58% rename from packages/@uppy/tus/src/index.test.js rename to packages/@uppy/tus/src/index.test.ts index 75eda875f6..949830500f 100644 --- a/packages/@uppy/tus/src/index.test.js +++ b/packages/@uppy/tus/src/index.test.ts @@ -1,29 +1,38 @@ import { describe, expect, it } from 'vitest' import Core from '@uppy/core' -import Tus from './index.js' +import Tus from './index.ts' describe('Tus', () => { it('Throws errors if autoRetry option is true', () => { const uppy = new Core() expect(() => { + // @ts-expect-error removed uppy.use(Tus, { autoRetry: true }) - }).toThrowError(/The `autoRetry` option was deprecated and has been removed/) + }).toThrowError( + /The `autoRetry` option was deprecated and has been removed/, + ) }) it('Throws errors if autoRetry option is false', () => { const uppy = new Core() expect(() => { + // @ts-expect-error removed uppy.use(Tus, { autoRetry: false }) - }).toThrowError(/The `autoRetry` option was deprecated and has been removed/) + }).toThrowError( + /The `autoRetry` option was deprecated and has been removed/, + ) }) it('Throws errors if autoRetry option is `undefined`', () => { const uppy = new Core() expect(() => { + // @ts-expect-error removed uppy.use(Tus, { autoRetry: undefined }) - }).toThrowError(/The `autoRetry` option was deprecated and has been removed/) + }).toThrowError( + /The `autoRetry` option was deprecated and has been removed/, + ) }) }) diff --git a/packages/@uppy/tus/src/index.js b/packages/@uppy/tus/src/index.ts similarity index 63% rename from packages/@uppy/tus/src/index.js rename to packages/@uppy/tus/src/index.ts index 694d7afcb7..9abf12b962 100644 --- a/packages/@uppy/tus/src/index.js +++ b/packages/@uppy/tus/src/index.ts @@ -1,26 +1,66 @@ -import BasePlugin from '@uppy/core/lib/BasePlugin.js' +import BasePlugin, { + type DefinePluginOpts, + type PluginOpts, +} from '@uppy/core/lib/BasePlugin.js' import * as tus from 'tus-js-client' -import EventManager from '@uppy/utils/lib/EventManager' +import EventManager from '@uppy/core/lib/EventManager.js' import NetworkError from '@uppy/utils/lib/NetworkError' import isNetworkError from '@uppy/utils/lib/isNetworkError' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore untyped import { RateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue' import hasProperty from '@uppy/utils/lib/hasProperty' -import { filterNonFailedFiles, filterFilesToEmitUploadStarted } from '@uppy/utils/lib/fileFilters' -import getFingerprint from './getFingerprint.js' - +import { + filterNonFailedFiles, + filterFilesToEmitUploadStarted, +} from '@uppy/utils/lib/fileFilters' +import type { Meta, Body, UppyFile } from '@uppy/utils/lib/UppyFile' +import type { Uppy } from '@uppy/core' +import type { RequestClient } from '@uppy/companion-client' +import getFingerprint from './getFingerprint.ts' + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore We don't want TS to generate types for the package.json import packageJson from '../package.json' -/** @typedef {import('..').TusOptions} TusOptions */ -/** @typedef {import('tus-js-client').UploadOptions} RawTusOptions */ -/** @typedef {import('@uppy/core').Uppy} Uppy */ -/** @typedef {import('@uppy/core').UppyFile} UppyFile */ -/** @typedef {import('@uppy/core').FailedUppyFile<{}>} FailedUppyFile */ +declare module '@uppy/utils/lib/UppyFile' { + // eslint-disable-next-line no-shadow, @typescript-eslint/no-unused-vars + export interface UppyFile { + // TODO: figure out what else is in this type + tus?: { uploadUrl?: string | null } + } +} + +type RestTusUploadOptions = Omit< + tus.UploadOptions, + 'onShouldRetry' | 'onBeforeRequest' | 'headers' +> + +export interface TusOpts + extends PluginOpts, + RestTusUploadOptions { + endpoint: string + headers?: + | Record + | ((file: UppyFile) => Record) + limit?: number + chunkSize?: number + onBeforeRequest?: (req: tus.HttpRequest, file: UppyFile) => void + onShouldRetry?: ( + err: tus.DetailedError, + retryAttempt: number, + options: TusOpts, + next: (e: tus.DetailedError) => void, + ) => boolean + retryDelays?: number[] + withCredentials?: boolean + allowedMetaFields?: string[] + rateLimitedQueue?: RateLimitedQueue +} /** * Extracted from https://github.com/tus/tus-js-client/blob/master/lib/upload.js#L13 * excepted we removed 'fingerprint' key to avoid adding more dependencies - * - * @type {RawTusOptions} */ const tusDefaultOptions = { endpoint: '', @@ -44,43 +84,52 @@ const tusDefaultOptions = { removeFingerprintOnSuccess: false, uploadLengthDeferred: false, uploadDataDuringCreation: false, -} +} satisfies tus.UploadOptions + +const defaultOptions = { + limit: 20, + retryDelays: tusDefaultOptions.retryDelays, + withCredentials: false, +} satisfies Partial> + +type Opts = DefinePluginOpts< + TusOpts, + keyof typeof defaultOptions +> /** * Tus resumable file uploader */ -export default class Tus extends BasePlugin { +export default class Tus extends BasePlugin< + Opts, + M, + B +> { static VERSION = packageJson.version #retryDelayIterator - /** - * @param {Uppy} uppy - * @param {TusOptions} opts - */ - constructor (uppy, opts) { - super(uppy, opts) - this.type = 'uploader' - this.id = this.opts.id || 'Tus' - this.title = 'Tus' + requests: RateLimitedQueue - // set default options - const defaultOptions = { - limit: 20, - retryDelays: tusDefaultOptions.retryDelays, - withCredentials: false, - } + uploaders: Record + + uploaderEvents: Record | null> - // merge default options with the ones set by user - /** @type {import("..").TusOptions} */ - this.opts = { ...defaultOptions, ...opts } + constructor(uppy: Uppy, opts: TusOpts) { + super(uppy, { ...defaultOptions, ...opts }) + this.type = 'uploader' + this.id = this.opts.id || 'Tus' if (opts?.allowedMetaFields === undefined && 'metaFields' in this.opts) { - throw new Error('The `metaFields` option has been renamed to `allowedMetaFields`.') + throw new Error( + 'The `metaFields` option has been renamed to `allowedMetaFields`.', + ) } if ('autoRetry' in opts) { - throw new Error('The `autoRetry` option was deprecated and has been removed.') + throw new Error( + 'The `autoRetry` option was deprecated and has been removed.', + ) } /** @@ -88,7 +137,8 @@ export default class Tus extends BasePlugin { * * @type {RateLimitedQueue} */ - this.requests = this.opts.rateLimitedQueue ?? new RateLimitedQueue(this.opts.limit) + this.requests = + this.opts.rateLimitedQueue ?? new RateLimitedQueue(this.opts.limit) this.#retryDelayIterator = this.opts.retryDelays?.values() this.uploaders = Object.create(null) @@ -97,11 +147,11 @@ export default class Tus extends BasePlugin { this.handleResetProgress = this.handleResetProgress.bind(this) } - handleResetProgress () { + handleResetProgress(): void { const files = { ...this.uppy.getState().files } Object.keys(files).forEach((fileID) => { // Only clone the file object if it has a Tus `uploadUrl` attached. - if (files[fileID].tus && files[fileID].tus.uploadUrl) { + if (files[fileID]?.tus?.uploadUrl) { const tusState = { ...files[fileID].tus } delete tusState.uploadUrl files[fileID] = { ...files[fileID], tus: tusState } @@ -114,23 +164,21 @@ export default class Tus extends BasePlugin { /** * Clean up all references for a file's upload: the tus.Upload instance, * any events related to the file, and the Companion WebSocket connection. - * - * @param {string} fileID */ - resetUploaderReferences (fileID, opts = {}) { + resetUploaderReferences(fileID: string, opts?: { abort: boolean }): void { if (this.uploaders[fileID]) { const uploader = this.uploaders[fileID] - uploader.abort() + uploader!.abort() - if (opts.abort) { - uploader.abort(true) + if (opts?.abort) { + uploader!.abort(true) } this.uploaders[fileID] = null } if (this.uploaderEvents[fileID]) { - this.uploaderEvents[fileID].remove() + this.uploaderEvents[fileID]!.remove() this.uploaderEvents[fileID] = null } } @@ -167,17 +215,15 @@ export default class Tus extends BasePlugin { * - Before replacing the `queuedRequest` variable, the previous `queuedRequest` must be aborted, else it will keep taking * up a spot in the queue. * - * @param {UppyFile} file for use with upload - * @returns {Promise} */ - #uploadLocalFile (file) { + #uploadLocalFile(file: UppyFile): Promise { this.resetUploaderReferences(file.id) // Create a new tus upload - return new Promise((resolve, reject) => { - let queuedRequest - let qRequest - let upload + return new Promise((resolve, reject) => { + let queuedRequest: RateLimitedQueue.QueueEntry + let qRequest: () => void + let upload: tus.Upload const opts = { ...this.opts, @@ -188,10 +234,11 @@ export default class Tus extends BasePlugin { opts.headers = opts.headers(file) } - /** @type {RawTusOptions} */ - const uploadOptions = { + const { onShouldRetry, onBeforeRequest, ...commonOpts } = opts + + const uploadOptions: tus.UploadOptions = { ...tusDefaultOptions, - ...opts, + ...commonOpts, } // We override tus fingerprint to uppy’s `file.id`, since the `file.id` @@ -200,19 +247,21 @@ export default class Tus extends BasePlugin { // the other in folder b. uploadOptions.fingerprint = getFingerprint(file) - uploadOptions.onBeforeRequest = (req) => { + uploadOptions.onBeforeRequest = async (req) => { const xhr = req.getUnderlyingObject() xhr.withCredentials = !!opts.withCredentials let userProvidedPromise - if (typeof opts.onBeforeRequest === 'function') { - userProvidedPromise = opts.onBeforeRequest(req, file) + if (typeof onBeforeRequest === 'function') { + userProvidedPromise = onBeforeRequest(req, file) } if (hasProperty(queuedRequest, 'shouldBeRequeued')) { if (!queuedRequest.shouldBeRequeued) return Promise.reject() - let done - const p = new Promise((res) => { // eslint-disable-line promise/param-names + // TODO: switch to `Promise.withResolvers` on the next major if available. + let done: () => void + // eslint-disable-next-line promise/param-names + const p = new Promise((res) => { done = res }) queuedRequest = this.requests.run(() => { @@ -230,7 +279,8 @@ export default class Tus extends BasePlugin { // This means we can hold the Tus retry here with a `Promise.all`, // together with the returned value of the user provided // `onBeforeRequest` option callback (in case it returns a promise). - return Promise.all([p, userProvidedPromise]) + await Promise.all([p, userProvidedPromise]) + return undefined } return userProvidedPromise } @@ -238,7 +288,10 @@ export default class Tus extends BasePlugin { uploadOptions.onError = (err) => { this.uppy.log(err) - const xhr = err.originalRequest ? err.originalRequest.getUnderlyingObject() : null + const xhr = + (err as tus.DetailedError).originalRequest != null ? + (err as tus.DetailedError).originalRequest.getUnderlyingObject() + : null if (isNetworkError(xhr)) { // eslint-disable-next-line no-param-reassign err = new NetworkError(err, xhr) @@ -260,6 +313,8 @@ export default class Tus extends BasePlugin { opts.onProgress(bytesUploaded, bytesTotal) } this.uppy.emit('upload-progress', file, { + // TODO: remove `uploader` in next major + // @ts-expect-error untyped uploader: this, bytesUploaded, bytesTotal, @@ -268,7 +323,9 @@ export default class Tus extends BasePlugin { uploadOptions.onSuccess = () => { const uploadResp = { - uploadURL: upload.url, + uploadURL: upload.url ?? undefined, + status: 200, + body: {} as B, } this.resetUploaderReferences(file.id) @@ -277,7 +334,9 @@ export default class Tus extends BasePlugin { this.uppy.emit('upload-success', file, uploadResp) if (upload.url) { - this.uppy.log(`Download ${upload.file.name} from ${upload.url}`) + // @ts-expect-error not typed in tus-js-client + const { name } = upload.file + this.uppy.log(`Download ${name} from ${upload.url}`) } if (typeof opts.onSuccess === 'function') { opts.onSuccess() @@ -286,7 +345,7 @@ export default class Tus extends BasePlugin { resolve(upload) } - const defaultOnShouldRetry = (err) => { + const defaultOnShouldRetry = (err: tus.DetailedError) => { const status = err?.originalResponse?.getStatus() if (status === 429) { @@ -298,57 +357,86 @@ export default class Tus extends BasePlugin { } this.requests.rateLimit(next.value) } - } else if (status > 400 && status < 500 && status !== 409 && status !== 423) { + } else if ( + status != null && + status > 400 && + status < 500 && + status !== 409 && + status !== 423 + ) { // HTTP 4xx, the server won't send anything, it's doesn't make sense to retry // HTTP 409 Conflict (happens if the Upload-Offset header does not match the one on the server) // HTTP 423 Locked (happens when a paused download is resumed too quickly) return false - } else if (typeof navigator !== 'undefined' && navigator.onLine === false) { + } else if ( + typeof navigator !== 'undefined' && + navigator.onLine === false + ) { // The navigator is offline, let's wait for it to come back online. if (!this.requests.isPaused) { this.requests.pause() - window.addEventListener('online', () => { - this.requests.resume() - }, { once: true }) + window.addEventListener( + 'online', + () => { + this.requests.resume() + }, + { once: true }, + ) } } queuedRequest.abort() queuedRequest = { shouldBeRequeued: true, - abort () { + abort() { this.shouldBeRequeued = false }, - done () { - throw new Error('Cannot mark a queued request as done: this indicates a bug') + done() { + throw new Error( + 'Cannot mark a queued request as done: this indicates a bug', + ) }, - fn () { + fn() { throw new Error('Cannot run a queued request: this indicates a bug') }, } return true } - if (opts.onShouldRetry != null) { - uploadOptions.onShouldRetry = (...args) => opts.onShouldRetry(...args, defaultOnShouldRetry) + if (onShouldRetry != null) { + uploadOptions.onShouldRetry = ( + error: tus.DetailedError, + retryAttempt: number, + ) => onShouldRetry(error, retryAttempt, opts, defaultOnShouldRetry) } else { uploadOptions.onShouldRetry = defaultOnShouldRetry } - const copyProp = (obj, srcProp, destProp) => { + const copyProp = ( + obj: Record, + srcProp: string, + destProp: string, + ) => { if (hasProperty(obj, srcProp) && !hasProperty(obj, destProp)) { // eslint-disable-next-line no-param-reassign obj[destProp] = obj[srcProp] } } - /** @type {Record} */ - const meta = {} - const allowedMetaFields = Array.isArray(opts.allowedMetaFields) - ? opts.allowedMetaFields - // Send along all fields by default. + // We can't use `allowedMetaFields` to index generic M + // and we also don't care about the type specifically here, + // we just want to pass the meta fields along. + const meta: Record = {} + const allowedMetaFields = + Array.isArray(opts.allowedMetaFields) ? + opts.allowedMetaFields + // Send along all fields by default. : Object.keys(file.meta) allowedMetaFields.forEach((item) => { - meta[item] = file.meta[item] + // tus type definition for metadata only accepts `Record` + // but in reality (at runtime) it accepts `Record` + // tus internally converts everything into a string, but let's do it here instead to be explicit. + // because Uppy can have anything inside meta values, (for example relativePath: null is often sent by uppy) + meta[item] = String(file.meta[item]) }) // tusd uses metadata fields 'filetype' and 'filename' @@ -379,7 +467,9 @@ export default class Tus extends BasePlugin { upload.findPreviousUploads().then((previousUploads) => { const previousUpload = previousUploads[0] if (previousUpload) { - this.uppy.log(`[Tus] Resuming upload of ${file.id} started at ${previousUpload.creationTime}`) + this.uppy.log( + `[Tus] Resuming upload of ${file.id} started at ${previousUpload.creationTime}`, + ) upload.resumeFromPreviousUpload(previousUpload) } }) @@ -433,11 +523,8 @@ export default class Tus extends BasePlugin { /** * Store the uploadUrl on the file options, so that when Golden Retriever * restores state, we will continue uploading to the correct URL. - * - * @param {UppyFile} file - * @param {string} uploadURL */ - onReceiveUploadUrl (file, uploadURL) { + onReceiveUploadUrl(file: UppyFile, uploadURL: string | null): void { const currentFile = this.uppy.getFile(file.id) if (!currentFile) return // Only do the update if we didn't have an upload URL yet. @@ -449,7 +536,7 @@ export default class Tus extends BasePlugin { } } - #getCompanionClientArgs (file) { + #getCompanionClientArgs(file: UppyFile) { const opts = { ...this.opts } if (file.tus) { @@ -458,7 +545,7 @@ export default class Tus extends BasePlugin { } return { - ...file.remote.body, + ...file.remote?.body, endpoint: opts.endpoint, uploadUrl: opts.uploadUrl, protocol: 'tus', @@ -468,48 +555,45 @@ export default class Tus extends BasePlugin { } } - /** - * @param {(UppyFile | FailedUppyFile)[]} files - */ - async #uploadFiles (files) { + async #uploadFiles(files: UppyFile[]) { const filesFiltered = filterNonFailedFiles(files) const filesToEmit = filterFilesToEmitUploadStarted(filesFiltered) this.uppy.emit('upload-start', filesToEmit) - await Promise.allSettled(filesFiltered.map((file, i) => { - const current = i + 1 - const total = files.length - - if (file.isRemote) { - const getQueue = () => this.requests - const controller = new AbortController() + await Promise.allSettled( + filesFiltered.map((file) => { + if (file.isRemote) { + const getQueue = () => this.requests + const controller = new AbortController() - const removedHandler = (removedFile) => { - if (removedFile.id === file.id) controller.abort() + const removedHandler = (removedFile: UppyFile) => { + if (removedFile.id === file.id) controller.abort() + } + this.uppy.on('file-removed', removedHandler) + + const uploadPromise = this.uppy + .getRequestClientForFile>(file) + .uploadRemoteFile(file, this.#getCompanionClientArgs(file), { + signal: controller.signal, + getQueue, + }) + + this.requests.wrapSyncFunction( + () => { + this.uppy.off('file-removed', removedHandler) + }, + { priority: -1 }, + )() + + return uploadPromise } - this.uppy.on('file-removed', removedHandler) - - const uploadPromise = this.uppy.getRequestClientForFile(file).uploadRemoteFile( - file, - this.#getCompanionClientArgs(file), - { signal: controller.signal, getQueue }, - ) - this.requests.wrapSyncFunction(() => { - this.uppy.off('file-removed', removedHandler) - }, { priority: -1 })() - - return uploadPromise - } - - return this.#uploadLocalFile(file, current, total) - })) + return this.#uploadLocalFile(file) + }), + ) } - /** - * @param {string[]} fileIDs - */ - #handleUpload = async (fileIDs) => { + #handleUpload = async (fileIDs: string[]) => { if (fileIDs.length === 0) { this.uppy.log('[Tus] No files to upload') return @@ -528,18 +612,24 @@ export default class Tus extends BasePlugin { await this.#uploadFiles(filesToUpload) } - install () { + install(): void { this.uppy.setState({ - capabilities: { ...this.uppy.getState().capabilities, resumableUploads: true }, + capabilities: { + ...this.uppy.getState().capabilities, + resumableUploads: true, + }, }) this.uppy.addUploader(this.#handleUpload) this.uppy.on('reset-progress', this.handleResetProgress) } - uninstall () { + uninstall(): void { this.uppy.setState({ - capabilities: { ...this.uppy.getState().capabilities, resumableUploads: false }, + capabilities: { + ...this.uppy.getState().capabilities, + resumableUploads: false, + }, }) this.uppy.removeUploader(this.#handleUpload) } diff --git a/packages/@uppy/tus/tsconfig.build.json b/packages/@uppy/tus/tsconfig.build.json new file mode 100644 index 0000000000..40df14c108 --- /dev/null +++ b/packages/@uppy/tus/tsconfig.build.json @@ -0,0 +1,30 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "noImplicitAny": false, + "outDir": "./lib", + "paths": { + "@uppy/companion-client": ["../companion-client/src/index.js"], + "@uppy/companion-client/lib/*": ["../companion-client/src/*"], + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"] + }, + "resolveJsonModule": false, + "rootDir": "./src", + "skipLibCheck": true + }, + "include": ["./src/**/*.*"], + "exclude": ["./src/**/*.test.ts"], + "references": [ + { + "path": "../companion-client/tsconfig.build.json" + }, + { + "path": "../utils/tsconfig.build.json" + }, + { + "path": "../core/tsconfig.build.json" + } + ] +} diff --git a/packages/@uppy/tus/tsconfig.json b/packages/@uppy/tus/tsconfig.json new file mode 100644 index 0000000000..f43408fa18 --- /dev/null +++ b/packages/@uppy/tus/tsconfig.json @@ -0,0 +1,26 @@ +{ + "extends": "../../../tsconfig.shared", + "compilerOptions": { + "emitDeclarationOnly": false, + "noEmit": true, + "paths": { + "@uppy/companion-client": ["../companion-client/src/index.js"], + "@uppy/companion-client/lib/*": ["../companion-client/src/*"], + "@uppy/utils/lib/*": ["../utils/src/*"], + "@uppy/core": ["../core/src/index.js"], + "@uppy/core/lib/*": ["../core/src/*"], + }, + }, + "include": ["./package.json", "./src/**/*.*"], + "references": [ + { + "path": "../companion-client/tsconfig.build.json", + }, + { + "path": "../utils/tsconfig.build.json", + }, + { + "path": "../core/tsconfig.build.json", + }, + ], +} diff --git a/packages/@uppy/utils/.npmignore b/packages/@uppy/utils/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/utils/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/packages/@uppy/utils/src/FileProgress.ts b/packages/@uppy/utils/src/FileProgress.ts index 9cc4e106d7..358dc41916 100644 --- a/packages/@uppy/utils/src/FileProgress.ts +++ b/packages/@uppy/utils/src/FileProgress.ts @@ -5,7 +5,7 @@ export interface DeterminateFileProcessing { } export interface IndeterminateFileProcessing { mode: 'indeterminate' - message?: undefined + message?: string value?: 0 } export type FileProcessingInfo = diff --git a/packages/@uppy/xhr-upload/.npmignore b/packages/@uppy/xhr-upload/.npmignore new file mode 100644 index 0000000000..6c816673f0 --- /dev/null +++ b/packages/@uppy/xhr-upload/.npmignore @@ -0,0 +1 @@ +tsconfig.* diff --git a/private/dev/dragdrop.html b/private/dev/dragdrop.html index 7579101fa1..003d9fd79f 100644 --- a/private/dev/dragdrop.html +++ b/private/dev/dragdrop.html @@ -4,6 +4,7 @@ Drag-Drop +