Browse Source

Merge branch `main`

Antoine du Hamel 1 year ago
parent
commit
35314b629a
100 changed files with 1996 additions and 1045 deletions
  1. 1 1
      .eslintrc.js
  2. 0 0
      .github/workflows/lockfile_check.yml
  3. 1 1
      BUNDLE-README.md
  4. 34 0
      CHANGELOG.md
  5. 53 53
      README.md
  6. 2 2
      examples/aws-nodejs/public/drag.html
  7. 2 2
      examples/aws-nodejs/public/index.html
  8. 3 3
      examples/cdn-example/index.html
  9. 2 0
      examples/custom-provider/client/MyCustomProvider.jsx
  10. 2 2
      examples/uppy-with-companion/client/index.html
  11. 2 2
      package.json
  12. 1 1
      packages/@uppy/aws-s3-multipart/package.json
  13. 1 1
      packages/@uppy/aws-s3-multipart/src/index.js
  14. 7 0
      packages/@uppy/aws-s3/CHANGELOG.md
  15. 1 1
      packages/@uppy/aws-s3/package.json
  16. 1 1
      packages/@uppy/aws-s3/src/index.js
  17. 1 1
      packages/@uppy/box/package.json
  18. 7 0
      packages/@uppy/companion-client/CHANGELOG.md
  19. 1 1
      packages/@uppy/companion-client/package.json
  20. 1 1
      packages/@uppy/companion-client/src/Provider.js
  21. 11 0
      packages/@uppy/companion/CHANGELOG.md
  22. 1 1
      packages/@uppy/companion/package.json
  23. 15 6
      packages/@uppy/companion/src/companion.js
  24. 34 23
      packages/@uppy/companion/src/server/Uploader.js
  25. 3 3
      packages/@uppy/companion/src/server/controllers/connect.js
  26. 1 1
      packages/@uppy/companion/src/server/controllers/logout.js
  27. 1 1
      packages/@uppy/companion/src/server/controllers/oauth-redirect.js
  28. 2 2
      packages/@uppy/companion/src/server/controllers/s3.js
  29. 1 1
      packages/@uppy/companion/src/server/helpers/jwt.js
  30. 3 2
      packages/@uppy/companion/src/server/middlewares.js
  31. 1 1
      packages/@uppy/companion/src/server/provider/Provider.js
  32. 2 2
      packages/@uppy/companion/src/server/provider/box/index.js
  33. 2 6
      packages/@uppy/companion/src/server/provider/drive/index.js
  34. 2 2
      packages/@uppy/companion/src/server/provider/dropbox/index.js
  35. 2 6
      packages/@uppy/companion/src/server/provider/facebook/index.js
  36. 7 25
      packages/@uppy/companion/src/server/provider/index.js
  37. 2 6
      packages/@uppy/companion/src/server/provider/instagram/graph/index.js
  38. 8 6
      packages/@uppy/companion/src/server/provider/onedrive/index.js
  39. 2 6
      packages/@uppy/companion/src/server/provider/zoom/index.js
  40. 29 12
      packages/@uppy/companion/src/server/s3-client.js
  41. 9 7
      packages/@uppy/companion/test/__tests__/provider-manager.js
  42. 7 0
      packages/@uppy/core/CHANGELOG.md
  43. 1 1
      packages/@uppy/core/package.json
  44. 0 85
      packages/@uppy/core/src/BasePlugin.js
  45. 106 0
      packages/@uppy/core/src/BasePlugin.ts
  46. 114 0
      packages/@uppy/core/src/EventManager.ts
  47. 0 137
      packages/@uppy/core/src/Restricter.js
  48. 204 0
      packages/@uppy/core/src/Restricter.ts
  49. 5 5
      packages/@uppy/core/src/UIPlugin.test.ts
  50. 61 29
      packages/@uppy/core/src/UIPlugin.ts
  51. 315 147
      packages/@uppy/core/src/Uppy.test.ts
  52. 530 151
      packages/@uppy/core/src/Uppy.ts
  53. 69 0
      packages/@uppy/core/src/__snapshots__/Uppy.test.ts.snap
  54. 4 1
      packages/@uppy/core/src/getFileName.ts
  55. 0 5
      packages/@uppy/core/src/index.js
  56. 5 0
      packages/@uppy/core/src/index.ts
  57. 2 1
      packages/@uppy/core/src/locale.ts
  58. 0 23
      packages/@uppy/core/src/loggers.js
  59. 25 0
      packages/@uppy/core/src/loggers.ts
  60. 13 6
      packages/@uppy/core/src/mocks/acquirerPlugin1.ts
  61. 13 6
      packages/@uppy/core/src/mocks/acquirerPlugin2.ts
  62. 0 0
      packages/@uppy/core/src/mocks/invalidPlugin.ts
  63. 0 19
      packages/@uppy/core/src/mocks/invalidPluginWithoutId.js
  64. 24 0
      packages/@uppy/core/src/mocks/invalidPluginWithoutId.ts
  65. 0 19
      packages/@uppy/core/src/mocks/invalidPluginWithoutType.js
  66. 24 0
      packages/@uppy/core/src/mocks/invalidPluginWithoutType.ts
  67. 0 29
      packages/@uppy/core/src/supportsUploadProgress.test.js
  68. 57 0
      packages/@uppy/core/src/supportsUploadProgress.test.ts
  69. 4 4
      packages/@uppy/core/src/supportsUploadProgress.ts
  70. 20 0
      packages/@uppy/core/tsconfig.build.json
  71. 16 0
      packages/@uppy/core/tsconfig.json
  72. 1 1
      packages/@uppy/dropbox/package.json
  73. 1 1
      packages/@uppy/facebook/package.json
  74. 1 1
      packages/@uppy/google-drive/package.json
  75. 7 0
      packages/@uppy/image-editor/CHANGELOG.md
  76. 1 1
      packages/@uppy/image-editor/package.json
  77. 1 1
      packages/@uppy/instagram/package.json
  78. 1 1
      packages/@uppy/onedrive/package.json
  79. 7 0
      packages/@uppy/provider-views/CHANGELOG.md
  80. 1 1
      packages/@uppy/provider-views/package.json
  81. 3 1
      packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx
  82. 2 0
      packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx
  83. 6 6
      packages/@uppy/provider-views/src/View.js
  84. 1 1
      packages/@uppy/store-default/package.json
  85. 1 1
      packages/@uppy/tus/package.json
  86. 1 1
      packages/@uppy/tus/src/index.js
  87. 1 1
      packages/@uppy/url/package.json
  88. 6 2
      packages/@uppy/url/src/Url.jsx
  89. 7 0
      packages/@uppy/utils/CHANGELOG.md
  90. 3 1
      packages/@uppy/utils/package.json
  91. 3 119
      packages/@uppy/utils/src/EventManager.ts
  92. 6 2
      packages/@uppy/utils/src/FileProgress.ts
  93. 13 7
      packages/@uppy/utils/src/Translator.ts
  94. 19 18
      packages/@uppy/utils/src/UppyFile.ts
  95. 1 1
      packages/@uppy/utils/src/emitSocketProgress.ts
  96. 8 3
      packages/@uppy/utils/src/fileFilters.ts
  97. 1 1
      packages/@uppy/utils/src/findDOMElement.ts
  98. 3 3
      packages/@uppy/utils/src/generateFileID.ts
  99. 9 9
      packages/@uppy/utils/src/getFileType.test.ts
  100. 1 1
      packages/@uppy/utils/src/getFileType.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 - 0
.github/workflows/lockile_check.yml → .github/workflows/lockfile_check.yml


+ 1 - 1
BUNDLE-README.md

@@ -1,7 +1,7 @@
 # Uppy
 
 Hi, thanks for trying out the bundled version of the Uppy File Uploader. You can use
-this from a CDN (`<script src="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.js"></script>`) or bundle it with your webapp.
+this from a CDN (`<script src="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.js"></script>`) or bundle it with your webapp.
 
 Note that the recommended way to use Uppy is to install it with yarn/npm and use a
 bundler like Webpack so that you can create a smaller custom build with only the

+ 34 - 0
CHANGELOG.md

@@ -12,6 +12,40 @@ Please add your entries in this format:
 
 In the current stage we aim to release a new version at least every month.
 
