Browse Source

Merge branch `3.x`

Antoine du Hamel 2 years ago
parent
commit
4996f44cf9
100 changed files with 1032 additions and 1035 deletions
  1. 20 2
      .eslintrc.js
  2. 1 3
      .github/workflows/ci.yml
  3. 0 38
      .github/workflows/companion.yml
  4. 0 76
      .github/workflows/release-beta-candidate.yml
  5. 0 2
      .github/workflows/release-candidate.yml
  6. 4 4
      .github/workflows/release.yml
  7. 13 0
      .yarn/patches/@types-connect-redis-npm-0.0.18-4fd2b614d3
  8. 1 1
      BUNDLE-README.md
  9. 153 1
      CHANGELOG.md
  10. 10 10
      README.md
  11. 0 1
      __mocks__/nanoid/non-secure.js
  12. 12 8
      bin/build-bundle.mjs
  13. 3 3
      bin/build-css.js
  14. 11 146
      bin/build-lib.js
  15. 3 3
      bin/upload-to-cdn.js
  16. 1 1
      e2e/cypress/integration/dashboard-transloadit.spec.ts
  17. 1 1
      examples/angular-example/e2e/protractor.conf.js
  18. 1 1
      examples/angular-example/karma.conf.js
  19. 1 1
      examples/angular-example/package.json
  20. 1 1
      examples/aws-companion/.gitignore
  21. 23 0
      examples/aws-companion/README.md
  22. 1 2
      examples/aws-companion/index.html
  23. 9 5
      examples/aws-companion/main.js
  24. 16 13
      examples/aws-companion/package.json
  25. 0 23
      examples/aws-companion/readme.md
  26. 21 13
      examples/aws-companion/server.cjs
  27. 11 0
      examples/cdn-example/README.md
  28. 36 12
      examples/cdn-example/index.html
  29. 5 2
      examples/cdn-example/package.json
  30. 0 4
      examples/custom-provider/.gitignore
  31. 25 0
      examples/custom-provider/README.md
  32. 0 14
      examples/custom-provider/babel.config.js
  33. 14 11
      examples/custom-provider/client/MyCustomProvider.jsx
  34. 8 5
      examples/custom-provider/client/main.js
  35. 1 2
      examples/custom-provider/index.html
  36. 0 0
      examples/custom-provider/output/.empty
  37. 13 9
      examples/custom-provider/package.json
  38. 0 21
      examples/custom-provider/readme.md
  39. 85 0
      examples/custom-provider/server/CustomProvider.cjs
  40. 0 122
      examples/custom-provider/server/customprovider.js
  41. 13 8
      examples/custom-provider/server/index.cjs
  42. 9 5
      examples/digitalocean-spaces/server.js
  43. 1 1
      examples/node-xhr/server.js
  44. 1 1
      examples/svelte-example/rollup.config.js
  45. 1 1
      examples/transloadit-textarea/index.html
  46. 1 1
      examples/transloadit/main.js
  47. 2 2
      examples/transloadit/server.js
  48. 2 2
      examples/uppy-with-companion/client/index.html
  49. 2 1
      examples/uppy-with-companion/server/index.js
  50. 8 18
      examples/vue/README.md
  51. 0 5
      examples/vue/babel.config.js
  52. 13 0
      examples/vue/index.html
  53. 13 15
      examples/vue/package.json
  54. 0 1
      examples/vue/src/App.vue
  55. 0 58
      examples/vue/src/components/HelloWorld.vue
  56. 7 0
      examples/vue/vite.config.js
  57. 9 0
      examples/vue3/README.md
  58. 10 10
      examples/vue3/index.html
  59. 9 6
      examples/vue3/package.json
  60. 20 20
      examples/vue3/src/App.vue
  61. 0 8
      examples/vue3/src/index.css
  62. 0 1
      examples/vue3/src/main.js
  63. 7 0
      examples/vue3/vite.config.js
  64. 9 7
      package.json
  65. 3 2
      packages/@uppy/audio/package.json
  66. 14 0
      packages/@uppy/aws-s3-multipart/CHANGELOG.md
  67. 3 2
      packages/@uppy/aws-s3-multipart/package.json
  68. 58 50
      packages/@uppy/aws-s3-multipart/src/MultipartUploader.js
  69. 0 5
      packages/@uppy/aws-s3-multipart/src/index.js
  70. 85 44
      packages/@uppy/aws-s3-multipart/src/index.test.js
  71. 1 1
      packages/@uppy/aws-s3-multipart/types/index.d.ts
  72. 1 2
      packages/@uppy/aws-s3-multipart/types/index.test-d.ts
  73. 7 0
      packages/@uppy/aws-s3/CHANGELOG.md
  74. 4 3
      packages/@uppy/aws-s3/package.json
  75. 3 2
      packages/@uppy/box/package.json
  76. 3 2
      packages/@uppy/companion-client/package.json
  77. 39 0
      packages/@uppy/companion/CHANGELOG.md
  78. 2 1
      packages/@uppy/companion/README.md
  79. 9 12
      packages/@uppy/companion/package.json
  80. 5 21
      packages/@uppy/companion/src/companion.js
  81. 20 15
      packages/@uppy/companion/src/config/companion.js
  82. 23 33
      packages/@uppy/companion/src/server/Uploader.js
  83. 2 2
      packages/@uppy/companion/src/server/controllers/oauth-redirect.js
  84. 1 1
      packages/@uppy/companion/src/server/controllers/send-token.js
  85. 1 3
      packages/@uppy/companion/src/server/controllers/url.js
  86. 1 1
      packages/@uppy/companion/src/server/emitter/default-emitter.js
  87. 86 17
      packages/@uppy/companion/src/server/emitter/redis-emitter.js
  88. 1 2
      packages/@uppy/companion/src/server/header-blacklist.js
  89. 0 1
      packages/@uppy/companion/src/server/helpers/jwt.js
  90. 1 2
      packages/@uppy/companion/src/server/helpers/oauth-state.js
  91. 4 4
      packages/@uppy/companion/src/server/helpers/request.js
  92. 3 1
      packages/@uppy/companion/src/server/helpers/utils.js
  93. 3 3
      packages/@uppy/companion/src/server/jobs.js
  94. 1 1
      packages/@uppy/companion/src/server/logger.js
  95. 5 8
      packages/@uppy/companion/src/server/middlewares.js
  96. 0 2
      packages/@uppy/companion/src/server/provider/Provider.js
  97. 0 54
      packages/@uppy/companion/src/server/provider/ProviderCompat.js
  98. 0 2
      packages/@uppy/companion/src/server/provider/SearchProvider.js
  99. 1 1
      packages/@uppy/companion/src/server/provider/box/adapter.js
  100. 1 3
      packages/@uppy/companion/src/server/provider/box/index.js

+ 20 - 2
.eslintrc.js

