From 90669a562f76a222bb7b60850927ec887d0024fe Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 25 Jan 2024 12:44:07 +0100 Subject: [PATCH 1/5] Finish types --- packages/@uppy/core/src/Uppy.ts | 10 +- packages/@uppy/core/src/index.ts | 2 +- packages/@uppy/utils/src/FileProgress.ts | 6 +- packages/@uppy/utils/src/UppyFile.ts | 2 +- .../src/{index.test.js => index.test.ts} | 50 ++- .../xhr-upload/src/{index.js => index.ts} | 384 +++++++++++------- .../xhr-upload/src/{locale.js => locale.ts} | 3 +- packages/@uppy/xhr-upload/tsconfig.build.json | 30 ++ packages/@uppy/xhr-upload/tsconfig.json | 26 ++ 9 files changed, 344 insertions(+), 169 deletions(-) rename packages/@uppy/xhr-upload/src/{index.test.js => index.test.ts} (69%) rename packages/@uppy/xhr-upload/src/{index.js => index.ts} (58%) rename packages/@uppy/xhr-upload/src/{locale.js => locale.ts} (53%) create mode 100644 packages/@uppy/xhr-upload/tsconfig.build.json create mode 100644 packages/@uppy/xhr-upload/tsconfig.json diff --git a/packages/@uppy/core/src/Uppy.ts b/packages/@uppy/core/src/Uppy.ts index 1f375d72a0..c2d932e541 100644 --- a/packages/@uppy/core/src/Uppy.ts +++ b/packages/@uppy/core/src/Uppy.ts @@ -212,7 +212,9 @@ type ErrorCallback = ( type UploadErrorCallback = ( file: UppyFile | undefined, error: { message: string; details?: string }, - response?: UppyFile['response'] | undefined, + response?: + | Omit['response']>, 'uploadURL'> + | undefined, ) => void type UploadStalledCallback = ( error: { message: string; details?: string }, @@ -1376,7 +1378,7 @@ export class Uppy { if (sizedFiles.length === 0) { const progressMax = inProgress.length * 100 const currentProgress = unsizedFiles.reduce((acc, file) => { - return acc + file.progress.percentage + return acc + (file.progress.percentage as number) }, 0) const totalProgress = Math.round((currentProgress / progressMax) * 100) this.setState({ totalProgress }) @@ -1871,7 +1873,7 @@ export class Uppy { } /** @protected */ - getRequestClientForFile(file: UppyFile): unknown { + getRequestClientForFile(file: UppyFile): Client { if (!file.remote) throw new Error( `Tried to get RequestClient for a non-remote file ${file.id}`, @@ -1883,7 +1885,7 @@ export class Uppy { throw new Error( `requestClientId "${file.remote.requestClientId}" not registered for file "${file.id}"`, ) - return requestClient + return requestClient as Client } /** diff --git a/packages/@uppy/core/src/index.ts b/packages/@uppy/core/src/index.ts index 76c165bccc..d626cc56a1 100644 --- a/packages/@uppy/core/src/index.ts +++ b/packages/@uppy/core/src/index.ts @@ -1,5 +1,5 @@ export { default } from './Uppy.ts' -export { default as Uppy, type UppyEventMap } from './Uppy.ts' +export { default as Uppy, type UppyEventMap, type State } from './Uppy.ts' export { default as UIPlugin } from './UIPlugin.ts' export { default as BasePlugin } from './BasePlugin.ts' export { debugLogger } from './loggers.ts' diff --git a/packages/@uppy/utils/src/FileProgress.ts b/packages/@uppy/utils/src/FileProgress.ts index b21a2b1f0b..9cc4e106d7 100644 --- a/packages/@uppy/utils/src/FileProgress.ts +++ b/packages/@uppy/utils/src/FileProgress.ts @@ -14,8 +14,8 @@ export type FileProcessingInfo = interface FileProgressBase { progress?: number - uploadComplete: boolean - percentage: number + uploadComplete?: boolean + percentage?: number bytesTotal: number preprocess?: FileProcessingInfo postprocess?: FileProcessingInfo @@ -26,7 +26,7 @@ interface FileProgressBase { export type FileProgressStarted = FileProgressBase & { uploadStarted: number bytesUploaded: number - progress: number + progress?: number } export type FileProgressNotStarted = FileProgressBase & { uploadStarted: null diff --git a/packages/@uppy/utils/src/UppyFile.ts b/packages/@uppy/utils/src/UppyFile.ts index ad910ebbce..1c8b682666 100644 --- a/packages/@uppy/utils/src/UppyFile.ts +++ b/packages/@uppy/utils/src/UppyFile.ts @@ -37,6 +37,6 @@ export interface UppyFile { body: B status: number bytesUploaded?: number - uploadURL: string + uploadURL?: string } } diff --git a/packages/@uppy/xhr-upload/src/index.test.js b/packages/@uppy/xhr-upload/src/index.test.ts similarity index 69% rename from packages/@uppy/xhr-upload/src/index.test.js rename to packages/@uppy/xhr-upload/src/index.test.ts index 520ae19274..68b1ea2b03 100644 --- a/packages/@uppy/xhr-upload/src/index.test.js +++ b/packages/@uppy/xhr-upload/src/index.test.ts @@ -1,7 +1,8 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ import { vi, describe, it, expect } from 'vitest' import nock from 'nock' import Core from '@uppy/core' -import XHRUpload from './index.js' +import XHRUpload from './index.ts' describe('XHRUpload', () => { describe('getResponseData', () => { @@ -11,11 +12,14 @@ describe('XHRUpload', () => { 'access-control-allow-method': 'POST', 'access-control-allow-origin': '*', }) - .options('/').reply(200, {}) - .post('/').reply(200, {}) + .options('/') + .reply(200, {}) + .post('/') + .reply(200, {}) const core = new Core() - const getResponseData = vi.fn(function getResponseData () { + const getResponseData = vi.fn(function getResponseData() { + // @ts-expect-error TS can't know the type expect(this.some).toEqual('option') return {} }) @@ -26,6 +30,8 @@ describe('XHRUpload', () => { getResponseData, }) core.addFile({ + type: 'image/png', + source: 'test', name: 'test.jpg', data: new Blob([new Uint8Array(8192)]), }) @@ -43,8 +49,10 @@ describe('XHRUpload', () => { 'access-control-allow-method': 'POST', 'access-control-allow-origin': '*', }) - .options('/').reply(200, {}) - .post('/').reply(200, { + .options('/') + .reply(200, {}) + .post('/') + .reply(200, { code: 40000, message: 'custom upload error', }) @@ -59,19 +67,21 @@ describe('XHRUpload', () => { endpoint: 'https://fake-endpoint.uppy.io', some: 'option', validateStatus, - getResponseError (responseText) { + getResponseError(responseText) { return JSON.parse(responseText).message }, }) core.addFile({ + type: 'image/png', + source: 'test', name: 'test.jpg', data: new Blob([new Uint8Array(8192)]), }) - return core.upload().then(result => { + return core.upload().then((result) => { expect(validateStatus).toHaveBeenCalled() - expect(result.failed.length).toBeGreaterThan(0) - result.failed.forEach(file => { + expect(result!.failed!.length).toBeGreaterThan(0) + result!.failed!.forEach((file) => { expect(file.error).toEqual('custom upload error') }) }) @@ -80,17 +90,13 @@ describe('XHRUpload', () => { describe('headers', () => { it('can be a function', async () => { - const scope = nock('https://fake-endpoint.uppy.io') - .defaultReplyHeaders({ - 'access-control-allow-method': 'POST', - 'access-control-allow-origin': '*', - 'access-control-allow-headers': 'x-sample-header', - }) - scope.options('/') - .reply(200, {}) - scope.post('/') - .matchHeader('x-sample-header', 'test.jpg') - .reply(200, {}) + const scope = nock('https://fake-endpoint.uppy.io').defaultReplyHeaders({ + 'access-control-allow-method': 'POST', + 'access-control-allow-origin': '*', + 'access-control-allow-headers': 'x-sample-header', + }) + scope.options('/').reply(200, {}) + scope.post('/').matchHeader('x-sample-header', 'test.jpg').reply(200, {}) const core = new Core() core.use(XHRUpload, { @@ -101,6 +107,8 @@ describe('XHRUpload', () => { }), }) core.addFile({ + type: 'image/png', + source: 'test', name: 'test.jpg', data: new Blob([new Uint8Array(8192)]), }) diff --git a/packages/@uppy/xhr-upload/src/index.js b/packages/@uppy/xhr-upload/src/index.ts similarity index 58% rename from packages/@uppy/xhr-upload/src/index.js rename to packages/@uppy/xhr-upload/src/index.ts index a22ccc7514..802c389387 100644 --- a/packages/@uppy/xhr-upload/src/index.js +++ b/packages/@uppy/xhr-upload/src/index.ts @@ -1,16 +1,78 @@ import BasePlugin from '@uppy/core/lib/BasePlugin.js' +import type { DefinePluginOpts, PluginOpts } from '@uppy/core/lib/BasePlugin.js' +import type { RequestClient } from '@uppy/companion-client' import { nanoid } from 'nanoid/non-secure' import EventManager from '@uppy/utils/lib/EventManager' import ProgressTimeout from '@uppy/utils/lib/ProgressTimeout' -import { RateLimitedQueue, internalRateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue' +import { + RateLimitedQueue, + internalRateLimitedQueue, + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore untyped +} from '@uppy/utils/lib/RateLimitedQueue' import NetworkError from '@uppy/utils/lib/NetworkError' import isNetworkError from '@uppy/utils/lib/isNetworkError' -import { filterNonFailedFiles, filterFilesToEmitUploadStarted } from '@uppy/utils/lib/fileFilters' - +import { + filterNonFailedFiles, + filterFilesToEmitUploadStarted, +} from '@uppy/utils/lib/fileFilters' +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore We don't want TS to generate types for the package.json +import type { Meta, Body, UppyFile } from '@uppy/utils/lib/UppyFile' +import type { State, Uppy } from '@uppy/core' +// 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' -import locale from './locale.js' +import locale from './locale.ts' + +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 + xhrUpload?: { headers: Record } + } +} + +declare module '@uppy/core' { + // eslint-disable-next-line no-shadow, @typescript-eslint/no-unused-vars + export interface State { + // TODO: figure out what else is in this type + xhrUpload?: { headers: Record } + } +} + +export interface XhrUploadOpts + extends PluginOpts { + endpoint: string + method?: 'post' | 'put' + formData?: boolean + fieldName?: string + headers?: + | Record + | ((file: UppyFile) => Record) + timeout?: number + limit?: number + responseType?: XMLHttpRequestResponseType + withCredentials?: boolean + validateStatus?: ( + status: number, + body: string, + xhr: XMLHttpRequest, + ) => boolean + getResponseData?: ( + body: string, + xhr: XMLHttpRequest, + ) => NonNullable['response']>['body'] + getResponseError?: (body: string, xhr: XMLHttpRequest) => Error | NetworkError + allowedMetaFields?: string[] | null + bundle?: boolean + responseUrlFieldName?: string +} -function buildResponseError (xhr, err) { +function buildResponseError( + xhr: XMLHttpRequest, + err?: string | Error | NetworkError, +) { let error = err // No error message if (!error) error = new Error('Upload error') @@ -26,6 +88,8 @@ function buildResponseError (xhr, err) { return error } + // @ts-expect-error request can only be set on NetworkError + // but we use NetworkError to distinguish between errors. error.request = xhr return error } @@ -34,78 +98,73 @@ function buildResponseError (xhr, err) { * Set `data.type` in the blob to `file.meta.type`, * because we might have detected a more accurate file type in Uppy * https://stackoverflow.com/a/50875615 - * - * @param {object} file File object with `data`, `size` and `meta` properties - * @returns {object} blob updated with the new `type` set from `file.meta.type` */ -function setTypeInBlob (file) { +function setTypeInBlob(file: UppyFile) { const dataWithUpdatedType = file.data.slice(0, file.data.size, file.meta.type) return dataWithUpdatedType } -export default class XHRUpload extends BasePlugin { +const defaultOptions = { + endpoint: '', + formData: true, + fieldName: 'file', + method: 'post', + allowedMetaFields: null, + responseUrlFieldName: 'url', + bundle: false, + headers: {}, + timeout: 30 * 1000, + limit: 5, + withCredentials: false, + responseType: '', + getResponseData(responseText) { + return JSON.parse(responseText) + }, + getResponseError(_, response) { + let error = new Error('Upload error') + + if (isNetworkError(response)) { + error = new NetworkError(error, response) + } + + return error + }, + validateStatus(status) { + return status >= 200 && status < 300 + }, +} satisfies XhrUploadOpts + +type Opts = DefinePluginOpts< + XhrUploadOpts, + keyof typeof defaultOptions +> + +interface OptsWithHeaders extends Opts { + headers: Record +} + +export default class XHRUpload< + M extends Meta, + B extends Body, +> extends BasePlugin, M, B> { // eslint-disable-next-line global-require static VERSION = packageJson.version - constructor (uppy, opts) { - super(uppy, opts) - this.type = 'uploader' - this.id = this.opts.id || 'XHRUpload' - this.title = 'XHRUpload' + requests: RateLimitedQueue - this.defaultLocale = locale + uploaderEvents: Record | null> - // Default options - const defaultOptions = { - formData: true, + constructor(uppy: Uppy, opts: XhrUploadOpts) { + super(uppy, { + ...defaultOptions, fieldName: opts.bundle ? 'files[]' : 'file', - method: 'post', - allowedMetaFields: null, - responseUrlFieldName: 'url', - bundle: false, - headers: {}, - timeout: 30 * 1000, - limit: 5, - withCredentials: false, - responseType: '', - /** - * @param {string} responseText the response body string - */ - getResponseData (responseText) { - let parsedResponse = {} - try { - parsedResponse = JSON.parse(responseText) - } catch (err) { - uppy.log(err) - } - - return parsedResponse - }, - /** - * - * @param {string} _ the response body string - * @param {XMLHttpRequest | respObj} response the response object (XHR or similar) - */ - getResponseError (_, response) { - let error = new Error('Upload error') - - if (isNetworkError(response)) { - error = new NetworkError(error, response) - } + ...opts, + }) + this.type = 'uploader' + this.id = this.opts.id || 'XHRUpload' - return error - }, - /** - * Check if the response from the upload endpoint indicates that the upload was successful. - * - * @param {number} status the response status code - */ - validateStatus (status) { - return status >= 200 && status < 300 - }, - } + this.defaultLocale = locale - this.opts = { ...defaultOptions, ...opts } this.i18nInit() // Simultaneous upload limiting is shared across all uploads with this plugin. @@ -116,17 +175,27 @@ export default class XHRUpload extends BasePlugin { } if (this.opts.bundle && !this.opts.formData) { - throw new Error('`opts.formData` must be true when `opts.bundle` is enabled.') + throw new Error( + '`opts.formData` must be true when `opts.bundle` is enabled.', + ) + } + + if (this.opts.bundle && typeof this.opts.headers === 'function') { + throw new Error( + '`opts.headers` can not be a function when the `bundle: true` option is set.', + ) } 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`.', + ) } this.uploaderEvents = Object.create(null) } - getOptions (file) { + getOptions(file: UppyFile): OptsWithHeaders { const overrides = this.uppy.getState().xhrUpload const { headers } = this.opts @@ -159,23 +228,29 @@ export default class XHRUpload extends BasePlugin { } // eslint-disable-next-line class-methods-use-this - addMetadata (formData, meta, opts) { - const allowedMetaFields = Array.isArray(opts.allowedMetaFields) - ? opts.allowedMetaFields + addMetadata( + formData: FormData, + meta: State['meta'], + opts: Opts, + ): void { + const allowedMetaFields = + Array.isArray(opts.allowedMetaFields) ? + opts.allowedMetaFields : Object.keys(meta) // Send along all fields by default. allowedMetaFields.forEach((item) => { - if (Array.isArray(meta[item])) { + const value = meta[item] + if (Array.isArray(value)) { // In this case we don't transform `item` to add brackets, it's up to // the user to add the brackets so it won't be overridden. - meta[item].forEach(subItem => formData.append(item, subItem)) + value.forEach((subItem) => formData.append(item, subItem)) } else { - formData.append(item, meta[item]) + formData.append(item, value as string) } }) } - createFormDataUpload (file, opts) { + createFormDataUpload(file: UppyFile, opts: Opts): FormData { const formPost = new FormData() this.addMetadata(formPost, file.meta, opts) @@ -191,7 +266,7 @@ export default class XHRUpload extends BasePlugin { return formPost } - createBundledUpload (files, opts) { + createBundledUpload(files: UppyFile[], opts: Opts): FormData { const formPost = new FormData() const { meta } = this.uppy.getState() @@ -212,22 +287,26 @@ export default class XHRUpload extends BasePlugin { return formPost } - async #uploadLocalFile (file, current, total) { + async #uploadLocalFile(file: UppyFile, current: number, total: number) { const opts = this.getOptions(file) + const uploadStarted = Date.now() this.uppy.log(`uploading ${current} of ${total}`) return new Promise((resolve, reject) => { - const data = opts.formData - ? this.createFormDataUpload(file, opts) - : file.data + const data = + opts.formData ? this.createFormDataUpload(file, opts) : file.data const xhr = new XMLHttpRequest() const eventManager = new EventManager(this.uppy) this.uploaderEvents[file.id] = eventManager - let queuedRequest + let queuedRequest: { abort: () => void; done: () => void } const timer = new ProgressTimeout(opts.timeout, () => { - const error = new Error(this.i18n('uploadStalled', { seconds: Math.ceil(opts.timeout / 1000) })) + const error = new Error( + this.i18n('uploadStalled', { + seconds: Math.ceil(opts.timeout / 1000), + }), + ) this.uppy.emit('upload-stalled', error, [file]) }) @@ -245,7 +324,7 @@ export default class XHRUpload extends BasePlugin { if (ev.lengthComputable) { this.uppy.emit('upload-progress', file, { - uploader: this, + uploadStarted, bytesUploaded: ev.loaded, bytesTotal: ev.total, }) @@ -257,13 +336,13 @@ export default class XHRUpload extends BasePlugin { timer.done() queuedRequest.done() if (this.uploaderEvents[file.id]) { - this.uploaderEvents[file.id].remove() + this.uploaderEvents[file.id]!.remove() this.uploaderEvents[file.id] = null } if (opts.validateStatus(xhr.status, xhr.responseText, xhr)) { const body = opts.getResponseData(xhr.responseText, xhr) - const uploadURL = body[opts.responseUrlFieldName] + const uploadURL = body[opts.responseUrlFieldName] as string const uploadResp = { status: xhr.status, @@ -280,7 +359,10 @@ export default class XHRUpload extends BasePlugin { return resolve(file) } const body = opts.getResponseData(xhr.responseText, xhr) - const error = buildResponseError(xhr, opts.getResponseError(xhr.responseText, xhr)) + const error = buildResponseError( + xhr, + opts.getResponseError(xhr.responseText, xhr), + ) const response = { status: xhr.status, @@ -296,11 +378,14 @@ export default class XHRUpload extends BasePlugin { timer.done() queuedRequest.done() if (this.uploaderEvents[file.id]) { - this.uploaderEvents[file.id].remove() + this.uploaderEvents[file.id]!.remove() this.uploaderEvents[file.id] = null } - const error = buildResponseError(xhr, opts.getResponseError(xhr.responseText, xhr)) + const error = buildResponseError( + xhr, + opts.getResponseError(xhr.responseText, xhr), + ) this.uppy.emit('upload-error', file, error) return reject(error) }) @@ -346,10 +431,11 @@ export default class XHRUpload extends BasePlugin { }) } - #uploadBundle (files) { + #uploadBundle(files: UppyFile[]): Promise { return new Promise((resolve, reject) => { const { endpoint } = this.opts const { method } = this.opts + const uploadStarted = Date.now() const optsFromState = this.uppy.getState().xhrUpload const formData = this.createBundledUpload(files, { @@ -359,14 +445,18 @@ export default class XHRUpload extends BasePlugin { const xhr = new XMLHttpRequest() - const emitError = (error) => { + const emitError = (error: Error) => { files.forEach((file) => { this.uppy.emit('upload-error', file, error) }) } const timer = new ProgressTimeout(this.opts.timeout, () => { - const error = new Error(this.i18n('uploadStalled', { seconds: Math.ceil(this.opts.timeout / 1000) })) + const error = new Error( + this.i18n('uploadStalled', { + seconds: Math.ceil(this.opts.timeout / 1000), + }), + ) this.uppy.emit('upload-stalled', error, files) }) @@ -382,20 +472,20 @@ export default class XHRUpload extends BasePlugin { files.forEach((file) => { this.uppy.emit('upload-progress', file, { - uploader: this, - bytesUploaded: (ev.loaded / ev.total) * file.size, - bytesTotal: file.size, + uploadStarted, + bytesUploaded: (ev.loaded / ev.total) * (file.size as number), + bytesTotal: file.size as number, }) }) }) - xhr.addEventListener('load', (ev) => { + xhr.addEventListener('load', () => { timer.done() - if (this.opts.validateStatus(ev.target.status, xhr.responseText, xhr)) { + if (this.opts.validateStatus(xhr.status, xhr.responseText, xhr)) { const body = this.opts.getResponseData(xhr.responseText, xhr) const uploadResp = { - status: ev.target.status, + status: xhr.status, body, } files.forEach((file) => { @@ -404,8 +494,9 @@ export default class XHRUpload extends BasePlugin { return resolve() } - const error = this.opts.getResponseError(xhr.responseText, xhr) || new Error('Upload error') - error.request = xhr + const error = + this.opts.getResponseError(xhr.responseText, xhr) || + new NetworkError('Upload error', xhr) emitError(error) return reject(error) }) @@ -413,7 +504,9 @@ export default class XHRUpload extends BasePlugin { xhr.addEventListener('error', () => { timer.done() - const error = this.opts.getResponseError(xhr.responseText, xhr) || new Error('Upload error') + const error = + this.opts.getResponseError(xhr.responseText, xhr) || + new Error('Upload error') emitError(error) return reject(error) }) @@ -432,65 +525,76 @@ export default class XHRUpload extends BasePlugin { xhr.responseType = this.opts.responseType } - Object.keys(this.opts.headers).forEach((header) => { - xhr.setRequestHeader(header, this.opts.headers[header]) + // In bundle mode headers can not be a function + const headers = this.opts.headers as Record + Object.keys(headers).forEach((header) => { + xhr.setRequestHeader(header, headers[header] as string) }) xhr.send(formData) }) } - #getCompanionClientArgs (file) { + #getCompanionClientArgs(file: UppyFile) { const opts = this.getOptions(file) - const allowedMetaFields = Array.isArray(opts.allowedMetaFields) - ? opts.allowedMetaFields - // Send along all fields by default. + const allowedMetaFields = + Array.isArray(opts.allowedMetaFields) ? + opts.allowedMetaFields + // Send along all fields by default. : Object.keys(file.meta) return { - ...file.remote.body, + ...file?.remote?.body, protocol: 'multipart', endpoint: opts.endpoint, size: file.data.size, fieldname: opts.fieldName, - metadata: Object.fromEntries(allowedMetaFields.map(name => [name, file.meta[name]])), + metadata: Object.fromEntries( + allowedMetaFields.map((name) => [name, file.meta[name]]), + ), httpMethod: opts.method, useFormData: opts.formData, headers: opts.headers, } } - async #uploadFiles (files) { - await Promise.allSettled(files.map((file, i) => { - const current = parseInt(i, 10) + 1 - const total = files.length + async #uploadFiles(files: UppyFile[]) { + await Promise.allSettled( + files.map((file, i) => { + const current = i + 1 + const total = files.length - if (file.isRemote) { - const getQueue = () => this.requests - const controller = new AbortController() + 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, current, total) + }), + ) } - #handleUpload = async (fileIDs) => { + #handleUpload = async (fileIDs: string[]) => { if (fileIDs.length === 0) { this.uppy.log('[XHRUpload] No files to upload!') return @@ -514,13 +618,17 @@ export default class XHRUpload extends BasePlugin { if (this.opts.bundle) { // if bundle: true, we don’t support remote uploads - const isSomeFileRemote = filesFiltered.some(file => file.isRemote) + const isSomeFileRemote = filesFiltered.some((file) => file.isRemote) if (isSomeFileRemote) { - throw new Error('Can’t upload remote files when the `bundle: true` option is set') + throw new Error( + 'Can’t upload remote files when the `bundle: true` option is set', + ) } if (typeof this.opts.headers === 'function') { - throw new TypeError('`headers` may not be a function when the `bundle: true` option is set') + throw new TypeError( + '`headers` may not be a function when the `bundle: true` option is set', + ) } await this.#uploadBundle(filesFiltered) @@ -529,7 +637,7 @@ export default class XHRUpload extends BasePlugin { } } - install () { + install(): void { if (this.opts.bundle) { const { capabilities } = this.uppy.getState() this.uppy.setState({ @@ -543,7 +651,7 @@ export default class XHRUpload extends BasePlugin { this.uppy.addUploader(this.#handleUpload) } - uninstall () { + uninstall(): void { if (this.opts.bundle) { const { capabilities } = this.uppy.getState() this.uppy.setState({ diff --git a/packages/@uppy/xhr-upload/src/locale.js b/packages/@uppy/xhr-upload/src/locale.ts similarity index 53% rename from packages/@uppy/xhr-upload/src/locale.js rename to packages/@uppy/xhr-upload/src/locale.ts index 6c9030db99..a5d7967f13 100644 --- a/packages/@uppy/xhr-upload/src/locale.js +++ b/packages/@uppy/xhr-upload/src/locale.ts @@ -1,6 +1,7 @@ export default { strings: { // Shown in the Informer if an upload is being canceled because it stalled for too long. - uploadStalled: 'Upload has not made any progress for %{seconds} seconds. You may want to retry it.', + uploadStalled: + 'Upload has not made any progress for %{seconds} seconds. You may want to retry it.', }, } diff --git a/packages/@uppy/xhr-upload/tsconfig.build.json b/packages/@uppy/xhr-upload/tsconfig.build.json new file mode 100644 index 0000000000..40df14c108 --- /dev/null +++ b/packages/@uppy/xhr-upload/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/xhr-upload/tsconfig.json b/packages/@uppy/xhr-upload/tsconfig.json new file mode 100644 index 0000000000..f43408fa18 --- /dev/null +++ b/packages/@uppy/xhr-upload/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", + }, + ], +} From 535c27c536264149ba239607b434b6a2821bc45f Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 25 Jan 2024 15:00:55 +0100 Subject: [PATCH 2/5] Import EventManager from core --- packages/@uppy/xhr-upload/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@uppy/xhr-upload/src/index.ts b/packages/@uppy/xhr-upload/src/index.ts index 802c389387..8f31f766bf 100644 --- a/packages/@uppy/xhr-upload/src/index.ts +++ b/packages/@uppy/xhr-upload/src/index.ts @@ -2,7 +2,7 @@ import BasePlugin from '@uppy/core/lib/BasePlugin.js' import type { DefinePluginOpts, PluginOpts } from '@uppy/core/lib/BasePlugin.js' import type { RequestClient } from '@uppy/companion-client' import { nanoid } from 'nanoid/non-secure' -import EventManager from '@uppy/utils/lib/EventManager' +import EventManager from '@uppy/core/lib/EventManager.js' import ProgressTimeout from '@uppy/utils/lib/ProgressTimeout' import { RateLimitedQueue, From 0225aa07da16f4ea158fc3aabe58d4b972be7c37 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 25 Jan 2024 15:02:51 +0100 Subject: [PATCH 3/5] Apply feedback --- packages/@uppy/xhr-upload/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@uppy/xhr-upload/src/index.ts b/packages/@uppy/xhr-upload/src/index.ts index 8f31f766bf..e9b02a5c0f 100644 --- a/packages/@uppy/xhr-upload/src/index.ts +++ b/packages/@uppy/xhr-upload/src/index.ts @@ -543,7 +543,7 @@ export default class XHRUpload< // Send along all fields by default. : Object.keys(file.meta) return { - ...file?.remote?.body, + ...file.remote?.body, protocol: 'multipart', endpoint: opts.endpoint, size: file.data.size, From 8906a8a2fe0d968e0f797bdcbefeecd27f753a85 Mon Sep 17 00:00:00 2001 From: Murderlon Date: Thu, 25 Jan 2024 15:11:25 +0100 Subject: [PATCH 4/5] Use any for RateLimitedQueue --- packages/@uppy/companion-client/src/RequestClient.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/@uppy/companion-client/src/RequestClient.ts b/packages/@uppy/companion-client/src/RequestClient.ts index 5db0438312..e45c3d9046 100644 --- a/packages/@uppy/companion-client/src/RequestClient.ts +++ b/packages/@uppy/companion-client/src/RequestClient.ts @@ -11,9 +11,6 @@ import getSocketHost from '@uppy/utils/lib/getSocketHost' import type Uppy from '@uppy/core' import type { UppyFile, Meta, Body } from '@uppy/utils/lib/UppyFile' -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore untyped because we're getting rid of it -import type { RateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue' import AuthError from './AuthError.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore We don't want TS to generate types for the package.json @@ -249,7 +246,7 @@ export default class RequestClient { async uploadRemoteFile( file: UppyFile, reqBody: Record, - options: { signal: AbortSignal; getQueue: () => RateLimitedQueue }, + options: { signal: AbortSignal; getQueue: () => any }, ): Promise { try { const { signal, getQueue } = options || {} @@ -376,7 +373,7 @@ export default class RequestClient { signal, }: { file: UppyFile - queue: RateLimitedQueue + queue: any signal: AbortSignal }): Promise { let removeEventHandlers: () => void From d85805f616359bc97ed5e6c1b1b5ecf9b532957c Mon Sep 17 00:00:00 2001 From: Murderlon Date: Fri, 26 Jan 2024 10:31:07 +0100 Subject: [PATCH 5/5] Undo breaking change --- packages/@uppy/xhr-upload/src/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/@uppy/xhr-upload/src/index.ts b/packages/@uppy/xhr-upload/src/index.ts index e9b02a5c0f..d465a2f47c 100644 --- a/packages/@uppy/xhr-upload/src/index.ts +++ b/packages/@uppy/xhr-upload/src/index.ts @@ -324,6 +324,9 @@ export default class XHRUpload< if (ev.lengthComputable) { this.uppy.emit('upload-progress', file, { + // TODO: do not send `uploader` in next major + // @ts-expect-error we can't type this and we should remove it + uploader: this, uploadStarted, bytesUploaded: ev.loaded, bytesTotal: ev.total, @@ -472,6 +475,9 @@ export default class XHRUpload< files.forEach((file) => { this.uppy.emit('upload-progress', file, { + // TODO: do not send `uploader` in next major + // @ts-expect-error we can't type this and we should remove it + uploader: this, uploadStarted, bytesUploaded: (ev.loaded / ev.total) * (file.size as number), bytesTotal: file.size as number,