Просмотр исходного кода

@uppy/core: refactor to TS

Co-authored-by: Antoine du Hamel <antoine@transloadit.com>
Murderlon 1 год назад
Родитель
Сommit
c48aa82a8a
41 измененных файлов с 1692 добавлено и 830 удалено
  1. 1 1
      .eslintrc.js
  2. 0 85
      packages/@uppy/core/src/BasePlugin.js
  3. 106 0
      packages/@uppy/core/src/BasePlugin.ts
  4. 114 0
      packages/@uppy/core/src/EventManager.ts
  5. 0 137
      packages/@uppy/core/src/Restricter.js
  6. 204 0
      packages/@uppy/core/src/Restricter.ts
  7. 5 5
      packages/@uppy/core/src/UIPlugin.test.ts
  8. 61 29
      packages/@uppy/core/src/UIPlugin.ts
  9. 315 147
      packages/@uppy/core/src/Uppy.test.ts
  10. 541 151
      packages/@uppy/core/src/Uppy.ts
  11. 69 0
      packages/@uppy/core/src/__snapshots__/Uppy.test.ts.snap
  12. 4 1
      packages/@uppy/core/src/getFileName.ts
  13. 0 5
      packages/@uppy/core/src/index.js
  14. 5 0
      packages/@uppy/core/src/index.ts
  15. 2 1
      packages/@uppy/core/src/locale.ts
  16. 0 23
      packages/@uppy/core/src/loggers.js
  17. 25 0
      packages/@uppy/core/src/loggers.ts
  18. 13 6
      packages/@uppy/core/src/mocks/acquirerPlugin1.ts
  19. 13 6
      packages/@uppy/core/src/mocks/acquirerPlugin2.ts
  20. 0 0
      packages/@uppy/core/src/mocks/invalidPlugin.ts
  21. 0 19
      packages/@uppy/core/src/mocks/invalidPluginWithoutId.js
  22. 24 0
      packages/@uppy/core/src/mocks/invalidPluginWithoutId.ts
  23. 0 19
      packages/@uppy/core/src/mocks/invalidPluginWithoutType.js
  24. 24 0
      packages/@uppy/core/src/mocks/invalidPluginWithoutType.ts
  25. 0 29
      packages/@uppy/core/src/supportsUploadProgress.test.js
  26. 57 0
      packages/@uppy/core/src/supportsUploadProgress.test.ts
  27. 4 4
      packages/@uppy/core/src/supportsUploadProgress.ts
  28. 20 0
      packages/@uppy/core/tsconfig.build.json
  29. 16 0
      packages/@uppy/core/tsconfig.json
  30. 2 0
      packages/@uppy/utils/package.json
  31. 3 115
      packages/@uppy/utils/src/EventManager.ts
  32. 6 2
      packages/@uppy/utils/src/FileProgress.ts
  33. 13 7
      packages/@uppy/utils/src/Translator.ts
  34. 19 18
      packages/@uppy/utils/src/UppyFile.ts
  35. 1 1
      packages/@uppy/utils/src/emitSocketProgress.ts
  36. 8 3
      packages/@uppy/utils/src/fileFilters.ts
  37. 1 1
      packages/@uppy/utils/src/findDOMElement.ts
  38. 3 3
      packages/@uppy/utils/src/generateFileID.ts
  39. 9 9
      packages/@uppy/utils/src/getFileType.test.ts
  40. 1 1
      packages/@uppy/utils/src/getFileType.ts
  41. 3 2
      packages/@uppy/utils/src/getSpeed.ts

+ 1 - 1
.eslintrc.js