@@ -32,6 +32,7 @@ module.exports = {
     // extra:
     'compat',
     'jsdoc',
+    'unicorn',
   ],
   parser: '@babel/eslint-parser',
   parserOptions: {
@@ -73,6 +74,7 @@ module.exports = {
     'node/handle-callback-err': 'error',
     'prefer-destructuring': 'error',
     'prefer-spread': 'error',
+    'unicorn/prefer-node-protocol': 'error',
 
     // transloadit rules we would like to enforce in the future
     // but will require separate PRs to gradually get there
@@ -122,7 +124,7 @@ module.exports = {
     'jsdoc/check-examples': 'off', // cannot yet be supported for ESLint 8, see https://github.com/eslint/eslint/issues/14745
     'jsdoc/check-param-names': ['warn'],
     'jsdoc/check-syntax': ['warn'],
-    'jsdoc/check-tag-names': 'error',
+    'jsdoc/check-tag-names': ['error', { jsxTags: true }],
     'jsdoc/check-types': 'error',
     'jsdoc/newline-after-description': 'error',
     'jsdoc/valid-types': 'error',
@@ -189,8 +191,10 @@ module.exports = {
       files: [
         '*.mjs',
         'e2e/clients/**/*.js',
+        'examples/aws-companion/*.js',
         'examples/aws-presigned-url/*.js',
         'examples/bundled/*.js',
+        'examples/custom-provider/client/*.js',
         'private/dev/*.js',
         'private/release/*.js',
         'private/remark-lint-uppy/*.js',
@@ -220,6 +224,7 @@ module.exports = {
         'packages/@uppy/onedrive/src/**/*.js',
         'packages/@uppy/progress-bar/src/**/*.js',
         'packages/@uppy/provider-views/src/**/*.js',
+        'packages/@uppy/react/src/**/*.js',
         'packages/@uppy/redux-dev-tools/src/**/*.js',
         'packages/@uppy/remote-sources/src/**/*.js',
         'packages/@uppy/screen-capture/src/**/*.js',
@@ -248,6 +253,8 @@ module.exports = {
       },
       rules: {
         'import/named': 'off', // Disabled because that rule tries and fails to parse JSX dependencies.
+        'import/no-named-as-default': 'off', // Disabled because that rule tries and fails to parse JSX dependencies.
+        'import/no-named-as-default-member': 'off', // Disabled because that rule tries and fails to parse JSX dependencies.
         'no-restricted-globals': [
           'error',
           {
@@ -274,6 +281,16 @@ module.exports = {
         'import/extensions': ['error', 'ignorePackages'],
       },
     },
+    {
+      files: ['packages/uppy/*.mjs'],
+      rules: {
+        'import/first': 'off',
+        'import/newline-after-import': 'off',
+        'import/no-extraneous-dependencies': ['error', {
+          devDependencies: true,
+        }],
+      },
+    },
     {
       files: [
         'packages/@uppy/*/types/*.d.ts',
@@ -347,7 +364,8 @@ module.exports = {
       files: [
         'bin/**.js',
         'bin/**.mjs',
-        'examples/**/*.js',
+        'examples/**/*.config.js',
+        'examples/**/*.cjs',
         'packages/@uppy/companion/test/**/*.js',
         'test/**/*.js',
         'test/**/*.ts',

+ 1 - 3
.github/workflows/ci.yml

@@ -13,12 +13,10 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        node-version: [12.x, 14.x, 16.x, 17.x]
+        node-version: [14.x, 16.x, 18.x]
     steps:
       - name: Checkout sources
         uses: actions/checkout@v3
-      - name: Install Corepack if needed
-        run: corepack -v || npm install -g corepack
       - name: Get yarn cache directory path
         id: yarn-cache-dir-path
         run: echo "::set-output name=dir::$(corepack yarn config get cacheFolder)"

+ 0 - 38
.github/workflows/companion.yml

@@ -7,44 +7,6 @@ on:
     types: [ opened, synchronize, reopened ]
 
 jobs:
-  test-legacy:
-    name: Unit tests (legacy)
-    runs-on: ubuntu-latest
-    strategy:
-      matrix:
-        node-version: [10.20.1, 12.x, 17.x]
-    steps:
-      - name: Checkout sources
-        uses: actions/checkout@v3
-      - name: Get yarn cache directory path
-        id: yarn-cache-dir-path
-        run: echo "::set-output name=dir::$(corepack yarn config get cacheFolder)"
-
-      - uses: actions/cache@v3
-        id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
-        with:
-          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
-          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
-          restore-keys: |
-            ${{ runner.os }}-yarn-
-      - name: Install Node.js
-        uses: actions/setup-node@v3
-        with:
-          node-version: ${{matrix.node-version}}
-      - name: Install Corepack if needed
-        run: corepack -v || npm install -g corepack
-      - name: Install dependencies
-        run: corepack yarn@3.1.1 install --no-immutable
-        env:
-          # Necessary for Node.js v10.x
-          NODE_OPTIONS: --experimental-worker
-          YARN_IGNORE_NODE: 1
-      - name: Run tests
-        run: corepack yarn run test:companion
-        env:
-          # Necessary for Node.js v10.x
-          NODE_OPTIONS: --experimental-worker
-          YARN_IGNORE_NODE: 1
   test:
     name: Unit tests
     runs-on: ubuntu-latest

+ 0 - 76
.github/workflows/release-beta-candidate.yml

@@ -1,76 +0,0 @@
-name: Release beta candidate
-on:
-  push:
-    branches: release-beta
-
-env:
-  BETA_BRANCH: 3.x
-
-jobs:
-  prepare-beta-release:
-    name: Prepare release candidate Pull Request
-    runs-on: ubuntu-latest
-    steps:
-      - name: Checkout sources
-        uses: actions/checkout@v3
-        with:
-          branch: release-beta
-          fetch-depth: 3 # the prepare commit, the merge commit, and the base ones.
-      - name: Get yarn cache directory path
-        id: yarn-cache-dir-path
-        run: echo "::set-output name=dir::$(corepack yarn config get cacheFolder)"
-
-      - uses: actions/cache@v3
-        id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
-        with:
-          path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
-          key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
-          restore-keys: |
-            ${{ runner.os }}-yarn-
-      - name: Install Node.js
-        uses: actions/setup-node@v3
-        with:
-          node-version: lts/*
-      - name: Install dependencies
-        run: corepack yarn install --immutable
-      - name: Bump candidate packages version
-        run: corepack yarn version apply --all --prerelease=beta.%n --json | jq -s > releases.json
-      - name: Prepare changelog
-        run: corepack yarn workspace @uppy-dev/release update-changelogs releases.json | xargs git add
-      - name: Update CDN URLs
-        run: corepack yarn workspace @uppy-dev/release update-version-URLs | xargs git add
-      - name: Stage changes and remove temp files
-        run: |
-          git rm -rf .yarn/versions
-          git rm CHANGELOG.next.md
-          jq -r 'map(.cwd) | join("\n")' < releases.json | awk '{ print "git add " $0 "/package.json" }' | sh
-      - name: Commit
-        run: |
-          echo "Release: uppy@$(jq -r 'map(select(.ident == "uppy"))[0].newVersion' < releases.json)" > commitMessage
-          echo >> commitMessage
-          echo "This is a release candidate for the following packages:" >> commitMessage
-          echo >> commitMessage
-          jq -r 'map("- `"+.ident+"`: "+.oldVersion+" -> "+.newVersion) | join("\n") ' < releases.json >> commitMessage
-          git config --global user.email "actions@github.com"
-          git config --global user.name "GitHub Actions"
-          git commit -n --amend --file commitMessage
-      - name: Open Pull Request
-        id: pr_opening
-        run: |
-          git push origin HEAD:release-candidate
-          gh api repos/${{ github.repository }}/pulls \
-            -F base="${{ env.BETA_BRANCH }}" \
-            -F head="release-candidate" \
-            -F title="$(head -1 commitMessage)" \
-            -F body="$(git --no-pager diff HEAD^ -- CHANGELOG.md | awk '{ if( substr($0,0,1) == "+" && $1 != "+##" && $1 != "+Released:" && $1 != "+++" ) { print substr($0,2) } }')" \
-            --jq '.number | tostring | "##[set-output name=pr_number;]"+.'
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      - name: Assign to the releaser
-        run: echo '{"assignees":[${{ toJSON(github.actor) }}]}' | gh api repos/${{ github.repository }}/issues/${{ steps.pr_opening.outputs.pr_number }}/assignees --input -
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      - name: Enable Release workflow
-        run: gh workflow enable Release --repo ${{ github.repository }}
-        env:
-          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

+ 0 - 2
.github/workflows/release-candidate.yml

@@ -39,8 +39,6 @@ jobs:
         run: corepack yarn version apply --all --json | jq -s > releases.json
       - name: Prepare changelog
         run: corepack yarn workspace @uppy-dev/release update-changelogs releases.json | xargs git add
-      - name: Update contributors table
-        run: corepack yarn contributors:save && corepack yarn remark -foq README.md && git add README.md
       - name: Update CDN URLs
         run: corepack yarn workspace @uppy-dev/release update-version-URLs | xargs git add
       - name: Stage changes and remove temp files

+ 4 - 4
.github/workflows/release.yml

@@ -41,7 +41,7 @@ jobs:
       - name: Login to NPM
         run: corepack yarn config set npmAuthToken ${{ toJSON(secrets.NPM_TOKEN) }}
       - name: Publish to NPM
-        run: corepack yarn workspaces foreach --no-private npm publish --access public --tolerate-republish
+        run: corepack yarn workspaces foreach --no-private npm publish --access public --tag next --tolerate-republish
       - name: Merge PR
         id: merge
         run: |
@@ -62,7 +62,7 @@ jobs:
         id: uppyVersion
         run: jq -r '"##[set-output name=version;]"+.version' < packages/uppy/package.json
       - name: Create GitHub release
-        run: gh release create uppy@${{ steps.uppyVersion.outputs.version }} -t "Uppy ${{ steps.uppyVersion.outputs.version }}" -F CHANGELOG.diff.md
+        run: gh release create uppy@${{ steps.uppyVersion.outputs.version }} -t "Uppy ${{ steps.uppyVersion.outputs.version }}" -F CHANGELOG.diff.md --prerelease
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
       - name: Upload `uppy` to CDN
@@ -84,8 +84,8 @@ jobs:
         run: gh api -X DELETE repos/${{ github.repository }}/git/refs/heads/release-candidate || echo "Already deleted"
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      - name: Remove release branch
-        run: gh api -X DELETE repos/${{ github.repository }}/git/refs/heads/release
+      - name: Remove release-beta branch
+        run: gh api -X DELETE repos/${{ github.repository }}/git/refs/heads/release-beta
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
       - name: Disable Release workflow

+ 13 - 0
.yarn/patches/@types-connect-redis-npm-0.0.18-4fd2b614d3

@@ -0,0 +1,13 @@
+diff --git a/index.d.ts b/index.d.ts
+index 413b15edf95c12b1b176279c82b3a9c3f06ade20..0158f6f7a95935acbb67aeea00cb94c241475565 100755
+--- a/index.d.ts
++++ b/index.d.ts
+@@ -19,7 +19,7 @@ declare module 'connect-redis' {
+     function s(options: (options?: session.SessionOptions) => express.RequestHandler): s.RedisStore;
+ 
+     namespace s {
+-        type Client = redis.RedisClient | ioRedis.Redis | ioRedis.Cluster;
++        type Client = redis.RedisClientType | ioRedis.Redis | ioRedis.Cluster;
+         interface RedisStore extends session.Store {
+             new (options: RedisStoreOptions): RedisStore;
+             client: Client;

+ 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/v2.13.2/uppy.min.js"></script>`) or bundle it with your webapp.
+this from a CDN (`<script src="https://releases.transloadit.com/uppy/v3.0.0-beta.4/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

+ 153 - 1
CHANGELOG.md

@@ -12,6 +12,159 @@ Please add your entries in this format:
 
 In the current stage we aim to release a new version at least every month.
 
+## 3.0.0-beta.4
+
+Released: 2022-08-03
+
+| Package                |      Version | Package                |      Version |
+| ---------------------- | ------------ | ---------------------- | ------------ |
+| @uppy/aws-s3-multipart | 3.0.0-beta.3 | @uppy/screen-capture   | 3.0.0-beta.2 |
+| @uppy/companion        | 4.0.0-beta.3 | @uppy/status-bar       | 3.0.0-beta.2 |
+| @uppy/core             | 3.0.0-beta.3 | @uppy/store-default    | 3.0.0-beta.2 |
+| @uppy/dashboard        | 3.0.0-beta.3 | @uppy/transloadit      | 3.0.0-beta.4 |
+| @uppy/drop-target      | 2.0.0-beta.2 | @uppy/tus              | 3.0.0-beta.2 |
+| @uppy/informer         | 3.0.0-beta.2 | @uppy/url              | 3.0.0-beta.2 |
+| @uppy/react            | 3.0.0-beta.3 | @uppy/robodog          | 3.0.0-beta.4 |
+| @uppy/remote-sources   | 1.0.0-beta.3 | uppy                   | 3.0.0-beta.4 |
+
+- @uppy/companion,@uppy/tus: Upgrade tus-js-client to 3.0.0 (Merlijn Vos / #3942)
+- meta: fix release script (Antoine du Hamel)
+- @uppy/aws-s3-multipart: Correctly handle errors for `prepareUploadParts` (Merlijn Vos / #3912)
+- @uppy/store-default: export the class, don't expose `.callbacks` (Antoine du Hamel / #3928)
+- @uppy/remote-sources: do not rely on `.name` property (Antoine du Hamel / #3941)
+- @uppy/screen-capture: fix TODOs (Antoine du Hamel / #3930)
+- @uppy/status-bar: rename internal modules (Antoine du Hamel / #3929)
+- @uppy/transloadit: remove static properties in favor of exports (Antoine du Hamel / #3927)
+- @uppy/informer: simplify `render` method (Antoine du Hamel / #3931)
+- @uppy/url: remove private methods from public API (Antoine du Hamel / #3934)
+- @uppy/dashboard: change `copyToClipboard` signature (Antoine du Hamel / #3933)
+- @uppy/drop-target: remove `isFileTransfer` from the public API (Antoine du Hamel / #3932)
+- meta: improve beta release script (Antoine du Hamel)
+
+
+## 3.0.0-beta.3
+
+Released: 2022-07-27
+
+| Package                |      Version | Package                |      Version |
+| ---------------------- | ------------ | ---------------------- | ------------ |
+| @uppy/aws-s3           | 3.0.0-beta.2 | @uppy/react            | 3.0.0-beta.2 |
+| @uppy/aws-s3-multipart | 3.0.0-beta.2 | @uppy/remote-sources   | 1.0.0-beta.2 |
+| @uppy/companion        | 4.0.0-beta.2 | @uppy/store-redux      | 3.0.0-beta.2 |
+| @uppy/compressor       | 1.0.0-beta.2 | @uppy/transloadit      | 3.0.0-beta.3 |
+| @uppy/core             | 3.0.0-beta.2 | @uppy/webcam           | 3.0.0-beta.2 |
+| @uppy/dashboard        | 3.0.0-beta.2 | @uppy/xhr-upload       | 3.0.0-beta.2 |
+| @uppy/image-editor     | 2.0.0-beta.2 | @uppy/robodog          | 3.0.0-beta.3 |
+| @uppy/locales          | 3.0.0-beta.3 | uppy                   | 3.0.0-beta.3 |
+
+- @uppy/react: Fix exports in propTypes.js to fix website build (Murderlon)
+- @uppy/dashboard,@uppy/webcam: Add support for `mobileNativeCamera` option to Webcam and Dashboard (Artur Paikin / #3844)
+- @uppy/aws-s3-multipart: make `headers` part indexed too in `prepareUploadParts` (Merlijn Vos / #3895)
+- @uppy/aws-s3,@uppy/core,@uppy/dashboard,@uppy/store-redux,@uppy/xhr-upload: upgrade `nanoid` to v4 (Antoine du Hamel / #3904)
+- @uppy/companion: update minimal supported Node.js version in the docs (Antoine du Hamel / #3902)
+- @uppy/companion: upgrade `redis` to version 4.x (Antoine du Hamel / #3589)
+- @uppy/companion: remove unnecessary ts-ignores (Mikael Finstad / #3900)
+- meta: use `node:` protocol when using Node.js built-in core modules (Antoine du Hamel / #3871)
+- meta: upgrade to Vite v3 (Antoine du Hamel / #3882)
+- @uppy/companion: remove `COMPANION_S3_GETKEY_SAFE_BEHAVIOR` env variable (Antoine du Hamel / #3869)
+- meta: fix release script for major beta versions (Antoine du Hamel)
+
+
+## 3.0.0-beta.2
+
+Released: 2022-07-06
+
+| Package              |      Version | Package              |      Version |
+| -------------------- | ------------ | -------------------- | ------------ |
+| @uppy/companion      | 4.0.0-beta.1 | @uppy/transloadit    | 3.0.0-beta.2 |
+| @uppy/locales        | 3.0.0-beta.2 | @uppy/robodog        | 3.0.0-beta.2 |
+| @uppy/provider-views | 3.0.0-beta.2 | uppy                 | 3.0.0-beta.2 |
+
+- example: fix `custom-provider` example (Antoine du Hamel / #3854)
+- example: fix Vue3 example (Antoine du Hamel / #3774)
+- @uppy/companion: remove deprecated duplicated metrics (Mikael Finstad / #3833)
+- example: update CDN example (Antoine du Hamel / #3803)
+- @uppy/companion: Companion 3 default to no s3 acl (Mikael Finstad / #3826)
+- @uppy/companion: rewrite companion.app() to return an object (Mikael Finstad / #3827)
+- @uppy/companion: remove companion provider compat api (Mikael Finstad / #3828)
+- @uppy/companion: rewrite code for node >=14 (Mikael Finstad / #3829)
+- @uppy/companion: remove chunkSize backwards compatibility (Mikael Finstad / #3830)
+- @uppy/companion: Companion: make `emitSuccess` and `emitError` private (Mikael Finstad / #3832)
+- @uppy/companion: do not use a default upload protocol (Mikael Finstad / #3834)
+
+
+## 3.0.0-beta.1
+
+Released: 2022-06-09
+
+| Package                   |      Version | Package                   |      Version |
+| ------------------------- | ------------ | ------------------------- | ------------ |
+| uppy                      | 3.0.0-beta.1 | @uppy/google-drive        | 3.0.0-beta.1 |
+| @uppy/audio               | 1.0.0-beta.1 | @uppy/informer            | 3.0.0-beta.1 |
+| @uppy/box                 | 2.0.0-beta.1 | @uppy/instagram           | 3.0.0-beta.1 |
+| @uppy/compressor          | 1.0.0-beta.1 | @uppy/locales             | 3.0.0-beta.1 |
+| @uppy/drop-target         | 2.0.0-beta.1 | @uppy/onedrive            | 3.0.0-beta.1 |
+| @uppy/image-editor        | 2.0.0-beta.1 | @uppy/progress-bar        | 3.0.0-beta.1 |
+| @uppy/remote-sources      | 1.0.0-beta.1 | @uppy/provider-views      | 3.0.0-beta.1 |
+| @uppy/svelte              | 2.0.0-beta.1 | @uppy/react               | 3.0.0-beta.1 |
+| @uppy/vue                 | 1.0.0-beta.1 | @uppy/redux-dev-tools     | 3.0.0-beta.1 |
+| @uppy/zoom                | 2.0.0-beta.1 | @uppy/robodog             | 3.0.0-beta.1 |
+| @uppy/aws-s3              | 3.0.0-beta.1 | @uppy/screen-capture      | 3.0.0-beta.1 |
+| @uppy/aws-s3-multipart    | 3.0.0-beta.1 | @uppy/status-bar          | 3.0.0-beta.1 |
+| @uppy/companion-client    | 3.0.0-beta.1 | @uppy/store-default       | 3.0.0-beta.1 |
+| @uppy/core                | 3.0.0-beta.1 | @uppy/store-redux         | 3.0.0-beta.1 |
+| @uppy/dashboard           | 3.0.0-beta.1 | @uppy/thumbnail-generator | 3.0.0-beta.1 |
+| @uppy/drag-drop           | 3.0.0-beta.1 | @uppy/transloadit         | 3.0.0-beta.1 |
+| @uppy/dropbox             | 3.0.0-beta.1 | @uppy/tus                 | 3.0.0-beta.1 |
+| @uppy/facebook            | 3.0.0-beta.1 | @uppy/unsplash            | 3.0.0-beta.1 |
+| @uppy/file-input          | 3.0.0-beta.1 | @uppy/url                 | 3.0.0-beta.1 |
+| @uppy/form                | 3.0.0-beta.1 | @uppy/webcam              | 3.0.0-beta.1 |
+| @uppy/golden-retriever    | 3.0.0-beta.1 | @uppy/xhr-upload          | 3.0.0-beta.1 |
+
+- meta: improve release process for beta branch (Antoine du Hamel / #3809)
+- uppy: refactor to ESM (Antoine du Hamel / #3807)
+- @uppy/core,@uppy/dashboard: fix types for some events (Antoine du Hamel / #3812)
+- example: update Vue2 example (Antoine du Hamel / #3802)
+
+
+## 3.0.0-beta
+
+Released: 2022-05-30
+
+| Package                   |    Version | Package                   |    Version |
+| ------------------------- | ---------- | ------------------------- | ---------- |
+| @uppy/audio               | 3.0.0-beta | @uppy/progress-bar        | 3.0.0-beta |
+| @uppy/aws-s3              | 3.0.0-beta | @uppy/provider-views      | 3.0.0-beta |
+| @uppy/aws-s3-multipart    | 3.0.0-beta | @uppy/react               | 3.0.0-beta |
+| @uppy/box                 | 3.0.0-beta | @uppy/redux-dev-tools     | 3.0.0-beta |
+| @uppy/companion           | 4.0.0-beta | @uppy/robodog             | 3.0.0-beta |
+| @uppy/companion-client    | 3.0.0-beta | @uppy/screen-capture      | 3.0.0-beta |
+| @uppy/compressor          | 3.0.0-beta | @uppy/status-bar          | 3.0.0-beta |
+| @uppy/core                | 3.0.0-beta | @uppy/store-default       | 3.0.0-beta |
+| @uppy/dashboard           | 3.0.0-beta | @uppy/store-redux         | 3.0.0-beta |
+| @uppy/drag-drop           | 3.0.0-beta | @uppy/svelte              | 3.0.0-beta |
+| @uppy/drop-target         | 3.0.0-beta | @uppy/thumbnail-generator | 3.0.0-beta |
+| @uppy/dropbox             | 3.0.0-beta | @uppy/transloadit         | 3.0.0-beta |
+| @uppy/facebook            | 3.0.0-beta | @uppy/tus                 | 3.0.0-beta |
+| @uppy/file-input          | 3.0.0-beta | @uppy/unsplash            | 3.0.0-beta |
+| @uppy/form                | 3.0.0-beta | @uppy/url                 | 3.0.0-beta |
+| @uppy/golden-retriever    | 3.0.0-beta | @uppy/utils               | 5.0.0-beta |
+| @uppy/google-drive        | 3.0.0-beta | @uppy/vue                 | 3.0.0-beta |
+| @uppy/image-editor        | 3.0.0-beta | @uppy/webcam              | 3.0.0-beta |
+| @uppy/informer            | 3.0.0-beta | @uppy/xhr-upload          | 3.0.0-beta |
+| @uppy/instagram           | 3.0.0-beta | @uppy/zoom                | 3.0.0-beta |
+| @uppy/locales             | 3.0.0-beta | uppy                      | 3.0.0-beta |
+| @uppy/onedrive            | 3.0.0-beta |                           |            |
+
+- meta: temporary adjust release script for the beta (Antoine du Hamel)
+- meta: disable ESM to CJS transform in dist files (Antoine du Hamel / #3773)
+- @uppy/companion: remove `searchProviders` wrapper & move `s3` options (Merlijn Vos / #3781)
+- meta: do not test on EOL versions of Node.js (Antoine du Hamel / #3786)
+- @uppy/companion: remove support for EOL versions of Node.js (Antoine du Hamel / #3784)
+- @uppy/react: refactor to ESM (Antoine du Hamel / #3780)
+- @uppy/transloadit: remove IE 10 hack (Antoine du Hamel / #3777)
+
+
 ## 2.13.2
 
 Released: 2022-08-02
@@ -28,7 +181,6 @@ Released: 2022-08-02
 - @uppy/tus: fix dependencies (Antoine du Hamel / #3923)
 - meta: doc: fix linter failure in `image-editor.md` (Antoine du Hamel / #3924)
 - meta: doc: Fix typo in image-editor.md (Ikko Ashimine / #3921)
-- @uppy/robodog: Fix Robodog CDN url in readme.md (Artur Paikin / #3922)
 - website: Docs and header fix (Artur Paikin / #3920)
 
 

+ 10 - 10
README.md

@@ -67,7 +67,7 @@ const uppy = new Uppy({ autoProceed: false })
 $ npm install @uppy/core @uppy/dashboard @uppy/tus
 ```
 
-Add CSS [uppy.min.css](https://releases.transloadit.com/uppy/v2.13.2/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.0.0-beta.4/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.
 
@@ -75,10 +75,10 @@ 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/v2.13.2/uppy.min.css" rel="stylesheet">
+<link href="https://releases.transloadit.com/uppy/v3.0.0-beta.4/uppy.min.css" rel="stylesheet">
 
 <!-- 2. Add JS before the closing `</body>` -->
-<script src="https://releases.transloadit.com/uppy/v2.13.2/uppy.min.js"></script>
+<script src="https://releases.transloadit.com/uppy/v3.0.0-beta.4/uppy.min.js"></script>
 
 <!-- 3. Initialize -->
 <div class="UppyDragDrop"></div>
@@ -184,7 +184,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/v2.13.2/uppy.legacy.min.js"></script>
+<script src="https://releases.transloadit.com/uppy/v3.0.0-beta.4/uppy.legacy.min.js"></script>
 ```
 
 ## FAQ
@@ -366,17 +366,17 @@ Use Uppy in your project? [Let us know](https://github.com/transloadit/uppy/issu
 :---: |:---: |:---: |:---: |:---: |:---: |
 [NaxYo](https://github.com/NaxYo) |[intenzive](https://github.com/intenzive) |[GreenJimmy](https://github.com/GreenJimmy) |[mazoruss](https://github.com/mazoruss) |[JacobMGEvans](https://github.com/JacobMGEvans) |[jdssem](https://github.com/jdssem) |
 
-[<img alt="JakubHaladej" src="https://avatars.githubusercontent.com/u/77832677?v=4&s=117" width="117">](https://github.com/JakubHaladej) |[<img alt="Jbithell" src="https://avatars.githubusercontent.com/u/8408967?v=4&s=117" width="117">](https://github.com/Jbithell) |[<img alt="jcjmcclean" src="https://avatars.githubusercontent.com/u/1822574?v=4&s=117" width="117">](https://github.com/jcjmcclean) |[<img alt="jamestiotio" src="https://avatars.githubusercontent.com/u/18364745?v=4&s=117" width="117">](https://github.com/jamestiotio) |[<img alt="janklimo" src="https://avatars.githubusercontent.com/u/7811733?v=4&s=117" width="117">](https://github.com/janklimo) |[<img alt="janwilts" src="https://avatars.githubusercontent.com/u/16721581?v=4&s=117" width="117">](https://github.com/janwilts) |
+[<img alt="Jbithell" src="https://avatars.githubusercontent.com/u/8408967?v=4&s=117" width="117">](https://github.com/Jbithell) |[<img alt="jcjmcclean" src="https://avatars.githubusercontent.com/u/1822574?v=4&s=117" width="117">](https://github.com/jcjmcclean) |[<img alt="janklimo" src="https://avatars.githubusercontent.com/u/7811733?v=4&s=117" width="117">](https://github.com/janklimo) |[<img alt="janwilts" src="https://avatars.githubusercontent.com/u/16721581?v=4&s=117" width="117">](https://github.com/janwilts) |[<img alt="vith" src="https://avatars.githubusercontent.com/u/3265539?v=4&s=117" width="117">](https://github.com/vith) |[<img alt="jessica-coursera" src="https://avatars.githubusercontent.com/u/35155465?v=4&s=117" width="117">](https://github.com/jessica-coursera) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[JakubHaladej](https://github.com/JakubHaladej) |[Jbithell](https://github.com/Jbithell) |[jcjmcclean](https://github.com/jcjmcclean) |[jamestiotio](https://github.com/jamestiotio) |[janklimo](https://github.com/janklimo) |[janwilts](https://github.com/janwilts) |
+[Jbithell](https://github.com/Jbithell) |[jcjmcclean](https://github.com/jcjmcclean) |[janklimo](https://github.com/janklimo) |[janwilts](https://github.com/janwilts) |[vith](https://github.com/vith) |[jessica-coursera](https://github.com/jessica-coursera) |
 
-[<img alt="vith" src="https://avatars.githubusercontent.com/u/3265539?v=4&s=117" width="117">](https://github.com/vith) |[<img alt="jessica-coursera" src="https://avatars.githubusercontent.com/u/35155465?v=4&s=117" width="117">](https://github.com/jessica-coursera) |[<img alt="Jmales" src="https://avatars.githubusercontent.com/u/22914881?v=4&s=117" width="117">](https://github.com/Jmales) |[<img alt="theJoeBiz" src="https://avatars.githubusercontent.com/u/189589?v=4&s=117" width="117">](https://github.com/theJoeBiz) |[<img alt="profsmallpine" src="https://avatars.githubusercontent.com/u/7328006?v=4&s=117" width="117">](https://github.com/profsmallpine) |[<img alt="chromacoma" src="https://avatars.githubusercontent.com/u/1535623?v=4&s=117" width="117">](https://github.com/chromacoma) |
+[<img alt="Jmales" src="https://avatars.githubusercontent.com/u/22914881?v=4&s=117" width="117">](https://github.com/Jmales) |[<img alt="theJoeBiz" src="https://avatars.githubusercontent.com/u/189589?v=4&s=117" width="117">](https://github.com/theJoeBiz) |[<img alt="profsmallpine" src="https://avatars.githubusercontent.com/u/7328006?v=4&s=117" width="117">](https://github.com/profsmallpine) |[<img alt="chromacoma" src="https://avatars.githubusercontent.com/u/1535623?v=4&s=117" width="117">](https://github.com/chromacoma) |[<img alt="jonathanarbely" src="https://avatars.githubusercontent.com/u/18177203?v=4&s=117" width="117">](https://github.com/jonathanarbely) |[<img alt="jderrough" src="https://avatars.githubusercontent.com/u/1108358?v=4&s=117" width="117">](https://github.com/jderrough) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[vith](https://github.com/vith) |[jessica-coursera](https://github.com/jessica-coursera) |[Jmales](https://github.com/Jmales) |[theJoeBiz](https://github.com/theJoeBiz) |[profsmallpine](https://github.com/profsmallpine) |[chromacoma](https://github.com/chromacoma) |
+[Jmales](https://github.com/Jmales) |[theJoeBiz](https://github.com/theJoeBiz) |[profsmallpine](https://github.com/profsmallpine) |[chromacoma](https://github.com/chromacoma) |[jonathanarbely](https://github.com/jonathanarbely) |[jderrough](https://github.com/jderrough) |
 
-[<img alt="jonathanarbely" src="https://avatars.githubusercontent.com/u/18177203?v=4&s=117" width="117">](https://github.com/jonathanarbely) |[<img alt="jderrough" src="https://avatars.githubusercontent.com/u/1108358?v=4&s=117" width="117">](https://github.com/jderrough) |[<img alt="jorgeepc" src="https://avatars.githubusercontent.com/u/3879892?v=4&s=117" width="117">](https://github.com/jorgeepc) |[<img alt="jszobody" src="https://avatars.githubusercontent.com/u/203749?v=4&s=117" width="117">](https://github.com/jszobody) |[<img alt="jcalonso" src="https://avatars.githubusercontent.com/u/664474?v=4&s=117" width="117">](https://github.com/jcalonso) |[<img alt="jmontoyaa" src="https://avatars.githubusercontent.com/u/158935?v=4&s=117" width="117">](https://github.com/jmontoyaa) |
+[<img alt="jorgeepc" src="https://avatars.githubusercontent.com/u/3879892?v=4&s=117" width="117">](https://github.com/jorgeepc) |[<img alt="jszobody" src="https://avatars.githubusercontent.com/u/203749?v=4&s=117" width="117">](https://github.com/jszobody) |[<img alt="jcalonso" src="https://avatars.githubusercontent.com/u/664474?v=4&s=117" width="117">](https://github.com/jcalonso) |[<img alt="jmontoyaa" src="https://avatars.githubusercontent.com/u/158935?v=4&s=117" width="117">](https://github.com/jmontoyaa) |[<img alt="tykarol" src="https://avatars.githubusercontent.com/u/9386320?v=4&s=117" width="117">](https://github.com/tykarol) |[<img alt="firesharkstudios" src="https://avatars.githubusercontent.com/u/17069637?v=4&s=117" width="117">](https://github.com/firesharkstudios) |
 :---: |:---: |:---: |:---: |:---: |:---: |
-[jonathanarbely](https://github.com/jonathanarbely) |[jderrough](https://github.com/jderrough) |[jorgeepc](https://github.com/jorgeepc) |[jszobody](https://github.com/jszobody) |[jcalonso](https://github.com/jcalonso) |[jmontoyaa](https://github.com/jmontoyaa) |
+[jorgeepc](https://github.com/jorgeepc) |[jszobody](https://github.com/jszobody) |[jcalonso](https://github.com/jcalonso) |[jmontoyaa](https://github.com/jmontoyaa) |[tykarol](https://github.com/tykarol) |[firesharkstudios](https://github.com/firesharkstudios) |
 
 [<img alt="mellow-fellow" src="https://avatars.githubusercontent.com/u/19280122?v=4&s=117" width="117">](https://github.com/mellow-fellow) |[<img alt="tykarol" src="https://avatars.githubusercontent.com/u/9386320?v=4&s=117" width="117">](https://github.com/tykarol) |[<img alt="kaspermeinema" src="https://avatars.githubusercontent.com/u/73821331?v=4&s=117" width="117">](https://github.com/kaspermeinema) |[<img alt="firesharkstudios" src="https://avatars.githubusercontent.com/u/17069637?v=4&s=117" width="117">](https://github.com/firesharkstudios) |[<img alt="kevin-west-10x" src="https://avatars.githubusercontent.com/u/65194914?v=4&s=117" width="117">](https://github.com/kevin-west-10x) |[<img alt="elkebab" src="https://avatars.githubusercontent.com/u/6313468?v=4&s=117" width="117">](https://github.com/elkebab) |
 :---: |:---: |:---: |:---: |:---: |:---: |

+ 0 - 1
__mocks__/nanoid/non-secure.js

@@ -1 +0,0 @@
-module.exports = { nanoid: () => 'cjd09qwxb000dlql4tp4doz8h' }

+ 12 - 8
bin/build-bundle.mjs

@@ -10,19 +10,18 @@ import babel from 'esbuild-plugin-babel'
 const UPPY_ROOT = new URL('../', import.meta.url)
 const PACKAGES_ROOT = new URL('./packages/', UPPY_ROOT)
 
-function buildBundle (srcFile, bundleFile, { minify = true, standalone = '', plugins, target } = {}) {
+function buildBundle (srcFile, bundleFile, { minify = true, standalone = '', plugins, target, format } = {}) {
   return esbuild.build({
     bundle: true,
     sourcemap: true,
     entryPoints: [srcFile],
     outfile: bundleFile,
-    banner: {
-      js: '"use strict";',
-    },
+    platform: 'browser',
     minify,
     keepNames: true,
     plugins,
     target,
+    format,
   }).then(() => {
     if (minify) {
       console.info(chalk.green(`✓ Built Minified Bundle [${standalone}]:`), chalk.magenta(bundleFile))
@@ -38,12 +37,17 @@ await fs.mkdir(new URL('./@uppy/locales/dist', PACKAGES_ROOT), { recursive: true
 
 const methods = [
   buildBundle(
-    './packages/uppy/bundle.js',
+    './packages/uppy/index.mjs',
+    './packages/uppy/dist/uppy.min.mjs',
+    { standalone: 'Uppy (ESM)', format: 'esm' },
+  ),
+  buildBundle(
+    './packages/uppy/bundle.mjs',
     './packages/uppy/dist/uppy.min.js',
-    { standalone: 'Uppy' },
+    { standalone: 'Uppy', format: 'iife' },
   ),
   buildBundle(
-    './packages/uppy/bundle-legacy.js',
+    './packages/uppy/bundle-legacy.mjs',
     './packages/uppy/dist/uppy.legacy.min.js',
     {
       standalone: 'Uppy (with polyfills)',
@@ -95,7 +99,7 @@ methods.push(
   ),
 )
 
-Promise.all(methods).then(() => {
+await Promise.all(methods).then(() => {
   console.info(chalk.yellow('✓ JS bundles 🎉'))
 }, (err) => {
   console.error(chalk.red('✗ Error:'), chalk.red(err.message))

+ 3 - 3
bin/build-css.js

@@ -5,9 +5,9 @@ const postcssLogical = require('postcss-logical')
 const postcssDirPseudoClass = require('postcss-dir-pseudo-class')
 const cssnano = require('cssnano')
 const chalk = require('chalk')
-const { promisify } = require('util')
-const fs = require('fs')
-const path = require('path')
+const { promisify } = require('node:util')
+const fs = require('node:fs')
+const path = require('node:path')
 const resolve = require('resolve')
 const glob = promisify(require('glob'))
 

+ 11 - 146
bin/build-lib.js

@@ -1,10 +1,10 @@
 const chalk = require('chalk')
 const babel = require('@babel/core')
 const t = require('@babel/types')
-const { promisify } = require('util')
+const { promisify } = require('node:util')
 const glob = promisify(require('glob'))
-const fs = require('fs')
-const path = require('path')
+const fs = require('node:fs')
+const path = require('node:path')
 
 const { mkdir, stat, writeFile } = fs.promises
 
@@ -21,13 +21,6 @@ const META_FILES = [
   'bin/build-lib.js',
 ]
 
-// Rollup uses get-form-data's ES modules build, and rollup-plugin-commonjs automatically resolves `.default`.
-// So, if we are being built using rollup, this require() won't have a `.default` property.
-const esPackagesThatNeedSpecialTreatmentForRollupInterop = [
-  'get-form-data',
-  'cropperjs',
-]
-
 function lastModified (file, createParentDir = false) {
   return stat(file).then((s) => s.mtime, async (err) => {
     if (err.code === 'ENOENT') {
@@ -55,29 +48,18 @@ async function isTypeModule (file) {
     // in case it hasn't been done before.
     await mkdir(path.join(packageFolder, 'lib'), { recursive: true })
   }
-  if (typeModule) {
-    await writeFile(path.join(packageFolder, 'lib', 'package.json'), '{"type":"commonjs"}')
-  }
   moduleTypeCache.set(packageFolder, typeModule)
   versionCache.set(packageFolder, version)
   return typeModule
 }
 
 // eslint-disable-next-line no-shadow
-function ExportAllDeclaration (path) {
+function transformJSXImportsToJS (path) {
   const { value } = path.node.source
   if (value.endsWith('.jsx') && (value.startsWith('./') || value.startsWith('../'))) {
     // Rewrite .jsx imports to .js:
     path.node.source.value = value.slice(0, -1) // eslint-disable-line no-param-reassign
   }
-
-  path.replaceWith(
-    t.assignmentExpression(
-      '=',
-      t.memberExpression(t.identifier('module'), t.identifier('exports')),
-      t.callExpression(t.identifier('require'), [path.node.source]),
-    ),
-  )
 }
 
 async function buildLib () {
@@ -104,19 +86,12 @@ async function buildLib () {
       }
     }
 
-    let idCounter = 0 // counter to ensure uniqueness of identifiers created by the build script.
-    const plugins = await isTypeModule(file) ? [['@babel/plugin-transform-modules-commonjs', {
-      importInterop: 'none',
-    }], {
+    const plugins = await isTypeModule(file) ? [{
       visitor: {
         // eslint-disable-next-line no-shadow
         ImportDeclaration (path) {
-          let { value } = path.node.source
-          if (value.endsWith('.jsx') && (value.startsWith('./') || value.startsWith('../'))) {
-            // Rewrite .jsx imports to .js:
-            value = path.node.source.value = value.slice(0, -1) // eslint-disable-line no-param-reassign,no-multi-assign
-          }
-          if (PACKAGE_JSON_IMPORT.test(value)
+          transformJSXImportsToJS(path)
+          if (PACKAGE_JSON_IMPORT.test(path.node.source.value)
               && path.node.specifiers.length === 1
               && path.node.specifiers[0].type === 'ImportDefaultSpecifier') {
             // Vendor-in version number from package.json files:
@@ -130,124 +105,14 @@ async function buildLib () {
                   ]))]),
               )
             }
-          } else if (path.node.specifiers[0].type === 'ImportDefaultSpecifier') {
-            const [{ local }, ...otherSpecifiers] = path.node.specifiers
-            if (otherSpecifiers.length === 1 && otherSpecifiers[0].type === 'ImportNamespaceSpecifier') {
-              // import defaultVal, * as namespaceImport from '@uppy/package'
-              // is transformed into:
-              // const defaultVal = require('@uppy/package'); const namespaceImport = defaultVal
-              path.insertAfter(
-                t.variableDeclaration('const', [
-                  t.variableDeclarator(
-                    otherSpecifiers[0].local,
-                    local,
-                  ),
-                ]),
-              )
-            } else if (otherSpecifiers.length !== 0) {
-              // import defaultVal, { exportedVal as importedName, other } from '@uppy/package'
-              // is transformed into:
-              // const defaultVal = require('@uppy/package'); const { exportedVal: importedName, other } = defaultVal
-              path.insertAfter(t.variableDeclaration('const', [t.variableDeclarator(
-                t.objectPattern(
-                  otherSpecifiers.map(specifier => t.objectProperty(
-                    t.identifier(specifier.imported.name),
-                    specifier.local,
-                  )),
-                ),
-                local,
-              )]))
-            }
-
-            let requireCall = t.callExpression(t.identifier('require'), [
-              t.stringLiteral(value),
-            ])
-            if (esPackagesThatNeedSpecialTreatmentForRollupInterop.includes(value)) {
-              requireCall = t.logicalExpression('||', t.memberExpression(requireCall, t.identifier('default')), requireCall)
-            }
-            path.replaceWith(
-              t.variableDeclaration('const', [
-                t.variableDeclarator(
-                  local,
-                  requireCall,
-                ),
-              ]),
-            )
           }
         },
-        ExportAllDeclaration,
-        // eslint-disable-next-line no-shadow,consistent-return
-        ExportNamedDeclaration (path) {
-          if (path.node.source != null) {
-            if (path.node.specifiers.length === 1
-                && path.node.specifiers[0].local.name === 'default'
-                && path.node.specifiers[0].exported.name === 'default') return ExportAllDeclaration(path)
 
-            if (path.node.specifiers.some(spec => spec.exported.name === 'default')) {
-              throw new Error('unsupported mix of named and default re-exports')
-            }
-
-            let { value } = path.node.source
-            if (value.endsWith('.jsx') && (value.startsWith('./') || value.startsWith('../'))) {
-              // Rewrite .jsx imports to .js:
-              value = path.node.source.value = value.slice(0, -1) // eslint-disable-line no-param-reassign,no-multi-assign
-            }
-
-            // If there are no default export/import involved, Babel can handle it with no problem.
-            if (path.node.specifiers.every(spec => spec.local.name !== 'default' && spec.exported.name !== 'default')) return undefined
-
-            let requireCall = t.callExpression(t.identifier('require'), [
-              t.stringLiteral(value),
-            ])
-            if (esPackagesThatNeedSpecialTreatmentForRollupInterop.includes(value)) {
-              requireCall = t.logicalExpression('||', t.memberExpression(requireCall, t.identifier('default')), requireCall)
-            }
-
-            const requireCallIdentifier = t.identifier(`_${idCounter++}`)
-            const namedExportIdentifiers = path.node.specifiers
-              .filter(spec => spec.local.name !== 'default')
-              .map(spec => [
-                t.identifier(requireCallIdentifier.name + spec.local.name),
-                t.memberExpression(requireCallIdentifier, spec.local),
-                spec,
-              ])
-            path.insertBefore(
-              t.variableDeclaration('const', [
-                t.variableDeclarator(
-                  requireCallIdentifier,
-                  requireCall,
-                ),
-                ...namedExportIdentifiers.map(([id, propertyAccessor]) => t.variableDeclarator(id, propertyAccessor)),
-              ]),
-            )
-            path.replaceWith(
-              t.exportNamedDeclaration(null, path.node.specifiers.map(spec => t.exportSpecifier(
-                spec.local.name === 'default' ? requireCallIdentifier : namedExportIdentifiers.find(([,, s]) => s === spec)[0],
-                spec.exported,
-              ))),
-            )
-          }
-        },
+        ExportAllDeclaration: transformJSXImportsToJS,
         // eslint-disable-next-line no-shadow
-        ExportDefaultDeclaration (path) {
-          const moduleExports =  t.memberExpression(t.identifier('module'), t.identifier('exports'))
-          if (!t.isDeclaration(path.node.declaration)) {
-            path.replaceWith(
-              t.assignmentExpression('=', moduleExports, path.node.declaration),
-            )
-          } else if (path.node.declaration.id != null) {
-            const { id } = path.node.declaration
-            path.insertBefore(path.node.declaration)
-            path.replaceWith(
-              t.assignmentExpression('=', moduleExports, id),
-            )
-          } else {
-            const id = t.identifier('_default')
-            path.node.declaration.id = id // eslint-disable-line no-param-reassign
-            path.insertBefore(path.node.declaration)
-            path.replaceWith(
-              t.assignmentExpression('=', moduleExports, id),
-            )
+        ExportNamedDeclaration (path) {
+          if (path.node.source != null) {
+            transformJSXImportsToJS(path)
           }
         },
       },

+ 3 - 3
bin/upload-to-cdn.js

@@ -21,9 +21,9 @@
 //
 //  - Kevin van Zonneveld <kevin@transloadit.com>
 
-const path = require('path')
-const { pipeline, finished } = require('stream/promises')
-const { readFile } = require('fs/promises')
+const path = require('node:path')
+const { pipeline, finished } = require('node:stream/promises')
+const { readFile } = require('node:fs/promises')
 const AWS = require('aws-sdk')
 const packlist = require('npm-packlist')
 const tar = require('tar')

+ 1 - 1
e2e/cypress/integration/dashboard-transloadit.spec.ts

@@ -119,7 +119,7 @@ describe('Dashboard with Transloadit', () => {
   })
 
   it('should not create assembly when all individual files have been cancelled', () => {
-    cy.get('@file-input').selectFile(['cypress/fixtures/images/cat.jpg', 'cypress/fixtures/images/traffic.jpg'], { force: true })
+    cy.get('@file-input').selectFile(['cypress/fixtures/images/cat.jpg', 'cypress/fixtures/images/traffic.jpg'], { force:true })
     cy.get('.uppy-StatusBar-actionBtn--upload').click()
 
     cy.window().then(({ uppy }) => {

+ 1 - 1
examples/angular-example/e2e/protractor.conf.js

@@ -26,7 +26,7 @@ exports.config = {
   },
   onPrepare () {
     require('ts-node').register({
-      project: require('path').join(__dirname, './tsconfig.json'),
+      project: require('node:path').join(__dirname, './tsconfig.json'),
     })
     // eslint-disable-next-line no-undef
     jasmine.getEnv().addReporter(new SpecReporter({

+ 1 - 1
examples/angular-example/karma.conf.js

@@ -25,7 +25,7 @@ module.exports = function karma (config) {
       suppressAll: true, // removes the duplicated traces
     },
     coverageReporter: {
-      dir: require('path').join(__dirname, './coverage/angular-example'),
+      dir: require('node:path').join(__dirname, './coverage/angular-example'),
       subdir: '.',
       reporters: [
         { type: 'html' },

+ 1 - 1
examples/angular-example/package.json

@@ -45,7 +45,7 @@
     "@typescript-eslint/parser": "^5.0.0",
     "eslint": "^8.0.0",
     "eslint-plugin-import": "^2.22.1",
-    "eslint-plugin-jsdoc": "^37.0.0",
+    "eslint-plugin-jsdoc": "^38.0.0",
     "eslint-plugin-prefer-arrow": "^1.2.3",
     "jasmine-core": "~3.6.0",
     "jasmine-spec-reporter": "~5.0.0",

+ 1 - 1
examples/aws-companion/.gitignore

@@ -1 +1 @@
-uppy.min.css
+tmp

+ 23 - 0
examples/aws-companion/README.md

@@ -0,0 +1,23 @@
+# Uppy + AWS S3 Example
+
+This example uses @uppy/companion with a custom AWS S3 configuration.
+Files are uploaded to a randomly named directory inside the `whatever/`
+directory in a bucket.
+
+## Run it
+
+First, set up the `COMPANION_AWS_KEY`, `COMPANION_AWS_SECRET`,
+`COMPANION_AWS_REGION`, and `COMPANION_AWS_BUCKET` environment variables for
+`@uppy/companion` in a `.env` file. You may find useful to first copy the
+`.env.example` file:
+
+```sh
+[ -f .env ] || cp .env.example .env
+```
+
+To run this example, from the **repository root**, run:
+
+```sh
+corepack yarn install
+corepack yarn workspace @uppy-example/aws-companion start
+```

+ 1 - 2
examples/aws-companion/index.html

@@ -4,9 +4,8 @@
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>Companion + AWS Example</title>
-    <link href="uppy.min.css" rel="stylesheet">
   </head>
   <body>
-    <script src="bundle.js"></script>
+    <script src="./main.js" type="module"></script>
   </body>
 </html>

+ 9 - 5
examples/aws-companion/main.js

@@ -1,8 +1,12 @@
-const Uppy = require('@uppy/core')
-const GoogleDrive = require('@uppy/google-drive')
-const Webcam = require('@uppy/webcam')
-const Dashboard = require('@uppy/dashboard')
-const AwsS3 = require('@uppy/aws-s3')
+import AwsS3 from '@uppy/aws-s3'
+import Uppy from '@uppy/core'
+import Dashboard from '@uppy/dashboard'
+import GoogleDrive from '@uppy/google-drive'
+import Webcam from '@uppy/webcam'
+
+import '@uppy/core/dist/style.css'
+import '@uppy/dashboard/dist/style.css'
+import '@uppy/webcam/dist/style.css'
 
 const uppy = new Uppy({
   debug: true,

+ 16 - 13
examples/aws-companion/package.json

@@ -1,30 +1,33 @@
 {
   "name": "@uppy-example/aws-companion",
   "version": "0.0.0",
+  "type": "module",
   "dependencies": {
-    "@babel/core": "^7.2.2",
     "@uppy/aws-s3": "workspace:*",
     "@uppy/core": "workspace:*",
     "@uppy/dashboard": "workspace:*",
     "@uppy/google-drive": "workspace:*",
-    "@uppy/webcam": "workspace:*",
-    "babelify": "^10.0.0",
-    "body-parser": "^1.18.3",
-    "budo": "^11.6.1",
+    "@uppy/webcam": "workspace:*"
+  },
+  "devDependencies": {
+    "@uppy/companion": "workspace:*",
+    "body-parser": "^1.20.0",
     "cookie-parser": "^1.4.6",
     "cors": "^2.8.5",
-    "express": "^4.16.4",
-    "express-session": "^1.15.6",
-    "npm-run-all": "^4.1.5"
+    "dotenv": "^16.0.1",
+    "express": "^4.18.1",
+    "express-session": "^1.17.3",
+    "npm-run-all": "^4.1.5",
+    "vite": "^2.7.1"
   },
   "private": true,
   "engines": {
-    "node": ">=14.14.0"
+    "node": ">=16.15.0"
   },
   "scripts": {
-    "copy": "cp ../../packages/uppy/dist/uppy.min.css .",
-    "start": "npm-run-all --serial copy --parallel start:*",
-    "start:client": "budo main.js:bundle.js -- -t babelify",
-    "start:server": "node server.js"
+    "dev": "vite",
+    "start": "npm-run-all --parallel start:client start:server",
+    "start:client": "vite",
+    "start:server": "node server.cjs"
   }
 }

+ 0 - 23
examples/aws-companion/readme.md

@@ -1,23 +0,0 @@
-# Uppy + AWS S3 Example
-
-This example uses @uppy/companion with a custom AWS S3 configuration.
-Files are uploaded to a randomly named directory inside the `whatever/` directory in a bucket.
-
-## Run it
-
-To run this example, make sure you've correctly installed the **repository root**:
-
-```bash
-npm install
-npm run build
-```
-
-That will also install the dependencies for this example.
-
-Then, set up the `COMPANION_AWS_KEY`, `COMPANION_AWS_SECRET`, `COMPANION_AWS_REGION`, and `COMPANION_AWS_BUCKET` environment variables for @uppy/companion.
-
-Then, again in the **repository root**, start this example by doing:
-
-```bash
-npm run example aws-companion
-```

+ 21 - 13
examples/aws-companion/server.js → examples/aws-companion/server.cjs

@@ -1,18 +1,24 @@
-const fs = require('fs')
-const path = require('path')
-const companion = require('../../packages/@uppy/companion')
+const fs = require('node:fs')
+const path = require('node:path')
+const crypto = require('node:crypto')
+const companion = require('@uppy/companion')
+
+require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env') })
 const app = require('express')()
 
 const DATA_DIR = path.join(__dirname, 'tmp')
 
 app.use(require('cors')({
-  origin: true,
+  origin: 'http://localhost:3000',
+  methods: ['GET', 'POST', 'OPTIONS'],
   credentials: true,
 }))
 app.use(require('cookie-parser')())
 app.use(require('body-parser').json())
 app.use(require('express-session')({
   secret: 'hello planet',
+  saveUninitialized: false,
+  resave: false,
 }))
 
 const options = {
@@ -21,14 +27,14 @@ const options = {
       key: process.env.COMPANION_GOOGLE_KEY,
       secret: process.env.COMPANION_GOOGLE_SECRET,
     },
-    s3: {
-      getKey: (req, filename) => `whatever/${Math.random().toString(32).slice(2)}/${filename}`,
-      key: process.env.COMPANION_AWS_KEY,
-      secret: process.env.COMPANION_AWS_SECRET,
-      bucket: process.env.COMPANION_AWS_BUCKET,
-      region: process.env.COMPANION_AWS_REGION,
-      endpoint: process.env.COMPANION_AWS_ENDPOINT,
-    },
+  },
+  s3: {
+    getKey: (req, filename) => `${crypto.randomUUID()}-${filename}`,
+    key: process.env.COMPANION_AWS_KEY,
+    secret: process.env.COMPANION_AWS_SECRET,
+    bucket: process.env.COMPANION_AWS_BUCKET,
+    region: process.env.COMPANION_AWS_REGION,
+    endpoint: process.env.COMPANION_AWS_ENDPOINT,
   },
   server: { host: 'localhost:3020' },
   filePath: DATA_DIR,
@@ -46,7 +52,9 @@ process.on('exit', () => {
   fs.rmSync(DATA_DIR, { recursive: true, force: true })
 })
 
-app.use(companion.app(options))
+const { app: companionApp } = companion.app(options)
+
+app.use(companionApp)
 
 const server = app.listen(3020, () => {
   console.log('listening on port 3020')

+ 11 - 0
examples/cdn-example/README.md

@@ -0,0 +1,11 @@
+# CDN example
+
+To run the example, open the `index.html` file in your browser.
+
+If you want to spawn a local webserver, you can use the following commands
+(requires Deno, Python, or PHP):
+
+```sh
+corepack yarn install
+corepack yarn workspace @uppy-example/cdn dev
+```

+ 36 - 12
examples/cdn-example/index.html

@@ -2,22 +2,46 @@
 <html lang="en">
   <head>
     <title></title>
-    <meta charset="UTF-8">
-    <meta name="viewport" content="width=device-width, initial-scale=1">
-    <link href="https://releases.transloadit.com/uppy/v2.13.2/uppy.min.css" rel="stylesheet">
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <link href="https://releases.transloadit.com/uppy/v3.0.0-beta.4/uppy.min.css" rel="stylesheet" />
   </head>
   <body>
+    <noscript>You need JavaScript enabled for this example to work.</noscript>
     <button id="uppyModalOpener">Open Modal</button>
-    <script src="https://releases.transloadit.com/uppy/v2.13.2/uppy.min.js"></script>
-    <script>
-      const uppy = new Uppy.Core({debug: true, autoProceed: false})
-        .use(Uppy.Dashboard, { trigger: '#uppyModalOpener' })
-        .use(Uppy.Webcam, {target: Uppy.Dashboard})
-        .use(Uppy.Tus, { endpoint: 'https://tusd.tusdemo.net/files/' })
 
-      uppy.on('success', (fileCount) => {
-        console.log(`${fileCount} files uploaded`)
-      })
+    <script type="module">
+      import {
+        Core,
+        Dashboard,
+        Webcam,
+        Tus,
+      } from "https://releases.transloadit.com/uppy/v3.0.0-beta.4/uppy.min.mjs";
+
+      const uppy = new Core.Uppy({ debug: true, autoProceed: false })
+        .use(Dashboard, { trigger: "#uppyModalOpener" })
+        .use(Webcam, { target: Dashboard })
+        .use(Tus, { endpoint: "https://tusd.tusdemo.net/files/" });
+
+      uppy.on("success", (fileCount) => {
+        console.log(`${fileCount} files uploaded`);
+      });
+    </script>
+
+    <!-- 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.0.0-beta.4/uppy.legacy.min.js"></script>
+    <script nomodule>
+      {
+        const { Core, Dashboard, Webcam, Tus } = Uppy;
+        const uppy = new Core.Uppy({ debug: true, autoProceed: false })
+          .use(Dashboard, { trigger: "#uppyModalOpener" })
+          .use(Webcam, { target: Dashboard })
+          .use(Tus, { endpoint: "https://tusd.tusdemo.net/files/" });
+
+        uppy.on("success", function (fileCount) {
+          console.log(`${fileCount} files uploaded`);
+        });
+      }
     </script>
   </body>
 </html>

+ 5 - 2
examples/cdn-example/package.json

@@ -1,5 +1,8 @@
 {
-  "name": "@uppy-example/cdn-example",
+  "name": "@uppy-example/cdn",
   "version": "0.0.0",
-  "private": true
+  "private": true,
+  "scripts": {
+    "dev": "deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts || python3 -m http.server || php -S localhost:8000"
+  }
 }

+ 0 - 4
examples/custom-provider/.gitignore

@@ -1,4 +0,0 @@
-uppy.min.css
-node_modules
-output/*
-!output/.empty

+ 25 - 0
examples/custom-provider/README.md

@@ -0,0 +1,25 @@
+# Uppy + Companion + Custom Provider Example
+
+This example uses @uppy/companion with a dummy custom provider.
+This serves as an illustration on how integrating custom providers would work
+
+## Run it
+
+**Note**: this example is using `fetch`, which is only available on Node.js 18+.
+
+First, you want to set up your environment variable. You can copy the content of
+`.env.example` and save it in a file named `.env`. You can modify in there all
+the information needed for the app to work that should not be committed
+(Google keys, Unsplash keys, etc.).
+
+```sh
+[ -f .env ] || cp .env.example .env
+```
+
+To run the example, from the root directory of this repo, run the following commands:
+
+```sh
+corepack yarn install
+corepack yarn build
+corepack yarn workspace @uppy-example/custom-provider start
+```

+ 0 - 14
examples/custom-provider/babel.config.js

@@ -1,14 +0,0 @@
-module.exports = (api) => {
-  api.env('test')
-  return {
-    presets: [
-      ['@babel/preset-env', {
-        modules: false,
-        loose: true,
-      }],
-    ],
-    plugins: [
-      ['@babel/plugin-transform-react-jsx', { pragma: 'h' }],
-    ].filter(Boolean),
-  }
-}

+ 14 - 11
examples/custom-provider/client/MyCustomProvider.js → examples/custom-provider/client/MyCustomProvider.jsx

@@ -1,9 +1,13 @@
-const { UIPlugin } = require('@uppy/core')
-const { Provider } = require('@uppy/companion-client')
-const { ProviderViews } = require('@uppy/provider-views')
-const { h } = require('preact')
+/** @jsx h */
 
-module.exports = class MyCustomProvider extends UIPlugin {
+import { UIPlugin } from '@uppy/core'
+import { Provider } from '@uppy/companion-client'
+import { ProviderViews } from '@uppy/provider-views'
+import { h } from 'preact'
+
+const defaultOptions = {}
+
+export default class MyCustomProvider extends UIPlugin {
   constructor (uppy, opts) {
     super(uppy, opts)
     this.type = 'acquirer'
@@ -28,15 +32,14 @@ module.exports = class MyCustomProvider extends UIPlugin {
         pluginNameMyUnsplash: 'MyUnsplash',
       },
     }
+
+    // merge default options with the ones set by user
+    this.opts = { ...defaultOptions, ...opts }
+
     this.i18nInit()
-    this.title = this.i18n('MyUnsplash')
+    this.title = this.i18n('pluginNameMyUnsplash')
 
     this.files = []
-    this.onFirstRender = this.onFirstRender.bind(this)
-    this.render = this.render.bind(this)
-
-    // merge default options with the ones set by user
-    this.opts = { ...opts }
   }
 
   install () {

+ 8 - 5
examples/custom-provider/client/main.js

@@ -1,8 +1,11 @@
-const Uppy = require('@uppy/core')
-const GoogleDrive = require('@uppy/google-drive')
-const Tus = require('@uppy/tus')
-const Dashboard = require('@uppy/dashboard')
-const MyCustomProvider = require('./MyCustomProvider')
+import Uppy from '@uppy/core'
+import GoogleDrive from '@uppy/google-drive'
+import Tus from '@uppy/tus'
+import Dashboard from '@uppy/dashboard'
+import MyCustomProvider from './MyCustomProvider.jsx'
+
+import '@uppy/core/dist/style.css'
+import '@uppy/dashboard/dist/style.css'
 
 const uppy = new Uppy({
   debug: true,

+ 1 - 2
examples/custom-provider/index.html

@@ -4,9 +4,8 @@
     <meta charset="utf-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <title>Uppy Custom provider Example</title>
-    <link href="uppy.min.css" rel="stylesheet">
   </head>
   <body>
-    <script src="bundle.js"></script>
+    <script src="./client/main.js" type="module"></script>
   </body>
 </html>

+ 0 - 0
examples/custom-provider/output/.empty


+ 13 - 9
examples/custom-provider/package.json

@@ -1,28 +1,32 @@
 {
   "name": "@uppy-example/custom-provider",
   "version": "0.0.0",
+  "type": "module",
   "dependencies": {
-    "@babel/core": "^7.2.2",
     "@uppy/companion-client": "workspace:*",
     "@uppy/core": "workspace:*",
     "@uppy/dashboard": "workspace:*",
     "@uppy/google-drive": "workspace:*",
     "@uppy/provider-views": "workspace:*",
     "@uppy/tus": "workspace:*",
-    "babelify": "^10.0.0",
+    "preact": "^10.5.13"
+  },
+  "engines": {
+    "node": ">=18.0.0"
+  },
+  "devDependencies": {
+    "@uppy/companion": "workspace:*",
     "body-parser": "^1.18.2",
-    "budo": "^11.3.2",
+    "dotenv": "^16.0.1",
     "express": "^4.16.2",
     "express-session": "^1.15.6",
     "npm-run-all": "^4.1.2",
-    "preact": "^10.5.13",
-    "request": "2.88.2"
+    "vite": "^3.0.0"
   },
   "private": true,
   "scripts": {
-    "copy": "cp ../../packages/uppy/dist/uppy.min.css .",
-    "start": "npm-run-all --serial copy --parallel start:*",
-    "start:client": "budo client/main.js:bundle.js -- -t babelify",
-    "start:server": "node server/index.js"
+    "start": "npm-run-all --parallel start:server start:client",
+    "start:client": "vite",
+    "start:server": "node server/index.cjs"
   }
 }

+ 0 - 21
examples/custom-provider/readme.md

@@ -1,21 +0,0 @@
-# Uppy + Companion + Custom Provider  Example
-
-This example uses @uppy/companion with a dummy custom provider.
-This serves as an illustration on how integrating custom providers would work
-
-## Run it
-
-To run this example, make sure you've correctly installed the **repository root**:
-
-```bash
-npm install
-npm run build
-```
-
-That will also install the dependencies for this example.
-
-Then, again in the **repository root**, start this example by doing:
-
-```bash
-npm run example custom-provider
-```

+ 85 - 0
examples/custom-provider/server/CustomProvider.cjs

@@ -0,0 +1,85 @@
+const { Readable } = require('node:stream')
+
+const BASE_URL = 'https://api.unsplash.com'
+
+function adaptData (res) {
+  const data = {
+    username: null,
+    items: [],
+    nextPagePath: null,
+  }
+
+  const items = res
+  items.forEach((item) => {
+    const isFolder = !!item.published_at
+    data.items.push({
+      isFolder,
+      icon: isFolder ? item.cover_photo.urls.thumb : item.urls.thumb,
+      name: item.title || item.description,
+      mimeType: isFolder ? null : 'image/jpeg',
+      id: item.id,
+      thumbnail: isFolder ? item.cover_photo.urls.thumb : item.urls.thumb,
+      requestPath: item.id,
+      modifiedDate: item.updated_at,
+      size: null,
+    })
+  })
+
+  return data
+}
+
+/**
+ * an example of a custom provider module. It implements @uppy/companion's Provider interface
+ */
+class MyCustomProvider {
+  static version = 2
+
+  authProvider = 'myunsplash'
+
+  // eslint-disable-next-line class-methods-use-this
+  async list ({ token, directory }) {
+    const path = directory ? `/${directory}/photos` : ''
+
+    const resp = await fetch(`${BASE_URL}/collections${path}`, {
+      headers:{
+        Authorization: `Bearer ${token}`,
+      },
+    })
+    if (!resp.ok) {
+      throw new Error(`Errornous HTTP response (${resp.status} ${resp.statusText})`)
+    }
+    return adaptData(await resp.json())
+  }
+
+  // eslint-disable-next-line class-methods-use-this
+  async download ({ id, token }) {
+    const resp = await fetch(`${BASE_URL}/photos/${id}`, {
+      headers: {
+        Authorization: `Bearer ${token}`,
+      },
+    })
+
+    if (!resp.ok) {
+      throw new Error(`Errornous HTTP response (${resp.status} ${resp.statusText})`)
+    }
+    return { stream: Readable.fromWeb(resp.body) }
+  }
+
+  // eslint-disable-next-line class-methods-use-this
+  async size ({ id, token }) {
+    const resp = await fetch(`${BASE_URL}/photos/${id}`, {
+      headers: {
+        Authorization: `Bearer ${token}`,
+      },
+    })
+
+    if (!resp.ok) {
+      throw new Error(`Errornous HTTP response (${resp.status} ${resp.statusText})`)
+    }
+
+    const { size } = await resp.json()
+    return size
+  }
+}
+
+module.exports = MyCustomProvider

+ 0 - 122
examples/custom-provider/server/customprovider.js

@@ -1,122 +0,0 @@
-const request = require('request')
-
-const BASE_URL = 'https://api.unsplash.com'
-
-function adaptData (res) {
-  const data = {
-    username: null,
-    items: [],
-    nextPagePath: null,
-  }
-
-  const items = res
-  items.forEach((item) => {
-    const isFolder = !!item.published_at
-    data.items.push({
-      isFolder,
-      icon: isFolder ? item.cover_photo.urls.thumb : item.urls.thumb,
-      name: item.title || item.description,
-      mimeType: isFolder ? null : 'image/jpeg',
-      id: item.id,
-      thumbnail: isFolder ? item.cover_photo.urls.thumb : item.urls.thumb,
-      requestPath: item.id,
-      modifiedDate: item.updated_at,
-      size: null,
-    })
-  })
-
-  return data
-}
-
-/**
- * an example of a custom provider module. It implements @uppy/companion's Provider interface
- */
-class MyCustomProvider {
-  static version = 2
-
-  constructor () {
-    this.authProvider = 'myunsplash'
-  }
-
-  // eslint-disable-next-line class-methods-use-this
-  async list ({ token, directory }) {
-    const path = directory ? `/${directory}/photos` : ''
-    const options = {
-      url: `${BASE_URL}/collections${path}`,
-      method: 'GET',
-      json: true,
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-    }
-
-    return new Promise((resolve, reject) => (
-      request(options, (err, resp, body) => {
-        if (err) {
-          console.log(err)
-          reject(err)
-          return
-        }
-
-        resolve(adaptData(body))
-      })))
-  }
-
-  // eslint-disable-next-line class-methods-use-this
-  async download ({ id, token }) {
-    const options = {
-      url: `${BASE_URL}/photos/${id}`,
-      method: 'GET',
-      json: true,
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-    }
-
-    const resp = await new Promise((resolve, reject) => {
-      const req = request(options)
-        .on('response', (response) => {
-          // Don't allow any more data to flow yet.
-          // https://github.com/request/request/issues/1990#issuecomment-184712275
-          response.pause()
-
-          if (resp.statusCode !== 200) {
-            req.abort() // Or we will leak memory
-            reject(new Error(`HTTP response ${resp.statusCode}`))
-            return
-          }
-
-          resolve(response)
-        })
-        .on('error', reject)
-    })
-
-    // The returned stream will be consumed and uploaded from the current position
-    return { stream: resp }
-  }
-
-  // eslint-disable-next-line class-methods-use-this
-  async size ({ id, token }) {
-    const options = {
-      url: `${BASE_URL}/photos/${id}`,
-      method: 'GET',
-      json: true,
-      headers: {
-        Authorization: `Bearer ${token}`,
-      },
-    }
-
-    return new Promise((resolve, reject) => (
-      request(options, (err, resp, body) => {
-        if (err) {
-          console.log(err)
-          reject(err)
-          return
-        }
-
-        resolve(body.size)
-      })))
-  }
-}
-
-module.exports = MyCustomProvider

+ 13 - 8
examples/custom-provider/server/index.js → examples/custom-provider/server/index.cjs

@@ -1,10 +1,15 @@
+const { mkdtempSync } = require('node:fs')
+const os = require('node:os')
+const path = require('node:path')
+
+require('dotenv').config({ path: path.join(__dirname, '..', '..', '..', '.env') })
 const express = require('express')
 // the ../../../packages is just to use the local version
 // instead of the npm version—in a real app use `require('@uppy/companion')`
 const bodyParser = require('body-parser')
 const session = require('express-session')
-const uppy = require('../../../packages/@uppy/companion')
-const MyCustomProvider = require('./customprovider')
+const uppy = require('@uppy/companion')
+const MyCustomProvider = require('./CustomProvider.cjs')
 
 const app = express()
 
@@ -29,8 +34,8 @@ const ACCESS_URL = 'https://unsplash.com/oauth/token'
 const uppyOptions = {
   providerOptions: {
     drive: {
-      key: 'your google drive key',
-      secret: 'your google drive secret',
+      key: process.env.COMPANION_GOOGLE_KEY,
+      secret: process.env.COMPANION_GOOGLE_SECRET,
     },
   },
   customProviders: {
@@ -40,8 +45,8 @@ const uppyOptions = {
         authorize_url: AUTHORIZE_URL,
         access_url: ACCESS_URL,
         oauth: 2,
-        key: 'your unsplash key here',
-        secret: 'your unsplash secret here',
+        key: process.env.COMPANION_UNSPLASH_KEY,
+        secret: process.env.COMPANION_UNSPLASH_SECRET,
       },
       // you provider class/module:
       module: MyCustomProvider,
@@ -51,12 +56,12 @@ const uppyOptions = {
     host: 'localhost:3020',
     protocol: 'http',
   },
-  filePath: './output',
+  filePath: mkdtempSync(path.join(os.tmpdir(), 'companion-')),
   secret: 'some-secret',
   debug: true,
 }
 
-app.use(uppy.app(uppyOptions))
+app.use(uppy.app(uppyOptions).app)
 
 // handle 404
 app.use((req, res) => {

+ 9 - 5
examples/digitalocean-spaces/server.js

@@ -1,7 +1,9 @@
-const fs = require('fs')
-const path = require('path')
+const fs = require('node:fs')
+const path = require('node:path')
 const budo = require('budo')
 const router = require('router')
+const crypto = require('node:crypto')
+
 const companion = require('../../packages/@uppy/companion')
 
 /**
@@ -27,12 +29,12 @@ const app = router()
 app.use(require('cors')())
 app.use(require('body-parser').json())
 
-app.use('/companion', companion.app({
+const { app: companionApp } = companion.app({
   providerOptions: {
     s3: {
       // This is the crucial part; set an endpoint template for the service you want to use.
       endpoint: 'https://{region}.digitaloceanspaces.com',
-      getKey: (req, filename) => `uploads/${filename}`,
+      getKey: (req, filename) => `${crypto.randomUUID()}-${filename}`,
 
       key: process.env.COMPANION_AWS_KEY,
       secret: process.env.COMPANION_AWS_SECRET,
@@ -41,7 +43,9 @@ app.use('/companion', companion.app({
     },
   },
   server: { serverUrl: `localhost:${PORT}` },
-}))
+})
+
+app.use('/companion', companionApp)
 
 // Serve the built CSS file.
 app.get('/uppy.min.css', (req, res) => {

+ 1 - 1
examples/node-xhr/server.js

@@ -1,5 +1,5 @@
 const formidable = require('formidable')
-const http = require('http')
+const http = require('node:http')
 
 http.createServer((req, res) => {
   const headers = {

+ 1 - 1
examples/svelte-example/rollup.config.js

@@ -19,7 +19,7 @@ function serve () {
   return {
     writeBundle () {
       if (server) return
-      server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
+      server = require('node:child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
         stdio: ['ignore', 'inherit', 'inherit'],
         shell: true,
       })

+ 1 - 1
examples/transloadit-textarea/index.html

@@ -2,7 +2,7 @@
 <html>
   <head>
     <meta charset="utf-8">
-    <link rel="stylesheet" href="https://releases.transloadit.com/uppy/robodog/v2.9.2/robodog.css">
+    <link rel="stylesheet" href="https://releases.transloadit.com/uppy/robodog/v3.0.0-beta.4/robodog.css">
     <style>
       body {
         font-family: Roboto, Open Sans;

+ 1 - 1
examples/transloadit/main.js

@@ -1,4 +1,4 @@
-const { inspect } = require('util')
+const { inspect } = require('node:util')
 const robodog = require('@uppy/robodog')
 
 const TRANSLOADIT_KEY = '35c1aed03f5011e982b6afe82599b6a0'

+ 2 - 2
examples/transloadit/server.js

@@ -1,6 +1,6 @@
 /* eslint-disable compat/compat */
-const http = require('http')
-const qs = require('querystring')
+const http = require('node:http')
+const qs = require('node:querystring')
 const e = require('he').encode
 
 /**

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

@@ -4,11 +4,11 @@
     <title></title>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1">
-    <link href="https://releases.transloadit.com/uppy/v2.13.2/uppy.min.css" rel="stylesheet">
+    <link href="https://releases.transloadit.com/uppy/v3.0.0-beta.4/uppy.min.css" rel="stylesheet">
   </head>
   <body>
     <button id="uppyModalOpener">Open Modal</button>
-    <script src="https://releases.transloadit.com/uppy/v2.13.2/uppy.min.js"></script>
+    <script src="https://releases.transloadit.com/uppy/v3.0.0-beta.4/uppy.min.js"></script>
     <script>
       const uppy = new Uppy.Core({debug: true, autoProceed: false})
         .use(Uppy.Dashboard, { trigger: '#uppyModalOpener' })

+ 2 - 1
examples/uppy-with-companion/server/index.js

@@ -53,7 +53,8 @@ const uppyOptions = {
   debug: true,
 }
 
-app.use(companion.app(uppyOptions))
+const { app: companionApp } = companion.app(uppyOptions)
+app.use(companionApp)
 
 // handle 404
 app.use((req, res) => {

+ 8 - 18
examples/vue/README.md

@@ -1,21 +1,11 @@
-# uppy-vue
+# Vue 2 example
 
-## Project setup
+You’re browsing the documentation for Vue v2.x and earlier. Check out
+[Vue 3 example](../vue3/) for new projects.
 
-    npm install
+To run the example, from the root directory of this repo, run the following commands:
 
-### Compiles and hot-reloads for development
-
-    npm run serve
-
-### Compiles and minifies for production
-
-    npm run build
-
-### Lints and fixes files
-
-    npm run lint
-
-### Customize configuration
-
-See [Configuration Reference](https://cli.vuejs.org/config/).
+```sh
+corepack yarn install
+corepack yarn workspace @uppy-example/vue2 dev
+```

+ 0 - 5
examples/vue/babel.config.js

@@ -1,5 +0,0 @@
-module.exports = {
-  presets: [
-    '@vue/cli-plugin-babel/preset',
-  ],
-}

+ 13 - 0
examples/vue/index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vite App</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 13 - 15
examples/vue/package.json

@@ -1,26 +1,24 @@
 {
-  "name": "@uppy-example/vue-example",
+  "name": "@uppy-example/vue2",
   "version": "0.0.0",
   "private": true,
   "scripts": {
-    "start": "vue-cli-service serve",
-    "build": "vue-cli-service build",
-    "lint": "vue-cli-service lint"
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview --port 5050"
   },
   "dependencies": {
+    "@uppy/core": "workspace:*",
+    "@uppy/dashboard": "workspace:*",
+    "@uppy/drag-drop": "workspace:*",
+    "@uppy/progress-bar": "workspace:*",
+    "@uppy/transloadit": "workspace:*",
     "@uppy/vue": "workspace:*",
-    "core-js": "^3.6.5",
-    "shallow-equal": "^1.2.1",
     "vue": "^2.6.14"
   },
   "devDependencies": {
-    "@vue/cli-plugin-babel": "~4.5.0",
-    "@vue/cli-service": "~4.5.0",
-    "vue-template-compiler": "^2.6.11"
-  },
-  "browserslist": [
-    "> 1%",
-    "last 2 versions",
-    "not dead"
-  ]
+    "vite": "^3.0.0",
+    "vite-plugin-vue2": "^2.0.1",
+    "vue-template-compiler": "^2.6.14"
+  }
 }

+ 0 - 1
examples/vue/src/App.vue

@@ -1,6 +1,5 @@
 <template>
   <div id="app">
-    <!-- <HelloWorld msg="Welcome to Uppy Vue Demo"/> -->
     <h1>Welcome to Uppy Vue Demo!</h1>
     <h2>Inline Dashboard</h2>
     <label>

+ 0 - 58
examples/vue/src/components/HelloWorld.vue

@@ -1,58 +0,0 @@
-<template>
-  <div class="hello">
-    <h1>{{ msg }}</h1>
-    <p>
-      For a guide and recipes on how to configure / customize this project,<br>
-      check out the
-      <a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
-    </p>
-    <h3>Installed CLI Plugins</h3>
-    <ul>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
-      <li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-eslint" target="_blank" rel="noopener">eslint</a></li>
-    </ul>
-    <h3>Essential Links</h3>
-    <ul>
-      <li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
-      <li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
-      <li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
-      <li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
-      <li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
-    </ul>
-    <h3>Ecosystem</h3>
-    <ul>
-      <li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
-      <li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
-      <li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
-      <li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
-      <li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
-    </ul>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'HelloWorld',
-  props: {
-    msg: String
-  }
-}
-</script>
-
-<!-- Add "scoped" attribute to limit CSS to this component only -->
-<style scoped>
-h3 {
-  margin: 40px 0 0;
-}
-ul {
-  list-style-type: none;
-  padding: 0;
-}
-li {
-  display: inline-block;
-  margin: 0 10px;
-}
-a {
-  color: #42b983;
-}
-</style>

+ 7 - 0
examples/vue/vite.config.js

@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import { createVuePlugin } from 'vite-plugin-vue2'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [createVuePlugin()],
+})

+ 9 - 0
examples/vue3/README.md

@@ -0,0 +1,9 @@
+# Vue 3 example
+
+To run the example, from the root directory of this repo, run the following commands:
+
+```sh
+cp .env.example .env
+corepack yarn install
+corepack yarn workspace @uppy-example/vue3 dev
+```

+ 10 - 10
examples/vue3/index.html

@@ -1,13 +1,13 @@
 <!DOCTYPE html>
 <html lang="en">
-<head>
-  <meta charset="UTF-8">
-  <link rel="icon" href="/favicon.ico" />
-  <meta name="viewport" content="width=device-width, initial-scale=1.0">
-  <title>Vite App</title>
-</head>
-<body>
-  <div id="app"></div>
-  <script type="module" src="/src/main.js"></script>
-</body>
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" href="/favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>Vite App</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
 </html>

+ 9 - 6
examples/vue3/package.json

@@ -4,17 +4,20 @@
   "private": true,
   "scripts": {
     "dev": "vite",
-    "build": "vite build"
+    "build": "vite build",
+    "preview": "vite preview --port 5050"
   },
   "dependencies": {
     "@uppy/core": "workspace:*",
+    "@uppy/dashboard": "workspace:*",
+    "@uppy/drag-drop": "workspace:*",
+    "@uppy/progress-bar": "workspace:*",
+    "@uppy/tus": "workspace:*",
     "@uppy/vue": "workspace:*",
-    "core-js": "^3.6.5",
-    "shallow-equal": "^1.2.1",
-    "vue": "^3.0.4"
+    "vue": "^3.2.33"
   },
   "devDependencies": {
-    "@vue/compiler-sfc": "^3.0.4",
-    "vite": "^1.0.0-rc.13"
+    "@vitejs/plugin-vue": "^2.3.1",
+    "vite": "^3.0.0"
   }
 }

+ 20 - 20
examples/vue3/src/App.vue

@@ -1,3 +1,7 @@
+<script setup>
+import { Dashboard, DashboardModal, DragDrop, ProgressBar } from '@uppy/vue'
+</script>
+
 <template>
   <div id="app">
     <!-- <HelloWorld msg="Welcome to Uppy Vue Demo"/> -->
@@ -13,7 +17,7 @@
       />
       Show Dashboard
     </label>
-    <dashboard
+    <Dashboard
       v-if="showInlineDashboard"
       :uppy="uppy"
       :props="{
@@ -23,7 +27,7 @@
     <h2>Modal Dashboard</h2>
     <div>
       <button @click="open = true">Show Dashboard</button>
-    <dashboard-modal
+    <DashboardModal
       :uppy="uppy2" 
       :open="open" 
       :props="{
@@ -33,7 +37,7 @@
     </div>
 
     <h2>Drag Drop Area</h2>
-    <drag-drop 
+    <DragDrop 
       :uppy="uppy"
       :props="{
         locale: {
@@ -46,7 +50,7 @@
     />
 
     <h2>Progress Bar</h2>
-    <progress-bar 
+    <ProgressBar 
       :uppy="uppy"
       :props="{
         hideAfterFinish: false
@@ -57,22 +61,19 @@
 
 <script>
 import Uppy from '@uppy/core'
-// import Tus from '@uppy/tus'
-import { Dashboard, DashboardModal, DragDrop, ProgressBar } from '@uppy/vue'
+import Tus from '@uppy/tus'
+import { defineComponent } from 'vue'
 
-export default {
-  name: 'App',
-  components: {
-    Dashboard,
-    DashboardModal,
-    DragDrop,
-    ProgressBar
-  },
+const {
+  VITE_TUS_ENDPOINT: TUS_ENDPOINT,
+} = import.meta.env
+
+export default defineComponent({
   computed: {
-    uppy: () => new Uppy({ id: 'uppy1', autoProceed: true, debug: true }),
-      // .use(Tus, { endpoint: 'https://tusd.tusdemo.net/files/' }),
+    uppy: () => new Uppy({ id: 'uppy1', autoProceed: true, debug: true })
+      .use(Tus, { endpoint: TUS_ENDPOINT }),
     uppy2: () => new Uppy({ id: 'uppy2', autoProceed: false, debug: true })
-      // .use(Tus, { endpoint: 'https://tusd.tusdemo.net/files/' }),
+      .use(Tus, { endpoint: TUS_ENDPOINT }),
   },
   data () {
     return {
@@ -83,15 +84,14 @@ export default {
   methods: {
     handleClose() { this.open = false }
   },
-  
-}
+})
 </script>
+
 <style src='@uppy/core/dist/style.css'></style> 
 <style src='@uppy/dashboard/dist/style.css'></style> 
 <style src='@uppy/drag-drop/dist/style.css'></style> 
 <style src='@uppy/progress-bar/dist/style.css'></style> 
 
-
 <style>
 #app {
   font-family: Avenir, Helvetica, Arial, sans-serif;

+ 0 - 8
examples/vue3/src/index.css

@@ -1,8 +0,0 @@
-#app {
-  font-family: Avenir, Helvetica, Arial, sans-serif;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-  text-align: center;
-  color: #2c3e50;
-  margin-top: 60px;
-}

+ 0 - 1
examples/vue3/src/main.js

@@ -1,5 +1,4 @@
 import { createApp } from 'vue'
 import App from './App.vue'
-import './index.css'
 
 createApp(App).mount('#app')

+ 7 - 0
examples/vue3/vite.config.js

@@ -0,0 +1,7 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+// https://vitejs.dev/config/
+export default defineConfig({
+  plugins: [vue()],
+})

+ 9 - 7
package.json

@@ -25,7 +25,7 @@
   "pre-commit": "lint:staged",
   "license": "MIT",
   "engines": {
-    "node": "^v14.17.0 || >=v16.0.0",
+    "node": "^16.15.0 || >=18.0.0",
     "yarn": "3.2.1"
   },
   "packageManager": "yarn@3.2.1",
@@ -77,15 +77,16 @@
     "eslint-plugin-compat": "^4.0.0",
     "eslint-plugin-cypress": "^2.12.1",
     "eslint-plugin-import": "^2.25.2",
-    "eslint-plugin-jest": "^25.0.0",
-    "eslint-plugin-jsdoc": "^37.0.0",
+    "eslint-plugin-jest": "^26.0.0",
+    "eslint-plugin-jsdoc": "^38.0.0",
     "eslint-plugin-jsx-a11y": "^6.4.1",
     "eslint-plugin-markdown": "^2.2.0",
     "eslint-plugin-node": "^11.1.0",
     "eslint-plugin-prefer-import": "^0.0.1",
-    "eslint-plugin-promise": "^5.1.1",
+    "eslint-plugin-promise": "^6.0.0",
     "eslint-plugin-react": "^7.22.0",
     "eslint-plugin-react-hooks": "^4.2.0",
+    "eslint-plugin-unicorn": "^43.0.0",
     "events.once": "^2.0.2",
     "exorcist": "^2.0.0",
     "fakefile": "^1.0.0",
@@ -121,7 +122,8 @@
     "temp-write": "^5.0.0",
     "tsd": "^0.17.0",
     "typescript": "~4.4",
-    "verdaccio": "^5.1.1"
+    "verdaccio": "^5.1.1",
+    "vue-template-compiler": "workspace:*"
   },
   "scripts": {
     "build:bundle": "yarn node ./bin/build-bundle.mjs",
@@ -161,7 +163,7 @@
     "test:locale-packs:warnings": "yarn workspace @uppy-dev/locale-pack test warnings",
     "test:type": "yarn workspaces foreach -piv --include '@uppy/*' --exclude '@uppy/{angular,react-native,locales,companion,provider-views,robodog,svelte}' exec tsd",
     "test:unit": "yarn run build:lib && NODE_OPTIONS=--experimental-vm-modules jest --env jsdom",
-    "test:watch": "jest --env jsdom --watch",
+    "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --env jsdom --watch --no-coverage",
     "test:size": "yarn build:lib && size-limit --why",
     "test": "npm-run-all lint test:locale-packs:unused test:locale-packs:warnings test:unit test:type test:companion",
     "uploadcdn": "yarn node ./bin/upload-to-cdn.js",
@@ -198,7 +200,7 @@
     ]
   },
   "resolutions": {
-    "@types/redis": "2",
+    "@types/connect-redis@0.0.18": "patch:@types/connect-redis@npm:0.0.18#.yarn/patches/@types-connect-redis-npm-0.0.18-4fd2b614d3",
     "@types/eslint@^7.2.13": "^8.2.0",
     "@types/react": "^17",
     "@types/webpack-dev-server": "^4",

+ 3 - 2
packages/@uppy/audio/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/audio",
   "description": "Uppy plugin that records audio using the device’s microphone.",
-  "version": "0.3.2",
+  "version": "1.0.0-beta.1",
   "license": "MIT",
   "main": "lib/index.js",
   "style": "dist/style.min.css",
@@ -37,5 +37,6 @@
   },
   "publishConfig": {
     "access": "public"
-  }
+  },
+  "stableVersion": "0.3.2"
 }

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

@@ -1,5 +1,19 @@
 # @uppy/aws-s3-multipart
 
+## 3.0.0-beta.3
+
+Released: 2022-08-03
+Included in: Uppy v3.0.0-beta.4
+
+- @uppy/aws-s3-multipart: Correctly handle errors for `prepareUploadParts` (Merlijn Vos / #3912)
+
+## 3.0.0-beta.2
+
+Released: 2022-07-27
+Included in: Uppy v3.0.0-beta.3
+
+- @uppy/aws-s3-multipart: make `headers` part indexed too in `prepareUploadParts` (Merlijn Vos / #3895)
+
 ## 2.4.1
 
 Released: 2022-06-07

+ 3 - 2
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": "2.4.1",
+  "version": "3.0.0-beta.3",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",
@@ -34,5 +34,6 @@
   },
   "peerDependencies": {
     "@uppy/core": "workspace:^"
-  }
+  },
+  "stableVersion": "2.4.0"
 }

+ 58 - 50
packages/@uppy/aws-s3-multipart/src/MultipartUploader.js

@@ -162,46 +162,58 @@ class MultipartUploader {
       return
     }
 
-    // For a 100MB file, with the default min chunk size of 5MB and a limit of 10:
-    //
-    // Total 20 parts
-    // ---------
-    // Need 1 is 10
-    // Need 2 is 5
-    // Need 3 is 5
-    const need = this.options.limit - this.partsInProgress
-    const completeChunks = this.chunkState.filter((state) => state.done).length
-    const remainingChunks = this.chunks.length - completeChunks
-    let minNeeded = Math.ceil(this.options.limit / 2)
-    if (minNeeded > remainingChunks) {
-      minNeeded = remainingChunks
-    }
-    if (need < minNeeded) return
+    const getChunkIndexes = () => {
+      // For a 100MB file, with the default min chunk size of 5MB and a limit of 10:
+      //
+      // Total 20 parts
+      // ---------
+      // Need 1 is 10
+      // Need 2 is 5
+      // Need 3 is 5
+      const need = this.options.limit - this.partsInProgress
+      const completeChunks = this.chunkState.filter((state) => state.done).length
+      const remainingChunks = this.chunks.length - completeChunks
+      let minNeeded = Math.ceil(this.options.limit / 2)
+      if (minNeeded > remainingChunks) {
+        minNeeded = remainingChunks
+      }
+      if (need < minNeeded) return []
 
-    const candidates = []
-    for (let i = 0; i < this.chunkState.length; i++) {
-      const state = this.chunkState[i]
-      // eslint-disable-next-line no-continue
-      if (state.done || state.busy) continue
+      const chunkIndexes = []
+      for (let i = 0; i < this.chunkState.length; i++) {
+        const state = this.chunkState[i]
+        // eslint-disable-next-line no-continue
+        if (state.done || state.busy) continue
 
-      candidates.push(i)
-      if (candidates.length >= need) {
-        break
+        chunkIndexes.push(i)
+        if (chunkIndexes.length >= need) {
+          break
+        }
       }
+
+      return chunkIndexes
     }
-    if (candidates.length === 0) return
-
-    this.#prepareUploadParts(candidates).then((result) => {
-      candidates.forEach((index) => {
-        const partNumber = index + 1
-        const prePreparedPart = { url: result.presignedUrls[partNumber], headers: result.headers }
-        this.#uploadPartRetryable(index, prePreparedPart).then(() => {
-          this.#uploadParts()
-        }, (err) => {
-          this.#onError(err)
-        })
-      })
-    })
+
+    const chunkIndexes = getChunkIndexes()
+
+    if (chunkIndexes.length === 0) return
+
+    this.#prepareUploadPartsRetryable(chunkIndexes).then(
+      ({ presignedUrls, headers }) => {
+        for (const index of chunkIndexes) {
+          const partNumber = index + 1
+          const prePreparedPart = {
+            url: presignedUrls[partNumber],
+            headers: headers?.[partNumber],
+          }
+          this.#uploadPartRetryable(index, prePreparedPart).then(
+            () => this.#uploadParts(),
+            (err) => this.#onError(err),
+          )
+        }
+      },
+      (err) => this.#onError(err),
+    )
   }
 
   #retryable ({ before, attempt, after }) {
@@ -238,8 +250,8 @@ class MultipartUploader {
     })
   }
 
-  async #prepareUploadParts (candidates) {
-    candidates.forEach((i) => {
+  async #prepareUploadPartsRetryable (chunkIndexes) {
+    chunkIndexes.forEach((i) => {
       this.chunkState[i].busy = true
     })
 
@@ -247,12 +259,10 @@ class MultipartUploader {
       attempt: () => this.options.prepareUploadParts({
         key: this.key,
         uploadId: this.uploadId,
-        partNumbers: candidates.map((index) => index + 1),
-        chunks: candidates.reduce((chunks, candidate) => ({
-          ...chunks,
-          // Use the part number as the index
-          [candidate + 1]: this.chunks[candidate],
-        }), {}),
+        parts: chunkIndexes.map((index) => ({
+          number: index + 1, // Use the part number as the index
+          chunk: this.chunks[index],
+        })),
       }),
     })
 
@@ -415,12 +425,10 @@ class MultipartUploader {
   #abortUpload () {
     this.abortController.abort()
 
-    this.createdPromise.then(() => {
-      this.options.abortMultipartUpload({
-        key: this.key,
-        uploadId: this.uploadId,
-      })
-    }, () => {
+    this.createdPromise.then(() => this.options.abortMultipartUpload({
+      key: this.key,
+      uploadId: this.uploadId,
+    })).catch(() => {
       // if the creation failed we do not need to abort
     })
   }

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

@@ -58,11 +58,6 @@ export default class AwsS3Multipart extends BasePlugin {
 
   [Symbol.for('uppy test: getClient')] () { return this.#client }
 
-  // TODO: remove getter and setter for #client on the next major release
-  get client () { return this.#client }
-
-  set client (client) { this.#client = client }
-
   /**
    * Clean up all references for a file's upload: the MultipartUploader instance,
    * any events related to the file, and the Companion WebSocket connection.

+ 85 - 44
packages/@uppy/aws-s3-multipart/src/index.test.js

@@ -62,7 +62,7 @@ describe('AwsS3Multipart', () => {
               partNumber
             ] = `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`
           })
-          return { presignedUrls }
+          return { presignedUrls, headers: { 1: { 'Content-MD5': 'foo' } } }
         }),
       })
       awsS3Multipart = core.getPlugin('AwsS3Multipart')
@@ -72,25 +72,32 @@ describe('AwsS3Multipart', () => {
       const scope = nock(
         'https://bucket.s3.us-east-2.amazonaws.com',
       ).defaultReplyHeaders({
+        'access-control-allow-headers': '*',
         'access-control-allow-method': 'PUT',
         'access-control-allow-origin': '*',
-        'access-control-expose-headers': 'ETag',
+        'access-control-expose-headers': 'ETag, Content-MD5',
       })
       // 6MB file will give us 2 chunks, so there will be 2 PUT and 2 OPTIONS
       // calls to the presigned URL from 1 prepareUploadParts calls
       const fileSize = 5 * MB + 1 * MB
 
       scope
-        .options((uri) => uri.includes('test/upload/multitest.dat'))
-        .reply(200, '')
+        .options((uri) => uri.includes('test/upload/multitest.dat?partNumber=1'))
+        .reply(function replyFn () {
+          expect(this.req.headers['access-control-request-headers']).toEqual('Content-MD5')
+          return [200, '']
+        })
       scope
-        .options((uri) => uri.includes('test/upload/multitest.dat'))
-        .reply(200, '')
+        .options((uri) => uri.includes('test/upload/multitest.dat?partNumber=2'))
+        .reply(function replyFn () {
+          expect(this.req.headers['access-control-request-headers']).toBeUndefined()
+          return [200, '']
+        })
       scope
-        .put((uri) => uri.includes('test/upload/multitest.dat'))
+        .put((uri) => uri.includes('test/upload/multitest.dat?partNumber=1'))
         .reply(200, '', { ETag: 'test1' })
       scope
-        .put((uri) => uri.includes('test/upload/multitest.dat'))
+        .put((uri) => uri.includes('test/upload/multitest.dat?partNumber=2'))
         .reply(200, '', { ETag: 'test2' })
 
       core.addFile({
@@ -115,6 +122,7 @@ describe('AwsS3Multipart', () => {
       const scope = nock(
         'https://bucket.s3.us-east-2.amazonaws.com',
       ).defaultReplyHeaders({
+        'access-control-allow-headers': '*',
         'access-control-allow-method': 'PUT',
         'access-control-allow-origin': '*',
         'access-control-expose-headers': 'ETag',
@@ -145,11 +153,12 @@ describe('AwsS3Multipart', () => {
 
       await core.upload()
 
-      function validatePartData ({ partNumbers, chunks }, expected) {
-        expect(partNumbers).toEqual(expected)
-        partNumbers.forEach(partNumber => {
-          expect(chunks[partNumber]).toBeDefined()
-        })
+      function validatePartData ({ parts }, expected) {
+        expect(parts.map((part) => part.number)).toEqual(expected)
+
+        for (const part of parts) {
+          expect(part.chunk).toBeDefined()
+        }
       }
 
       expect(awsS3Multipart.opts.prepareUploadParts.mock.calls.length).toEqual(3)
@@ -176,41 +185,42 @@ describe('AwsS3Multipart', () => {
   })
 
   describe('MultipartUploader', () => {
-    let core
-    let awsS3Multipart
+    const createMultipartUpload = jest.fn(() => {
+      return {
+        uploadId: '6aeb1980f3fc7ce0b5454d25b71992',
+        key: 'test/upload/multitest.dat',
+      }
+    })
 
-    beforeEach(() => {
-      core = new Core()
-      core.use(AwsS3Multipart, {
-        createMultipartUpload: jest.fn(() => {
-          return {
-            uploadId: '6aeb1980f3fc7ce0b5454d25b71992',
-            key: 'test/upload/multitest.dat',
-          }
-        }),
-        completeMultipartUpload: jest.fn(async () => ({ location: 'test' })),
-        abortMultipartUpload: jest.fn(),
-        prepareUploadParts: jest
-          .fn(async () => {
-            const presignedUrls = {}
-            const possiblePartNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
-
-            possiblePartNumbers.forEach((partNumber) => {
-              presignedUrls[
-                partNumber
-              ] = `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`
-            })
-
-            return { presignedUrls }
-          })
-          // This runs first and only once
-          // eslint-disable-next-line prefer-promise-reject-errors
-          .mockImplementationOnce(() => Promise.reject({ source: { status: 500 } })),
+    const prepareUploadParts = jest
+      .fn(async () => {
+        const presignedUrls = {}
+        const possiblePartNumbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+
+        possiblePartNumbers.forEach((partNumber) => {
+          presignedUrls[
+            partNumber
+          ] = `https://bucket.s3.us-east-2.amazonaws.com/test/upload/multitest.dat?partNumber=${partNumber}&uploadId=6aeb1980f3fc7ce0b5454d25b71992&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIATEST%2F20210729%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Date=20210729T014044Z&X-Amz-Expires=600&X-Amz-SignedHeaders=host&X-Amz-Signature=test`
+        })
+
+        return { presignedUrls }
       })
-      awsS3Multipart = core.getPlugin('AwsS3Multipart')
-    })
+
+    afterEach(() => jest.clearAllMocks())
 
     it('retries prepareUploadParts when it fails once', async () => {
+      const core = new Core()
+        .use(AwsS3Multipart, {
+          createMultipartUpload,
+          completeMultipartUpload: jest.fn(async () => ({ location: 'test' })),
+          // eslint-disable-next-line no-throw-literal
+          abortMultipartUpload: jest.fn(() => { throw 'should ignore' }),
+          prepareUploadParts:
+            prepareUploadParts
+              // eslint-disable-next-line prefer-promise-reject-errors
+              .mockImplementationOnce(() => Promise.reject({ source: { status: 500 } })),
+        })
+      const awsS3Multipart = core.getPlugin('AwsS3Multipart')
       const fileSize = 5 * MB + 1 * MB
 
       core.addFile({
@@ -226,6 +236,37 @@ describe('AwsS3Multipart', () => {
 
       expect(awsS3Multipart.opts.prepareUploadParts.mock.calls.length).toEqual(2)
     })
+
+    it('calls `upload-error` when prepareUploadParts fails after all retries', async () => {
+      const core = new Core()
+        .use(AwsS3Multipart, {
+          retryDelays: [100],
+          createMultipartUpload,
+          completeMultipartUpload: jest.fn(async () => ({ location: 'test' })),
+          abortMultipartUpload: jest.fn(),
+          prepareUploadParts: prepareUploadParts
+            // eslint-disable-next-line prefer-promise-reject-errors
+            .mockImplementation(() => Promise.reject({ source: { status: 500 } })),
+        })
+      const awsS3Multipart = core.getPlugin('AwsS3Multipart')
+      const fileSize = 5 * MB + 1 * MB
+      const mock = jest.fn()
+      core.on('upload-error', mock)
+
+      core.addFile({
+        source: 'jest',
+        name: 'multitest.dat',
+        type: 'application/octet-stream',
+        data: new File([new Uint8Array(fileSize)], {
+          type: 'application/octet-stream',
+        }),
+      })
+
+      await expect(core.upload()).rejects.toEqual({ source: { status: 500 } })
+
+      expect(awsS3Multipart.opts.prepareUploadParts.mock.calls.length).toEqual(2)
+      expect(mock.mock.calls.length).toEqual(1)
+    })
   })
 
   describe('dynamic companionHeader', () => {

+ 1 - 1
packages/@uppy/aws-s3-multipart/types/index.d.ts

@@ -22,7 +22,7 @@ export interface AwsS3MultipartOptions extends PluginOptions {
     ) => MaybePromise<AwsS3Part[]>
     prepareUploadParts?: (
       file: UppyFile,
-      partData: { uploadId: string; key: string; partNumbers: Array<number>; chunks: { [k: number]: Blob } }
+      partData: { uploadId: string; key: string; parts: Array<{ number: number, chunk: Blob }> }
     ) => MaybePromise<{ presignedUrls: { [k: number]: string }, headers?: { [k: string]: string } }>
     abortMultipartUpload?: (
       file: UppyFile,

+ 1 - 2
packages/@uppy/aws-s3-multipart/types/index.test-d.ts

@@ -21,8 +21,7 @@ import type { AwsS3Part } from '..'
       expectType<UppyFile>(file)
       expectType<string>(partData.uploadId)
       expectType<string>(partData.key)
-      expectType<Array<number>>(partData.partNumbers)
-      expectType<{ [k: number]: Blob }>(partData.chunks)
+      expectType<Array<{number: number, chunk: Blob}>>(partData.parts)
       return { presignedUrls: {} }
     },
     abortMultipartUpload (file, opts) {

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

@@ -1,5 +1,12 @@
 # @uppy/aws-s3
 
+## 3.0.0-beta.2
+
+Released: 2022-07-27
+Included in: Uppy v3.0.0-beta.3
+
+- @uppy/aws-s3,@uppy/core,@uppy/dashboard,@uppy/store-redux,@uppy/xhr-upload: upgrade `nanoid` to v4 (Antoine du Hamel / #3904)
+
 ## 2.2.1
 
 Released: 2022-06-07

+ 4 - 3
packages/@uppy/aws-s3/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/aws-s3",
   "description": "Upload to Amazon S3 with Uppy",
-  "version": "2.2.1",
+  "version": "3.0.0-beta.2",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",
@@ -26,7 +26,7 @@
     "@uppy/companion-client": "workspace:^",
     "@uppy/utils": "workspace:^",
     "@uppy/xhr-upload": "workspace:^",
-    "nanoid": "^3.1.25"
+    "nanoid": "^4.0.0"
   },
   "devDependencies": {
     "@jest/globals": "^27.4.2",
@@ -34,5 +34,6 @@
   },
   "peerDependencies": {
     "@uppy/core": "workspace:^"
-  }
+  },
+  "stableVersion": "2.2.0"
 }

+ 3 - 2
packages/@uppy/box/package.json

@@ -1,7 +1,7 @@
 {
   "name": "@uppy/box",
   "description": "Import files from Box, into Uppy.",
-  "version": "1.0.7",
+  "version": "2.0.0-beta.1",
   "license": "MIT",
   "main": "lib/index.js",
   "type": "module",
@@ -31,5 +31,6 @@
   },
   "publishConfig": {
     "access": "public"
-  }
+  },
+  "stableVersion": "1.0.7"
 }

+ 3 - 2
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": "2.2.1",
+  "version": "3.0.0-beta.1",
   "license": "MIT",
   "main": "lib/index.js",
   "types": "types/index.d.ts",
@@ -27,5 +27,6 @@
   },
   "devDependencies": {
     "@jest/globals": "^27.4.2"
-  }
+  },
+  "stableVersion": "2.2.0"
 }

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

@@ -1,5 +1,44 @@
 # @uppy/companion
 
+## 4.0.0-beta.3
+
+Released: 2022-08-03
+Included in: Uppy v3.0.0-beta.4
+
+- @uppy/companion,@uppy/tus: Upgrade tus-js-client to 3.0.0 (Merlijn Vos / #3942)
+
+## 4.0.0-beta.2
+
+Released: 2022-07-27
+Included in: Uppy v3.0.0-beta.3
+
+- @uppy/companion: update minimal supported Node.js version in the docs (Antoine du Hamel / #3902)
+- @uppy/companion: upgrade `redis` to version 4.x (Antoine du Hamel / #3589)
+- @uppy/companion: remove unnecessary ts-ignores (Mikael Finstad / #3900)
+- @uppy/companion: remove `COMPANION_S3_GETKEY_SAFE_BEHAVIOR` env variable (Antoine du Hamel / #3869)
+
+## 4.0.0-beta.1
+
+Released: 2022-07-06
+Included in: Uppy v3.0.0-beta.2
+
+- @uppy/companion: remove deprecated duplicated metrics (Mikael Finstad / #3833)
+- @uppy/companion: Companion 3 default to no s3 acl (Mikael Finstad / #3826)
+- @uppy/companion: rewrite companion.app() to return an object (Mikael Finstad / #3827)
+- @uppy/companion: remove companion provider compat api (Mikael Finstad / #3828)
+- @uppy/companion: rewrite code for node >=14 (Mikael Finstad / #3829)
+- @uppy/companion: remove chunkSize backwards compatibility (Mikael Finstad / #3830)
+- @uppy/companion: Companion: make `emitSuccess` and `emitError` private (Mikael Finstad / #3832)
+- @uppy/companion: do not use a default upload protocol (Mikael Finstad / #3834)
+
+## 4.0.0-beta
+
+Released: 2022-05-30
+Included in: Uppy v3.0.0-beta
+
+- @uppy/companion: remove `searchProviders` wrapper & move `s3` options (Merlijn Vos / #3781)
+- @uppy/companion: remove support for EOL versions of Node.js (Antoine du Hamel / #3784)
+
 ## 3.7.1
 
 Released: 2022-07-27

+ 2 - 1
packages/@uppy/companion/README.md

@@ -48,7 +48,8 @@ const options = {
   filePath: '/path/to/folder/',
 }
 
-app.use(companion.app(options))
+const { app: companionApp } = companion.app(options)
+app.use(companionApp)
 ```
 
 To enable companion socket for realtime feed to the client while upload is going on, you call the `socket` method like so.

+ 9 - 12
packages/@uppy/companion/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@uppy/companion",
-  "version": "3.7.1",
+  "version": "4.0.0-beta.3",
   "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",
@@ -34,7 +34,7 @@
     "body-parser": "1.19.0",
     "chalk": "2.4.2",
     "common-tags": "1.8.0",
-    "connect-redis": "4.0.3",
+    "connect-redis": "6.1.3",
     "cookie-parser": "1.4.6",
     "cors": "^2.8.5",
     "escape-goat": "3.0.0",
@@ -47,7 +47,6 @@
     "grant": "4.7.0",
     "helmet": "^4.6.0",
     "ipaddr.js": "^2.0.1",
-    "isobject": "3.0.1",
     "jsonwebtoken": "8.5.1",
     "lodash.merge": "^4.6.2",
     "mime-types": "2.1.25",
@@ -55,23 +54,21 @@
     "moment-timezone": "^0.5.31",
     "morgan": "1.10.0",
     "ms": "2.1.2",
-    "node-redis-pubsub": "^5.0.0",
     "node-schedule": "1.3.2",
     "prom-client": "12.0.0",
     "purest": "3.1.0",
-    "redis": "3.1.1",
+    "redis": "4.2.0",
     "request": "2.88.2",
     "semver": "6.3.0",
     "serialize-error": "^2.1.0",
     "serialize-javascript": "^6.0.0",
-    "tus-js-client": "2.1.1",
-    "uuid": "8.1.0",
+    "tus-js-client": "^3.0.0",
     "validator": "^12.1.0",
     "ws": "6.2.2"
   },
   "devDependencies": {
     "@types/compression": "1.7.0",
-    "@types/connect-redis": "0.0.17",
+    "@types/connect-redis": "0.0.18",
     "@types/cookie-parser": "1.4.2",
     "@types/cors": "2.8.6",
     "@types/eslint": "^8.2.0",
@@ -80,10 +77,9 @@
     "@types/lodash.merge": "4.6.6",
     "@types/morgan": "1.7.37",
     "@types/ms": "0.7.31",
-    "@types/node": "12.12.27",
+    "@types/node": "^18.0.3",
     "@types/react": "^17.0.13",
     "@types/request": "2.48.4",
-    "@types/uuid": "3.4.7",
     "@types/webpack": "^5.28.0",
     "@types/ws": "6.0.4",
     "into-stream": "^6.0.0",
@@ -114,9 +110,10 @@
     "test": "jest"
   },
   "engines": {
-    "node": ">=10.20.1"
+    "node": "^14.19.0 || ^16.15.0 || >=18.0.0"
   },
   "installConfig": {
     "hoistingLimits": "workspaces"
-  }
+  },
+  "stableVersion": "3.6.0"
 }

+ 5 - 21
packages/@uppy/companion/src/companion.js

@@ -1,10 +1,9 @@
 const express = require('express')
-// @ts-ignore
 const Grant = require('grant').express()
 const merge = require('lodash.merge')
 const cookieParser = require('cookie-parser')
 const interceptor = require('express-interceptor')
-const uuid = require('uuid')
+const { randomUUID } = require('node:crypto')
 
 const grantConfig = require('./config/grant')()
 const providerManager = require('./server/provider')
@@ -13,7 +12,6 @@ const s3 = require('./server/controllers/s3')
 const url = require('./server/controllers/url')
 const createEmitter = require('./server/emitter')
 const redis = require('./server/redis')
-const { getURLBuilder } = require('./server/helpers/utils')
 const jobs = require('./server/jobs')
 const logger = require('./server/logger')
 const middlewares = require('./server/middlewares')
@@ -57,16 +55,13 @@ module.exports.socket = require('./server/socket')
  * Entry point into initializing the Companion app.
  *
  * @param {object} optionsArg
- * @returns {import('express').Express}
+ * @returns {{ app: import('express').Express, emitter: any }}}
  */
 module.exports.app = (optionsArg = {}) => {
   validateConfig(optionsArg)
 
   const options = merge({}, defaultOptions, optionsArg)
 
-  // todo remove in next major and default to the safer getKey instead
-  if (options.providerOptions.s3.getKey === defaultOptions.providerOptions.s3.getKey) process.emitWarning('The current default getKey implementation is not safe because it will cause files with the same name to be overwritten and should be avoided. Please use the environment variable COMPANION_S3_GETKEY_SAFE_BEHAVIOR=true (standalone) or provide your own getKey implementation instead')
-
   const providers = providerManager.getDefaultProviders()
   const searchProviders = providerManager.getSearchProviders()
   providerManager.addProviderOptions(options, grantConfig)
@@ -89,17 +84,6 @@ module.exports.app = (optionsArg = {}) => {
 
   if (options.metrics) {
     app.use(middlewares.metrics({ path: options.server.path }))
-
-    // backward compatibility
-    // TODO remove in next major semver
-    if (options.server.path) {
-      const buildUrl = getURLBuilder(options)
-      app.get('/metrics', (req, res) => {
-        process.emitWarning('/metrics is deprecated when specifying a path to companion')
-        const metricsUrl = buildUrl('/metrics', true)
-        res.redirect(metricsUrl)
-      })
-    }
   }
 
   app.use(cookieParser()) // server tokens are added to cookies
@@ -121,7 +105,7 @@ module.exports.app = (optionsArg = {}) => {
 
   // add uppy options to the request object so it can be accessed by subsequent handlers.
   app.use('*', middlewares.getCompanionMiddleware(options))
-  app.use('/s3', s3(options.providerOptions.s3))
+  app.use('/s3', s3(options.s3))
   app.use('/url', url())
 
   app.post('/:providerName/preauth', middlewares.hasSessionAndProvider, controllers.preauth)
@@ -145,7 +129,7 @@ module.exports.app = (optionsArg = {}) => {
     jobs.startCleanUpJob(options.filePath)
   }
 
-  const processId = uuid.v4()
+  const processId = randomUUID()
 
   jobs.startPeriodicPingJob({
     urls: options.periodicPingUrls,
@@ -156,5 +140,5 @@ module.exports.app = (optionsArg = {}) => {
     processId,
   })
 
-  return Object.assign(app, { companionEmitter: emitter })
+  return { app, emitter }
 }

+ 20 - 15
packages/@uppy/companion/src/config/companion.js

@@ -1,28 +1,28 @@
 const ms = require('ms')
-const fs = require('fs')
+const fs = require('node:fs')
 const { isURL } = require('validator')
 const logger = require('../server/logger')
+const { defaultGetKey } = require('../server/helpers/utils')
 
 const defaultOptions = {
   server: {
     protocol: 'http',
     path: '',
   },
-  providerOptions: {
-    s3: {
-      acl: 'public-read', // todo default to no ACL in next major
-      endpoint: 'https://{service}.{region}.amazonaws.com',
-      conditions: [],
-      useAccelerateEndpoint: false,
-      getKey: (req, filename) => filename,
-      expires: ms('5 minutes') / 1000,
-    },
+  providerOptions: {},
+  s3: {
+    endpoint: 'https://{service}.{region}.amazonaws.com',
+    conditions: [],
+    useAccelerateEndpoint: false,
+    getKey: defaultGetKey,
+    expires: ms('5 minutes') / 1000,
   },
   allowLocalUrls: false,
   logClientVersion: true,
   periodicPingUrls: [],
   streamingUpload: false,
   clientSocketConnectTimeout: 60000,
+  metrics: true,
 }
 
 /**
@@ -30,7 +30,8 @@ const defaultOptions = {
  */
 function getMaskableSecrets (companionOptions) {
   const secrets = []
-  const { providerOptions, customProviders } = companionOptions
+  const { providerOptions, customProviders, s3 } = companionOptions
+
   Object.keys(providerOptions).forEach((provider) => {
     if (providerOptions[provider].secret) {
       secrets.push(providerOptions[provider].secret)
@@ -45,6 +46,10 @@ function getMaskableSecrets (companionOptions) {
     })
   }
 
+  if (s3?.secret) {
+    secrets.push(s3.secret)
+  }
+
   return secrets
 }
 
@@ -85,10 +90,10 @@ const validateConfig = (companionOptions) => {
   const { providerOptions, periodicPingUrls } = companionOptions
 
   if (providerOptions) {
-    const deprecatedOptions = { microsoft: 'onedrive', google: 'drive' }
-    Object.keys(deprecatedOptions).forEach((deprected) => {
-      if (providerOptions[deprected]) {
-        throw new Error(`The Provider option "${deprected}" is no longer supported. Please use the option "${deprecatedOptions[deprected]}" instead.`)
+    const deprecatedOptions = { microsoft: 'providerOptions.onedrive', google: 'providerOptions.drive', s3: 's3' }
+    Object.keys(deprecatedOptions).forEach((deprecated) => {
+      if (Object.prototype.hasOwnProperty.call(providerOptions, deprecated)) {
+        throw new Error(`The Provider option "providerOptions.${deprecated}" is no longer supported. Please use the option "${deprecatedOptions[deprecated]}" instead.`)
       }
     })
   }

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

@@ -1,13 +1,12 @@
 // eslint-disable-next-line max-classes-per-file
 const tus = require('tus-js-client')
-const uuid = require('uuid')
-const isObject = require('isobject')
+const { randomUUID } = require('node:crypto')
 const validator = require('validator')
 const request = require('request')
-const { pipeline: pipelineCb } = require('stream')
-const { join } = require('path')
-const fs = require('fs')
-const { promisify } = require('util')
+const { pipeline: pipelineCb } = require('node:stream')
+const { join } = require('node:path')
+const fs = require('node:fs')
+const { promisify } = require('node:util')
 
 // TODO move to `require('streams/promises').pipeline` when dropping support for Node.js 14.x.
 const pipeline = promisify(pipelineCb)
@@ -78,24 +77,23 @@ function validateOptions (options) {
   }
 
   // validate fieldname
-  if (options.fieldname && typeof options.fieldname !== 'string') {
+  if (options.fieldname != null && typeof options.fieldname !== 'string') {
     throw new ValidationError('fieldname must be a string')
   }
 
   // validate metadata
-  if (options.metadata != null) {
-    if (!isObject(options.metadata)) throw new ValidationError('metadata must be an object')
+  if (options.metadata != null && typeof options.metadata !== 'object') {
+    throw new ValidationError('metadata must be an object')
   }
 
   // validate headers
-  if (options.headers && !isObject(options.headers)) {
+  if (options.headers != null && typeof options.headers !== 'object') {
     throw new ValidationError('headers must be an object')
   }
 
   // validate protocol
-  // @todo this validation should not be conditional once the protocol field is mandatory
-  if (options.protocol && !Object.keys(PROTOCOLS).some((key) => PROTOCOLS[key] === options.protocol)) {
-    throw new ValidationError('unsupported protocol specified')
+  if (options.protocol == null || !Object.keys(PROTOCOLS).some((key) => PROTOCOLS[key] === options.protocol)) {
+    throw new ValidationError('please specify a valid protocol')
   }
 
   // s3 uploads don't require upload destination
@@ -154,7 +152,7 @@ class Uploader {
     validateOptions(options)
 
     this.options = options
-    this.token = uuid.v4()
+    this.token = randomUUID()
     this.fileName = `${Uploader.FILE_NAME_PREFIX}-${this.token}`
     this.options.metadata = sanitizeMetadata(this.options.metadata)
     this.options.fieldname = this.options.fieldname || DEFAULT_FIELD_NAME
@@ -208,8 +206,7 @@ class Uploader {
   }
 
   async _uploadByProtocol () {
-    // @todo a default protocol should not be set. We should ensure that the user specifies their protocol.
-    const protocol = this.options.protocol || PROTOCOLS.multipart
+    const { protocol } = this.options
 
     switch (protocol) {
       case PROTOCOLS.multipart:
@@ -295,7 +292,7 @@ class Uploader {
       const ret = await this.uploadStream(stream)
       if (!ret) return
       const { url, extraData } = ret
-      this.emitSuccess(url, extraData)
+      this.#emitSuccess(url, extraData)
     } catch (err) {
       if (err instanceof AbortError) {
         logger.error('Aborted upload', 'uploader.aborted', this.shortToken)
@@ -303,7 +300,7 @@ class Uploader {
       }
       // console.log(err)
       logger.error(err, 'uploader.error', this.shortToken)
-      this.emitError(err)
+      this.#emitError(err)
     } finally {
       emitter().removeAllListeners(`pause:${this.token}`)
       emitter().removeAllListeners(`resume:${this.token}`)
@@ -342,10 +339,10 @@ class Uploader {
       size,
       companionOptions: req.companion.options,
       pathPrefix: `${req.companion.options.filePath}`,
-      storage: redis.client(),
+      storage: redis.client()?.v4,
       s3: req.companion.s3Client ? {
         client: req.companion.s3Client,
-        options: req.companion.options.providerOptions.s3,
+        options: req.companion.options.s3,
       } : null,
       chunkSize: req.companion.options.chunkSize,
     }
@@ -460,7 +457,7 @@ class Uploader {
    * @param {string} url
    * @param {object} extraData
    */
-  emitSuccess (url, extraData) {
+  #emitSuccess (url, extraData) {
     const emitData = {
       action: 'success',
       payload: { ...extraData, complete: true, url },
@@ -473,9 +470,10 @@ class Uploader {
    *
    * @param {Error} err
    */
-  emitError (err) {
+  #emitError (err) {
     // delete stack to avoid sending server info to client
-    // todo remove also extraData from serializedErr in next major
+    // todo remove also extraData from serializedErr in next major,
+    // see PR discussion https://github.com/transloadit/uppy/pull/3832
     const { stack, ...serializedErr } = serializeError(err)
     const dataToEmit = {
       action: 'error',
@@ -636,18 +634,9 @@ class Uploader {
     const filename = this.uploadFileName
     const { client, options } = this.options.s3
 
-    function getPartSize (chunkSize) {
-      // backwards compatibility https://github.com/transloadit/uppy/pull/3511#issuecomment-1050797935
-      // requires min 5MiB and max 5GiB partSize
-      // todo remove this logic in the next major semver
-      if (chunkSize == null || chunkSize >= 5368709120 || chunkSize <= 5242880) return undefined
-      return chunkSize
-    }
-
     const params = {
       Bucket: options.bucket,
       Key: options.getKey(null, filename, this.options.metadata),
-      ACL: options.acl,
       ContentType: this.options.metadata.type,
       Metadata: this.options.metadata,
       Body: stream,
@@ -656,7 +645,8 @@ class Uploader {
     if (options.acl != null) params.ACL = options.acl
 
     const upload = client.upload(params, {
-      partSize: getPartSize(this.options.chunkSize),
+      // using chunkSize as partSize too, see https://github.com/transloadit/uppy/pull/3511
+      partSize: this.options.chunkSize,
     })
 
     upload.on('httpUploadProgress', ({ loaded, total }) => {

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

@@ -1,5 +1,5 @@
-const qs = require('querystring')
-const { URL } = require('url')
+const qs = require('node:querystring')
+const { URL } = require('node:url')
 const { hasMatch } = require('../helpers/utils')
 const oAuthState = require('../helpers/oauth-state')
 

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

@@ -1,4 +1,4 @@
-const { URL } = require('url')
+const { URL } = require('node:url')
 const serialize = require('serialize-javascript')
 
 const tokenService = require('../helpers/jwt')

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

@@ -1,6 +1,6 @@
 const router = require('express').Router
 const request = require('request')
-const { URL } = require('url')
+const { URL } = require('node:url')
 const validator = require('validator')
 
 const { startDownUpload } = require('../helpers/upload')
@@ -93,7 +93,6 @@ const meta = async (req, res) => {
     return res.json(urlMeta)
   } catch (err) {
     logger.error(err, 'controller.url.meta.error', req.id)
-    // @todo send more meaningful error message and status code to client if possible
     return res.status(err.status || 500).json({ message: 'failed to fetch URL metadata' })
   }
 }
@@ -125,7 +124,6 @@ const get = async (req, res) => {
 
   function onUnhandledError (err) {
     logger.error(err, 'controller.url.error', req.id)
-    // @todo send more meaningful error message and status code to client if possible
     res.status(err.status || 500).json({ message: 'failed to fetch URL metadata' })
   }
 

+ 1 - 1
packages/@uppy/companion/src/server/emitter/default-emitter.js

@@ -1,4 +1,4 @@
-const { EventEmitter } = require('events')
+const { EventEmitter } = require('node:events')
 
 module.exports = () => {
   return new EventEmitter()

+ 86 - 17
packages/@uppy/companion/src/server/emitter/redis-emitter.js

@@ -1,4 +1,5 @@
-const NRP = require('node-redis-pubsub')
+const redis = require('redis')
+const { EventEmitter } = require('node:events')
 
 /**
  * This module simulates the builtin events.EventEmitter but with the use of redis.
@@ -6,44 +7,108 @@ const NRP = require('node-redis-pubsub')
  * to be distributed across.
  */
 module.exports = (redisUrl, redisPubSubScope) => {
-  const nrp = new NRP({ url: redisUrl, scope: redisPubSubScope })
+  const prefix = redisPubSubScope ? `${redisPubSubScope}:` : ''
+  const getPrefixedEventName = (eventName) => `${prefix}${eventName}`
+  const publisher = redis.createClient({ url: redisUrl })
+  let subscriber
 
+  const connectedPromise = publisher.connect().then(() => {
+    subscriber = publisher.duplicate()
+    return subscriber.connect()
+  })
+
+  const handlersByEvent = new Map()
+
+  const errorEmitter = new EventEmitter()
+  const handleError = (err) => errorEmitter.emit('error', err)
+
+  connectedPromise.catch((err) => handleError(err))
+
+  async function runWhenConnected (fn) {
+    try {
+      await connectedPromise
+      await fn()
+    } catch (err) {
+      handleError(err)
+    }
+  }
+
+  function addListener (eventName, handler, _once = false) {
+    function actualHandler (message) {
+      if (_once) removeListener(eventName, handler)
+      let args
+      try {
+        args = JSON.parse(message)
+      } catch (ex) {
+        return handleError(new Error(`Invalid JSON received! Channel: ${eventName} Message: ${message}`))
+      }
+      return handler(...args)
+    }
+
+    let handlersByThisEventName = handlersByEvent.get(eventName)
+    if (handlersByThisEventName == null) {
+      handlersByThisEventName = new WeakMap()
+      handlersByEvent.set(eventName, handlersByThisEventName)
+    }
+    handlersByThisEventName.set(handler, actualHandler)
+
+    runWhenConnected(() => subscriber.pSubscribe(getPrefixedEventName(eventName), actualHandler))
+  }
+
+  /**
+   * Add an event listener
+   *
+   * @param {string} eventName name of the event
+   * @param {any} handler the handler of the event
+   */
   function on (eventName, handler) {
-    nrp.on(eventName, handler)
+    if (eventName === 'error') return errorEmitter.on('error', handler)
+
+    return addListener(eventName, handler)
   }
 
   /**
-   * Add a one-off event listener
+   * Add an event listener (will be triggered at most once)
    *
    * @param {string} eventName name of the event
-   * @param {Function} handler the handler of the event
+   * @param {any} handler the handler of the event
    */
   function once (eventName, handler) {
-    const off = nrp.on(eventName, (message) => {
-      handler(message)
-      off()
-    })
+    if (eventName === 'error') return errorEmitter.once('error', handler)
+
+    return addListener(eventName, handler, true)
   }
 
   /**
    * Announce the occurence of an event
    *
    * @param {string} eventName name of the event
-   * @param {object} message the message to pass along with the event
    */
-  function emit (eventName, message) {
-    return nrp.emit(eventName, message || {})
+  function emit (eventName, ...args) {
+    runWhenConnected(() => publisher.publish(getPrefixedEventName(eventName), JSON.stringify(args)))
   }
 
   /**
    * Remove an event listener
    *
    * @param {string} eventName name of the event
-   * @param {Function} handler the handler of the event to remove
+   * @param {any} handler the handler of the event to remove
    */
   function removeListener (eventName, handler) {
-    nrp.receiver.removeListener(eventName, handler)
-    nrp.receiver.punsubscribe(`${nrp.prefix}${eventName}`)
+    if (eventName === 'error') return errorEmitter.removeListener('error', handler)
+
+    return runWhenConnected(() => {
+      const handlersByThisEventName = handlersByEvent.get(eventName)
+      if (handlersByThisEventName == null) return undefined
+
+      const actualHandler = handlersByThisEventName.get(handler)
+      if (actualHandler == null) return undefined
+
+      handlersByThisEventName.delete(handler)
+      if (handlersByThisEventName.size === 0) handlersByEvent.delete(eventName)
+
+      return subscriber.pUnsubscribe(getPrefixedEventName(eventName), actualHandler)
+    })
   }
 
   /**
@@ -52,8 +117,12 @@ module.exports = (redisUrl, redisPubSubScope) => {
    * @param {string} eventName name of the event
    */
   function removeAllListeners (eventName) {
-    nrp.receiver.removeAllListeners(eventName)
-    nrp.receiver.punsubscribe(`${nrp.prefix}${eventName}`)
+    if (eventName === 'error') return errorEmitter.removeAllListeners(eventName)
+
+    return runWhenConnected(() => {
+      handlersByEvent.delete(eventName)
+      return subscriber.pUnsubscribe(getPrefixedEventName(eventName))
+    })
   }
 
   return {

+ 1 - 2
packages/@uppy/companion/src/server/header-blacklist.js

@@ -1,4 +1,3 @@
-const isObject = require('isobject')
 const logger = require('./logger')
 
 /**
@@ -50,7 +49,7 @@ const isForbiddenHeader = (header) => {
 }
 
 module.exports = (headers) => {
-  if (!isObject(headers)) {
+  if (headers == null || typeof headers !== 'object' || Array.isArray(headers)) {
     return {}
   }
 

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

@@ -42,7 +42,6 @@ module.exports.generateEncryptedToken = (payload, secret) => {
  */
 module.exports.verifyEncryptedToken = (token, secret) => {
   try {
-    // @ts-ignore
     return module.exports.verifyToken(decrypt(token, secret), secret)
   } catch (err) {
     return { err }

+ 1 - 2
packages/@uppy/companion/src/server/helpers/oauth-state.js

@@ -1,5 +1,4 @@
-const crypto = require('crypto')
-// @ts-ignore
+const crypto = require('node:crypto')
 const atob = require('atob')
 const { encrypt, decrypt } = require('./utils')
 

+ 4 - 4
packages/@uppy/companion/src/server/helpers/request.js

@@ -1,8 +1,8 @@
 // eslint-disable-next-line max-classes-per-file
-const http = require('http')
-const https = require('https')
-const { URL } = require('url')
-const dns = require('dns')
+const http = require('node:http')
+const https = require('node:https')
+const { URL } = require('node:url')
+const dns = require('node:dns')
 const request = require('request')
 const ipaddr = require('ipaddr.js')
 

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

@@ -1,4 +1,4 @@
-const crypto = require('crypto')
+const crypto = require('node:crypto')
 
 /**
  *
@@ -164,3 +164,5 @@ module.exports.requestStream = async (req, convertResponseToError) => {
 
   return { stream: resp }
 }
+
+module.exports.defaultGetKey = (req, filename) => `${crypto.randomUUID()}-${filename}`

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

@@ -1,7 +1,7 @@
 const schedule = require('node-schedule')
-const fs = require('fs')
-const path = require('path')
-const { promisify } = require('util')
+const fs = require('node:fs')
+const path = require('node:path')
+const { promisify } = require('node:util')
 const request = require('request')
 
 const { FILE_NAME_PREFIX } = require('./Uploader')

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

@@ -1,6 +1,6 @@
 const chalk = require('chalk')
 const escapeStringRegexp = require('escape-string-regexp')
-const util = require('util')
+const util = require('node:util')
 const { ProviderApiError, ProviderAuthError } = require('./provider/error')
 
 const valuesToMask = []

+ 5 - 8
packages/@uppy/companion/src/server/middlewares.js

@@ -1,5 +1,4 @@
 const cors = require('cors')
-// @ts-ignore
 const promBundle = require('express-prom-bundle')
 
 // @ts-ignore
@@ -69,14 +68,14 @@ exports.cookieAuthToken = (req, res, next) => {
 }
 
 exports.loadSearchProviderToken = (req, res, next) => {
-  const { searchProviders } = req.companion.options.providerOptions
+  const { providerOptions } = req.companion.options
   const providerName = req.params.searchProviderName
-  if (!searchProviders || !searchProviders[providerName] || !searchProviders[providerName].key) {
+  if (!providerOptions[providerName] || !providerOptions[providerName].key) {
     logger.info(`unconfigured credentials for ${providerName}`, 'searchtoken.load.unset', req.id)
     return res.sendStatus(501)
   }
 
-  req.companion.providerToken = searchProviders[providerName].key
+  req.companion.providerToken = providerOptions[providerName].key
   next()
 }
 
@@ -84,9 +83,8 @@ exports.cors = (options = {}) => (req, res, next) => {
   // HTTP headers are not case sensitive, and express always handles them in lower case, so that's why we lower case them.
   // I believe that HTTP verbs are case sensitive, and should be uppercase.
 
-  // TODO: Move to optional chaining when we drop Node.js v12.x support
   const existingExposeHeaders = res.get('Access-Control-Expose-Headers')
-  const exposeHeadersSet = new Set(existingExposeHeaders && existingExposeHeaders.split(',').map(method => method.trim().toLowerCase()))
+  const exposeHeadersSet = new Set(existingExposeHeaders?.split(',')?.map((method) => method.trim().toLowerCase()))
 
   // exposed so it can be accessed for our custom uppy client preflight
   exposeHeadersSet.add('access-control-allow-headers')
@@ -111,7 +109,7 @@ exports.cors = (options = {}) => (req, res, next) => {
     : allowedHeaders)
 
   const existingAllowMethods = res.get('Access-Control-Allow-Methods')
-  const allowMethodsSet = new Set(existingAllowMethods && existingAllowMethods.split(',').map(method => method.trim().toUpperCase()))
+  const allowMethodsSet = new Set(existingAllowMethods?.split(',')?.map((method) => method.trim().toUpperCase()))
   // Needed for basic operation:
   allowMethodsSet.add('GET').add('POST').add('OPTIONS').add('DELETE')
 
@@ -141,7 +139,6 @@ exports.metrics = ({ path = undefined } = {}) => {
 
   // Add version as a prometheus gauge
   const versionGauge = new promClient.Gauge({ name: 'companion_version', help: 'npm version as an integer' })
-  // @ts-ignore
   const numberVersion = Number(version.replace(/\D/g, ''))
   versionGauge.set(numberVersion)
   return metricsMiddleware

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

@@ -77,6 +77,4 @@ class Provider {
   }
 }
 
-Provider.version = 1
-
 module.exports = Provider

+ 0 - 54
packages/@uppy/companion/src/server/provider/ProviderCompat.js

@@ -1,54 +0,0 @@
-const { promisify } = require('util')
-
-const Stream = require('stream')
-
-/**
- * Backward compatibility layer for old provider API using callbacks and onData cb
- *
- * @returns {any}
- */
-const wrapLegacyProvider = (legacyProvider) => {
-  class CompatProvider extends legacyProvider {
-    constructor (...args) {
-      super(...args)
-
-      this.list = promisify((options, cb) => super.list(options, cb))
-      this.size = promisify((options, cb) => super.size(options, cb))
-      this.thumbnail = promisify((options, cb) => super.thumbnail(options, cb))
-      this.deauthorizationCallback = promisify((options, cb) => super.deauthorizationCallback(options, cb))
-      this.logout = promisify((options, cb) => super.logout(options, cb))
-
-      const superDownload = super.download
-
-      this.download = async (options) => {
-        let stream
-
-        return new Promise((resolve, reject) => {
-          superDownload(options, (err, chunk) => {
-            if (err) {
-              if (stream && !stream.destroyed) stream.destroy(err)
-              reject(err)
-              return
-            }
-
-            // Initialize on first chunk
-            if (chunk != null && !stream) {
-              stream = new Stream.PassThrough()
-              // stream.on('end', () => console.log('stream end'))
-              stream.pause()
-              stream.push(chunk)
-              resolve({ stream })
-              return
-            }
-
-            stream.push(chunk)
-          })
-        })
-      }
-    }
-  }
-
-  return CompatProvider
-}
-
-module.exports = { wrapLegacyProvider }

+ 0 - 2
packages/@uppy/companion/src/server/provider/SearchProvider.js

@@ -33,6 +33,4 @@ class SearchProvider {
   }
 }
 
-SearchProvider.version = 1
-
 module.exports = SearchProvider

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

@@ -1,5 +1,5 @@
 const mime = require('mime-types')
-const querystring = require('querystring')
+const querystring = require('node:querystring')
 
 exports.isFolder = (item) => {
   return item.type === 'folder'

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

@@ -1,6 +1,6 @@
 const request = require('request')
 const purest = require('purest')({ request })
-const { promisify } = require('util')
+const { promisify } = require('node:util')
 
 const Provider = require('../Provider')
 const logger = require('../../logger')
@@ -216,8 +216,6 @@ class Box extends Provider {
   }
 }
 
-Box.version = 2
-
 Box.prototype.list = promisify(Box.prototype._list)
 Box.prototype.size = promisify(Box.prototype._size)
 Box.prototype.logout = promisify(Box.prototype._logout)

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