+## 3.21.0
+
+Released: 2023-12-12
+
+| Package                | Version | Package                | Version |
+| ---------------------- | ------- | ---------------------- | ------- |
+| @uppy/aws-s3           |   3.6.0 | @uppy/instagram        |   3.2.0 |
+| @uppy/aws-s3-multipart |  3.10.0 | @uppy/onedrive         |   3.2.0 |
+| @uppy/box              |   2.2.0 | @uppy/provider-views   |   3.8.0 |
+| @uppy/companion        |  4.12.0 | @uppy/store-default    |   3.2.0 |
+| @uppy/companion-client |   3.7.0 | @uppy/tus              |   3.5.0 |
+| @uppy/core             |   3.8.0 | @uppy/url              |   3.5.0 |
+| @uppy/dropbox          |   3.2.0 | @uppy/utils            |   5.7.0 |
+| @uppy/facebook         |   3.2.0 | @uppy/xhr-upload       |   3.6.0 |
+| @uppy/google-drive     |   3.4.0 | @uppy/zoom             |   2.2.0 |
+| @uppy/image-editor     |   2.4.0 | uppy                   |  3.21.0 |
+
+- @uppy/provider-views: fix uploadRemoteFile undefined (Mikael Finstad / #4814)
+- @uppy/companion: fix double tus uploads (Mikael Finstad / #4816)
+- @uppy/companion: fix accelerated endpoints for presigned POST  (Mikael Finstad / #4817)
+- @uppy/companion: fix `authProvider` property inconsistency (Mikael Finstad / #4672)
+- @uppy/companion:  send certain onedrive errors to the user (Mikael Finstad / #4671)
+- meta: fix typo in `lockfile_check.yml` name (Antoine du Hamel)
+- @uppy/aws-s3: change Companion URL in tests (Antoine du Hamel)
+- @uppy/set-state: fix types (Antoine du Hamel)
+- @uppy/companion: Provider user sessions (Mikael Finstad / #4619)
+- meta: fix `js2ts` script on Node.js 20+ (Merlijn Vos / #4802)
+- @uppy/companion-client: avoid unnecessary preflight requests (Antoine du Hamel / #4462)
+- meta: Migrate to AWS-SDK V3 syntax (Artur Paikin / #4810)
+- @uppy/utils: fix import in test files (Antoine du Hamel / #4806)
+- @uppy/core: Fix onBeforeFileAdded with Golden Retriever (Merlijn Vos / #4799)
+- @uppy/image-editor: respect `cropperOptions.initialAspectRatio` (Lucklj521 / #4805)
+
+
 ## 3.20.0
 
 Released: 2023-11-24

+ 53 - 53
README.md

@@ -65,7 +65,7 @@ const uppy = new Uppy()
 npm install @uppy/core @uppy/dashboard @uppy/tus
 ```
 
-Add CSS [uppy.min.css](https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css), either to your HTML page’s `<head>` or include in JS, if your bundler of choice supports it.
+Add CSS [uppy.min.css](https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css), either to your HTML page’s `<head>` or include in JS, if your bundler of choice supports it.
 
 Alternatively, you can also use a pre-built bundle from Transloadit’s CDN: Edgly. In that case `Uppy` will attach itself to the global `window.Uppy` object.
 
@@ -73,12 +73,12 @@ Alternatively, you can also use a pre-built bundle from Transloadit’s CDN: Edg
 
 ```html
 <!-- 1. Add CSS to `<head>` -->
-<link href="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css" rel="stylesheet">
+<link href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css" rel="stylesheet">
 
 <!-- 2. Initialize -->
 <div id="files-drag-drop"></div>
 <script type="module">
-  import { Uppy, Dashboard, Tus } from "https://releases.transloadit.com/uppy/v3.20.0/uppy.min.mjs"
+  import { Uppy, Dashboard, Tus } from "https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs"
 
   const uppy = new Uppy()
   uppy.use(Dashboard, { target: '#files-drag-drop' })
@@ -180,7 +180,7 @@ If you’re using Uppy from CDN, those polyfills are already included in the leg
 bundle, so no need to include anything additionally:
 
 ```html
-<script src="https://releases.transloadit.com/uppy/v3.20.0/uppy.legacy.min.js"></script>
+<script src="https://releases.transloadit.com/uppy/v3.21.0/uppy.legacy.min.js"></script>
 ```
 
 ## FAQ
@@ -399,101 +399,101 @@ Use Uppy in your project? [Let us know](https://github.com/transloadit/uppy/issu
 :---: |:---: |:---: |:---: |:---: |:---: |
 [dviry](https://github.com/dviry) |[galli-leo](https://github.com/galli-leo) |[leods92](https://github.com/leods92) |[leomelzer](https://github.com/leomelzer) |[dolphinigle](https://github.com/dolphinigle) |[louim](https://github.com/louim) |
 
-[<img alt="ombr" src="https://avatars.githubusercontent.com/u/857339?v=4&s=117" width="117">](https://github.com/ombr) |[<img alt="lucaperret" src="https://avatars.githubusercontent.com/u/1887122?v=4&s=117" width="117">](https://github.com/lucaperret) |[<img alt="lucax88x" src="https://avatars.githubusercontent.com/u/6294464?v=4&s=117" width="117">](https://github.com/lucax88x) |[<img alt="marc-mabe" src="https://avatars.githubusercontent.com/u/302689?v=4&s=117" width="117">](https://github.com/marc-mabe) |[<img alt="onhate" src="https://avatars.githubusercontent.com/u/980905?v=4&s=117" width="117">](https://github.com/onhate) |[<img alt="mperrando" src="https://avatars.githubusercontent.com/u/525572?v=4&s=117" width="117">](https://github.com/mperrando) |
+[<img alt="ombr" src="https://avatars.githubusercontent.com/u/857339?v=4&s=117" width="117">](https://github.com/ombr) |[<img alt="lucaperret" src="https://avatars.githubusercontent.com/u/1887122?v=4&s=117" width="117">](https://github.com/lucaperret) |[<img alt="lucax88x" src="https://avatars.githubusercontent.com/u/6294464?v=4&s=117" width="117">](https://github.com/lucax88x) |[<img alt="Lucklj521" src="https://avatars.githubusercontent.com/u/93632042?v=4&s=117" width="117">](https://github.com/Lucklj521) |[<img alt="marc-mabe" src="https://avatars.githubusercontent.com/u/302689?v=4&s=117" width="117">](https://github.com/marc-mabe) |[<img alt="onhate" src="https://avatars.githubusercontent.com/u/980905?v=4&s=117" width="117">](https://github.com/onhate) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[ombr](https://github.com/ombr) |[lucaperret](https://github.com/lucaperret) |[lucax88x](https://github.com/lucax88x) |[marc-mabe](https://github.com/marc-mabe) |[onhate](https://github.com/onhate) |[mperrando](https://github.com/mperrando) |
+[ombr](https://github.com/ombr) |[lucaperret](https://github.com/lucaperret) |[lucax88x](https://github.com/lucax88x) |[Lucklj521](https://github.com/Lucklj521) |[marc-mabe](https://github.com/marc-mabe) |[onhate](https://github.com/onhate) |
 
-[<img alt="marcosthejew" src="https://avatars.githubusercontent.com/u/1500967?v=4&s=117" width="117">](https://github.com/marcosthejew) |[<img alt="marcusforsberg" src="https://avatars.githubusercontent.com/u/1009069?v=4&s=117" width="117">](https://github.com/marcusforsberg) |[<img alt="martin-brennan" src="https://avatars.githubusercontent.com/u/920448?v=4&s=117" width="117">](https://github.com/martin-brennan) |[<img alt="masaok" src="https://avatars.githubusercontent.com/u/1320083?v=4&s=117" width="117">](https://github.com/masaok) |[<img alt="masumulu28" src="https://avatars.githubusercontent.com/u/49063256?v=4&s=117" width="117">](https://github.com/masumulu28) |[<img alt="mateuscruz" src="https://avatars.githubusercontent.com/u/8962842?v=4&s=117" width="117">](https://github.com/mateuscruz) |
+[<img alt="mperrando" src="https://avatars.githubusercontent.com/u/525572?v=4&s=117" width="117">](https://github.com/mperrando) |[<img alt="marcosthejew" src="https://avatars.githubusercontent.com/u/1500967?v=4&s=117" width="117">](https://github.com/marcosthejew) |[<img alt="marcusforsberg" src="https://avatars.githubusercontent.com/u/1009069?v=4&s=117" width="117">](https://github.com/marcusforsberg) |[<img alt="martin-brennan" src="https://avatars.githubusercontent.com/u/920448?v=4&s=117" width="117">](https://github.com/martin-brennan) |[<img alt="masaok" src="https://avatars.githubusercontent.com/u/1320083?v=4&s=117" width="117">](https://github.com/masaok) |[<img alt="masumulu28" src="https://avatars.githubusercontent.com/u/49063256?v=4&s=117" width="117">](https://github.com/masumulu28) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[marcosthejew](https://github.com/marcosthejew) |[marcusforsberg](https://github.com/marcusforsberg) |[martin-brennan](https://github.com/martin-brennan) |[masaok](https://github.com/masaok) |[masumulu28](https://github.com/masumulu28) |[mateuscruz](https://github.com/mateuscruz) |
+[mperrando](https://github.com/mperrando) |[marcosthejew](https://github.com/marcosthejew) |[marcusforsberg](https://github.com/marcusforsberg) |[martin-brennan](https://github.com/martin-brennan) |[masaok](https://github.com/masaok) |[masumulu28](https://github.com/masumulu28) |
 
-[<img alt="mattfik" src="https://avatars.githubusercontent.com/u/1638028?v=4&s=117" width="117">](https://github.com/mattfik) |[<img alt="mjesuele" src="https://avatars.githubusercontent.com/u/871117?v=4&s=117" width="117">](https://github.com/mjesuele) |[<img alt="matthewhartstonge" src="https://avatars.githubusercontent.com/u/6119549?v=4&s=117" width="117">](https://github.com/matthewhartstonge) |[<img alt="mauricioribeiro" src="https://avatars.githubusercontent.com/u/2589856?v=4&s=117" width="117">](https://github.com/mauricioribeiro) |[<img alt="hrsh" src="https://avatars.githubusercontent.com/u/1929359?v=4&s=117" width="117">](https://github.com/hrsh) |[<img alt="mhulet" src="https://avatars.githubusercontent.com/u/293355?v=4&s=117" width="117">](https://github.com/mhulet) |
+[<img alt="mateuscruz" src="https://avatars.githubusercontent.com/u/8962842?v=4&s=117" width="117">](https://github.com/mateuscruz) |[<img alt="mattfik" src="https://avatars.githubusercontent.com/u/1638028?v=4&s=117" width="117">](https://github.com/mattfik) |[<img alt="mjesuele" src="https://avatars.githubusercontent.com/u/871117?v=4&s=117" width="117">](https://github.com/mjesuele) |[<img alt="matthewhartstonge" src="https://avatars.githubusercontent.com/u/6119549?v=4&s=117" width="117">](https://github.com/matthewhartstonge) |[<img alt="mauricioribeiro" src="https://avatars.githubusercontent.com/u/2589856?v=4&s=117" width="117">](https://github.com/mauricioribeiro) |[<img alt="hrsh" src="https://avatars.githubusercontent.com/u/1929359?v=4&s=117" width="117">](https://github.com/hrsh) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[mattfik](https://github.com/mattfik) |[mjesuele](https://github.com/mjesuele) |[matthewhartstonge](https://github.com/matthewhartstonge) |[mauricioribeiro](https://github.com/mauricioribeiro) |[hrsh](https://github.com/hrsh) |[mhulet](https://github.com/mhulet) |
+[mateuscruz](https://github.com/mateuscruz) |[mattfik](https://github.com/mattfik) |[mjesuele](https://github.com/mjesuele) |[matthewhartstonge](https://github.com/matthewhartstonge) |[mauricioribeiro](https://github.com/mauricioribeiro) |[hrsh](https://github.com/hrsh) |
 
-[<img alt="mkopinsky" src="https://avatars.githubusercontent.com/u/591435?v=4&s=117" width="117">](https://github.com/mkopinsky) |[<img alt="ken-kuro" src="https://avatars.githubusercontent.com/u/47441476?v=4&s=117" width="117">](https://github.com/ken-kuro) |[<img alt="achmiral" src="https://avatars.githubusercontent.com/u/10906059?v=4&s=117" width="117">](https://github.com/achmiral) |[<img alt="boudra" src="https://avatars.githubusercontent.com/u/711886?v=4&s=117" width="117">](https://github.com/boudra) |[<img alt="mnafees" src="https://avatars.githubusercontent.com/u/1763885?v=4&s=117" width="117">](https://github.com/mnafees) |[<img alt="shahimclt" src="https://avatars.githubusercontent.com/u/8318002?v=4&s=117" width="117">](https://github.com/shahimclt) |
+[<img alt="mhulet" src="https://avatars.githubusercontent.com/u/293355?v=4&s=117" width="117">](https://github.com/mhulet) |[<img alt="mkopinsky" src="https://avatars.githubusercontent.com/u/591435?v=4&s=117" width="117">](https://github.com/mkopinsky) |[<img alt="ken-kuro" src="https://avatars.githubusercontent.com/u/47441476?v=4&s=117" width="117">](https://github.com/ken-kuro) |[<img alt="achmiral" src="https://avatars.githubusercontent.com/u/10906059?v=4&s=117" width="117">](https://github.com/achmiral) |[<img alt="boudra" src="https://avatars.githubusercontent.com/u/711886?v=4&s=117" width="117">](https://github.com/boudra) |[<img alt="mnafees" src="https://avatars.githubusercontent.com/u/1763885?v=4&s=117" width="117">](https://github.com/mnafees) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[mkopinsky](https://github.com/mkopinsky) |[ken-kuro](https://github.com/ken-kuro) |[achmiral](https://github.com/achmiral) |[boudra](https://github.com/boudra) |[mnafees](https://github.com/mnafees) |[shahimclt](https://github.com/shahimclt) |
+[mhulet](https://github.com/mhulet) |[mkopinsky](https://github.com/mkopinsky) |[ken-kuro](https://github.com/ken-kuro) |[achmiral](https://github.com/achmiral) |[boudra](https://github.com/boudra) |[mnafees](https://github.com/mnafees) |
 
-[<img alt="mogzol" src="https://avatars.githubusercontent.com/u/11789801?v=4&s=117" width="117">](https://github.com/mogzol) |[<img alt="navruzm" src="https://avatars.githubusercontent.com/u/168341?v=4&s=117" width="117">](https://github.com/navruzm) |[<img alt="marton-laszlo-attila" src="https://avatars.githubusercontent.com/u/73295321?v=4&s=117" width="117">](https://github.com/marton-laszlo-attila) |[<img alt="pleasespammelater" src="https://avatars.githubusercontent.com/u/11870394?v=4&s=117" width="117">](https://github.com/pleasespammelater) |[<img alt="naveed-ahmad" src="https://avatars.githubusercontent.com/u/701567?v=4&s=117" width="117">](https://github.com/naveed-ahmad) |[<img alt="trungcva10a6tn" src="https://avatars.githubusercontent.com/u/18293783?v=4&s=117" width="117">](https://github.com/trungcva10a6tn) |
+[<img alt="shahimclt" src="https://avatars.githubusercontent.com/u/8318002?v=4&s=117" width="117">](https://github.com/shahimclt) |[<img alt="mogzol" src="https://avatars.githubusercontent.com/u/11789801?v=4&s=117" width="117">](https://github.com/mogzol) |[<img alt="navruzm" src="https://avatars.githubusercontent.com/u/168341?v=4&s=117" width="117">](https://github.com/navruzm) |[<img alt="marton-laszlo-attila" src="https://avatars.githubusercontent.com/u/73295321?v=4&s=117" width="117">](https://github.com/marton-laszlo-attila) |[<img alt="pleasespammelater" src="https://avatars.githubusercontent.com/u/11870394?v=4&s=117" width="117">](https://github.com/pleasespammelater) |[<img alt="naveed-ahmad" src="https://avatars.githubusercontent.com/u/701567?v=4&s=117" width="117">](https://github.com/naveed-ahmad) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[mogzol](https://github.com/mogzol) |[navruzm](https://github.com/navruzm) |[marton-laszlo-attila](https://github.com/marton-laszlo-attila) |[pleasespammelater](https://github.com/pleasespammelater) |[naveed-ahmad](https://github.com/naveed-ahmad) |[trungcva10a6tn](https://github.com/trungcva10a6tn) |
+[shahimclt](https://github.com/shahimclt) |[mogzol](https://github.com/mogzol) |[navruzm](https://github.com/navruzm) |[marton-laszlo-attila](https://github.com/marton-laszlo-attila) |[pleasespammelater](https://github.com/pleasespammelater) |[naveed-ahmad](https://github.com/naveed-ahmad) |
 
-[<img alt="nicojones" src="https://avatars.githubusercontent.com/u/6078915?v=4&s=117" width="117">](https://github.com/nicojones) |[<img alt="coreprocess" src="https://avatars.githubusercontent.com/u/1226918?v=4&s=117" width="117">](https://github.com/coreprocess) |[<img alt="nil1511" src="https://avatars.githubusercontent.com/u/2058170?v=4&s=117" width="117">](https://github.com/nil1511) |[<img alt="leftdevel" src="https://avatars.githubusercontent.com/u/843337?v=4&s=117" width="117">](https://github.com/leftdevel) |[<img alt="Ozodbek1405" src="https://avatars.githubusercontent.com/u/86141593?v=4&s=117" width="117">](https://github.com/Ozodbek1405) |[<img alt="cryptic022" src="https://avatars.githubusercontent.com/u/18145703?v=4&s=117" width="117">](https://github.com/cryptic022) |
+[<img alt="trungcva10a6tn" src="https://avatars.githubusercontent.com/u/18293783?v=4&s=117" width="117">](https://github.com/trungcva10a6tn) |[<img alt="nicojones" src="https://avatars.githubusercontent.com/u/6078915?v=4&s=117" width="117">](https://github.com/nicojones) |[<img alt="coreprocess" src="https://avatars.githubusercontent.com/u/1226918?v=4&s=117" width="117">](https://github.com/coreprocess) |[<img alt="nil1511" src="https://avatars.githubusercontent.com/u/2058170?v=4&s=117" width="117">](https://github.com/nil1511) |[<img alt="leftdevel" src="https://avatars.githubusercontent.com/u/843337?v=4&s=117" width="117">](https://github.com/leftdevel) |[<img alt="Ozodbek1405" src="https://avatars.githubusercontent.com/u/86141593?v=4&s=117" width="117">](https://github.com/Ozodbek1405) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[nicojones](https://github.com/nicojones) |[coreprocess](https://github.com/coreprocess) |[nil1511](https://github.com/nil1511) |[leftdevel](https://github.com/leftdevel) |[Ozodbek1405](https://github.com/Ozodbek1405) |[cryptic022](https://github.com/cryptic022) |
+[trungcva10a6tn](https://github.com/trungcva10a6tn) |[nicojones](https://github.com/nicojones) |[coreprocess](https://github.com/coreprocess) |[nil1511](https://github.com/nil1511) |[leftdevel](https://github.com/leftdevel) |[Ozodbek1405](https://github.com/Ozodbek1405) |
 
-[<img alt="ParsaArvanehPA" src="https://avatars.githubusercontent.com/u/62149413?v=4&s=117" width="117">](https://github.com/ParsaArvanehPA) |[<img alt="pascalwengerter" src="https://avatars.githubusercontent.com/u/16822008?v=4&s=117" width="117">](https://github.com/pascalwengerter) |[<img alt="patricklindsay" src="https://avatars.githubusercontent.com/u/7923681?v=4&s=117" width="117">](https://github.com/patricklindsay) |[<img alt="plneto" src="https://avatars.githubusercontent.com/u/5697434?v=4&s=117" width="117">](https://github.com/plneto) |[<img alt="pedrofs" src="https://avatars.githubusercontent.com/u/56484?v=4&s=117" width="117">](https://github.com/pedrofs) |[<img alt="pmusaraj" src="https://avatars.githubusercontent.com/u/368961?v=4&s=117" width="117">](https://github.com/pmusaraj) |
+[<img alt="cryptic022" src="https://avatars.githubusercontent.com/u/18145703?v=4&s=117" width="117">](https://github.com/cryptic022) |[<img alt="ParsaArvanehPA" src="https://avatars.githubusercontent.com/u/62149413?v=4&s=117" width="117">](https://github.com/ParsaArvanehPA) |[<img alt="pascalwengerter" src="https://avatars.githubusercontent.com/u/16822008?v=4&s=117" width="117">](https://github.com/pascalwengerter) |[<img alt="patricklindsay" src="https://avatars.githubusercontent.com/u/7923681?v=4&s=117" width="117">](https://github.com/patricklindsay) |[<img alt="plneto" src="https://avatars.githubusercontent.com/u/5697434?v=4&s=117" width="117">](https://github.com/plneto) |[<img alt="pedrofs" src="https://avatars.githubusercontent.com/u/56484?v=4&s=117" width="117">](https://github.com/pedrofs) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[ParsaArvanehPA](https://github.com/ParsaArvanehPA) |[pascalwengerter](https://github.com/pascalwengerter) |[patricklindsay](https://github.com/patricklindsay) |[plneto](https://github.com/plneto) |[pedrofs](https://github.com/pedrofs) |[pmusaraj](https://github.com/pmusaraj) |
+[cryptic022](https://github.com/cryptic022) |[ParsaArvanehPA](https://github.com/ParsaArvanehPA) |[pascalwengerter](https://github.com/pascalwengerter) |[patricklindsay](https://github.com/patricklindsay) |[plneto](https://github.com/plneto) |[pedrofs](https://github.com/pedrofs) |
 
-[<img alt="phillipalexander" src="https://avatars.githubusercontent.com/u/1577682?v=4&s=117" width="117">](https://github.com/phillipalexander) |[<img alt="ppadmavilasom" src="https://avatars.githubusercontent.com/u/11167452?v=4&s=117" width="117">](https://github.com/ppadmavilasom) |[<img alt="Pzoco" src="https://avatars.githubusercontent.com/u/3101348?v=4&s=117" width="117">](https://github.com/Pzoco) |[<img alt="eman8519" src="https://avatars.githubusercontent.com/u/2380804?v=4&s=117" width="117">](https://github.com/eman8519) |[<img alt="luarmr" src="https://avatars.githubusercontent.com/u/817416?v=4&s=117" width="117">](https://github.com/luarmr) |[<img alt="raulibanez" src="https://avatars.githubusercontent.com/u/1070825?v=4&s=117" width="117">](https://github.com/raulibanez) |
+[<img alt="pmusaraj" src="https://avatars.githubusercontent.com/u/368961?v=4&s=117" width="117">](https://github.com/pmusaraj) |[<img alt="phillipalexander" src="https://avatars.githubusercontent.com/u/1577682?v=4&s=117" width="117">](https://github.com/phillipalexander) |[<img alt="ppadmavilasom" src="https://avatars.githubusercontent.com/u/11167452?v=4&s=117" width="117">](https://github.com/ppadmavilasom) |[<img alt="Pzoco" src="https://avatars.githubusercontent.com/u/3101348?v=4&s=117" width="117">](https://github.com/Pzoco) |[<img alt="eman8519" src="https://avatars.githubusercontent.com/u/2380804?v=4&s=117" width="117">](https://github.com/eman8519) |[<img alt="luarmr" src="https://avatars.githubusercontent.com/u/817416?v=4&s=117" width="117">](https://github.com/luarmr) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[phillipalexander](https://github.com/phillipalexander) |[ppadmavilasom](https://github.com/ppadmavilasom) |[Pzoco](https://github.com/Pzoco) |[eman8519](https://github.com/eman8519) |[luarmr](https://github.com/luarmr) |[raulibanez](https://github.com/raulibanez) |
+[pmusaraj](https://github.com/pmusaraj) |[phillipalexander](https://github.com/phillipalexander) |[ppadmavilasom](https://github.com/ppadmavilasom) |[Pzoco](https://github.com/Pzoco) |[eman8519](https://github.com/eman8519) |[luarmr](https://github.com/luarmr) |
 
-[<img alt="refo" src="https://avatars.githubusercontent.com/u/1114116?v=4&s=117" width="117">](https://github.com/refo) |[<img alt="SxDx" src="https://avatars.githubusercontent.com/u/2004247?v=4&s=117" width="117">](https://github.com/SxDx) |[<img alt="robwilson1" src="https://avatars.githubusercontent.com/u/7114944?v=4&s=117" width="117">](https://github.com/robwilson1) |[<img alt="scherroman" src="https://avatars.githubusercontent.com/u/7923938?v=4&s=117" width="117">](https://github.com/scherroman) |[<img alt="rossng" src="https://avatars.githubusercontent.com/u/565371?v=4&s=117" width="117">](https://github.com/rossng) |[<img alt="rart" src="https://avatars.githubusercontent.com/u/3928341?v=4&s=117" width="117">](https://github.com/rart) |
+[<img alt="raulibanez" src="https://avatars.githubusercontent.com/u/1070825?v=4&s=117" width="117">](https://github.com/raulibanez) |[<img alt="refo" src="https://avatars.githubusercontent.com/u/1114116?v=4&s=117" width="117">](https://github.com/refo) |[<img alt="SxDx" src="https://avatars.githubusercontent.com/u/2004247?v=4&s=117" width="117">](https://github.com/SxDx) |[<img alt="robwilson1" src="https://avatars.githubusercontent.com/u/7114944?v=4&s=117" width="117">](https://github.com/robwilson1) |[<img alt="scherroman" src="https://avatars.githubusercontent.com/u/7923938?v=4&s=117" width="117">](https://github.com/scherroman) |[<img alt="rossng" src="https://avatars.githubusercontent.com/u/565371?v=4&s=117" width="117">](https://github.com/rossng) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[refo](https://github.com/refo) |[SxDx](https://github.com/SxDx) |[robwilson1](https://github.com/robwilson1) |[scherroman](https://github.com/scherroman) |[rossng](https://github.com/rossng) |[rart](https://github.com/rart) |
+[raulibanez](https://github.com/raulibanez) |[refo](https://github.com/refo) |[SxDx](https://github.com/SxDx) |[robwilson1](https://github.com/robwilson1) |[scherroman](https://github.com/scherroman) |[rossng](https://github.com/rossng) |
 
-[<img alt="GNURub" src="https://avatars.githubusercontent.com/u/1318648?v=4&s=117" width="117">](https://github.com/GNURub) |[<img alt="fortunto2" src="https://avatars.githubusercontent.com/u/1236751?v=4&s=117" width="117">](https://github.com/fortunto2) |[<img alt="samuelcolburn" src="https://avatars.githubusercontent.com/u/9741902?v=4&s=117" width="117">](https://github.com/samuelcolburn) |[<img alt="sdebacker" src="https://avatars.githubusercontent.com/u/134503?v=4&s=117" width="117">](https://github.com/sdebacker) |[<img alt="sebasegovia01" src="https://avatars.githubusercontent.com/u/35777287?v=4&s=117" width="117">](https://github.com/sebasegovia01) |[<img alt="sergei-zelinsky" src="https://avatars.githubusercontent.com/u/19428086?v=4&s=117" width="117">](https://github.com/sergei-zelinsky) |
+[<img alt="rart" src="https://avatars.githubusercontent.com/u/3928341?v=4&s=117" width="117">](https://github.com/rart) |[<img alt="GNURub" src="https://avatars.githubusercontent.com/u/1318648?v=4&s=117" width="117">](https://github.com/GNURub) |[<img alt="fortunto2" src="https://avatars.githubusercontent.com/u/1236751?v=4&s=117" width="117">](https://github.com/fortunto2) |[<img alt="samuelcolburn" src="https://avatars.githubusercontent.com/u/9741902?v=4&s=117" width="117">](https://github.com/samuelcolburn) |[<img alt="sdebacker" src="https://avatars.githubusercontent.com/u/134503?v=4&s=117" width="117">](https://github.com/sdebacker) |[<img alt="sebasegovia01" src="https://avatars.githubusercontent.com/u/35777287?v=4&s=117" width="117">](https://github.com/sebasegovia01) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[GNURub](https://github.com/GNURub) |[fortunto2](https://github.com/fortunto2) |[samuelcolburn](https://github.com/samuelcolburn) |[sdebacker](https://github.com/sdebacker) |[sebasegovia01](https://github.com/sebasegovia01) |[sergei-zelinsky](https://github.com/sergei-zelinsky) |
+[rart](https://github.com/rart) |[GNURub](https://github.com/GNURub) |[fortunto2](https://github.com/fortunto2) |[samuelcolburn](https://github.com/samuelcolburn) |[sdebacker](https://github.com/sdebacker) |[sebasegovia01](https://github.com/sebasegovia01) |
 
-[<img alt="szh" src="https://avatars.githubusercontent.com/u/546965?v=4&s=117" width="117">](https://github.com/szh) |[<img alt="SpazzMarticus" src="https://avatars.githubusercontent.com/u/5716457?v=4&s=117" width="117">](https://github.com/SpazzMarticus) |[<img alt="waptik" src="https://avatars.githubusercontent.com/u/1687551?v=4&s=117" width="117">](https://github.com/waptik) |[<img alt="quigebo" src="https://avatars.githubusercontent.com/u/741?v=4&s=117" width="117">](https://github.com/quigebo) |[<img alt="amaitu" src="https://avatars.githubusercontent.com/u/15688439?v=4&s=117" width="117">](https://github.com/amaitu) |[<img alt="steverob" src="https://avatars.githubusercontent.com/u/1220480?v=4&s=117" width="117">](https://github.com/steverob) |
+[<img alt="sergei-zelinsky" src="https://avatars.githubusercontent.com/u/19428086?v=4&s=117" width="117">](https://github.com/sergei-zelinsky) |[<img alt="szh" src="https://avatars.githubusercontent.com/u/546965?v=4&s=117" width="117">](https://github.com/szh) |[<img alt="SpazzMarticus" src="https://avatars.githubusercontent.com/u/5716457?v=4&s=117" width="117">](https://github.com/SpazzMarticus) |[<img alt="waptik" src="https://avatars.githubusercontent.com/u/1687551?v=4&s=117" width="117">](https://github.com/waptik) |[<img alt="quigebo" src="https://avatars.githubusercontent.com/u/741?v=4&s=117" width="117">](https://github.com/quigebo) |[<img alt="amaitu" src="https://avatars.githubusercontent.com/u/15688439?v=4&s=117" width="117">](https://github.com/amaitu) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[szh](https://github.com/szh) |[SpazzMarticus](https://github.com/SpazzMarticus) |[waptik](https://github.com/waptik) |[quigebo](https://github.com/quigebo) |[amaitu](https://github.com/amaitu) |[steverob](https://github.com/steverob) |
+[sergei-zelinsky](https://github.com/sergei-zelinsky) |[szh](https://github.com/szh) |[SpazzMarticus](https://github.com/SpazzMarticus) |[waptik](https://github.com/waptik) |[quigebo](https://github.com/quigebo) |[amaitu](https://github.com/amaitu) |
 
-[<img alt="sjauld" src="https://avatars.githubusercontent.com/u/8232503?v=4&s=117" width="117">](https://github.com/sjauld) |[<img alt="strayer" src="https://avatars.githubusercontent.com/u/310624?v=4&s=117" width="117">](https://github.com/strayer) |[<img alt="taj" src="https://avatars.githubusercontent.com/u/16062635?v=4&s=117" width="117">](https://github.com/taj) |[<img alt="Tashows" src="https://avatars.githubusercontent.com/u/16656928?v=4&s=117" width="117">](https://github.com/Tashows) |[<img alt="tcgj" src="https://avatars.githubusercontent.com/u/7994529?v=4&s=117" width="117">](https://github.com/tcgj) |[<img alt="twarlop" src="https://avatars.githubusercontent.com/u/2856082?v=4&s=117" width="117">](https://github.com/twarlop) |
+[<img alt="steverob" src="https://avatars.githubusercontent.com/u/1220480?v=4&s=117" width="117">](https://github.com/steverob) |[<img alt="sjauld" src="https://avatars.githubusercontent.com/u/8232503?v=4&s=117" width="117">](https://github.com/sjauld) |[<img alt="strayer" src="https://avatars.githubusercontent.com/u/310624?v=4&s=117" width="117">](https://github.com/strayer) |[<img alt="taj" src="https://avatars.githubusercontent.com/u/16062635?v=4&s=117" width="117">](https://github.com/taj) |[<img alt="Tashows" src="https://avatars.githubusercontent.com/u/16656928?v=4&s=117" width="117">](https://github.com/Tashows) |[<img alt="tcgj" src="https://avatars.githubusercontent.com/u/7994529?v=4&s=117" width="117">](https://github.com/tcgj) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[sjauld](https://github.com/sjauld) |[strayer](https://github.com/strayer) |[taj](https://github.com/taj) |[Tashows](https://github.com/Tashows) |[tcgj](https://github.com/tcgj) |[twarlop](https://github.com/twarlop) |
+[steverob](https://github.com/steverob) |[sjauld](https://github.com/sjauld) |[strayer](https://github.com/strayer) |[taj](https://github.com/taj) |[Tashows](https://github.com/Tashows) |[tcgj](https://github.com/tcgj) |
 
-[<img alt="tmaier" src="https://avatars.githubusercontent.com/u/350038?v=4&s=117" width="117">](https://github.com/tmaier) |[<img alt="WIStudent" src="https://avatars.githubusercontent.com/u/2707930?v=4&s=117" width="117">](https://github.com/WIStudent) |[<img alt="tomsaleeba" src="https://avatars.githubusercontent.com/u/1773838?v=4&s=117" width="117">](https://github.com/tomsaleeba) |[<img alt="tomekp" src="https://avatars.githubusercontent.com/u/1856393?v=4&s=117" width="117">](https://github.com/tomekp) |[<img alt="tvaliasek" src="https://avatars.githubusercontent.com/u/8644946?v=4&s=117" width="117">](https://github.com/tvaliasek) |[<img alt="top-master" src="https://avatars.githubusercontent.com/u/31405473?v=4&s=117" width="117">](https://github.com/top-master) |
+[<img alt="twarlop" src="https://avatars.githubusercontent.com/u/2856082?v=4&s=117" width="117">](https://github.com/twarlop) |[<img alt="tmaier" src="https://avatars.githubusercontent.com/u/350038?v=4&s=117" width="117">](https://github.com/tmaier) |[<img alt="WIStudent" src="https://avatars.githubusercontent.com/u/2707930?v=4&s=117" width="117">](https://github.com/WIStudent) |[<img alt="tomsaleeba" src="https://avatars.githubusercontent.com/u/1773838?v=4&s=117" width="117">](https://github.com/tomsaleeba) |[<img alt="tomekp" src="https://avatars.githubusercontent.com/u/1856393?v=4&s=117" width="117">](https://github.com/tomekp) |[<img alt="tvaliasek" src="https://avatars.githubusercontent.com/u/8644946?v=4&s=117" width="117">](https://github.com/tvaliasek) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[tmaier](https://github.com/tmaier) |[WIStudent](https://github.com/WIStudent) |[tomsaleeba](https://github.com/tomsaleeba) |[tomekp](https://github.com/tomekp) |[tvaliasek](https://github.com/tvaliasek) |[top-master](https://github.com/top-master) |
+[twarlop](https://github.com/twarlop) |[tmaier](https://github.com/tmaier) |[WIStudent](https://github.com/WIStudent) |[tomsaleeba](https://github.com/tomsaleeba) |[tomekp](https://github.com/tomekp) |[tvaliasek](https://github.com/tvaliasek) |
 
-[<img alt="trivikr" src="https://avatars.githubusercontent.com/u/16024985?v=4&s=117" width="117">](https://github.com/trivikr) |[<img alt="vially" src="https://avatars.githubusercontent.com/u/433598?v=4&s=117" width="117">](https://github.com/vially) |[<img alt="valentinoli" src="https://avatars.githubusercontent.com/u/23453691?v=4&s=117" width="117">](https://github.com/valentinoli) |[<img alt="stiig" src="https://avatars.githubusercontent.com/u/8639922?v=4&s=117" width="117">](https://github.com/stiig) |[<img alt="nagyv" src="https://avatars.githubusercontent.com/u/126671?v=4&s=117" width="117">](https://github.com/nagyv) |[<img alt="dwnste" src="https://avatars.githubusercontent.com/u/17119722?v=4&s=117" width="117">](https://github.com/dwnste) |
+[<img alt="top-master" src="https://avatars.githubusercontent.com/u/31405473?v=4&s=117" width="117">](https://github.com/top-master) |[<img alt="trivikr" src="https://avatars.githubusercontent.com/u/16024985?v=4&s=117" width="117">](https://github.com/trivikr) |[<img alt="vially" src="https://avatars.githubusercontent.com/u/433598?v=4&s=117" width="117">](https://github.com/vially) |[<img alt="valentinoli" src="https://avatars.githubusercontent.com/u/23453691?v=4&s=117" width="117">](https://github.com/valentinoli) |[<img alt="stiig" src="https://avatars.githubusercontent.com/u/8639922?v=4&s=117" width="117">](https://github.com/stiig) |[<img alt="nagyv" src="https://avatars.githubusercontent.com/u/126671?v=4&s=117" width="117">](https://github.com/nagyv) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[trivikr](https://github.com/trivikr) |[vially](https://github.com/vially) |[valentinoli](https://github.com/valentinoli) |[stiig](https://github.com/stiig) |[nagyv](https://github.com/nagyv) |[dwnste](https://github.com/dwnste) |
+[top-master](https://github.com/top-master) |[trivikr](https://github.com/trivikr) |[vially](https://github.com/vially) |[valentinoli](https://github.com/valentinoli) |[stiig](https://github.com/stiig) |[nagyv](https://github.com/nagyv) |
 
-[<img alt="weston-sankey-mark43" src="https://avatars.githubusercontent.com/u/97678695?v=4&s=117" width="117">](https://github.com/weston-sankey-mark43) |[<img alt="willycamargo" src="https://avatars.githubusercontent.com/u/5041887?v=4&s=117" width="117">](https://github.com/willycamargo) |[<img alt="xhocquet" src="https://avatars.githubusercontent.com/u/8116516?v=4&s=117" width="117">](https://github.com/xhocquet) |[<img alt="YehudaKremer" src="https://avatars.githubusercontent.com/u/946652?v=4&s=117" width="117">](https://github.com/YehudaKremer) |[<img alt="zachconner" src="https://avatars.githubusercontent.com/u/11339326?v=4&s=117" width="117">](https://github.com/zachconner) |[<img alt="zlawson-ut" src="https://avatars.githubusercontent.com/u/7375444?v=4&s=117" width="117">](https://github.com/zlawson-ut) |
+[<img alt="dwnste" src="https://avatars.githubusercontent.com/u/17119722?v=4&s=117" width="117">](https://github.com/dwnste) |[<img alt="weston-sankey-mark43" src="https://avatars.githubusercontent.com/u/97678695?v=4&s=117" width="117">](https://github.com/weston-sankey-mark43) |[<img alt="willycamargo" src="https://avatars.githubusercontent.com/u/5041887?v=4&s=117" width="117">](https://github.com/willycamargo) |[<img alt="xhocquet" src="https://avatars.githubusercontent.com/u/8116516?v=4&s=117" width="117">](https://github.com/xhocquet) |[<img alt="YehudaKremer" src="https://avatars.githubusercontent.com/u/946652?v=4&s=117" width="117">](https://github.com/YehudaKremer) |[<img alt="zachconner" src="https://avatars.githubusercontent.com/u/11339326?v=4&s=117" width="117">](https://github.com/zachconner) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[weston-sankey-mark43](https://github.com/weston-sankey-mark43) |[willycamargo](https://github.com/willycamargo) |[xhocquet](https://github.com/xhocquet) |[YehudaKremer](https://github.com/YehudaKremer) |[zachconner](https://github.com/zachconner) |[zlawson-ut](https://github.com/zlawson-ut) |
+[dwnste](https://github.com/dwnste) |[weston-sankey-mark43](https://github.com/weston-sankey-mark43) |[willycamargo](https://github.com/willycamargo) |[xhocquet](https://github.com/xhocquet) |[YehudaKremer](https://github.com/YehudaKremer) |[zachconner](https://github.com/zachconner) |
 
-[<img alt="zackbloom" src="https://avatars.githubusercontent.com/u/55347?v=4&s=117" width="117">](https://github.com/zackbloom) |[<img alt="sartoshi-foot-dao" src="https://avatars.githubusercontent.com/u/99770068?v=4&s=117" width="117">](https://github.com/sartoshi-foot-dao) |[<img alt="aduh95-test-account" src="https://avatars.githubusercontent.com/u/93441190?v=4&s=117" width="117">](https://github.com/aduh95-test-account) |[<img alt="agreene-coursera" src="https://avatars.githubusercontent.com/u/30501355?v=4&s=117" width="117">](https://github.com/agreene-coursera) |[<img alt="alfatv" src="https://avatars.githubusercontent.com/u/62238673?v=4&s=117" width="117">](https://github.com/alfatv) |[<img alt="arggh" src="https://avatars.githubusercontent.com/u/17210302?v=4&s=117" width="117">](https://github.com/arggh) |
+[<img alt="zlawson-ut" src="https://avatars.githubusercontent.com/u/7375444?v=4&s=117" width="117">](https://github.com/zlawson-ut) |[<img alt="zackbloom" src="https://avatars.githubusercontent.com/u/55347?v=4&s=117" width="117">](https://github.com/zackbloom) |[<img alt="sartoshi-foot-dao" src="https://avatars.githubusercontent.com/u/99770068?v=4&s=117" width="117">](https://github.com/sartoshi-foot-dao) |[<img alt="aduh95-test-account" src="https://avatars.githubusercontent.com/u/93441190?v=4&s=117" width="117">](https://github.com/aduh95-test-account) |[<img alt="agreene-coursera" src="https://avatars.githubusercontent.com/u/30501355?v=4&s=117" width="117">](https://github.com/agreene-coursera) |[<img alt="alfatv" src="https://avatars.githubusercontent.com/u/62238673?v=4&s=117" width="117">](https://github.com/alfatv) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[zackbloom](https://github.com/zackbloom) |[sartoshi-foot-dao](https://github.com/sartoshi-foot-dao) |[aduh95-test-account](https://github.com/aduh95-test-account) |[agreene-coursera](https://github.com/agreene-coursera) |[alfatv](https://github.com/alfatv) |[arggh](https://github.com/arggh) |
+[zlawson-ut](https://github.com/zlawson-ut) |[zackbloom](https://github.com/zackbloom) |[sartoshi-foot-dao](https://github.com/sartoshi-foot-dao) |[aduh95-test-account](https://github.com/aduh95-test-account) |[agreene-coursera](https://github.com/agreene-coursera) |[alfatv](https://github.com/alfatv) |
 
-[<img alt="avalla" src="https://avatars.githubusercontent.com/u/986614?v=4&s=117" width="117">](https://github.com/avalla) |[<img alt="c0b41" src="https://avatars.githubusercontent.com/u/2834954?v=4&s=117" width="117">](https://github.com/c0b41) |[<img alt="canvasbh" src="https://avatars.githubusercontent.com/u/44477734?v=4&s=117" width="117">](https://github.com/canvasbh) |[<img alt="cgoinglove" src="https://avatars.githubusercontent.com/u/86150470?v=4&s=117" width="117">](https://github.com/cgoinglove) |[<img alt="christianwengert" src="https://avatars.githubusercontent.com/u/12936057?v=4&s=117" width="117">](https://github.com/christianwengert) |[<img alt="codehero7386" src="https://avatars.githubusercontent.com/u/56253286?v=4&s=117" width="117">](https://github.com/codehero7386) |
+[<img alt="arggh" src="https://avatars.githubusercontent.com/u/17210302?v=4&s=117" width="117">](https://github.com/arggh) |[<img alt="avalla" src="https://avatars.githubusercontent.com/u/986614?v=4&s=117" width="117">](https://github.com/avalla) |[<img alt="c0b41" src="https://avatars.githubusercontent.com/u/2834954?v=4&s=117" width="117">](https://github.com/c0b41) |[<img alt="canvasbh" src="https://avatars.githubusercontent.com/u/44477734?v=4&s=117" width="117">](https://github.com/canvasbh) |[<img alt="cgoinglove" src="https://avatars.githubusercontent.com/u/86150470?v=4&s=117" width="117">](https://github.com/cgoinglove) |[<img alt="christianwengert" src="https://avatars.githubusercontent.com/u/12936057?v=4&s=117" width="117">](https://github.com/christianwengert) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[avalla](https://github.com/avalla) |[c0b41](https://github.com/c0b41) |[canvasbh](https://github.com/canvasbh) |[cgoinglove](https://github.com/cgoinglove) |[christianwengert](https://github.com/christianwengert) |[codehero7386](https://github.com/codehero7386) |
+[arggh](https://github.com/arggh) |[avalla](https://github.com/avalla) |[c0b41](https://github.com/c0b41) |[canvasbh](https://github.com/canvasbh) |[cgoinglove](https://github.com/cgoinglove) |[christianwengert](https://github.com/christianwengert) |
 
-[<img alt="craigcbrunner" src="https://avatars.githubusercontent.com/u/2780521?v=4&s=117" width="117">](https://github.com/craigcbrunner) |[<img alt="darthf1" src="https://avatars.githubusercontent.com/u/17253332?v=4&s=117" width="117">](https://github.com/darthf1) |[<img alt="dkisic" src="https://avatars.githubusercontent.com/u/32257921?v=4&s=117" width="117">](https://github.com/dkisic) |[<img alt="dzcpy" src="https://avatars.githubusercontent.com/u/203980?v=4&s=117" width="117">](https://github.com/dzcpy) |[<img alt="elliotsayes" src="https://avatars.githubusercontent.com/u/7699058?v=4&s=117" width="117">](https://github.com/elliotsayes) |[<img alt="fingul" src="https://avatars.githubusercontent.com/u/894739?v=4&s=117" width="117">](https://github.com/fingul) |
+[<img alt="codehero7386" src="https://avatars.githubusercontent.com/u/56253286?v=4&s=117" width="117">](https://github.com/codehero7386) |[<img alt="craigcbrunner" src="https://avatars.githubusercontent.com/u/2780521?v=4&s=117" width="117">](https://github.com/craigcbrunner) |[<img alt="darthf1" src="https://avatars.githubusercontent.com/u/17253332?v=4&s=117" width="117">](https://github.com/darthf1) |[<img alt="dkisic" src="https://avatars.githubusercontent.com/u/32257921?v=4&s=117" width="117">](https://github.com/dkisic) |[<img alt="dzcpy" src="https://avatars.githubusercontent.com/u/203980?v=4&s=117" width="117">](https://github.com/dzcpy) |[<img alt="elliotsayes" src="https://avatars.githubusercontent.com/u/7699058?v=4&s=117" width="117">](https://github.com/elliotsayes) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[craigcbrunner](https://github.com/craigcbrunner) |[darthf1](https://github.com/darthf1) |[dkisic](https://github.com/dkisic) |[dzcpy](https://github.com/dzcpy) |[elliotsayes](https://github.com/elliotsayes) |[fingul](https://github.com/fingul) |
+[codehero7386](https://github.com/codehero7386) |[craigcbrunner](https://github.com/craigcbrunner) |[darthf1](https://github.com/darthf1) |[dkisic](https://github.com/dkisic) |[dzcpy](https://github.com/dzcpy) |[elliotsayes](https://github.com/elliotsayes) |
 
-[<img alt="franckl" src="https://avatars.githubusercontent.com/u/3875803?v=4&s=117" width="117">](https://github.com/franckl) |[<img alt="frederikhors" src="https://avatars.githubusercontent.com/u/41120635?v=4&s=117" width="117">](https://github.com/frederikhors) |[<img alt="gaelicwinter" src="https://avatars.githubusercontent.com/u/6510266?v=4&s=117" width="117">](https://github.com/gaelicwinter) |[<img alt="green-mike" src="https://avatars.githubusercontent.com/u/5584225?v=4&s=117" width="117">](https://github.com/green-mike) |[<img alt="hxgf" src="https://avatars.githubusercontent.com/u/56104?v=4&s=117" width="117">](https://github.com/hxgf) |[<img alt="johnmanjiro13" src="https://avatars.githubusercontent.com/u/28798279?v=4&s=117" width="117">](https://github.com/johnmanjiro13) |
+[<img alt="fingul" src="https://avatars.githubusercontent.com/u/894739?v=4&s=117" width="117">](https://github.com/fingul) |[<img alt="franckl" src="https://avatars.githubusercontent.com/u/3875803?v=4&s=117" width="117">](https://github.com/franckl) |[<img alt="frederikhors" src="https://avatars.githubusercontent.com/u/41120635?v=4&s=117" width="117">](https://github.com/frederikhors) |[<img alt="gaelicwinter" src="https://avatars.githubusercontent.com/u/6510266?v=4&s=117" width="117">](https://github.com/gaelicwinter) |[<img alt="green-mike" src="https://avatars.githubusercontent.com/u/5584225?v=4&s=117" width="117">](https://github.com/green-mike) |[<img alt="hxgf" src="https://avatars.githubusercontent.com/u/56104?v=4&s=117" width="117">](https://github.com/hxgf) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[franckl](https://github.com/franckl) |[frederikhors](https://github.com/frederikhors) |[gaelicwinter](https://github.com/gaelicwinter) |[green-mike](https://github.com/green-mike) |[hxgf](https://github.com/hxgf) |[johnmanjiro13](https://github.com/johnmanjiro13) |
+[fingul](https://github.com/fingul) |[franckl](https://github.com/franckl) |[frederikhors](https://github.com/frederikhors) |[gaelicwinter](https://github.com/gaelicwinter) |[green-mike](https://github.com/green-mike) |[hxgf](https://github.com/hxgf) |
 
-[<img alt="jur-ng" src="https://avatars.githubusercontent.com/u/111122756?v=4&s=117" width="117">](https://github.com/jur-ng) |[<img alt="sontixyou" src="https://avatars.githubusercontent.com/u/19817196?v=4&s=117" width="117">](https://github.com/sontixyou) |[<img alt="kode-ninja" src="https://avatars.githubusercontent.com/u/7857611?v=4&s=117" width="117">](https://github.com/kode-ninja) |[<img alt="jx-zyf" src="https://avatars.githubusercontent.com/u/26456842?v=4&s=117" width="117">](https://github.com/jx-zyf) |[<img alt="magumbo" src="https://avatars.githubusercontent.com/u/6683765?v=4&s=117" width="117">](https://github.com/magumbo) |[<img alt="mdxiaohu" src="https://avatars.githubusercontent.com/u/42248614?v=4&s=117" width="117">](https://github.com/mdxiaohu) |
+[<img alt="johnmanjiro13" src="https://avatars.githubusercontent.com/u/28798279?v=4&s=117" width="117">](https://github.com/johnmanjiro13) |[<img alt="jur-ng" src="https://avatars.githubusercontent.com/u/111122756?v=4&s=117" width="117">](https://github.com/jur-ng) |[<img alt="sontixyou" src="https://avatars.githubusercontent.com/u/19817196?v=4&s=117" width="117">](https://github.com/sontixyou) |[<img alt="kode-ninja" src="https://avatars.githubusercontent.com/u/7857611?v=4&s=117" width="117">](https://github.com/kode-ninja) |[<img alt="jx-zyf" src="https://avatars.githubusercontent.com/u/26456842?v=4&s=117" width="117">](https://github.com/jx-zyf) |[<img alt="magumbo" src="https://avatars.githubusercontent.com/u/6683765?v=4&s=117" width="117">](https://github.com/magumbo) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[jur-ng](https://github.com/jur-ng) |[sontixyou](https://github.com/sontixyou) |[kode-ninja](https://github.com/kode-ninja) |[jx-zyf](https://github.com/jx-zyf) |[magumbo](https://github.com/magumbo) |[mdxiaohu](https://github.com/mdxiaohu) |
+[johnmanjiro13](https://github.com/johnmanjiro13) |[jur-ng](https://github.com/jur-ng) |[sontixyou](https://github.com/sontixyou) |[kode-ninja](https://github.com/kode-ninja) |[jx-zyf](https://github.com/jx-zyf) |[magumbo](https://github.com/magumbo) |
 
-[<img alt="mjlumetta" src="https://avatars.githubusercontent.com/u/3241493?v=4&s=117" width="117">](https://github.com/mjlumetta) |[<img alt="mosi-kha" src="https://avatars.githubusercontent.com/u/35611016?v=4&s=117" width="117">](https://github.com/mosi-kha) |[<img alt="neuronet77" src="https://avatars.githubusercontent.com/u/4220037?v=4&s=117" width="117">](https://github.com/neuronet77) |[<img alt="ninesalt" src="https://avatars.githubusercontent.com/u/7952255?v=4&s=117" width="117">](https://github.com/ninesalt) |[<img alt="odselsevier" src="https://avatars.githubusercontent.com/u/95745934?v=4&s=117" width="117">](https://github.com/odselsevier) |[<img alt="ordago" src="https://avatars.githubusercontent.com/u/6376814?v=4&s=117" width="117">](https://github.com/ordago) |
+[<img alt="mdxiaohu" src="https://avatars.githubusercontent.com/u/42248614?v=4&s=117" width="117">](https://github.com/mdxiaohu) |[<img alt="maddy-jo" src="https://avatars.githubusercontent.com/u/3241493?v=4&s=117" width="117">](https://github.com/maddy-jo) |[<img alt="mosi-kha" src="https://avatars.githubusercontent.com/u/35611016?v=4&s=117" width="117">](https://github.com/mosi-kha) |[<img alt="neuronet77" src="https://avatars.githubusercontent.com/u/4220037?v=4&s=117" width="117">](https://github.com/neuronet77) |[<img alt="ninesalt" src="https://avatars.githubusercontent.com/u/7952255?v=4&s=117" width="117">](https://github.com/ninesalt) |[<img alt="odselsevier" src="https://avatars.githubusercontent.com/u/95745934?v=4&s=117" width="117">](https://github.com/odselsevier) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[mjlumetta](https://github.com/mjlumetta) |[mosi-kha](https://github.com/mosi-kha) |[neuronet77](https://github.com/neuronet77) |[ninesalt](https://github.com/ninesalt) |[odselsevier](https://github.com/odselsevier) |[ordago](https://github.com/ordago) |
+[mdxiaohu](https://github.com/mdxiaohu) |[maddy-jo](https://github.com/maddy-jo) |[mosi-kha](https://github.com/mosi-kha) |[neuronet77](https://github.com/neuronet77) |[ninesalt](https://github.com/ninesalt) |[odselsevier](https://github.com/odselsevier) |
 
-[<img alt="phil714" src="https://avatars.githubusercontent.com/u/7584581?v=4&s=117" width="117">](https://github.com/phil714) |[<img alt="luntta" src="https://avatars.githubusercontent.com/u/14221637?v=4&s=117" width="117">](https://github.com/luntta) |[<img alt="rhymes" src="https://avatars.githubusercontent.com/u/146201?v=4&s=117" width="117">](https://github.com/rhymes) |[<img alt="rlebosse" src="https://avatars.githubusercontent.com/u/2794137?v=4&s=117" width="117">](https://github.com/rlebosse) |[<img alt="rmoura-92" src="https://avatars.githubusercontent.com/u/419044?v=4&s=117" width="117">](https://github.com/rmoura-92) |[<img alt="rtaieb" src="https://avatars.githubusercontent.com/u/35224301?v=4&s=117" width="117">](https://github.com/rtaieb) |
+[<img alt="ordago" src="https://avatars.githubusercontent.com/u/6376814?v=4&s=117" width="117">](https://github.com/ordago) |[<img alt="phil714" src="https://avatars.githubusercontent.com/u/7584581?v=4&s=117" width="117">](https://github.com/phil714) |[<img alt="luntta" src="https://avatars.githubusercontent.com/u/14221637?v=4&s=117" width="117">](https://github.com/luntta) |[<img alt="rhymes" src="https://avatars.githubusercontent.com/u/146201?v=4&s=117" width="117">](https://github.com/rhymes) |[<img alt="rlebosse" src="https://avatars.githubusercontent.com/u/2794137?v=4&s=117" width="117">](https://github.com/rlebosse) |[<img alt="rmoura-92" src="https://avatars.githubusercontent.com/u/419044?v=4&s=117" width="117">](https://github.com/rmoura-92) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[phil714](https://github.com/phil714) |[luntta](https://github.com/luntta) |[rhymes](https://github.com/rhymes) |[rlebosse](https://github.com/rlebosse) |[rmoura-92](https://github.com/rmoura-92) |[rtaieb](https://github.com/rtaieb) |
+[ordago](https://github.com/ordago) |[phil714](https://github.com/phil714) |[luntta](https://github.com/luntta) |[rhymes](https://github.com/rhymes) |[rlebosse](https://github.com/rlebosse) |[rmoura-92](https://github.com/rmoura-92) |
 
-[<img alt="slawexxx44" src="https://avatars.githubusercontent.com/u/11180644?v=4&s=117" width="117">](https://github.com/slawexxx44) |[<img alt="stduhpf" src="https://avatars.githubusercontent.com/u/28208228?v=4&s=117" width="117">](https://github.com/stduhpf) |[<img alt="thanhthot" src="https://avatars.githubusercontent.com/u/50633205?v=4&s=117" width="117">](https://github.com/thanhthot) |[<img alt="tusharjkhunt" src="https://avatars.githubusercontent.com/u/31904234?v=4&s=117" width="117">](https://github.com/tusharjkhunt) |[<img alt="vedran555" src="https://avatars.githubusercontent.com/u/38395951?v=4&s=117" width="117">](https://github.com/vedran555) |[<img alt="yoann-hellopret" src="https://avatars.githubusercontent.com/u/46525558?v=4&s=117" width="117">](https://github.com/yoann-hellopret) |
+[<img alt="rtaieb" src="https://avatars.githubusercontent.com/u/35224301?v=4&s=117" width="117">](https://github.com/rtaieb) |[<img alt="slawexxx44" src="https://avatars.githubusercontent.com/u/11180644?v=4&s=117" width="117">](https://github.com/slawexxx44) |[<img alt="stduhpf" src="https://avatars.githubusercontent.com/u/28208228?v=4&s=117" width="117">](https://github.com/stduhpf) |[<img alt="thanhthot" src="https://avatars.githubusercontent.com/u/50633205?v=4&s=117" width="117">](https://github.com/thanhthot) |[<img alt="tusharjkhunt" src="https://avatars.githubusercontent.com/u/31904234?v=4&s=117" width="117">](https://github.com/tusharjkhunt) |[<img alt="vedran555" src="https://avatars.githubusercontent.com/u/38395951?v=4&s=117" width="117">](https://github.com/vedran555) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[slawexxx44](https://github.com/slawexxx44) |[stduhpf](https://github.com/stduhpf) |[thanhthot](https://github.com/thanhthot) |[tusharjkhunt](https://github.com/tusharjkhunt) |[vedran555](https://github.com/vedran555) |[yoann-hellopret](https://github.com/yoann-hellopret) |
+[rtaieb](https://github.com/rtaieb) |[slawexxx44](https://github.com/slawexxx44) |[stduhpf](https://github.com/stduhpf) |[thanhthot](https://github.com/thanhthot) |[tusharjkhunt](https://github.com/tusharjkhunt) |[vedran555](https://github.com/vedran555) |
 
-[<img alt="olitomas" src="https://avatars.githubusercontent.com/u/6918659?v=4&s=117" width="117">](https://github.com/olitomas) |[<img alt="JimmyLv" src="https://avatars.githubusercontent.com/u/4997466?v=4&s=117" width="117">](https://github.com/JimmyLv) |
-:---: |:---: |
-[olitomas](https://github.com/olitomas) |[JimmyLv](https://github.com/JimmyLv) |
+[<img alt="yoann-hellopret" src="https://avatars.githubusercontent.com/u/46525558?v=4&s=117" width="117">](https://github.com/yoann-hellopret) |[<img alt="olitomas" src="https://avatars.githubusercontent.com/u/6918659?v=4&s=117" width="117">](https://github.com/olitomas) |[<img alt="JimmyLv" src="https://avatars.githubusercontent.com/u/4997466?v=4&s=117" width="117">](https://github.com/JimmyLv) |
+:---: |:---: |:---: |
+[yoann-hellopret](https://github.com/yoann-hellopret) |[olitomas](https://github.com/olitomas) |[JimmyLv](https://github.com/JimmyLv) |
 
 <!--/contributors-->
 

+ 2 - 2
examples/aws-nodejs/public/drag.html

@@ -4,7 +4,7 @@
     <meta charset="utf-8" />
     <title>Uppy</title>
     <link
-      href="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css"
+      href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
       rel="stylesheet"
     />
   </head>
@@ -22,7 +22,7 @@
           DragDrop,
           ProgressBar,
           AwsS3,
-        } from 'https://releases.transloadit.com/uppy/v3.20.0/uppy.min.mjs'
+        } from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
 
         // Function for displaying uploaded files
         const onUploadSuccess = (elForUploadedFiles) => (file, response) => {

+ 2 - 2
examples/aws-nodejs/public/index.html

@@ -4,7 +4,7 @@
     <meta charset="utf-8" />
     <title>Uppy – AWS upload example</title>
     <link
-      href="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css"
+      href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
       rel="stylesheet"
     />
   </head>
@@ -16,7 +16,7 @@
         Uppy,
         Dashboard,
         AwsS3,
-      } from 'https://releases.transloadit.com/uppy/v3.20.0/uppy.min.mjs'
+      } from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
       /**
        * This generator transforms a deep object into URL-encodable pairs
        * to work with `URLSearchParams` on the client and `body-parser` on the server.

+ 3 - 3
examples/cdn-example/index.html

@@ -5,7 +5,7 @@
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <link
-      href="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css"
+      href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
       rel="stylesheet"
     />
   </head>
@@ -19,7 +19,7 @@
         Dashboard,
         Webcam,
         Tus,
-      } from 'https://releases.transloadit.com/uppy/v3.20.0/uppy.min.mjs'
+      } from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
 
       const uppy = new Uppy({ debug: true, autoProceed: false })
         .use(Dashboard, { trigger: '#uppyModalOpener' })
@@ -34,7 +34,7 @@
     <!-- To support older browsers, you can use the legacy bundle which adds a global `Uppy` object.  -->
     <script
       nomodule
-      src="https://releases.transloadit.com/uppy/v3.20.0/uppy.legacy.min.js"
+      src="https://releases.transloadit.com/uppy/v3.21.0/uppy.legacy.min.js"
     ></script>
     <script nomodule>
       {

+ 2 - 0
examples/custom-provider/client/MyCustomProvider.jsx

@@ -27,6 +27,8 @@ export default class MyCustomProvider extends UIPlugin {
       pluginId: this.id,
     })
 
+    uppy.registerRequestClient(MyCustomProvider.name, this.provider)
+
     this.defaultLocale = {
       strings: {
         pluginNameMyUnsplash: 'MyUnsplash',

+ 2 - 2
examples/uppy-with-companion/client/index.html

@@ -5,7 +5,7 @@
     <meta charset="UTF-8" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <link
-      href="https://releases.transloadit.com/uppy/v3.20.0/uppy.min.css"
+      href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
       rel="stylesheet"
     />
   </head>
@@ -19,7 +19,7 @@
         Instagram,
         GoogleDrive,
         Tus,
-      } from 'https://releases.transloadit.com/uppy/v3.20.0/uppy.min.mjs'
+      } from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
 
       const uppy = new Uppy({ debug: true, autoProceed: false })
         .use(Dashboard, { trigger: '#uppyModalOpener' })

+ 2 - 2
package.json

@@ -115,7 +115,7 @@
     "start:companion": "bash bin/companion.sh",
     "start:companion:with-loadbalancer": "e2e/start-companion-with-load-balancer.mjs",
     "build:bundle": "yarn node ./bin/build-bundle.mjs",
-    "build:clean": "rm -rf packages/*/lib packages/@uppy/*/lib packages/*/dist packages/@uppy/*/dist",
+    "build:clean": "git clean -e node_modules -xfd packages e2e .parcel-cache coverage",
     "build:companion": "yarn workspace @uppy/companion build",
     "build:css": "yarn node ./bin/build-css.js",
     "build:svelte": "yarn workspace @uppy/svelte build",
@@ -140,7 +140,7 @@
     "format": "prettier -w .",
     "release": "PACKAGES=$(yarn workspaces list --json) yarn workspace @uppy-dev/release interactive",
     "size": "echo 'JS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.js | gzip | wc -c && echo 'CSS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.css | gzip | wc -c",
-    "e2e": "yarn build && yarn e2e:skip-build",
+    "e2e": "build:clean && yarn build && yarn e2e:skip-build",
     "e2e:skip-build": "npm-run-all --parallel watch:js:lib e2e:client start:companion:with-loadbalancer e2e:cypress",
     "e2e:ci": "start-server-and-test 'npm-run-all --parallel e2e:client start:companion:with-loadbalancer' '1234|3020' e2e:headless",
     "e2e:client": "yarn workspace e2e client:start",

+ 1 - 1
packages/@uppy/aws-s3-multipart/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/aws-s3-multipart",
   "description": "Upload to Amazon S3 with Uppy and S3's Multipart upload strategy",
-  "version": "3.9.0",
+  "version": "3.10.0",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",

+ 1 - 1
packages/@uppy/aws-s3-multipart/src/index.js

@@ -878,7 +878,7 @@ export default class AwsS3Multipart extends BasePlugin {
         }
         this.uppy.on('file-removed', removedHandler)
 
-        const uploadPromise = file.remote.requestClient.uploadRemoteFile(
+        const uploadPromise = this.uppy.getRequestClientForFile(file).uploadRemoteFile(
           file,
           this.#getCompanionClientArgs(file),
           { signal: controller.signal, getQueue },

+ 7 - 0
packages/@uppy/aws-s3/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/aws-s3
 
+## 3.6.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/aws-s3: change Companion URL in tests (Antoine du Hamel)
+
 ## 3.3.0
 
 Released: 2023-09-05

+ 1 - 1
packages/@uppy/aws-s3/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/aws-s3",
   "description": "Upload to Amazon S3 with Uppy",
-  "version": "3.5.0",
+  "version": "3.6.0",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",

+ 1 - 1
packages/@uppy/aws-s3/src/index.js

@@ -281,7 +281,7 @@ export default class AwsS3 extends BasePlugin {
       }
       this.uppy.on('file-removed', removedHandler)
 
-      const uploadPromise = file.remote.requestClient.uploadRemoteFile(
+      const uploadPromise = this.uppy.getRequestClientForFile(file).uploadRemoteFile(
         file,
         this.#getCompanionClientArgs(file),
         { signal: controller.signal, getQueue },

+ 1 - 1
packages/@uppy/box/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/box",
   "description": "Import files from Box, into Uppy.",
-  "version": "2.1.4",
+  "version": "2.2.0",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",

+ 7 - 0
packages/@uppy/companion-client/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/companion-client
 
+## 3.7.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/companion-client: avoid unnecessary preflight requests (Antoine du Hamel / #4462)
+
 ## 3.6.1
 
 Released: 2023-11-24

+ 1 - 1
packages/@uppy/companion-client/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/companion-client",
   "description": "Client library for communication with Companion. Intended for use in Uppy plugins.",
-  "version": "3.6.1",
+  "version": "3.7.0",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",

+ 1 - 1
packages/@uppy/companion-client/src/Provider.js

@@ -286,7 +286,7 @@ export default class Provider extends RequestClient {
       plugin.opts.companionAllowedHosts = pattern
     } else if (/^(?!https?:\/\/).*$/i.test(opts.companionUrl)) {
       // does not start with https://
-      plugin.opts.companionAllowedHosts = `https://${opts.companionUrl.replace(/^\/\//, '')}`
+      plugin.opts.companionAllowedHosts = `https://${opts.companionUrl?.replace(/^\/\//, '')}`
     } else {
       plugin.opts.companionAllowedHosts = new URL(opts.companionUrl).origin
     }

+ 11 - 0
packages/@uppy/companion/CHANGELOG.md

@@ -1,5 +1,16 @@
 # @uppy/companion
 
+## 4.12.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/companion: fix double tus uploads (Mikael Finstad / #4816)
+- @uppy/companion: fix accelerated endpoints for presigned POST  (Mikael Finstad / #4817)
+- @uppy/companion: fix `authProvider` property inconsistency (Mikael Finstad / #4672)
+- @uppy/companion:  send certain onedrive errors to the user (Mikael Finstad / #4671)
+- @uppy/companion: Provider user sessions (Mikael Finstad / #4619)
+
 ## 4.11.0
 
 Released: 2023-11-08

+ 1 - 1
packages/@uppy/companion/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@uppy/companion",
-  "version": "4.11.0",
+  "version": "4.12.0",
   "description": "OAuth helper and remote fetcher for Uppy's (https://uppy.io) extensible file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox and Google Drive, S3 and more :dog:",
   "main": "lib/companion.js",
   "types": "lib/companion.d.ts",

+ 15 - 6
packages/@uppy/companion/src/companion.js

@@ -20,8 +20,10 @@ const { ProviderApiError, ProviderUserError, ProviderAuthError } = require('./se
 const { getCredentialsOverrideMiddleware } = require('./server/provider/credentials')
 // @ts-ignore
 const { version } = require('../package.json')
+const { isOAuthProvider } = require('./server/provider/Provider')
 
-function setLoggerProcessName ({ loggerProcessName }) {
+
+function setLoggerProcessName({ loggerProcessName }) {
   if (loggerProcessName != null) logger.setProcessName(loggerProcessName)
 }
 
@@ -72,13 +74,15 @@ module.exports.app = (optionsArg = {}) => {
 
   const providers = providerManager.getDefaultProviders()
 
-  providerManager.addProviderOptions(options, grantConfig)
-
   const { customProviders } = options
   if (customProviders) {
     providerManager.addCustomProviders(customProviders, providers, grantConfig)
   }
 
+  const getAuthProvider = (providerName) => providers[providerName]?.authProvider
+
+  providerManager.addProviderOptions(options, grantConfig, getAuthProvider)
+
   // mask provider secrets from log messages
   logger.setMaskables(getMaskableSecrets(options))
 
@@ -147,13 +151,18 @@ module.exports.app = (optionsArg = {}) => {
       // for simplicity, we just return the normal credentials for the provider, but in a real-world scenario,
       // we would query based on parameters
       const { key, secret } = options.providerOptions[providerName]
+
+      function getRedirectUri() {
+        const authProvider = getAuthProvider(providerName)
+        if (!isOAuthProvider(authProvider)) return undefined
+        return grantConfig[authProvider]?.redirect_uri
+      }
+
       res.send({
         credentials: {
           key,
           secret,
-          redirect_uri: providerManager.getGrantConfigForProvider({
-            providerName, companionOptions: options, grantConfig,
-          })?.redirect_uri,
+          redirect_uri: getRedirectUri(),
         },
       })
     })

+ 34 - 23
packages/@uppy/companion/src/server/Uploader.js

@@ -56,10 +56,6 @@ function sanitizeMetadata(inputMetadata) {
   return outputMetadata
 }
 
-class AbortError extends Error {
-  isAbortError = true
-}
-
 class ValidationError extends Error {
   constructor(message) {
     super(message)
@@ -139,6 +135,13 @@ function validateOptions(options) {
   }
 }
 
+const states = {
+  idle: 'idle',
+  uploading: 'uploading',
+  paused: 'paused',
+  done: 'done',
+}
+
 class Uploader {
   /**
    * Uploads file to destination based on the supplied protocol (tus, s3-multipart, multipart)
@@ -176,10 +179,7 @@ class Uploader {
       ? this.options.metadata.name.substring(0, MAX_FILENAME_LENGTH)
       : this.fileName
 
-    this.uploadStopped = false
-
     this.storage = options.storage
-    this._paused = false
 
     this.downloadedBytes = 0
 
@@ -188,7 +188,8 @@ class Uploader {
     if (this.options.protocol === PROTOCOLS.tus) {
       emitter().on(`pause:${this.token}`, () => {
         logger.debug('Received from client: pause', 'uploader', this.shortToken)
-        this._paused = true
+        if (this.#uploadState !== states.uploading) return
+        this.#uploadState = states.paused
         if (this.tus) {
           this.tus.abort()
         }
@@ -196,7 +197,8 @@ class Uploader {
 
       emitter().on(`resume:${this.token}`, () => {
         logger.debug('Received from client: resume', 'uploader', this.shortToken)
-        this._paused = false
+        if (this.#uploadState !== states.paused) return
+        this.#uploadState = states.uploading
         if (this.tus) {
           this.tus.start()
         }
@@ -205,17 +207,21 @@ class Uploader {
 
     emitter().on(`cancel:${this.token}`, () => {
       logger.debug('Received from client: cancel', 'uploader', this.shortToken)
-      this._paused = true
       if (this.tus) {
         const shouldTerminate = !!this.tus.url
         this.tus.abort(shouldTerminate).catch(() => { })
       }
-      this.abortReadStream(new AbortError())
+      this.#canceled = true
+      this.abortReadStream(new Error('Canceled'))
     })
   }
 
+  #uploadState = states.idle
+
+  #canceled = false
+
   abortReadStream(err) {
-    this.uploadStopped = true
+    this.#uploadState = states.done
     if (this.readStream) this.readStream.destroy(err)
   }
 
@@ -244,7 +250,9 @@ class Uploader {
 
     const onData = (chunk) => {
       this.downloadedBytes += chunk.length
-      if (exceedsMaxFileSize(this.options.companionOptions.maxFileSize, this.downloadedBytes)) this.abortReadStream(new Error('maxFileSize exceeded'))
+      if (exceedsMaxFileSize(this.options.companionOptions.maxFileSize, this.downloadedBytes)) {
+        this.abortReadStream(new Error('maxFileSize exceeded'))
+      }
       this.onProgress(0, undefined)
     }
 
@@ -271,9 +279,11 @@ class Uploader {
    */
   async uploadStream(stream) {
     try {
-      if (this.uploadStopped) throw new Error('Cannot upload stream after upload stopped')
+      if (this.#uploadState !== states.idle) throw new Error('Can only start an upload in the idle state')
       if (this.readStream) throw new Error('Already uploading')
 
+      this.#uploadState = states.uploading
+
       this.readStream = stream
       if (this._needDownloadFirst()) {
         logger.debug('need to download the whole file first', 'controller.get.provider.size', this.shortToken)
@@ -282,7 +292,7 @@ class Uploader {
         // The stream will then typically come from a "Transfer-Encoding: chunked" response
         await this._downloadStreamAsFile(this.readStream)
       }
-      if (this.uploadStopped) return undefined
+      if (this.#uploadState !== states.uploading) return undefined
 
       const { url, extraData } = await Promise.race([
         this._uploadByProtocol(),
@@ -291,6 +301,7 @@ class Uploader {
       ])
       return { url, extraData }
     } finally {
+      this.#uploadState = states.done
       logger.debug('cleanup', this.shortToken)
       if (this.readStream && !this.readStream.destroyed) this.readStream.destroy()
       await this.tryDeleteTmpPath()
@@ -314,11 +325,10 @@ class Uploader {
       const { url, extraData } = ret
       this.#emitSuccess(url, extraData)
     } catch (err) {
-      if (err?.isAbortError) {
+      if (this.#canceled) {
         logger.error('Aborted upload', 'uploader.aborted', this.shortToken)
         return
       }
-      // console.log(err)
       logger.error(err, 'uploader.error', this.shortToken)
       this.#emitError(err)
     } finally {
@@ -458,7 +468,7 @@ class Uploader {
 
     const formattedPercentage = percentage.toFixed(2)
 
-    if (this._paused || this.uploadStopped) {
+    if (this.#uploadState !== states.uploading) {
       return
     }
 
@@ -519,7 +529,8 @@ class Uploader {
     const chunkSize = this.options.chunkSize || (isFileStream ? Infinity : 50e6)
 
     return new Promise((resolve, reject) => {
-      this.tus = new tus.Upload(stream, {
+
+      const tusOptions = {
         endpoint: this.options.endpoint,
         uploadUrl: this.options.uploadUrl,
         uploadLengthDeferred: !isFileStream,
@@ -564,11 +575,11 @@ class Uploader {
         onSuccess() {
           resolve({ url: uploader.tus.url })
         },
-      })
-
-      if (!this._paused) {
-        this.tus.start()
       }
+
+      this.tus = new tus.Upload(stream, tusOptions)
+
+      this.tus.start()
     })
   }
 

+ 3 - 3
packages/@uppy/companion/src/server/controllers/connect.js

@@ -30,7 +30,7 @@ module.exports = function connect(req, res) {
   }
 
   const state = oAuthState.encodeState(stateObj, secret)
-  const { provider, providerGrantConfig } = req.companion
+  const { providerClass, providerGrantConfig } = req.companion
 
   // pass along grant's dynamic config (if specified for the provider in its grant config `dynamic` section)
   // this is needed for things like custom oauth domain (e.g. webdav)
@@ -45,12 +45,12 @@ module.exports = function connect(req, res) {
     ]]
   }) || [])
 
-  const providerName = provider.authProvider
+  const { authProvider } = providerClass
   const qs = queryString({
     ...grantDynamicConfig,
     state,
   })
 
   // Now we redirect to grant's /connect endpoint, see `app.use(Grant(grantConfig))`
-  res.redirect(req.companion.buildURL(`/connect/${providerName}${qs}`, true))
+  res.redirect(req.companion.buildURL(`/connect/${authProvider}${qs}`, true))
 }

+ 1 - 1
packages/@uppy/companion/src/server/controllers/logout.js

@@ -26,7 +26,7 @@ async function logout (req, res, next) {
     const { accessToken } = providerUserSession
     const data = await companion.provider.logout({ token: accessToken, providerUserSession, companion })
     delete companion.providerUserSession
-    tokenService.removeFromCookies(res, companion.options, companion.provider.authProvider)
+    tokenService.removeFromCookies(res, companion.options, companion.providerClass.authProvider)
     cleanSession()
     res.json({ ok: true, ...data })
   } catch (err) {

+ 1 - 1
packages/@uppy/companion/src/server/controllers/oauth-redirect.js

@@ -10,7 +10,7 @@ const oAuthState = require('../helpers/oauth-state')
  */
 module.exports = function oauthRedirect (req, res) {
   const params = qs.stringify(req.query)
-  const { authProvider } = req.companion.provider
+  const { authProvider } = req.companion.providerClass
   if (!req.companion.options.server.oauthDomain) {
     res.redirect(req.companion.buildURL(`/connect/${authProvider}/callback?${params}`, true))
     return

+ 2 - 2
packages/@uppy/companion/src/server/controllers/s3.js

@@ -24,11 +24,11 @@ module.exports = function s3 (config) {
     throw new TypeError('s3: The `getKey` option must be a function')
   }
 
-  function getS3Client (req, res) {
+  function getS3Client (req, res, createPresignedPostMode = false) {
     /**
      * @type {import('@aws-sdk/client-s3').S3Client}
      */
-    const client = req.companion.s3Client
+    const client = createPresignedPostMode ? req.companion.s3ClientCreatePresignedPost : req.companion.s3Client
     if (!client) res.status(400).json({ error: 'This Companion server does not support uploading to S3' })
     return client
   }

+ 1 - 1
packages/@uppy/companion/src/server/helpers/jwt.js

@@ -125,7 +125,7 @@ module.exports.addToCookiesIfNeeded = (req, res, uppyAuthToken, maxAge) => {
       res,
       token: uppyAuthToken,
       companionOptions: req.companion.options,
-      authProvider: req.companion.provider.authProvider,
+      authProvider: req.companion.providerClass.authProvider,
       maxAge,
     })
   }

+ 3 - 2
packages/@uppy/companion/src/server/middlewares.js

@@ -122,7 +122,7 @@ exports.gentleVerifyToken = (req, res, next) => {
 }
 
 exports.cookieAuthToken = (req, res, next) => {
-  req.companion.authToken = req.cookies[`uppyAuthToken--${req.companion.provider.authProvider}`]
+  req.companion.authToken = req.cookies[`uppyAuthToken--${req.companion.providerClass.authProvider}`]
   return next()
 }
 
@@ -204,7 +204,8 @@ exports.getCompanionMiddleware = (options) => {
   const middleware = (req, res, next) => {
     req.companion = {
       options,
-      s3Client: getS3Client(options),
+      s3Client: getS3Client(options, false),
+      s3ClientCreatePresignedPost: getS3Client(options, true),
       authToken: req.header('uppy-auth-token') || req.query.uppyAuthToken,
       buildURL: getURLBuilder(options),
     }

+ 1 - 1
packages/@uppy/companion/src/server/provider/Provider.js

@@ -122,5 +122,5 @@ class Provider {
 }
 
 module.exports = Provider
-// OAuth providers are those that have a `static authProvider` set. It means they require OAuth authentication to work
+// OAuth providers are those that have an `authProvider` set. It means they require OAuth authentication to work
 module.exports.isOAuthProvider = (authProvider) => typeof authProvider === 'string' && authProvider.length > 0

+ 2 - 2
packages/@uppy/companion/src/server/provider/box/index.js

@@ -31,7 +31,6 @@ async function list ({ directory, query, token }) {
 class Box extends Provider {
   constructor (options) {
     super(options)
-    this.authProvider = Box.authProvider
     // needed for the thumbnails fetched via companion
     this.needsCookieAuth = true
   }
@@ -116,11 +115,12 @@ class Box extends Provider {
     })
   }
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
       fn,
       tag,
-      providerName: this.authProvider,
+      providerName: Box.authProvider,
       isAuthError: (response) => response.statusCode === 401,
       getJsonErrorMessage: (body) => body?.message,
     })

+ 2 - 6
packages/@uppy/companion/src/server/provider/drive/index.js

@@ -51,11 +51,6 @@ async function getStats ({ id, token }) {
  * Adapter for API https://developers.google.com/drive/api/v3/
  */
 class Drive extends Provider {
-  constructor (options) {
-    super(options)
-    this.authProvider = Drive.authProvider
-  }
-
   static get authProvider () {
     return 'google'
   }
@@ -200,11 +195,12 @@ class Drive extends Provider {
     })
   }
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
       fn,
       tag,
-      providerName: this.authProvider,
+      providerName: Drive.authProvider,
       isAuthError: (response) => (
         response.statusCode === 401
         || (response.statusCode === 400 && response.body?.error === 'invalid_grant') // Refresh token has expired or been revoked

+ 2 - 2
packages/@uppy/companion/src/server/provider/dropbox/index.js

@@ -56,7 +56,6 @@ async function userInfo ({ token }) {
 class DropBox extends Provider {
   constructor (options) {
     super(options)
-    this.authProvider = DropBox.authProvider
     this.needsCookieAuth = true
   }
 
@@ -136,11 +135,12 @@ class DropBox extends Provider {
     })
   }
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
       fn,
       tag,
-      providerName: this.authProvider,
+      providerName: DropBox.authProvider,
       isAuthError: (response) => response.statusCode === 401,
       getJsonErrorMessage: (body) => body?.error_summary,
     })

+ 2 - 6
packages/@uppy/companion/src/server/provider/facebook/index.js

@@ -24,11 +24,6 @@ async function getMediaUrl ({ token, id }) {
  * Adapter for API https://developers.facebook.com/docs/graph-api/using-graph-api/
  */
 class Facebook extends Provider {
-  constructor (options) {
-    super(options)
-    this.authProvider = Facebook.authProvider
-  }
-
   static get authProvider () {
     return 'facebook'
   }
@@ -86,11 +81,12 @@ class Facebook extends Provider {
     })
   }
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
       fn,
       tag,
-      providerName: this.authProvider,
+      providerName: Facebook.authProvider,
       isAuthError: (response) => typeof response.body === 'object' && response.body?.error?.code === 190, // Invalid OAuth 2.0 Access Token
       getJsonErrorMessage: (body) => body?.error?.message,
     })

+ 7 - 25
packages/@uppy/companion/src/server/provider/index.js

@@ -12,7 +12,6 @@ const zoom = require('./zoom')
 const { getURLBuilder } = require('../helpers/utils')
 const logger = require('../logger')
 const { getCredentialsResolver } = require('./credentials')
-// eslint-disable-next-line
 const Provider = require('./Provider')
 
 const { isOAuthProvider } = Provider
@@ -25,26 +24,6 @@ const validOptions = (options) => {
   return options.server.host && options.server.protocol
 }
 
-/**
- *
- * @param {string} name of the provider
- * @param {{server: object, providerOptions: object}} options
- * @returns {string} the authProvider for this provider
- */
-const providerNameToAuthName = (name, options) => { // eslint-disable-line no-unused-vars
-  const providers = exports.getDefaultProviders()
-  return (providers[name] || {}).authProvider
-}
-
-function getGrantConfigForProvider({ providerName, companionOptions, grantConfig }) {
-  const authProvider = providerNameToAuthName(providerName, companionOptions)
-
-  if (!isOAuthProvider(authProvider)) return undefined
-  return grantConfig[authProvider]
-}
-
-module.exports.getGrantConfigForProvider = getGrantConfigForProvider
-
 /**
  * adds the desired provider module to the request object,
  * based on the providerName parameter specified
@@ -106,10 +85,11 @@ module.exports.addCustomProviders = (customProviders, providers, grantConfig) =>
 
     // eslint-disable-next-line no-param-reassign
     providers[providerName] = customProvider.module
+    const { authProvider } = customProvider.module
 
-    if (isOAuthProvider(customProvider.module.authProvider)) {
+    if (isOAuthProvider(authProvider)) {
       // eslint-disable-next-line no-param-reassign
-      grantConfig[providerName] = {
+      grantConfig[authProvider] = {
         ...customProvider.config,
         // todo: consider setting these options from a universal point also used
         // by official providers. It'll prevent these from getting left out if the
@@ -125,8 +105,9 @@ module.exports.addCustomProviders = (customProviders, providers, grantConfig) =>
  *
  * @param {{server: object, providerOptions: object}} companionOptions
  * @param {object} grantConfig
+ * @param {(a: string) => string} getAuthProvider
  */
-module.exports.addProviderOptions = (companionOptions, grantConfig) => {
+module.exports.addProviderOptions = (companionOptions, grantConfig, getAuthProvider) => {
   const { server, providerOptions } = companionOptions
   if (!validOptions({ server })) {
     logger.warn('invalid provider options detected. Providers will not be loaded', 'provider.options.invalid')
@@ -143,7 +124,8 @@ module.exports.addProviderOptions = (companionOptions, grantConfig) => {
   const { oauthDomain } = server
   const keys = Object.keys(providerOptions).filter((key) => key !== 'server')
   keys.forEach((providerName) => {
-    const authProvider = providerNameToAuthName(providerName, companionOptions)
+    const authProvider = getAuthProvider?.(providerName)
+
     if (isOAuthProvider(authProvider) && grantConfig[authProvider]) {
       // explicitly add providerOptions so users don't override other providerOptions.
       // eslint-disable-next-line no-param-reassign

+ 2 - 6
packages/@uppy/companion/src/server/provider/instagram/graph/index.js

@@ -23,11 +23,6 @@ async function getMediaUrl ({ token, id }) {
  * Adapter for API https://developers.facebook.com/docs/instagram-api/overview
  */
 class Instagram extends Provider {
-  constructor (options) {
-    super(options)
-    this.authProvider = Instagram.authProvider
-  }
-
   // for "grant"
   static getExtraConfig () {
     return {
@@ -86,11 +81,12 @@ class Instagram extends Provider {
     return { revoked: false, manual_revoke_url: 'https://www.instagram.com/accounts/manage_access/' }
   }
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
       fn,
       tag,
-      providerName: this.authProvider,
+      providerName: Instagram.authProvider,
       isAuthError: (response) => typeof response.body === 'object' && response.body?.error?.code === 190, // Invalid OAuth 2.0 Access Token
       getJsonErrorMessage: (body) => body?.error?.message,
     })

+ 8 - 6
packages/@uppy/companion/src/server/provider/onedrive/index.js

@@ -23,11 +23,6 @@ const getRootPath = (query) => (query.driveId ? `drives/${query.driveId}` : 'me/
  * Adapter for API https://docs.microsoft.com/en-us/onedrive/developer/rest-api/
  */
 class OneDrive extends Provider {
-  constructor (options) {
-    super(options)
-    this.authProvider = OneDrive.authProvider
-  }
-
   static get authProvider () {
     return 'microsoft'
   }
@@ -98,12 +93,19 @@ class OneDrive extends Provider {
     })
   }
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
     return withProviderErrorHandling({
       fn,
       tag,
-      providerName: this.authProvider,
+      providerName: OneDrive.authProvider,
       isAuthError: (response) => response.statusCode === 401,
+      isUserFacingError: (response) => [400, 403].includes(response.statusCode),
+      // onedrive gives some errors here that the user might want to know about
+      // e.g. these happen if you try to login to a users in an organization,
+      // without an Office365 licence or OneDrive account setup completed
+      // 400: Tenant does not have a SPO license
+      // 403: You do not have access to create this personal site or you do not have a valid license
       getJsonErrorMessage: (body) => body?.error?.message,
     })
   }

+ 2 - 6
packages/@uppy/companion/src/server/provider/zoom/index.js

@@ -29,11 +29,6 @@ async function findFile ({ client, meetingId, fileId, recordingStart }) {
  * Adapter for API https://marketplace.zoom.us/docs/api-reference/zoom-api
  */
 class Zoom extends Provider {
-  constructor (options) {
-    super(options)
-    this.authProvider = Zoom.authProvider
-  }
-
   static get authProvider () {
     return 'zoom'
   }
@@ -157,6 +152,7 @@ class Zoom extends Provider {
     })
   }
 
+  // eslint-disable-next-line class-methods-use-this
   async #withErrorHandling (tag, fn) {
     const authErrorCodes = [
       124, // expired token
@@ -166,7 +162,7 @@ class Zoom extends Provider {
     return withProviderErrorHandling({
       fn,
       tag,
-      providerName: this.authProvider,
+      providerName: Zoom.authProvider,
       isAuthError: (response) => authErrorCodes.includes(response.statusCode),
       getJsonErrorMessage: (body) => body?.message,
     })

+ 29 - 12
packages/@uppy/companion/src/server/s3-client.js

@@ -4,8 +4,9 @@ const { S3Client } = require('@aws-sdk/client-s3')
  * instantiates the aws-sdk s3 client that will be used for s3 uploads.
  *
  * @param {object} companionOptions the companion options object
+ * @param {boolean} createPresignedPostMode whether this s3 client is for createPresignedPost
  */
-module.exports = (companionOptions) => {
+module.exports = (companionOptions, createPresignedPostMode = false) => {
   let s3Client = null
   if (companionOptions.s3) {
     const { s3 } = companionOptions
@@ -19,23 +20,39 @@ module.exports = (companionOptions) => {
       throw new Error('Found unsupported `providerOptions.s3.awsClientOptions.accessKeyId` or `providerOptions.s3.awsClientOptions.secretAccessKey` configuration. Please use the `providerOptions.s3.key` and `providerOptions.s3.secret` options instead.')
     }
 
+    let { endpoint } = s3
+    if (typeof endpoint === 'string') {
+      // TODO: deprecate those replacements in favor of what AWS SDK supports out of the box.
+      endpoint = endpoint.replace(/{service}/, 's3').replace(/{region}/, s3.region)
+    }
+
+    /** @type {import('@aws-sdk/client-s3').S3ClientConfig} */
     let s3ClientOptions = {
-      endpoint: s3.endpoint,
       region: s3.region,
     }
-    if (typeof s3.endpoint === 'string') {
-      // TODO: deprecate those replacements in favor of what AWS SDK supports out of the box.
-      s3ClientOptions.endpoint = s3.endpoint.replace(/{service}/, 's3').replace(/{region}/, s3.region)
-    }
 
-    if (s3.useAccelerateEndpoint && s3.bucket != null) {
+    if (s3.useAccelerateEndpoint) {
+      // https://github.com/transloadit/uppy/issues/4809#issuecomment-1847320742
+      if (createPresignedPostMode) {
+        if (s3.bucket != null) {
+          s3ClientOptions = {
+            ...s3ClientOptions,
+            useAccelerateEndpoint: true,
+            // This is a workaround for lacking support for useAccelerateEndpoint in createPresignedPost
+            // See https://github.com/transloadit/uppy/issues/4135#issuecomment-1276450023
+            endpoint: `https://${s3.bucket}.s3-accelerate.amazonaws.com/`,
+          }
+        }
+      } else { // normal useAccelerateEndpoint mode
+        s3ClientOptions = {
+          ...s3ClientOptions,
+          useAccelerateEndpoint: true,
+        }
+      }
+    } else { // no accelearate, we allow custom s3 endpoint
       s3ClientOptions = {
         ...s3ClientOptions,
-        useAccelerateEndpoint: true,
-        bucketEndpoint: true,
-        // This is a workaround for lacking support for useAccelerateEndpoint in createPresignedPost
-        // See https://github.com/transloadit/uppy/issues/4135#issuecomment-1276450023
-        endpoint: `https://${s3.bucket}.s3-accelerate.amazonaws.com/`,
+        endpoint,
       }
     }
 

+ 9 - 7
packages/@uppy/companion/test/__tests__/provider-manager.js

@@ -5,6 +5,8 @@ const { setDefaultEnv } = require('../mockserver')
 let grantConfig
 let companionOptions
 
+const getAuthProvider = (providerName) => providerManager.getDefaultProviders()[providerName]?.authProvider
+
 describe('Test Provider options', () => {
   beforeEach(() => {
     setDefaultEnv()
@@ -14,7 +16,7 @@ describe('Test Provider options', () => {
   })
 
   test('adds provider options', () => {
-    providerManager.addProviderOptions(companionOptions, grantConfig)
+    providerManager.addProviderOptions(companionOptions, grantConfig, getAuthProvider)
     expect(grantConfig.dropbox.key).toBe('dropbox_key')
     expect(grantConfig.dropbox.secret).toBe('dropbox_secret')
 
@@ -33,7 +35,7 @@ describe('Test Provider options', () => {
 
   test('adds extra provider config', () => {
     process.env.COMPANION_INSTAGRAM_KEY = '123456'
-    providerManager.addProviderOptions(getCompanionOptions(), grantConfig)
+    providerManager.addProviderOptions(getCompanionOptions(), grantConfig, getAuthProvider)
     expect(grantConfig.instagram).toEqual({
       transport: 'session',
       callback: '/instagram/callback',
@@ -102,7 +104,7 @@ describe('Test Provider options', () => {
 
     companionOptions = getCompanionOptions()
 
-    providerManager.addProviderOptions(companionOptions, grantConfig)
+    providerManager.addProviderOptions(companionOptions, grantConfig, getAuthProvider)
 
     expect(grantConfig.dropbox.secret).toBe('xobpord')
     expect(grantConfig.box.secret).toBe('xwbepqd')
@@ -116,7 +118,7 @@ describe('Test Provider options', () => {
     delete companionOptions.server.host
     delete companionOptions.server.protocol
 
-    providerManager.addProviderOptions(companionOptions, grantConfig)
+    providerManager.addProviderOptions(companionOptions, grantConfig, getAuthProvider)
     expect(grantConfig.dropbox.key).toBeUndefined()
     expect(grantConfig.dropbox.secret).toBeUndefined()
 
@@ -135,7 +137,7 @@ describe('Test Provider options', () => {
 
   test('sets a main redirect uri, if oauthDomain is set', () => {
     companionOptions.server.oauthDomain = 'domain.com'
-    providerManager.addProviderOptions(companionOptions, grantConfig)
+    providerManager.addProviderOptions(companionOptions, grantConfig, getAuthProvider)
 
     expect(grantConfig.dropbox.redirect_uri).toBe('http://domain.com/dropbox/redirect')
     expect(grantConfig.box.redirect_uri).toBe('http://domain.com/box/redirect')
@@ -158,8 +160,8 @@ describe('Test Custom Provider options', () => {
       },
     }, providers, grantConfig)
 
-    expect(grantConfig.foo.key).toBe('foo_key')
-    expect(grantConfig.foo.secret).toBe('foo_secret')
+    expect(grantConfig.some_provider.key).toBe('foo_key')
+    expect(grantConfig.some_provider.secret).toBe('foo_secret')
     expect(providers.foo).toBeTruthy()
   })
 })

+ 7 - 0
packages/@uppy/core/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/core
 
+## 3.8.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/core: Fix onBeforeFileAdded with Golden Retriever (Merlijn Vos / #4799)
+
 ## 3.7.1
 
 Released: 2023-11-12

+ 1 - 1
packages/@uppy/core/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/core",
   "description": "Core module for the extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
-  "version": "3.7.1",
+  "version": "3.8.0",
   "license": "MIT",
   "main": "lib/index.js",
   "style": "dist/style.min.css",

+ 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

File diff suppressed because it is too large
+ 315 - 147
packages/@uppy/core/src/Uppy.test.ts


File diff suppressed because it is too large
+ 530 - 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"
+    }
+  ]
+}

+ 1 - 1
packages/@uppy/dropbox/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/dropbox",
   "description": "Import files from Dropbox, into Uppy.",
-  "version": "3.1.4",
+  "version": "3.2.0",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",

+ 1 - 1
packages/@uppy/facebook/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/facebook",
   "description": "Import files from Facebook, into Uppy.",
-  "version": "3.1.3",
+  "version": "3.2.0",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",

+ 1 - 1
packages/@uppy/google-drive/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/google-drive",
   "description": "The Google Drive plugin for Uppy lets users import files from their Google Drive account",
-  "version": "3.3.0",
+  "version": "3.4.0",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",

+ 7 - 0
packages/@uppy/image-editor/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/image-editor
 
+## 2.4.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/image-editor: respect `cropperOptions.initialAspectRatio` (Lucklj521 / #4805)
+
 ## 2.3.0
 
 Released: 2023-11-08

+ 1 - 1
packages/@uppy/image-editor/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/image-editor",
   "description": "Image editor and cropping UI",
-  "version": "2.3.0",
+  "version": "2.4.0",
   "license": "MIT",
   "main": "lib/index.js",
   "style": "dist/style.min.css",

+ 1 - 1
packages/@uppy/instagram/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/instagram",
   "description": "Import photos and videos from Instagram, into Uppy.",
-  "version": "3.1.3",
+  "version": "3.2.0",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",

+ 1 - 1
packages/@uppy/onedrive/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/onedrive",
   "description": "Import files from OneDrive, into Uppy.",
-  "version": "3.1.4",
+  "version": "3.2.0",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",

+ 7 - 0
packages/@uppy/provider-views/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/provider-views
 
+## 3.8.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/provider-views: fix uploadRemoteFile undefined (Mikael Finstad / #4814)
+
 ## 3.5.0
 
 Released: 2023-08-15

+ 1 - 1
packages/@uppy/provider-views/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/provider-views",
   "description": "View library for Uppy remote provider plugins.",
-  "version": "3.7.0",
+  "version": "3.8.0",
   "license": "MIT",
   "main": "lib/index.js",
   "style": "dist/style.min.css",

+ 3 - 1
packages/@uppy/provider-views/src/ProviderView/ProviderView.jsx

@@ -75,6 +75,8 @@ export default class ProviderView extends View {
       isSearchVisible: false,
       currentSelection: [],
     })
+
+    this.registerRequestClient()
   }
 
   // eslint-disable-next-line class-methods-use-this
@@ -399,7 +401,7 @@ export default class ProviderView extends View {
         // finished all async operations before we add any file
         // see https://github.com/transloadit/uppy/pull/4384
         this.plugin.uppy.log('Adding files from a remote provider')
-        this.plugin.uppy.addFiles(newFiles.map((file) => this.getTagFile(file)))
+        this.plugin.uppy.addFiles(newFiles.map((file) => this.getTagFile(file, this.requestClientId)))
 
         this.plugin.setPluginState({ filterInput: '' })
         messages.forEach(message => this.plugin.uppy.info(message))

+ 2 - 0
packages/@uppy/provider-views/src/SearchProviderView/SearchProviderView.jsx

@@ -54,6 +54,8 @@ export default class SearchProviderView extends View {
 
     // Set default state for the plugin
     this.plugin.setPluginState(this.defaultState)
+
+    this.registerRequestClient()
   }
 
   // eslint-disable-next-line class-methods-use-this

+ 6 - 6
packages/@uppy/provider-views/src/View.js

@@ -57,6 +57,11 @@ export default class View {
     uppy.info({ message, details: error.toString() }, 'error', 5000)
   }
 
+  registerRequestClient() {
+    this.requestClientId = this.provider.provider;
+    this.plugin.uppy.registerRequestClient(this.requestClientId, this.provider)
+  }
+
   // todo document what is a "tagFile" or get rid of this concept
   getTagFile (file) {
     const tagFile = {
@@ -78,15 +83,10 @@ export default class View {
         },
         providerName: this.provider.name,
         provider: this.provider.provider,
+        requestClientId: this.requestClientId,
       },
     }
 
-    // all properties on this object get saved into the Uppy store.
-    // Some users might serialize their store (for example using JSON.stringify),
-    // or when using Golden Retriever it will serialize state into e.g. localStorage.
-    // However RequestClient is not serializable so we need to prevent it from being serialized.
-    Object.defineProperty(tagFile.remote, 'requestClient', { value: this.provider, enumerable: false })
-
     const fileType = getFileType(tagFile)
 
     // TODO Should we just always use the thumbnail URL if it exists?

+ 1 - 1
packages/@uppy/store-default/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/store-default",
   "description": "The default simple object-based store for Uppy.",
-  "version": "3.1.0",
+  "version": "3.2.0",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",

+ 1 - 1
packages/@uppy/tus/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/tus",
   "description": "Resumable uploads for Uppy using Tus.io",
-  "version": "3.4.0",
+  "version": "3.5.0",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",

+ 1 - 1
packages/@uppy/tus/src/index.js

@@ -489,7 +489,7 @@ export default class Tus extends BasePlugin {
         }
         this.uppy.on('file-removed', removedHandler)
 
-        const uploadPromise = file.remote.requestClient.uploadRemoteFile(
+        const uploadPromise = this.uppy.getRequestClientForFile(file).uploadRemoteFile(
           file,
           this.#getCompanionClientArgs(file),
           { signal: controller.signal, getQueue },

+ 1 - 1
packages/@uppy/url/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/url",
   "description": "The Url plugin lets users import files from the Internet. Paste any URL and it’ll be added!",
-  "version": "3.4.0",
+  "version": "3.5.0",
   "license": "MIT",
   "main": "lib/index.js",
   "style": "dist/style.min.css",

+ 6 - 2
packages/@uppy/url/src/Url.jsx

@@ -48,6 +48,7 @@ function getFileNameFromUrl (url) {
   const { pathname } = new URL(url)
   return pathname.substring(pathname.lastIndexOf('/') + 1)
 }
+
 /**
  * Url
  *
@@ -55,6 +56,8 @@ function getFileNameFromUrl (url) {
 export default class Url extends UIPlugin {
   static VERSION = packageJson.version
 
+  static requestClientId = Url.name
+
   constructor (uppy, opts) {
     super(uppy, opts)
     this.id = this.opts.id || 'Url'
@@ -88,6 +91,8 @@ export default class Url extends UIPlugin {
       companionHeaders: this.opts.companionHeaders,
       companionCookiesRule: this.opts.companionCookiesRule,
     })
+
+    this.uppy.registerRequestClient(Url.requestClientId, this.client)
   }
 
   getMeta (url) {
@@ -132,11 +137,10 @@ export default class Url extends UIPlugin {
             fileId: url,
             url,
           },
+          requestClientId: Url.requestClientId,
         },
       }
 
-      Object.defineProperty(tagFile.remote, 'requestClient', { value: this.client, enumerable: false })
-
       this.uppy.log('[Url] Adding remote file')
       try {
         return this.uppy.addFile(tagFile)

+ 7 - 0
packages/@uppy/utils/CHANGELOG.md

@@ -1,5 +1,12 @@
 # @uppy/utils
 
+## 5.7.0
+
+Released: 2023-12-12
+Included in: Uppy v3.21.0
+
+- @uppy/utils: fix import in test files (Antoine du Hamel / #4806)
+
 ## 5.6.0
 
 Released: 2023-11-08

+ 3 - 1
packages/@uppy/utils/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/utils",
   "description": "Shared utility functions for Uppy Core and plugins maintained by the Uppy team.",
-  "version": "5.6.0",
+  "version": "5.7.0",
   "license": "MIT",
   "type": "module",
   "keywords": [
@@ -64,6 +64,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 - 119
packages/@uppy/utils/src/EventManager.ts

@@ -1,119 +1,3 @@
-import type {
-  Uppy,
-  UploadPauseCallback,
-  FileRemovedCallback,
-  UploadRetryCallback,
-  GenericEventCallback,
-  // @ts-expect-error @uppy/core has not been typed yet
-} 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) {
-          // @ts-expect-error @uppy/core has not been typed yet
-          cb(isPaused)
-        }
-      },
-    )
-  }
-
-  onFileRemove(
-    fileID: UppyFile['id'],
-    cb: (isPaused: UppyFile['id']) => void,
-  ): void {
-    this.on('file-removed', (file: Parameters<FileRemovedCallback<any>>[0]) => {
-      // @ts-expect-error @uppy/core has not been typed yet
-      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)
-          // @ts-expect-error @uppy/core has not been typed yet
-          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

Some files were not shown because too many files changed in this diff