@@ -469,7 +469,7 @@ module.exports = {
     },
     {
       files: ['packages/@uppy/*/src/**/*.ts', 'packages/@uppy/*/src/**/*.tsx'],
-      excludedFiles: ['packages/@uppy/**/*.test.ts'],
+      excludedFiles: ['packages/@uppy/**/*.test.ts', 'packages/@uppy/core/src/mocks/*.ts'],
       rules: {
         '@typescript-eslint/explicit-function-return-type': 'error',
       },

+ 0 - 85
packages/@uppy/core/src/BasePlugin.js

@@ -1,85 +0,0 @@
-/**
- * Core plugin logic that all plugins share.
- *
- * BasePlugin does not contain DOM rendering so it can be used for plugins
- * without a user interface.
- *
- * See `Plugin` for the extended version with Preact rendering for interfaces.
- */
-
-import Translator from '@uppy/utils/lib/Translator'
-
-export default class BasePlugin {
-  constructor (uppy, opts = {}) {
-    this.uppy = uppy
-    this.opts = opts
-  }
-
-  getPluginState () {
-    const { plugins } = this.uppy.getState()
-    return plugins[this.id] || {}
-  }
-
-  setPluginState (update) {
-    const { plugins } = this.uppy.getState()
-
-    this.uppy.setState({
-      plugins: {
-        ...plugins,
-        [this.id]: {
-          ...plugins[this.id],
-          ...update,
-        },
-      },
-    })
-  }
-
-  setOptions (newOpts) {
-    this.opts = { ...this.opts, ...newOpts }
-    this.setPluginState() // so that UI re-renders with new options
-    this.i18nInit()
-  }
-
-  i18nInit () {
-    const onMissingKey = (key) => this.uppy.log(`Missing i18n string: ${key}`, 'error')
-    const translator = new Translator([this.defaultLocale, this.uppy.locale, this.opts.locale], { onMissingKey })
-    this.i18n = translator.translate.bind(translator)
-    this.i18nArray = translator.translateArray.bind(translator)
-    this.setPluginState() // so that UI re-renders and we see the updated locale
-  }
-
-  /**
-   * Extendable methods
-   * ==================
-   * These methods are here to serve as an overview of the extendable methods as well as
-   * making them not conditional in use, such as `if (this.afterUpdate)`.
-   */
-
-  // eslint-disable-next-line class-methods-use-this
-  addTarget () {
-    throw new Error('Extend the addTarget method to add your plugin to another plugin\'s target')
-  }
-
-  // eslint-disable-next-line class-methods-use-this
-  install () {}
-
-  // eslint-disable-next-line class-methods-use-this
-  uninstall () {}
-
-  /**
-   * Called when plugin is mounted, whether in DOM or into another plugin.
-   * Needed because sometimes plugins are mounted separately/after `install`,
-   * so this.el and this.parent might not be available in `install`.
-   * This is the case with @uppy/react plugins, for example.
-   */
-  render () {
-    throw new Error('Extend the render method to add your plugin to a DOM element')
-  }
-
-  // eslint-disable-next-line class-methods-use-this
-  update () {}
-
-  // Called after every state update, after everything's mounted. Debounced.
-  // eslint-disable-next-line class-methods-use-this
-  afterUpdate () {}
-}

+ 106 - 0
packages/@uppy/core/src/BasePlugin.ts

@@ -0,0 +1,106 @@
+/* eslint-disable class-methods-use-this */
+/* eslint-disable @typescript-eslint/no-empty-function */
+
+/**
+ * Core plugin logic that all plugins share.
+ *
+ * BasePlugin does not contain DOM rendering so it can be used for plugins
+ * without a user interface.
+ *
+ * See `Plugin` for the extended version with Preact rendering for interfaces.
+ */
+
+import Translator from '@uppy/utils/lib/Translator'
+import type { I18n, Locale } from '@uppy/utils/lib/Translator'
+import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
+import type { Uppy } from '.'
+
+export type PluginOpts = { locale?: Locale; [key: string]: unknown }
+
+export default class BasePlugin<
+  Opts extends PluginOpts,
+  M extends Meta,
+  B extends Body,
+> {
+  uppy: Uppy<M, B>
+
+  opts: Opts
+
+  id: string
+
+  defaultLocale: Locale
+
+  i18n: I18n
+
+  i18nArray: Translator['translateArray']
+
+  type: string
+
+  VERSION: string
+
+  constructor(uppy: Uppy<M, B>, opts: Opts) {
+    this.uppy = uppy
+    this.opts = opts ?? {}
+  }
+
+  getPluginState(): Record<string, unknown> {
+    const { plugins } = this.uppy.getState()
+    return plugins?.[this.id] || {}
+  }
+
+  setPluginState(update: unknown): void {
+    if (!update) return
+    const { plugins } = this.uppy.getState()
+
+    this.uppy.setState({
+      plugins: {
+        ...plugins,
+        [this.id]: {
+          ...plugins[this.id],
+          ...update,
+        },
+      },
+    })
+  }
+
+  setOptions(newOpts: Partial<Opts>): void {
+    this.opts = { ...this.opts, ...newOpts }
+    this.setPluginState(undefined) // so that UI re-renders with new options
+    this.i18nInit()
+  }
+
+  i18nInit(): void {
+    const translator = new Translator([
+      this.defaultLocale,
+      this.uppy.locale,
+      this.opts.locale,
+    ])
+    this.i18n = translator.translate.bind(translator)
+    this.i18nArray = translator.translateArray.bind(translator)
+    this.setPluginState(undefined) // so that UI re-renders and we see the updated locale
+  }
+
+  /**
+   * Extendable methods
+   * ==================
+   * These methods are here to serve as an overview of the extendable methods as well as
+   * making them not conditional in use, such as `if (this.afterUpdate)`.
+   */
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  addTarget(plugin: unknown): HTMLElement {
+    throw new Error(
+      "Extend the addTarget method to add your plugin to another plugin's target",
+    )
+  }
+
+  install(): void {}
+
+  uninstall(): void {}
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  update(state: any): void {}
+
+  // Called after every state update, after everything's mounted. Debounced.
+  afterUpdate(): void {}
+}

+ 114 - 0
packages/@uppy/core/src/EventManager.ts

@@ -0,0 +1,114 @@
+import type { Meta, Body, UppyFile } from '@uppy/utils/lib/UppyFile'
+import type {
+  DeprecatedUppyEventMap,
+  Uppy,
+  UppyEventMap,
+  _UppyEventMap,
+} from './Uppy'
+
+/**
+ * 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<M extends Meta, B extends Body> {
+  #uppy: Uppy<M, B>
+
+  #events: Array<[keyof UppyEventMap<M, B>, (...args: any[]) => void]> = []
+
+  constructor(uppy: Uppy<M, B>) {
+    this.#uppy = uppy
+  }
+
+  on<K extends keyof _UppyEventMap<M, B>>(
+    event: K,
+    fn: _UppyEventMap<M, B>[K],
+  ): Uppy<M, B>
+
+  /** @deprecated */
+  on<K extends keyof DeprecatedUppyEventMap<M, B>>(
+    event: K,
+    fn: DeprecatedUppyEventMap<M, B>[K],
+  ): Uppy<M, B>
+
+  on<K extends keyof UppyEventMap<M, B>>(
+    event: K,
+    fn: UppyEventMap<M, B>[K],
+  ): Uppy<M, B> {
+    this.#events.push([event, fn])
+    return this.#uppy.on(event as keyof _UppyEventMap<M, B>, fn)
+  }
+
+  remove(): void {
+    for (const [event, fn] of this.#events.splice(0)) {
+      this.#uppy.off(event, fn)
+    }
+  }
+
+  onFilePause(
+    fileID: UppyFile<M, B>['id'],
+    cb: (isPaused: boolean) => void,
+  ): void {
+    this.on('upload-pause', (targetFileID, isPaused) => {
+      if (fileID === targetFileID) {
+        cb(isPaused)
+      }
+    })
+  }
+
+  onFileRemove(
+    fileID: UppyFile<M, B>['id'],
+    cb: (isPaused: UppyFile<M, B>['id']) => void,
+  ): void {
+    this.on('file-removed', (file) => {
+      if (fileID === file.id) cb(file.id)
+    })
+  }
+
+  onPause(fileID: UppyFile<M, B>['id'], cb: (isPaused: boolean) => void): void {
+    this.on('upload-pause', (targetFileID, isPaused) => {
+      if (fileID === targetFileID) {
+        // const isPaused = this.#uppy.pauseResume(fileID)
+        cb(isPaused)
+      }
+    })
+  }
+
+  onRetry(fileID: UppyFile<M, B>['id'], cb: () => void): void {
+    this.on('upload-retry', (targetFileID) => {
+      if (fileID === targetFileID) {
+        cb()
+      }
+    })
+  }
+
+  onRetryAll(fileID: UppyFile<M, B>['id'], cb: () => void): void {
+    this.on('retry-all', () => {
+      if (!this.#uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+
+  onPauseAll(fileID: UppyFile<M, B>['id'], cb: () => void): void {
+    this.on('pause-all', () => {
+      if (!this.#uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+
+  onCancelAll(
+    fileID: UppyFile<M, B>['id'],
+    eventHandler: UppyEventMap<M, B>['cancel-all'],
+  ): void {
+    this.on('cancel-all', (...args) => {
+      if (!this.#uppy.getFile(fileID)) return
+      eventHandler(...args)
+    })
+  }
+
+  onResumeAll(fileID: UppyFile<M, B>['id'], cb: () => void): void {
+    this.on('resume-all', () => {
+      if (!this.#uppy.getFile(fileID)) return
+      cb()
+    })
+  }
+}

+ 0 - 137
packages/@uppy/core/src/Restricter.js

@@ -1,137 +0,0 @@
-/* eslint-disable max-classes-per-file, class-methods-use-this */
-import prettierBytes from '@transloadit/prettier-bytes'
-import match from 'mime-match'
-
-const defaultOptions = {
-  maxFileSize: null,
-  minFileSize: null,
-  maxTotalFileSize: null,
-  maxNumberOfFiles: null,
-  minNumberOfFiles: null,
-  allowedFileTypes: null,
-  requiredMetaFields: [],
-}
-
-class RestrictionError extends Error {
-  constructor (message, { isUserFacing = true, file } = {}) {
-    super(message)
-    this.isUserFacing = isUserFacing
-    if (file != null) this.file = file // only some restriction errors are related to a particular file
-  }
-
-  isRestriction = true
-}
-
-class Restricter {
-  constructor (getOpts, i18n) {
-    this.i18n = i18n
-    this.getOpts = () => {
-      const opts = getOpts()
-
-      if (opts.restrictions.allowedFileTypes != null
-          && !Array.isArray(opts.restrictions.allowedFileTypes)) {
-        throw new TypeError('`restrictions.allowedFileTypes` must be an array')
-      }
-      return opts
-    }
-  }
-
-  // Because these operations are slow, we cannot run them for every file (if we are adding multiple files)
-  validateAggregateRestrictions (existingFiles, addingFiles) {
-    const { maxTotalFileSize, maxNumberOfFiles } = this.getOpts().restrictions
-
-    if (maxNumberOfFiles) {
-      const nonGhostFiles = existingFiles.filter(f => !f.isGhost)
-      if (nonGhostFiles.length + addingFiles.length > maxNumberOfFiles) {
-        throw new RestrictionError(`${this.i18n('youCanOnlyUploadX', { smart_count: maxNumberOfFiles })}`)
-      }
-    }
-
-    if (maxTotalFileSize) {
-      let totalFilesSize = existingFiles.reduce((total, f) => (total + f.size), 0)
-
-      for (const addingFile of addingFiles) {
-        if (addingFile.size != null) { // We can't check maxTotalFileSize if the size is unknown.
-          totalFilesSize += addingFile.size
-
-          if (totalFilesSize > maxTotalFileSize) {
-            throw new RestrictionError(this.i18n('exceedsSize', {
-              size: prettierBytes(maxTotalFileSize),
-              file: addingFile.name,
-            }))
-          }
-        }
-      }
-    }
-  }
-
-  validateSingleFile (file) {
-    const { maxFileSize, minFileSize, allowedFileTypes } = this.getOpts().restrictions
-
-    if (allowedFileTypes) {
-      const isCorrectFileType = allowedFileTypes.some((type) => {
-        // check if this is a mime-type
-        if (type.includes('/')) {
-          if (!file.type) return false
-          return match(file.type.replace(/;.*?$/, ''), type)
-        }
-
-        // otherwise this is likely an extension
-        if (type[0] === '.' && file.extension) {
-          return file.extension.toLowerCase() === type.slice(1).toLowerCase()
-        }
-        return false
-      })
-
-      if (!isCorrectFileType) {
-        const allowedFileTypesString = allowedFileTypes.join(', ')
-        throw new RestrictionError(this.i18n('youCanOnlyUploadFileTypes', { types: allowedFileTypesString }), { file })
-      }
-    }
-
-    // We can't check maxFileSize if the size is unknown.
-    if (maxFileSize && file.size != null && file.size > maxFileSize) {
-      throw new RestrictionError(this.i18n('exceedsSize', {
-        size: prettierBytes(maxFileSize),
-        file: file.name,
-      }), { file })
-    }
-
-    // We can't check minFileSize if the size is unknown.
-    if (minFileSize && file.size != null && file.size < minFileSize) {
-      throw new RestrictionError(this.i18n('inferiorSize', {
-        size: prettierBytes(minFileSize),
-      }), { file })
-    }
-  }
-
-  validate (existingFiles, addingFiles) {
-    addingFiles.forEach((addingFile) => {
-      this.validateSingleFile(addingFile)
-    })
-    this.validateAggregateRestrictions(existingFiles, addingFiles)
-  }
-
-  validateMinNumberOfFiles (files) {
-    const { minNumberOfFiles } = this.getOpts().restrictions
-    if (Object.keys(files).length < minNumberOfFiles) {
-      throw new RestrictionError(this.i18n('youHaveToAtLeastSelectX', { smart_count: minNumberOfFiles }))
-    }
-  }
-
-  getMissingRequiredMetaFields (file) {
-    const error = new RestrictionError(this.i18n('missingRequiredMetaFieldOnFile', { fileName: file.name }))
-    const { requiredMetaFields } = this.getOpts().restrictions
-    const missingFields = []
-
-    for (const field of requiredMetaFields) {
-      if (!Object.hasOwn(file.meta, field) || file.meta[field] === '') {
-        missingFields.push(field)
-      }
-    }
-
-    return { missingFields, error }
-  }
-}
-
-export { Restricter, defaultOptions, RestrictionError }

+ 204 - 0
packages/@uppy/core/src/Restricter.ts

@@ -0,0 +1,204 @@
+/* eslint-disable @typescript-eslint/ban-ts-comment */
+/* eslint-disable max-classes-per-file, class-methods-use-this */
+// @ts-ignore untyped
+import prettierBytes from '@transloadit/prettier-bytes'
+// @ts-ignore untyped
+import match from 'mime-match'
+import Translator from '@uppy/utils/lib/Translator'
+import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
+import type { I18n } from '@uppy/utils/lib/Translator'
+import type { State, NonNullableUppyOptions } from './Uppy'
+
+export type Restrictions = {
+  maxFileSize: number | null
+  minFileSize: number | null
+  maxTotalFileSize: number | null
+  maxNumberOfFiles: number | null
+  minNumberOfFiles: number | null
+  allowedFileTypes: string[] | null
+  requiredMetaFields: string[]
+}
+
+const defaultOptions = {
+  maxFileSize: null,
+  minFileSize: null,
+  maxTotalFileSize: null,
+  maxNumberOfFiles: null,
+  minNumberOfFiles: null,
+  allowedFileTypes: null,
+  requiredMetaFields: [],
+}
+
+class RestrictionError<M extends Meta, B extends Body> extends Error {
+  isUserFacing: boolean
+
+  file: UppyFile<M, B>
+
+  constructor(
+    message: string,
+    opts?: { isUserFacing?: boolean; file?: UppyFile<M, B> },
+  ) {
+    super(message)
+    this.isUserFacing = opts?.isUserFacing ?? true
+    if (opts?.file) {
+      this.file = opts.file // only some restriction errors are related to a particular file
+    }
+  }
+
+  isRestriction = true
+}
+
+class Restricter<M extends Meta, B extends Body> {
+  i18n: Translator['translate']
+
+  getOpts: () => NonNullableUppyOptions<M, B>
+
+  constructor(getOpts: () => NonNullableUppyOptions<M, B>, i18n: I18n) {
+    this.i18n = i18n
+    this.getOpts = (): NonNullableUppyOptions<M, B> => {
+      const opts = getOpts()
+
+      if (
+        opts.restrictions?.allowedFileTypes != null &&
+        !Array.isArray(opts.restrictions.allowedFileTypes)
+      ) {
+        throw new TypeError('`restrictions.allowedFileTypes` must be an array')
+      }
+      return opts
+    }
+  }
+
+  // Because these operations are slow, we cannot run them for every file (if we are adding multiple files)
+  validateAggregateRestrictions(
+    existingFiles: UppyFile<M, B>[],
+    addingFiles: UppyFile<M, B>[],
+  ): void {
+    const { maxTotalFileSize, maxNumberOfFiles } = this.getOpts().restrictions
+
+    if (maxNumberOfFiles) {
+      const nonGhostFiles = existingFiles.filter((f) => !f.isGhost)
+      if (nonGhostFiles.length + addingFiles.length > maxNumberOfFiles) {
+        throw new RestrictionError(
+          `${this.i18n('youCanOnlyUploadX', {
+            smart_count: maxNumberOfFiles,
+          })}`,
+        )
+      }
+    }
+
+    if (maxTotalFileSize) {
+      let totalFilesSize = existingFiles.reduce(
+        (total, f) => (total + (f.size ?? 0)) as number,
+        0,
+      )
+
+      for (const addingFile of addingFiles) {
+        if (addingFile.size != null) {
+          // We can't check maxTotalFileSize if the size is unknown.
+          totalFilesSize += addingFile.size
+
+          if (totalFilesSize > maxTotalFileSize) {
+            throw new RestrictionError(
+              this.i18n('exceedsSize', {
+                size: prettierBytes(maxTotalFileSize),
+                file: addingFile.name,
+              }),
+            )
+          }
+        }
+      }
+    }
+  }
+
+  validateSingleFile(file: UppyFile<M, B>): void {
+    const { maxFileSize, minFileSize, allowedFileTypes } =
+      this.getOpts().restrictions
+
+    if (allowedFileTypes) {
+      const isCorrectFileType = allowedFileTypes.some((type) => {
+        // check if this is a mime-type
+        if (type.includes('/')) {
+          if (!file.type) return false
+          return match(file.type.replace(/;.*?$/, ''), type)
+        }
+
+        // otherwise this is likely an extension
+        if (type[0] === '.' && file.extension) {
+          return file.extension.toLowerCase() === type.slice(1).toLowerCase()
+        }
+        return false
+      })
+
+      if (!isCorrectFileType) {
+        const allowedFileTypesString = allowedFileTypes.join(', ')
+        throw new RestrictionError(
+          this.i18n('youCanOnlyUploadFileTypes', {
+            types: allowedFileTypesString,
+          }),
+          { file },
+        )
+      }
+    }
+
+    // We can't check maxFileSize if the size is unknown.
+    if (maxFileSize && file.size != null && file.size > maxFileSize) {
+      throw new RestrictionError(
+        this.i18n('exceedsSize', {
+          size: prettierBytes(maxFileSize),
+          file: file.name,
+        }),
+        { file },
+      )
+    }
+
+    // We can't check minFileSize if the size is unknown.
+    if (minFileSize && file.size != null && file.size < minFileSize) {
+      throw new RestrictionError(
+        this.i18n('inferiorSize', {
+          size: prettierBytes(minFileSize),
+        }),
+        { file },
+      )
+    }
+  }
+
+  validate(
+    existingFiles: UppyFile<M, B>[],
+    addingFiles: UppyFile<M, B>[],
+  ): void {
+    addingFiles.forEach((addingFile) => {
+      this.validateSingleFile(addingFile)
+    })
+    this.validateAggregateRestrictions(existingFiles, addingFiles)
+  }
+
+  validateMinNumberOfFiles(files: State<M, B>['files']): void {
+    const { minNumberOfFiles } = this.getOpts().restrictions
+    if (minNumberOfFiles && Object.keys(files).length < minNumberOfFiles) {
+      throw new RestrictionError(
+        this.i18n('youHaveToAtLeastSelectX', { smart_count: minNumberOfFiles }),
+      )
+    }
+  }
+
+  getMissingRequiredMetaFields(file: UppyFile<M, B>): {
+    missingFields: string[]
+    error: RestrictionError<M, B>
+  } {
+    const error = new RestrictionError<M, B>(
+      this.i18n('missingRequiredMetaFieldOnFile', { fileName: file.name }),
+    )
+    const { requiredMetaFields } = this.getOpts().restrictions
+    const missingFields: string[] = []
+
+    for (const field of requiredMetaFields) {
+      if (!Object.hasOwn(file.meta, field) || file.meta[field] === '') {
+        missingFields.push(field)
+      }
+    }
+
+    return { missingFields, error }
+  }
+}
+
+export { Restricter, defaultOptions, RestrictionError }

+ 5 - 5
packages/@uppy/core/src/UIPlugin.test.js → packages/@uppy/core/src/UIPlugin.test.ts

@@ -1,12 +1,12 @@
 import { describe, expect, it } from 'vitest'
-import UIPlugin from './UIPlugin.js'
-import Core from './index.js'
+import UIPlugin from './UIPlugin.ts'
+import Core from './index.ts'
 
 describe('UIPlugin', () => {
   describe('getPluginState', () => {
     it('returns an empty object if no state is available', () => {
-      class Example extends UIPlugin {}
-      const inst = new Example(new Core(), {})
+      class Example extends UIPlugin<any, any, any> {}
+      const inst = new Example(new Core<any, any>(), {})
 
       expect(inst.getPluginState()).toEqual({})
     })
@@ -14,7 +14,7 @@ describe('UIPlugin', () => {
 
   describe('setPluginState', () => {
     it('applies patches', () => {
-      class Example extends UIPlugin {}
+      class Example extends UIPlugin<any, any, any> {}
       const inst = new Example(new Core(), {})
 
       inst.setPluginState({ a: 1 })

+ 61 - 29
packages/@uppy/core/src/UIPlugin.js → packages/@uppy/core/src/UIPlugin.ts

@@ -1,18 +1,21 @@
-import { render } from 'preact'
+/* eslint-disable class-methods-use-this */
+/* eslint-disable @typescript-eslint/no-empty-function */
+import { render, type ComponentChild } from 'preact'
 import findDOMElement from '@uppy/utils/lib/findDOMElement'
 import getTextDirection from '@uppy/utils/lib/getTextDirection'
 
-import BasePlugin from './BasePlugin.js'
+import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
+import BasePlugin from './BasePlugin.ts'
+import type { PluginOpts } from './BasePlugin.ts'
 
 /**
  * Defer a frequent call to the microtask queue.
- *
- * @param {() => T} fn
- * @returns {Promise<T>}
  */
-function debounce (fn) {
-  let calling = null
-  let latestArgs = null
+function debounce<T extends (...args: any[]) => any>(
+  fn: T,
+): (...args: Parameters<T>) => Promise<ReturnType<T>> {
+  let calling: Promise<ReturnType<T>> | null = null
+  let latestArgs: Parameters<T>
   return (...args) => {
     latestArgs = args
     if (!calling) {
@@ -35,10 +38,20 @@ function debounce (fn) {
  *
  * For plugins without an user interface, see BasePlugin.
  */
-class UIPlugin extends BasePlugin {
-  #updateUI
+class UIPlugin<
+  Opts extends PluginOpts & { direction?: 'ltr' | 'rtl' },
+  M extends Meta,
+  B extends Body,
+> extends BasePlugin<Opts, M, B> {
+  #updateUI: (state: any) => void
+
+  isTargetDOMEl: boolean
+
+  el: HTMLElement | null
+
+  parent: unknown
 
-  getTargetPlugin (target) {
+  getTargetPlugin(target: unknown): UIPlugin<any, any, any> | undefined {
     let targetPlugin
     if (typeof target === 'object' && target instanceof UIPlugin) {
       // Targeting a plugin *instance*
@@ -47,7 +60,7 @@ class UIPlugin extends BasePlugin {
       // Targeting a plugin type
       const Target = target
       // Find the target plugin instance.
-      this.uppy.iteratePlugins(p => {
+      this.uppy.iteratePlugins((p) => {
         if (p instanceof Target) {
           targetPlugin = p
         }
@@ -62,7 +75,10 @@ class UIPlugin extends BasePlugin {
    * If it’s an object — target is a plugin, and we search `plugins`
    * for a plugin with same name and return its target.
    */
-  mount (target, plugin) {
+  mount(
+    target: HTMLElement | string,
+    plugin: UIPlugin<any, any, any>,
+  ): HTMLElement {
     const callerPluginName = plugin.id
 
     const targetElement = findDOMElement(target)
@@ -85,7 +101,9 @@ class UIPlugin extends BasePlugin {
         this.afterUpdate()
       })
 
-      this.uppy.log(`Installing ${callerPluginName} to a DOM element '${target}'`)
+      this.uppy.log(
+        `Installing ${callerPluginName} to a DOM element '${target}'`,
+      )
 
       if (this.opts.replaceTargetContent) {
         // Doing render(h(null), targetElement), which should have been
@@ -99,7 +117,8 @@ class UIPlugin extends BasePlugin {
       targetElement.appendChild(uppyRootElement)
 
       // Set the text direction if the page has not defined one.
-      uppyRootElement.dir = this.opts.direction || getTextDirection(uppyRootElement) || 'ltr'
+      uppyRootElement.dir =
+        this.opts.direction || getTextDirection(uppyRootElement) || 'ltr'
 
       this.onMount()
 
@@ -121,37 +140,50 @@ class UIPlugin extends BasePlugin {
 
     let message = `Invalid target option given to ${callerPluginName}.`
     if (typeof target === 'function') {
-      message += ' The given target is not a Plugin class. '
-        + 'Please check that you\'re not specifying a React Component instead of a plugin. '
-        + 'If you are using @uppy/* packages directly, make sure you have only 1 version of @uppy/core installed: '
-        + 'run `npm ls @uppy/core` on the command line and verify that all the versions match and are deduped correctly.'
+      message +=
+        ' The given target is not a Plugin class. ' +
+        "Please check that you're not specifying a React Component instead of a plugin. " +
+        'If you are using @uppy/* packages directly, make sure you have only 1 version of @uppy/core installed: ' +
+        'run `npm ls @uppy/core` on the command line and verify that all the versions match and are deduped correctly.'
     } else {
-      message += 'If you meant to target an HTML element, please make sure that the element exists. '
-        + 'Check that the <script> tag initializing Uppy is right before the closing </body> tag at the end of the page. '
-        + '(see https://github.com/transloadit/uppy/issues/1042)\n\n'
-        + 'If you meant to target a plugin, please confirm that your `import` statements or `require` calls are correct.'
+      message +=
+        'If you meant to target an HTML element, please make sure that the element exists. ' +
+        'Check that the <script> tag initializing Uppy is right before the closing </body> tag at the end of the page. ' +
+        '(see https://github.com/transloadit/uppy/issues/1042)\n\n' +
+        'If you meant to target a plugin, please confirm that your `import` statements or `require` calls are correct.'
     }
     throw new Error(message)
   }
 
-  update (state) {
+  /**
+   * Called when plugin is mounted, whether in DOM or into another plugin.
+   * Needed because sometimes plugins are mounted separately/after `install`,
+   * so this.el and this.parent might not be available in `install`.
+   * This is the case with @uppy/react plugins, for example.
+   */
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  render(state: Record<string, unknown>): ComponentChild {
+    throw new Error(
+      'Extend the render method to add your plugin to a DOM element',
+    )
+  }
+
+  update(state: any): void {
     if (this.el != null) {
       this.#updateUI?.(state)
     }
   }
 
-  unmount () {
+  unmount(): void {
     if (this.isTargetDOMEl) {
       this.el?.remove()
     }
     this.onUnmount()
   }
 
-  // eslint-disable-next-line class-methods-use-this
-  onMount () {}
+  onMount(): void {}
 
-  // eslint-disable-next-line class-methods-use-this
-  onUnmount () {}
+  onUnmount(): void {}
 }
 
 export default UIPlugin

Разница между файлами не показана из-за своего большого размера
+ 315 - 147
packages/@uppy/core/src/Uppy.test.ts


Разница между файлами не показана из-за своего большого размера
+ 541 - 151
packages/@uppy/core/src/Uppy.ts


+ 69 - 0
packages/@uppy/core/src/__snapshots__/Uppy.test.ts.snap

@@ -0,0 +1,69 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`src/Core > plugins > should not be able to add a plugin that has no id 1`] = `"Your plugin must have an id"`;
+
+exports[`src/Core > plugins > should not be able to add a plugin that has no type 1`] = `"Your plugin must have a type"`;
+
+exports[`src/Core > plugins > should not be able to add an invalid plugin 1`] = `"Expected a plugin class, but got object. Please verify that the plugin was imported and spelled correctly."`;
+
+exports[`src/Core > plugins > should prevent the same plugin from being added more than once 1`] = `
+"Already found a plugin named 'TestSelector1'. Tried to use: 'TestSelector1'.
+Uppy plugins must have unique \`id\` options. See https://uppy.io/docs/plugins/#id."
+`;
+
+exports[`src/Core > uploading a file > should only upload files that are not already assigned to another upload id 1`] = `
+{
+  "failed": [],
+  "successful": [
+    {
+      "data": Uint8Array [],
+      "extension": "jpg",
+      "id": "uppy-foo/jpg-1e-image/jpeg",
+      "isGhost": false,
+      "isRemote": false,
+      "meta": {
+        "name": "foo.jpg",
+        "type": "image/jpeg",
+      },
+      "name": "foo.jpg",
+      "preview": undefined,
+      "progress": {
+        "bytesTotal": null,
+        "bytesUploaded": 0,
+        "percentage": 0,
+        "uploadComplete": false,
+        "uploadStarted": null,
+      },
+      "remote": "",
+      "size": null,
+      "source": "vi",
+      "type": "image/jpeg",
+    },
+    {
+      "data": Uint8Array [],
+      "extension": "jpg",
+      "id": "uppy-bar/jpg-1e-image/jpeg",
+      "isGhost": false,
+      "isRemote": false,
+      "meta": {
+        "name": "bar.jpg",
+        "type": "image/jpeg",
+      },
+      "name": "bar.jpg",
+      "preview": undefined,
+      "progress": {
+        "bytesTotal": null,
+        "bytesUploaded": 0,
+        "percentage": 0,
+        "uploadComplete": false,
+        "uploadStarted": null,
+      },
+      "remote": "",
+      "size": null,
+      "source": "vi",
+      "type": "image/jpeg",
+    },
+  ],
+  "uploadID": "cjd09qwxb000dlql4tp4doz8h",
+}
+`;

+ 4 - 1
packages/@uppy/core/src/getFileName.js → packages/@uppy/core/src/getFileName.ts

@@ -1,4 +1,7 @@
-export default function getFileName (fileType, fileDescriptor) {
+export default function getFileName(
+  fileType: string,
+  fileDescriptor: { name?: string },
+): string {
   if (fileDescriptor.name) {
     return fileDescriptor.name
   }

+ 0 - 5
packages/@uppy/core/src/index.js

@@ -1,5 +0,0 @@
-export { default } from './Uppy.js'
-export { default as Uppy } from './Uppy.js'
-export { default as UIPlugin } from './UIPlugin.js'
-export { default as BasePlugin } from './BasePlugin.js'
-export { debugLogger } from './loggers.js'

+ 5 - 0
packages/@uppy/core/src/index.ts

@@ -0,0 +1,5 @@
+export { default } from './Uppy.ts'
+export { default as Uppy } from './Uppy.ts'
+export { default as UIPlugin } from './UIPlugin.ts'
+export { default as BasePlugin } from './BasePlugin.ts'
+export { debugLogger } from './loggers.ts'

+ 2 - 1
packages/@uppy/core/src/locale.js → packages/@uppy/core/src/locale.ts

@@ -58,6 +58,7 @@ export default {
       0: 'Added %{smart_count} file from %{folder}',
       1: 'Added %{smart_count} files from %{folder}',
     },
-    additionalRestrictionsFailed: '%{count} additional restrictions were not fulfilled',
+    additionalRestrictionsFailed:
+      '%{count} additional restrictions were not fulfilled',
   },
 }

+ 0 - 23
packages/@uppy/core/src/loggers.js

@@ -1,23 +0,0 @@
-/* eslint-disable no-console */
-import getTimeStamp from '@uppy/utils/lib/getTimeStamp'
-
-// Swallow all logs, except errors.
-// default if logger is not set or debug: false
-const justErrorsLogger = {
-  debug: () => {},
-  warn: () => {},
-  error: (...args) => console.error(`[Uppy] [${getTimeStamp()}]`, ...args),
-}
-
-// Print logs to console with namespace + timestamp,
-// set by logger: Uppy.debugLogger or debug: true
-const debugLogger = {
-  debug: (...args) => console.debug(`[Uppy] [${getTimeStamp()}]`, ...args),
-  warn: (...args) => console.warn(`[Uppy] [${getTimeStamp()}]`, ...args),
-  error: (...args) => console.error(`[Uppy] [${getTimeStamp()}]`, ...args),
-}
-
-export {
-  justErrorsLogger,
-  debugLogger,
-}

+ 25 - 0
packages/@uppy/core/src/loggers.ts

@@ -0,0 +1,25 @@
+/* eslint-disable @typescript-eslint/no-empty-function */
+/* eslint-disable no-console */
+import getTimeStamp from '@uppy/utils/lib/getTimeStamp'
+
+// Swallow all logs, except errors.
+// default if logger is not set or debug: false
+const justErrorsLogger = {
+  debug: (): void => {},
+  warn: (): void => {},
+  error: (...args: any[]): void =>
+    console.error(`[Uppy] [${getTimeStamp()}]`, ...args),
+}
+
+// Print logs to console with namespace + timestamp,
+// set by logger: Uppy.debugLogger or debug: true
+const debugLogger = {
+  debug: (...args: any[]): void =>
+    console.debug(`[Uppy] [${getTimeStamp()}]`, ...args),
+  warn: (...args: any[]): void =>
+    console.warn(`[Uppy] [${getTimeStamp()}]`, ...args),
+  error: (...args: any[]): void =>
+    console.error(`[Uppy] [${getTimeStamp()}]`, ...args),
+}
+
+export { justErrorsLogger, debugLogger }

+ 13 - 6
packages/@uppy/core/src/mocks/acquirerPlugin1.js → packages/@uppy/core/src/mocks/acquirerPlugin1.ts

@@ -1,8 +1,15 @@
 import { vi } from 'vitest' // eslint-disable-line import/no-extraneous-dependencies
-import UIPlugin from '../UIPlugin.js'
+import UIPlugin from '../UIPlugin.ts'
+import type Uppy from '../Uppy.ts'
 
-export default class TestSelector1 extends UIPlugin {
-  constructor (uppy, opts) {
+type mock = ReturnType<typeof vi.fn>
+
+export default class TestSelector1 extends UIPlugin<any, any, any> {
+  name: string
+
+  mocks: { run: mock; update: mock; uninstall: mock }
+
+  constructor(uppy: Uppy<any, any>, opts: any) {
     super(uppy, opts)
     this.type = 'acquirer'
     this.id = 'TestSelector1'
@@ -15,7 +22,7 @@ export default class TestSelector1 extends UIPlugin {
     }
   }
 
-  run (results) {
+  run(results: any) {
     this.uppy.log({
       class: this.constructor.name,
       method: 'run',
@@ -25,11 +32,11 @@ export default class TestSelector1 extends UIPlugin {
     return Promise.resolve('success')
   }
 
-  update (state) {
+  update(state: any) {
     this.mocks.update(state)
   }
 
-  uninstall () {
+  uninstall() {
     this.mocks.uninstall()
   }
 }

+ 13 - 6
packages/@uppy/core/src/mocks/acquirerPlugin2.js → packages/@uppy/core/src/mocks/acquirerPlugin2.ts

@@ -1,8 +1,15 @@
 import { vi } from 'vitest' // eslint-disable-line import/no-extraneous-dependencies
-import UIPlugin from '../UIPlugin.js'
+import UIPlugin from '../UIPlugin.ts'
+import type Uppy from '../Uppy.ts'
 
-export default class TestSelector2 extends UIPlugin {
-  constructor (uppy, opts) {
+type mock = ReturnType<typeof vi.fn>
+
+export default class TestSelector2 extends UIPlugin<any, any, any> {
+  name: string
+
+  mocks: { run: mock; update: mock; uninstall: mock }
+
+  constructor(uppy: Uppy<any, any>, opts: any) {
     super(uppy, opts)
     this.type = 'acquirer'
     this.id = 'TestSelector2'
@@ -15,7 +22,7 @@ export default class TestSelector2 extends UIPlugin {
     }
   }
 
-  run (results) {
+  run(results: any) {
     this.uppy.log({
       class: this.constructor.name,
       method: 'run',
@@ -25,11 +32,11 @@ export default class TestSelector2 extends UIPlugin {
     return Promise.resolve('success')
   }
 
-  update (state) {
+  update(state: any) {
     this.mocks.update(state)
   }
 
-  uninstall () {
+  uninstall() {
     this.mocks.uninstall()
   }
 }

+ 0 - 0
packages/@uppy/core/src/mocks/invalidPlugin.js → packages/@uppy/core/src/mocks/invalidPlugin.ts


+ 0 - 19
packages/@uppy/core/src/mocks/invalidPluginWithoutId.js

@@ -1,19 +0,0 @@
-import UIPlugin from '../UIPlugin.js'
-
-export default class InvalidPluginWithoutName extends UIPlugin {
-  constructor (uppy, opts) {
-    super(uppy, opts)
-    this.type = 'acquirer'
-    this.name = this.constructor.name
-  }
-
-  run (results) {
-    this.uppy.log({
-      class: this.constructor.name,
-      method: 'run',
-      results,
-    })
-
-    return Promise.resolve('success')
-  }
-}

+ 24 - 0
packages/@uppy/core/src/mocks/invalidPluginWithoutId.ts

@@ -0,0 +1,24 @@
+import UIPlugin from '../UIPlugin.ts'
+import type Uppy from '../Uppy.ts'
+
+export default class InvalidPluginWithoutName extends UIPlugin<any, any, any> {
+  public type: string
+
+  public name: string
+
+  constructor(uppy: Uppy<any, any>, opts: any) {
+    super(uppy, opts)
+    this.type = 'acquirer'
+    this.name = this.constructor.name
+  }
+
+  run(results: any) {
+    this.uppy.log({
+      class: this.constructor.name,
+      method: 'run',
+      results,
+    })
+
+    return Promise.resolve('success')
+  }
+}

+ 0 - 19
packages/@uppy/core/src/mocks/invalidPluginWithoutType.js

@@ -1,19 +0,0 @@
-import UIPlugin from '../UIPlugin.js'
-
-export default class InvalidPluginWithoutType extends UIPlugin {
-  constructor (uppy, opts) {
-    super(uppy, opts)
-    this.id = 'InvalidPluginWithoutType'
-    this.name = this.constructor.name
-  }
-
-  run (results) {
-    this.uppy.log({
-      class: this.constructor.name,
-      method: 'run',
-      results,
-    })
-
-    return Promise.resolve('success')
-  }
-}

+ 24 - 0
packages/@uppy/core/src/mocks/invalidPluginWithoutType.ts

@@ -0,0 +1,24 @@
+import UIPlugin from '../UIPlugin.ts'
+import type Uppy from '../Uppy.ts'
+
+export default class InvalidPluginWithoutType extends UIPlugin<any, any, any> {
+  public id: string
+
+  public name: string
+
+  constructor(uppy: Uppy<any, any>, opts: any) {
+    super(uppy, opts)
+    this.id = 'InvalidPluginWithoutType'
+    this.name = this.constructor.name
+  }
+
+  run(results: any) {
+    this.uppy.log({
+      class: this.constructor.name,
+      method: 'run',
+      results,
+    })
+
+    return Promise.resolve('success')
+  }
+}

+ 0 - 29
packages/@uppy/core/src/supportsUploadProgress.test.js

@@ -1,29 +0,0 @@
-import { describe, expect, it } from 'vitest'
-import supportsUploadProgress from './supportsUploadProgress.js'
-
-describe('supportsUploadProgress', () => {
-  it('returns true in working browsers', () => {
-    // Firefox 64 (dev edition)
-    expect(supportsUploadProgress('Mozilla/5.0 (X11; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0')).toBe(true)
-
-    // Chromium 70
-    expect(supportsUploadProgress('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36')).toBe(true)
-
-    // IE 11
-    expect(supportsUploadProgress('Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; rv:11.0) like Gecko')).toBe(true)
-
-    // MS Edge 14
-    expect(supportsUploadProgress('Chrome (AppleWebKit/537.1; Chrome50.0; Windows NT 6.3) AppleWebKit/537.36 (KHTML like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393')).toBe(true)
-
-    // MS Edge 18, supposedly fixed
-    expect(supportsUploadProgress('Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/18.18218')).toBe(true)
-  })
-
-  it('returns false in broken browsers', () => {
-    // MS Edge 15, first broken version
-    expect(supportsUploadProgress('Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063')).toBe(false)
-
-    // MS Edge 17
-    expect(supportsUploadProgress('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134')).toBe(false)
-  })
-})

+ 57 - 0
packages/@uppy/core/src/supportsUploadProgress.test.ts

@@ -0,0 +1,57 @@
+import { describe, expect, it } from 'vitest'
+import supportsUploadProgress from './supportsUploadProgress.ts'
+
+describe('supportsUploadProgress', () => {
+  it('returns true in working browsers', () => {
+    // Firefox 64 (dev edition)
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (X11; Linux x86_64; rv:64.0) Gecko/20100101 Firefox/64.0',
+      ),
+    ).toBe(true)
+
+    // Chromium 70
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.110 Safari/537.36',
+      ),
+    ).toBe(true)
+
+    // IE 11
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (Windows NT 10.0; WOW64; Trident/7.0; .NET4.0C; .NET4.0E; rv:11.0) like Gecko',
+      ),
+    ).toBe(true)
+
+    // MS Edge 14
+    expect(
+      supportsUploadProgress(
+        'Chrome (AppleWebKit/537.1; Chrome50.0; Windows NT 6.3) AppleWebKit/537.36 (KHTML like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393',
+      ),
+    ).toBe(true)
+
+    // MS Edge 18, supposedly fixed
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/18.18218',
+      ),
+    ).toBe(true)
+  })
+
+  it('returns false in broken browsers', () => {
+    // MS Edge 15, first broken version
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063',
+      ),
+    ).toBe(false)
+
+    // MS Edge 17
+    expect(
+      supportsUploadProgress(
+        'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.140 Safari/537.36 Edge/17.17134',
+      ),
+    ).toBe(false)
+  })
+})

+ 4 - 4
packages/@uppy/core/src/supportsUploadProgress.js → packages/@uppy/core/src/supportsUploadProgress.ts

@@ -1,7 +1,7 @@
 // Edge 15.x does not fire 'progress' events on uploads.
 // See https://github.com/transloadit/uppy/issues/945
 // And https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/12224510/
-export default function supportsUploadProgress (userAgent) {
+export default function supportsUploadProgress(userAgent?: string): boolean {
   // Allow passing in userAgent for tests
   if (userAgent == null && typeof navigator !== 'undefined') {
     // eslint-disable-next-line no-param-reassign
@@ -14,9 +14,9 @@ export default function supportsUploadProgress (userAgent) {
   if (!m) return true
 
   const edgeVersion = m[1]
-  let [major, minor] = edgeVersion.split('.')
-  major = parseInt(major, 10)
-  minor = parseInt(minor, 10)
+  const version = edgeVersion.split('.', 2)
+  const major = parseInt(version[0], 10)
+  const minor = parseInt(version[1], 10)
 
   // Worked before:
   // Edge 40.15063.0.0

+ 20 - 0
packages/@uppy/core/tsconfig.build.json

@@ -0,0 +1,20 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "outDir": "./lib",
+    "rootDir": "./src",
+    "resolveJsonModule": false,
+    "noImplicitAny": false,
+    "skipLibCheck": true
+  },
+  "include": ["./src/**/*.*"],
+  "exclude": ["./src/**/*.test.ts"],
+  "references": [
+    {
+      "path": "../store-default/tsconfig.build.json"
+    },
+    {
+      "path": "../utils/tsconfig.build.json"
+    }
+  ]
+}

+ 16 - 0
packages/@uppy/core/tsconfig.json

@@ -0,0 +1,16 @@
+{
+  "extends": "../../../tsconfig.shared",
+  "compilerOptions": {
+    "emitDeclarationOnly": false,
+    "noEmit": true
+  },
+  "include": ["./package.json", "./src/**/*.*"],
+  "references": [
+    {
+      "path": "../store-default/tsconfig.build.json"
+    },
+    {
+      "path": "../utils/tsconfig.build.json"
+    }
+  ]
+}

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

@@ -65,6 +65,8 @@
     "./lib/FOCUSABLE_ELEMENTS.js": "./lib/FOCUSABLE_ELEMENTS.js",
     "./lib/fileFilters": "./lib/fileFilters.js",
     "./lib/VirtualList": "./lib/VirtualList.js",
+    "./lib/UppyFile": "./lib/UppyFile.js",
+    "./lib/FileProgress": "./lib/FileProgress.js",
     "./src/microtip.scss": "./src/microtip.scss",
     "./lib/UserFacingApiError": "./lib/UserFacingApiError.js"
   },

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

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

+ 6 - 2
packages/@uppy/utils/src/FileProgress.ts

@@ -1,8 +1,10 @@
 interface FileProgressBase {
-  progress: number
+  progress?: number
   uploadComplete: boolean
   percentage: number
   bytesTotal: number
+  preprocess?: { mode: string; message?: string; value?: number }
+  postprocess?: { mode: string; message?: string; value?: number }
 }
 
 // FileProgress is either started or not started. We want to make sure TS doesn't
@@ -10,9 +12,11 @@ interface FileProgressBase {
 export type FileProgressStarted = FileProgressBase & {
   uploadStarted: number
   bytesUploaded: number
+  progress: number
 }
 export type FileProgressNotStarted = FileProgressBase & {
   uploadStarted: null
-  bytesUploaded: false
+  // TODO: remove `|0` (or maybe `false|`?)
+  bytesUploaded: false | 0
 }
 export type FileProgress = FileProgressStarted | FileProgressNotStarted

+ 13 - 7
packages/@uppy/utils/src/Translator.ts

@@ -4,6 +4,13 @@ export interface Locale<T extends number = number> {
   pluralize: (n: number) => T
 }
 
+export type OptionalPluralizeLocale<T extends number = number> =
+  | (Omit<Locale<T>, 'pluralize'> & Partial<Pick<Locale<T>, 'pluralize'>>)
+  | undefined
+
+// eslint-disable-next-line no-use-before-define
+export type I18n = Translator['translate']
+
 type Options = {
   smart_count?: number
 } & {
@@ -98,10 +105,10 @@ const defaultOnMissingKey = (key: string): void => {
  * Usage example: `translator.translate('files_chosen', {smart_count: 3})`
  */
 export default class Translator {
-  protected locale: Locale
+  readonly locale: Locale
 
   constructor(
-    locales: Locale | Locale[],
+    locales: Locale | Array<OptionalPluralizeLocale | undefined>,
     { onMissingKey = defaultOnMissingKey } = {},
   ) {
     this.locale = {
@@ -125,17 +132,16 @@ export default class Translator {
 
   #onMissingKey
 
-  #apply(locale?: Locale): void {
+  #apply(locale?: OptionalPluralizeLocale): void {
     if (!locale?.strings) {
       return
     }
 
     const prevLocale = this.locale
-    this.locale = {
-      ...prevLocale,
+    Object.assign(this.locale, {
       strings: { ...prevLocale.strings, ...locale.strings },
-    } as any
-    this.locale.pluralize = locale.pluralize || prevLocale.pluralize
+      pluralize: locale.pluralize || prevLocale.pluralize,
+    })
   }
 
   /**

+ 19 - 18
packages/@uppy/utils/src/UppyFile.ts

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

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

@@ -5,7 +5,7 @@ import type { FileProgress } from './FileProgress'
 function emitSocketProgress(
   uploader: any,
   progressData: FileProgress,
-  file: UppyFile,
+  file: UppyFile<any, any>,
 ): void {
   const { progress, bytesUploaded, bytesTotal } = progressData
   if (progress) {

+ 8 - 3
packages/@uppy/utils/src/fileFilters.ts

@@ -1,13 +1,18 @@
 import type { UppyFile } from './UppyFile'
 
-export function filterNonFailedFiles(files: UppyFile[]): UppyFile[] {
-  const hasError = (file: UppyFile): boolean => 'error' in file && !!file.error
+export function filterNonFailedFiles(
+  files: UppyFile<any, any>[],
+): UppyFile<any, any>[] {
+  const hasError = (file: UppyFile<any, any>): 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[] {
+export function filterFilesToEmitUploadStarted(
+  files: UppyFile<any, any>[],
+): UppyFile<any, any>[] {
   return files.filter(
     (file) => !file.progress?.uploadStarted || !file.isRestored,
   )

+ 1 - 1
packages/@uppy/utils/src/findDOMElement.ts

@@ -6,7 +6,7 @@ import isDOMElement from './isDOMElement.ts'
 export default function findDOMElement(
   element: Node | string,
   context = document,
-): Node | null {
+): Element | null {
   if (typeof element === 'string') {
     return context.querySelector(element)
   }

+ 3 - 3
packages/@uppy/utils/src/generateFileID.ts

@@ -19,7 +19,7 @@ function encodeFilename(name: string): string {
  * Takes a file object and turns it into fileID, by converting file.name to lowercase,
  * removing extra characters and adding type, size and lastModified
  */
-export default function generateFileID(file: UppyFile): string {
+export default function generateFileID(file: UppyFile<any, any>): string {
   // It's tempting to do `[items].filter(Boolean).join('-')` here, but that
   // is slower! simple string concatenation is fast
 
@@ -48,7 +48,7 @@ export default function generateFileID(file: UppyFile): string {
 
 // 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: UppyFile): boolean {
+function hasFileStableId(file: UppyFile<any, any>): 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([
@@ -61,7 +61,7 @@ function hasFileStableId(file: UppyFile): boolean {
   return stableIdProviders.has(file.remote.provider as any)
 }
 
-export function getSafeFileId(file: UppyFile): string {
+export function getSafeFileId(file: UppyFile<any, any>): string {
   if (hasFileStableId(file)) return file.id
 
   const fileType = getFileType(file)

+ 9 - 9
packages/@uppy/utils/src/getFileType.test.ts

@@ -8,7 +8,7 @@ describe('getFileType', () => {
       isRemote: true,
       type: 'audio/webm',
       name: 'foo.webm',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     expect(getFileType(file)).toEqual('audio/webm')
   })
 
@@ -17,7 +17,7 @@ describe('getFileType', () => {
       type: 'audio/webm',
       name: 'foo.webm',
       data: 'sdfsdfhq9efbicw',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     expect(getFileType(file)).toEqual('audio/webm')
   })
 
@@ -25,24 +25,24 @@ describe('getFileType', () => {
     const fileMP3 = {
       name: 'foo.mp3',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     const fileYAML = {
       name: 'bar.yaml',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     const fileMKV = {
       name: 'bar.mkv',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     const fileDicom = {
       name: 'bar.dicom',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     const fileWebp = {
       name: 'bar.webp',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
-    const toUpper = (file: UppyFile) => ({
+    } as any as UppyFile<any, any>
+    const toUpper = (file: UppyFile<any, any>) => ({
       ...file,
       name: file.name.toUpperCase(),
     })
@@ -62,7 +62,7 @@ describe('getFileType', () => {
     const file = {
       name: 'foobar',
       data: 'sdfsfhfh329fhwihs',
-    } as any as UppyFile
+    } as any as UppyFile<any, any>
     expect(getFileType(file)).toEqual('application/octet-stream')
   })
 })

+ 1 - 1
packages/@uppy/utils/src/getFileType.ts

@@ -2,7 +2,7 @@ import type { UppyFile } from './UppyFile'
 import getFileNameAndExtension from './getFileNameAndExtension.ts'
 import mimeTypes from './mimeTypes.ts'
 
-export default function getFileType(file: UppyFile): string {
+export default function getFileType(file: Partial<UppyFile<any, any>>): string {
   if (file.type) return file.type
 
   const fileExtension = file.name

+ 3 - 2
packages/@uppy/utils/src/getSpeed.ts

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

Некоторые файлы не были показаны из-за большого количества измененных файлов