mirror of
https://github.com/transloadit/uppy.git
synced 2026-01-23 02:25:07 +00:00
Merge branch 'main' into 4.x
* main: (90 commits) crash if trying to set path to / (#5003) fix `super.toggleCheckbox` bug (#5004) @uppy/aws-s3-multipart: fix escaping issue with client signed request (#5006) add missing exports (#5009) @uppy/transloadit: migrate to TS (#4987) @uppy/utils: fix `RateLimitedQueue#wrapPromiseFunction` types (#5007) @uppy/golden-retriever: migrate to TS (#4989) Bump follow-redirects from 1.15.4 to 1.15.6 (#5002) meta: fix `resize-observer-polyfill` types (#4994) @uppy/core: various type fixes (#4995) @uppy/utils: fix `findAllDOMElements` type (#4997) @uppy/status-bar: fix `recoveredState` type (#4996) @uppy/utils: fix `AbortablePromise` type (#4988) Fix breadcrumbs (#4986) @uppy/drag-drop: refactor to TypeScript (#4983) @uppy/webcam: refactor to TypeScript (#4870) @uppy/url: migrate to TS (#4980) @uppy/zoom: refactor to TypeScript (#4979) @uppy/unsplash: refactor to TypeScript (#4979) @uppy/onedrive: refactor to TypeScript (#4979) ...
This commit is contained in:
commit
a787a6496f
458 changed files with 11401 additions and 5397 deletions
|
|
@ -340,6 +340,7 @@ module.exports = {
|
|||
{
|
||||
files: [
|
||||
'*.test.js',
|
||||
'*.test.ts',
|
||||
'test/endtoend/*.js',
|
||||
'bin/**.js',
|
||||
],
|
||||
|
|
@ -462,9 +463,11 @@ module.exports = {
|
|||
],
|
||||
rules: {
|
||||
'import/prefer-default-export': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-extra-semi': 'off',
|
||||
'@typescript-eslint/no-namespace': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
|||
15
.github/workflows/e2e.yml
vendored
15
.github/workflows/e2e.yml
vendored
|
|
@ -184,17 +184,14 @@ jobs:
|
|||
needs: [compare_diff]
|
||||
if:
|
||||
github.event.pull_request.state == 'open' &&
|
||||
github.event.pull_request.head.repo.full_name != github.repository &&
|
||||
github.event.action != 'labeled'
|
||||
github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add label
|
||||
if:
|
||||
(needs.compare_diff.outputs.diff != '' ||
|
||||
!needs.compare_diff.outputs.is_accurate_diff) &&
|
||||
(!contains(github.event.pull_request.labels.*.name, 'safe to test') &&
|
||||
!contains(github.event.pull_request.labels.*.name, 'pending end-to-end
|
||||
tests'))
|
||||
!needs.compare_diff.outputs.is_accurate_diff) && (github.event.action
|
||||
!= 'labeled' || github.event.label.name != 'safe to test')
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
@ -204,10 +201,8 @@ jobs:
|
|||
- name: Remove label
|
||||
if:
|
||||
needs.compare_diff.outputs.diff == '' &&
|
||||
needs.compare_diff.outputs.is_accurate_diff &&
|
||||
(contains(github.event.pull_request.labels.*.name, 'safe to test') ||
|
||||
contains(github.event.pull_request.labels.*.name, 'pending end-to-end
|
||||
tests'))
|
||||
needs.compare_diff.outputs.is_accurate_diff && github.event.action ==
|
||||
'labeled' && github.event.label.name == 'safe to test'
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -26,6 +26,7 @@ test/endtoend/create-react-app/build/
|
|||
test/endtoend/create-react-app/coverage/
|
||||
uppy-*.tgz
|
||||
generatedLocale.d.ts
|
||||
packages/@uppy/locales/src/en_US.js
|
||||
|
||||
**/output/*
|
||||
!output/.keep
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
/** @type {import("prettier").Config} */
|
||||
module.exports = {
|
||||
proseWrap: 'always',
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
semi: false,
|
||||
experimentalTernaries: true,
|
||||
overrides: [
|
||||
{
|
||||
files: 'packages/@uppy/angular/**',
|
||||
|
|
|
|||
54
.vscode/uppy.code-workspace
vendored
54
.vscode/uppy.code-workspace
vendored
|
|
@ -1,29 +1,29 @@
|
|||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"workbench.colorCustomizations": {
|
||||
"titleBar.activeForeground": "#ffffff",
|
||||
"titleBar.activeBackground": "#ff009d",
|
||||
},
|
||||
"search.exclude": {
|
||||
"website/public/": true,
|
||||
"node_modules/": true,
|
||||
"website/node_modules/": true,
|
||||
"dist/": true,
|
||||
"lib/": true,
|
||||
"package-lock.json": true,
|
||||
"website/package-lock.json": true,
|
||||
"yarn-error.log": true,
|
||||
"website/.deploy_git": true,
|
||||
"npm-debug.log": true,
|
||||
"website/npm-debug.log": true,
|
||||
"website/debug.log": true,
|
||||
"nohup.out": true,
|
||||
"yarn.lock": true
|
||||
}
|
||||
}
|
||||
"folders": [
|
||||
{
|
||||
"path": "..",
|
||||
},
|
||||
],
|
||||
"settings": {
|
||||
"workbench.colorCustomizations": {
|
||||
"titleBar.activeForeground": "#ffffff",
|
||||
"titleBar.activeBackground": "#ff009d",
|
||||
},
|
||||
"search.exclude": {
|
||||
"website/public/": true,
|
||||
"node_modules/": true,
|
||||
"website/node_modules/": true,
|
||||
"dist/": true,
|
||||
"lib/": true,
|
||||
"package-lock.json": true,
|
||||
"website/package-lock.json": true,
|
||||
"yarn-error.log": true,
|
||||
"website/.deploy_git": true,
|
||||
"npm-debug.log": true,
|
||||
"website/npm-debug.log": true,
|
||||
"website/debug.log": true,
|
||||
"nohup.out": true,
|
||||
"yarn.lock": true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
diff --git a/src/index.d.ts b/src/index.d.ts
|
||||
index 74aacc0526ff554e9248c3f6fb44c353b5465efc..1b236d215a9db4cbc1c83f4d8bce24add202483e 100644
|
||||
--- a/src/index.d.ts
|
||||
+++ b/src/index.d.ts
|
||||
@@ -1,14 +1,3 @@
|
||||
-interface DOMRectReadOnly {
|
||||
- readonly x: number;
|
||||
- readonly y: number;
|
||||
- readonly width: number;
|
||||
- readonly height: number;
|
||||
- readonly top: number;
|
||||
- readonly right: number;
|
||||
- readonly bottom: number;
|
||||
- readonly left: number;
|
||||
-}
|
||||
-
|
||||
declare global {
|
||||
interface ResizeObserverCallback {
|
||||
(entries: ResizeObserverEntry[], observer: ResizeObserver): void
|
||||
36
.yarn/patches/tus-js-client-npm-3.1.3-dc57874d23.patch
Normal file
36
.yarn/patches/tus-js-client-npm-3.1.3-dc57874d23.patch
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
diff --git a/lib/index.d.ts b/lib/index.d.ts
|
||||
index 7a4efead6df94263db77b12c72ddaeafaeaa406c..e47e63f1159f19dd780986f7e33ffdd70bfdc371 100644
|
||||
--- a/lib/index.d.ts
|
||||
+++ b/lib/index.d.ts
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
export const isSupported: boolean
|
||||
export const canStoreURLs: boolean
|
||||
-export const defaultOptions: UploadOptions
|
||||
+export const defaultOptions: UploadOptions & Required<Pick<UploadOptions,
|
||||
+| 'httpStack'
|
||||
+| 'fileReader'
|
||||
+| 'urlStorage'
|
||||
+| 'fingerprint'
|
||||
+>>
|
||||
|
||||
// TODO: Consider using { read: () => Promise<{ done: boolean; value?: any; }>; } as type
|
||||
export class Upload {
|
||||
@@ -12,7 +17,7 @@ export class Upload {
|
||||
options: UploadOptions
|
||||
url: string | null
|
||||
|
||||
- static terminate(url: string, options?: UploadOptions): Promise<void>
|
||||
+ static terminate(url: string, options: UploadOptions): Promise<void>
|
||||
start(): void
|
||||
abort(shouldTerminate?: boolean): Promise<void>
|
||||
findPreviousUploads(): Promise<PreviousUpload[]>
|
||||
@@ -25,7 +30,7 @@ interface UploadOptions {
|
||||
|
||||
uploadUrl?: string | null
|
||||
metadata?: { [key: string]: string }
|
||||
- fingerprint?: (file: File, options?: UploadOptions) => Promise<string>
|
||||
+ fingerprint?: (file: File, options: UploadOptions) => Promise<string>
|
||||
uploadSize?: number | null
|
||||
|
||||
onProgress?: ((bytesSent: number, bytesTotal: number) => void) | null
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Uppy
|
||||
|
||||
Hi, thanks for trying out the bundled version of the Uppy File Uploader. You can use
|
||||
this from a CDN (`<script src="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.js"></script>`) or bundle it with your webapp.
|
||||
this from a CDN (`<script src="https://releases.transloadit.com/uppy/v3.23.0/uppy.min.js"></script>`) or bundle it with your webapp.
|
||||
|
||||
Note that the recommended way to use Uppy is to install it with yarn/npm and use a
|
||||
bundler like Webpack so that you can create a smaller custom build with only the
|
||||
|
|
|
|||
177
CHANGELOG.md
177
CHANGELOG.md
|
|
@ -12,6 +12,183 @@ Please add your entries in this format:
|
|||
|
||||
In the current stage we aim to release a new version at least every month.
|
||||
|
||||
## 3.23.0
|
||||
|
||||
Released: 2024-02-28
|
||||
|
||||
| Package | Version | Package | Version |
|
||||
| ---------------------- | ------- | ---------------------- | ------- |
|
||||
| @uppy/box | 2.2.1 | @uppy/onedrive | 3.2.1 |
|
||||
| @uppy/companion-client | 3.7.4 | @uppy/progress-bar | 3.1.0 |
|
||||
| @uppy/core | 3.9.3 | @uppy/provider-views | 3.10.0 |
|
||||
| @uppy/dashboard | 3.7.5 | @uppy/status-bar | 3.3.0 |
|
||||
| @uppy/file-input | 3.1.0 | @uppy/utils | 5.7.4 |
|
||||
| @uppy/form | 3.2.0 | @uppy/xhr-upload | 3.6.4 |
|
||||
| @uppy/image-editor | 2.4.4 | uppy | 3.23.0 |
|
||||
| @uppy/informer | 3.1.0 | | |
|
||||
|
||||
- @uppy/form: migrate to TS (Merlijn Vos / #4937)
|
||||
- @uppy/box: fetchPreAuthToken in box too (Mikael Finstad / #4969)
|
||||
- @uppy/progress-bar: refactor to TypeScript (Mikael Finstad / #4921)
|
||||
- @uppy/onedrive: fix custom oauth2 credentials for onedrive (Mikael Finstad / #4968)
|
||||
- @uppy/companion-client,@uppy/utils,@uppy/xhr-upload: improvements for #4922 (Mikael Finstad / #4960)
|
||||
- @uppy/utils: fix various type issues (Mikael Finstad / #4958)
|
||||
- @uppy/provider-views: migrate to TS (Merlijn Vos / #4919)
|
||||
- @uppy/utils: simplify `findDOMElements` (Mikael Finstad / #4957)
|
||||
- @uppy/xhr-upload: fix getResponseData regression (Merlijn Vos / #4964)
|
||||
- @uppy/informer: migrate to TS (Merlijn Vos / #4967)
|
||||
- @uppy/core: remove unused import (Antoine du Hamel / #4972)
|
||||
- @uppy/image-editor: remove default target (Merlijn Vos / #4966)
|
||||
- @uppy/angular: Build fixes (Mikael Finstad / #4959)
|
||||
- meta: Fix flaky e2e test (Murderlon)
|
||||
- meta: fix e2e flake (Mikael Finstad / #4961)
|
||||
- meta: add support for `Fragment` short syntax (Antoine du Hamel / #4953)
|
||||
- @uppy/file-input: refactor to TypeScript (Antoine du Hamel / #4954)
|
||||
|
||||
|
||||
## 3.22.2
|
||||
|
||||
Released: 2024-02-22
|
||||
|
||||
| Package | Version | Package | Version |
|
||||
| ---------------------- | ------- | ---------------------- | ------- |
|
||||
| @uppy/audio | 1.1.7 | @uppy/react | 3.2.2 |
|
||||
| @uppy/companion | 4.12.3 | @uppy/status-bar | 3.2.8 |
|
||||
| @uppy/companion-client | 3.7.3 | @uppy/tus | 3.5.3 |
|
||||
| @uppy/core | 3.9.2 | @uppy/utils | 5.7.3 |
|
||||
| @uppy/dashboard | 3.7.4 | @uppy/xhr-upload | 3.6.3 |
|
||||
| @uppy/image-editor | 2.4.3 | uppy | 3.22.2 |
|
||||
|
||||
- @uppy/core: fix plugin detection (Antoine du Hamel / #4951)
|
||||
- @uppy/core,@uppy/utils: Introduce `ValidateableFile` & move `MinimalRequiredUppyFile` into utils (Antoine du Hamel / #4944)
|
||||
- meta: uppy: fix bundle builder (Antoine du Hamel / #4950)
|
||||
- @uppy/core: improve `UIPluginOptions` types (Merlijn Vos / #4946)
|
||||
- @uppy/companion-client: fix body/url on upload-success (Merlijn Vos / #4922)
|
||||
- @uppy/utils: remove EventManager circular reference (Merlijn Vos / #4949)
|
||||
- @uppy/dashboard: MetaEditor + ImageEditor - new state machine logic (Evgenia Karunus / #4939)
|
||||
- meta: disable `@typescript-eslint/no-non-null-assertion` lint rule (Antoine du Hamel / #4945)
|
||||
- @uppy/companion-client: remove unnecessary `'use strict'` directives (Antoine du Hamel / #4943)
|
||||
- @uppy/companion-client: type changes for provider-views (Antoine du Hamel / #4938)
|
||||
- meta: bump ip from 1.1.8 to 1.1.9 (dependabot[bot] / #4941)
|
||||
- @uppy/companion-client: update types (Antoine du Hamel / #4927)
|
||||
|
||||
|
||||
## 3.22.1
|
||||
|
||||
Released: 2024-02-20
|
||||
|
||||
| Package | Version | Package | Version |
|
||||
| ------------------------- | ------- | ------------------------- | ------- |
|
||||
| @uppy/audio | 1.1.6 | @uppy/remote-sources | 1.1.2 |
|
||||
| @uppy/aws-s3 | 3.6.2 | @uppy/status-bar | 3.2.7 |
|
||||
| @uppy/aws-s3-multipart | 3.10.2 | @uppy/store-default | 3.2.2 |
|
||||
| @uppy/companion | 4.12.2 | @uppy/store-redux | 3.0.7 |
|
||||
| @uppy/companion-client | 3.7.2 | @uppy/svelte | 3.1.3 |
|
||||
| @uppy/compressor | 1.1.1 | @uppy/thumbnail-generator | 3.0.8 |
|
||||
| @uppy/core | 3.9.1 | @uppy/transloadit | 3.5.1 |
|
||||
| @uppy/dashboard | 3.7.3 | @uppy/tus | 3.5.2 |
|
||||
| @uppy/drop-target | 2.0.4 | @uppy/utils | 5.7.2 |
|
||||
| @uppy/form | 3.1.1 | @uppy/vue | 1.1.2 |
|
||||
| @uppy/golden-retriever | 3.1.3 | @uppy/webcam | 3.3.6 |
|
||||
| @uppy/image-editor | 2.4.2 | @uppy/xhr-upload | 3.6.2 |
|
||||
| @uppy/locales | 3.5.2 | uppy | 3.22.1 |
|
||||
| @uppy/provider-views | 3.9.1 | | |
|
||||
|
||||
- @uppy/locales: update vi_VN translation (David Nguyen / #4930)
|
||||
- @uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/status-bar: bump `@transloadit/prettier-bytes` (Antoine du Hamel / #4933)
|
||||
|
||||
|
||||
## 3.22.0
|
||||
|
||||
Released: 2024-02-19
|
||||
|
||||
| Package | Version | Package | Version |
|
||||
| ------------------------- | ------- | ------------------------- | ------- |
|
||||
| @uppy/audio | 1.1.5 | @uppy/remote-sources | 1.1.1 |
|
||||
| @uppy/aws-s3 | 3.6.1 | @uppy/status-bar | 3.2.6 |
|
||||
| @uppy/aws-s3-multipart | 3.10.1 | @uppy/store-default | 3.2.1 |
|
||||
| @uppy/companion | 4.12.1 | @uppy/store-redux | 3.0.6 |
|
||||
| @uppy/companion-client | 3.7.1 | @uppy/svelte | 3.1.2 |
|
||||
| @uppy/compressor | 1.1.0 | @uppy/thumbnail-generator | 3.0.7 |
|
||||
| @uppy/core | 3.9.0 | @uppy/transloadit | 3.5.0 |
|
||||
| @uppy/dashboard | 3.7.2 | @uppy/tus | 3.5.1 |
|
||||
| @uppy/drop-target | 2.0.3 | @uppy/utils | 5.7.1 |
|
||||
| @uppy/form | 3.1.0 | @uppy/vue | 1.1.1 |
|
||||
| @uppy/golden-retriever | 3.1.2 | @uppy/webcam | 3.3.5 |
|
||||
| @uppy/image-editor | 2.4.1 | @uppy/xhr-upload | 3.6.1 |
|
||||
| @uppy/locales | 3.5.1 | uppy | 3.22.0 |
|
||||
| @uppy/provider-views | 3.9.0 | | |
|
||||
|
||||
- @uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/companion-client,@uppy/tus,@uppy/xhr-upload: update `uppyfile` objects before emitting events (antoine du hamel / #4928)
|
||||
- @uppy/transloadit: add `clientname` option (marius / #4920)
|
||||
- @uppy/thumbnail-generator: fix broken previews after cropping (evgenia karunus / #4926)
|
||||
- @uppy/compressor: upgrade compressorjs (merlijn vos / #4924)
|
||||
- @uppy/companion: fix companion dns and allow redirects from http->https again (mikael finstad / #4895)
|
||||
- @uppy/dashboard: autoopenfileeditor - rename "edit file" to "edit image" (evgenia karunus / #4925)
|
||||
- meta: resolve jsx to preact in shared tsconfig (merlijn vos / #4923)
|
||||
- @uppy/image-editor: image editor: make compressor work after the image editor, too (evgenia karunus / #4918)
|
||||
- meta: exclude `tsconfig` files from npm bundles (antoine du hamel / #4916)
|
||||
- @uppy/compressor: migrate to ts (mikael finstad / #4907)
|
||||
- @uppy/provider-views: update uppy-providerbrowser-viewtype--list.scss (aditya patadia / #4913)
|
||||
- @uppy/tus: migrate to ts (merlijn vos / #4899)
|
||||
- meta: bump yarn version (antoine du hamel / #4906)
|
||||
- meta: validate `defaultoptions` for stricter option types (antoine du hamel / #4901)
|
||||
- @uppy/dashboard: Uncouple native camera and video buttons from the `disableLocalFiles` option (jake mcallister / #4894)
|
||||
- meta: put experimental ternaries in .prettierrc.js (merlijn vos / #4900)
|
||||
- @uppy/xhr-upload: migrate to ts (merlijn vos / #4892)
|
||||
- @uppy/drop-target: refactor to typescript (artur paikin / #4863)
|
||||
- meta: fix missing line return in js2ts script (antoine du hamel)
|
||||
- meta: disable `@typescript-eslint/no-empty-function` lint rule (antoine du hamel / #4891)
|
||||
- @uppy/companion-client: fix tests and linter (antoine du hamel / #4890)
|
||||
- @uppy/companion-client: migrate to ts (merlijn vos / #4864)
|
||||
- meta: prettier 3.0.3 -> 3.2.4 (antoine du hamel / #4889)
|
||||
- @uppy/image-editor: migrate to ts (merlijn vos / #4880)
|
||||
- meta: fix race condition in `e2e.yml` (antoine du hamel)
|
||||
- @uppy/core: add utility type to help define plugin option types (antoine du hamel / #4885)
|
||||
- meta: merge `output-watcher` and `e2e` workflows (antoine du hamel / #4886)
|
||||
- @uppy/status-bar: fix `statusbaroptions` type (antoine du hamel / #4883)
|
||||
- @uppy/core: improve types of .use() (merlijn vos / #4882)
|
||||
- @uppy/audio: fix `audiooptions` (antoine du hamel / #4884)
|
||||
- meta: upgrade vite and vitest (antoine du hamel / #4881)
|
||||
- meta: fix `yarn build:clean` (antoine du hamel)
|
||||
- @uppy/audio: refactor to typescript (antoine du hamel / #4860)
|
||||
- @uppy/status-bar: refactor to typescript (antoine du hamel / #4839)
|
||||
- @uppy/core: add `plugintarget` type and mark options as optional (antoine du hamel / #4874)
|
||||
- meta: improve output watcher diff (antoine du hamel / #4876)
|
||||
- meta: minify the output watcher diff further (antoine du hamel)
|
||||
- meta: remove comments from output watcher (mikael finstad / #4875)
|
||||
- @uppy/utils: improve types for `finddomelement` (antoine du hamel / #4873)
|
||||
- @uppy/code: allow plugins to type `pluginstate` (antoine du hamel / #4872)
|
||||
- meta: build(deps): bump follow-redirects from 1.15.1 to 1.15.4 (dependabot[bot] / #4862)
|
||||
- meta: add `output-watcher` gha to help check output diff (antoine du hamel / #4868)
|
||||
- meta: generate locale pack from output file (antoine du hamel / #4867)
|
||||
- meta: comment on what we want to do about close, resetprogress, clearuploadedfiles, etc in the next major (artur paikin / #4865)
|
||||
- meta: fix `yarn build:clean` (antoine du hamel / #4866)
|
||||
- meta: use `explicit-module-boundary-types` lint rule (antoine du hamel / #4858)
|
||||
- @uppy/form: use requestsubmit (merlijn vos / #4852)
|
||||
- @uppy/provider-views: add referrerpolicy to images (merlijn vos / #4853)
|
||||
- @uppy/core: add `debuglogger` as export in manual types (antoine du hamel / #4831)
|
||||
- meta: fix `js2ts` script (antoine du hamel / #4846)
|
||||
- @uppy/xhr-upload: show remove button (merlijn vos / #4851)
|
||||
- meta: upgrade `@transloadit/prettier-bytes` (antoine du hamel / #4850)
|
||||
- @uppy/core: add missing requiredmetafields key in restrictions (darthf1 / #4819)
|
||||
- @uppy/companion,@uppy/tus: bump `tus-js-client` version range (merlijn vos / #4848)
|
||||
- meta: build(deps): bump aws/aws-sdk-php from 3.272.1 to 3.288.1 in /examples/aws-php (dependabot[bot] / #4838)
|
||||
- @uppy/dashboard: fix `typeerror` when `file.remote` is nullish (antoine du hamel / #4825)
|
||||
- meta: fix `js2ts` script (antoine du hamel / #4844)
|
||||
- @uppy/locales: fix "save" button translation in hr_hr.ts (žan žlender / #4830)
|
||||
- meta: fix linting of `.tsx` files (antoine du hamel / #4843)
|
||||
- @uppy/core: fix types (antoine du hamel / #4842)
|
||||
- @uppy/utils: improve `preprocess` and `postprocess` types (antoine du hamel / #4841)
|
||||
- meta: fix `yarn build:clean` (mikael finstad / #4840)
|
||||
- meta: dev: remove extensions from vite aliases (antoine du hamel)
|
||||
- meta: fix `"e2e"` script (antoine du hamel)
|
||||
- @uppy/core: refactor to ts (murderlon)
|
||||
- meta: fix typescript ci (antoine du hamel)
|
||||
- meta: fix clean script (mikael finstad / #4820)
|
||||
- @uppy/companion-client: fix `typeerror` (antoine du hamel)
|
||||
|
||||
|
||||
## 3.21.0
|
||||
|
||||
Released: 2023-12-12
|
||||
|
|
|
|||
261
README.md
261
README.md
|
|
@ -65,7 +65,7 @@ const uppy = new Uppy()
|
|||
npm install @uppy/core @uppy/dashboard @uppy/tus
|
||||
```
|
||||
|
||||
Add CSS [uppy.min.css](https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css), either to your HTML page’s `<head>` or include in JS, if your bundler of choice supports it.
|
||||
Add CSS [uppy.min.css](https://releases.transloadit.com/uppy/v3.23.0/uppy.min.css), either to your HTML page’s `<head>` or include in JS, if your bundler of choice supports it.
|
||||
|
||||
Alternatively, you can also use a pre-built bundle from Transloadit’s CDN: Edgly. In that case `Uppy` will attach itself to the global `window.Uppy` object.
|
||||
|
||||
|
|
@ -73,12 +73,12 @@ Alternatively, you can also use a pre-built bundle from Transloadit’s CDN: Edg
|
|||
|
||||
```html
|
||||
<!-- 1. Add CSS to `<head>` -->
|
||||
<link href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css" rel="stylesheet">
|
||||
<link href="https://releases.transloadit.com/uppy/v3.23.0/uppy.min.css" rel="stylesheet">
|
||||
|
||||
<!-- 2. Initialize -->
|
||||
<div id="files-drag-drop"></div>
|
||||
<script type="module">
|
||||
import { Uppy, Dashboard, Tus } from "https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs"
|
||||
import { Uppy, Dashboard, Tus } from "https://releases.transloadit.com/uppy/v3.23.0/uppy.min.mjs"
|
||||
|
||||
const uppy = new Uppy()
|
||||
uppy.use(Dashboard, { target: '#files-drag-drop' })
|
||||
|
|
@ -199,265 +199,265 @@ Use Uppy in your project? [Let us know](https://github.com/transloadit/uppy/issu
|
|||
|
||||
<!--contributors-->
|
||||
|
||||
[<img alt="arturi" src="https://avatars.githubusercontent.com/u/1199054?v=4&s=117" width="117">](https://github.com/arturi) |[<img alt="goto-bus-stop" src="https://avatars.githubusercontent.com/u/1006268?v=4&s=117" width="117">](https://github.com/goto-bus-stop) |[<img alt="kvz" src="https://avatars.githubusercontent.com/u/26752?v=4&s=117" width="117">](https://github.com/kvz) |[<img alt="ifedapoolarewaju" src="https://avatars.githubusercontent.com/u/8383781?v=4&s=117" width="117">](https://github.com/ifedapoolarewaju) |[<img alt="aduh95" src="https://avatars.githubusercontent.com/u/14309773?v=4&s=117" width="117">](https://github.com/aduh95) |[<img alt="hedgerh" src="https://avatars.githubusercontent.com/u/2524280?v=4&s=117" width="117">](https://github.com/hedgerh) |
|
||||
[<img alt="arturi" src="https://avatars.githubusercontent.com/u/1199054?v=4&s=117" width="117">](https://github.com/arturi) |[<img alt="goto-bus-stop" src="https://avatars.githubusercontent.com/u/1006268?v=4&s=117" width="117">](https://github.com/goto-bus-stop) |[<img alt="kvz" src="https://avatars.githubusercontent.com/u/26752?v=4&s=117" width="117">](https://github.com/kvz) |[<img alt="aduh95" src="https://avatars.githubusercontent.com/u/14309773?v=4&s=117" width="117">](https://github.com/aduh95) |[<img alt="ifedapoolarewaju" src="https://avatars.githubusercontent.com/u/8383781?v=4&s=117" width="117">](https://github.com/ifedapoolarewaju) |[<img alt="hedgerh" src="https://avatars.githubusercontent.com/u/2524280?v=4&s=117" width="117">](https://github.com/hedgerh) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[arturi](https://github.com/arturi) |[goto-bus-stop](https://github.com/goto-bus-stop) |[kvz](https://github.com/kvz) |[ifedapoolarewaju](https://github.com/ifedapoolarewaju) |[aduh95](https://github.com/aduh95) |[hedgerh](https://github.com/hedgerh) |
|
||||
[arturi](https://github.com/arturi) |[goto-bus-stop](https://github.com/goto-bus-stop) |[kvz](https://github.com/kvz) |[aduh95](https://github.com/aduh95) |[ifedapoolarewaju](https://github.com/ifedapoolarewaju) |[hedgerh](https://github.com/hedgerh) |
|
||||
|
||||
[<img alt="AJvanLoon" src="https://avatars.githubusercontent.com/u/15716628?v=4&s=117" width="117">](https://github.com/AJvanLoon) |[<img alt="nqst" src="https://avatars.githubusercontent.com/u/375537?v=4&s=117" width="117">](https://github.com/nqst) |[<img alt="Murderlon" src="https://avatars.githubusercontent.com/u/9060226?v=4&s=117" width="117">](https://github.com/Murderlon) |[<img alt="mifi" src="https://avatars.githubusercontent.com/u/402547?v=4&s=117" width="117">](https://github.com/mifi) |[<img alt="github-actions[bot]" src="https://avatars.githubusercontent.com/in/15368?v=4&s=117" width="117">](https://github.com/apps/github-actions) |[<img alt="lakesare" src="https://avatars.githubusercontent.com/u/7578559?v=4&s=117" width="117">](https://github.com/lakesare) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[AJvanLoon](https://github.com/AJvanLoon) |[nqst](https://github.com/nqst) |[Murderlon](https://github.com/Murderlon) |[mifi](https://github.com/mifi) |[github-actions\[bot\]](https://github.com/apps/github-actions) |[lakesare](https://github.com/lakesare) |
|
||||
|
||||
[<img alt="kiloreux" src="https://avatars.githubusercontent.com/u/6282557?v=4&s=117" width="117">](https://github.com/kiloreux) |[<img alt="dependabot[bot]" src="https://avatars.githubusercontent.com/in/29110?v=4&s=117" width="117">](https://github.com/apps/dependabot) |[<img alt="sadovnychyi" src="https://avatars.githubusercontent.com/u/193864?v=4&s=117" width="117">](https://github.com/sadovnychyi) |[<img alt="samuelayo" src="https://avatars.githubusercontent.com/u/14964486?v=4&s=117" width="117">](https://github.com/samuelayo) |[<img alt="richardwillars" src="https://avatars.githubusercontent.com/u/291004?v=4&s=117" width="117">](https://github.com/richardwillars) |[<img alt="ajkachnic" src="https://avatars.githubusercontent.com/u/44317699?v=4&s=117" width="117">](https://github.com/ajkachnic) |
|
||||
[<img alt="kiloreux" src="https://avatars.githubusercontent.com/u/6282557?v=4&s=117" width="117">](https://github.com/kiloreux) |[<img alt="dependabot[bot]" src="https://avatars.githubusercontent.com/in/29110?v=4&s=117" width="117">](https://github.com/apps/dependabot) |[<img alt="samuelayo" src="https://avatars.githubusercontent.com/u/14964486?v=4&s=117" width="117">](https://github.com/samuelayo) |[<img alt="sadovnychyi" src="https://avatars.githubusercontent.com/u/193864?v=4&s=117" width="117">](https://github.com/sadovnychyi) |[<img alt="richardwillars" src="https://avatars.githubusercontent.com/u/291004?v=4&s=117" width="117">](https://github.com/richardwillars) |[<img alt="ajkachnic" src="https://avatars.githubusercontent.com/u/44317699?v=4&s=117" width="117">](https://github.com/ajkachnic) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[kiloreux](https://github.com/kiloreux) |[dependabot\[bot\]](https://github.com/apps/dependabot) |[sadovnychyi](https://github.com/sadovnychyi) |[samuelayo](https://github.com/samuelayo) |[richardwillars](https://github.com/richardwillars) |[ajkachnic](https://github.com/ajkachnic) |
|
||||
[kiloreux](https://github.com/kiloreux) |[dependabot\[bot\]](https://github.com/apps/dependabot) |[samuelayo](https://github.com/samuelayo) |[sadovnychyi](https://github.com/sadovnychyi) |[richardwillars](https://github.com/richardwillars) |[ajkachnic](https://github.com/ajkachnic) |
|
||||
|
||||
[<img alt="zcallan" src="https://avatars.githubusercontent.com/u/13760738?v=4&s=117" width="117">](https://github.com/zcallan) |[<img alt="YukeshShr" src="https://avatars.githubusercontent.com/u/71844521?v=4&s=117" width="117">](https://github.com/YukeshShr) |[<img alt="janko" src="https://avatars.githubusercontent.com/u/795488?v=4&s=117" width="117">](https://github.com/janko) |[<img alt="oliverpool" src="https://avatars.githubusercontent.com/u/3864879?v=4&s=117" width="117">](https://github.com/oliverpool) |[<img alt="Botz" src="https://avatars.githubusercontent.com/u/2706678?v=4&s=117" width="117">](https://github.com/Botz) |[<img alt="mcallistertyler" src="https://avatars.githubusercontent.com/u/14939210?v=4&s=117" width="117">](https://github.com/mcallistertyler) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[zcallan](https://github.com/zcallan) |[YukeshShr](https://github.com/YukeshShr) |[janko](https://github.com/janko) |[oliverpool](https://github.com/oliverpool) |[Botz](https://github.com/Botz) |[mcallistertyler](https://github.com/mcallistertyler) |
|
||||
|
||||
[<img alt="mokutsu-coursera" src="https://avatars.githubusercontent.com/u/65177495?v=4&s=117" width="117">](https://github.com/mokutsu-coursera) |[<img alt="dschmidt" src="https://avatars.githubusercontent.com/u/448487?v=4&s=117" width="117">](https://github.com/dschmidt) |[<img alt="DJWassink" src="https://avatars.githubusercontent.com/u/1822404?v=4&s=117" width="117">](https://github.com/DJWassink) |[<img alt="timodwhit" src="https://avatars.githubusercontent.com/u/2761203?v=4&s=117" width="117">](https://github.com/timodwhit) |[<img alt="taoqf" src="https://avatars.githubusercontent.com/u/15901911?v=4&s=117" width="117">](https://github.com/taoqf) |[<img alt="mrbatista" src="https://avatars.githubusercontent.com/u/6544817?v=4&s=117" width="117">](https://github.com/mrbatista) |
|
||||
[<img alt="mokutsu-coursera" src="https://avatars.githubusercontent.com/u/65177495?v=4&s=117" width="117">](https://github.com/mokutsu-coursera) |[<img alt="dschmidt" src="https://avatars.githubusercontent.com/u/448487?v=4&s=117" width="117">](https://github.com/dschmidt) |[<img alt="DJWassink" src="https://avatars.githubusercontent.com/u/1822404?v=4&s=117" width="117">](https://github.com/DJWassink) |[<img alt="mrbatista" src="https://avatars.githubusercontent.com/u/6544817?v=4&s=117" width="117">](https://github.com/mrbatista) |[<img alt="taoqf" src="https://avatars.githubusercontent.com/u/15901911?v=4&s=117" width="117">](https://github.com/taoqf) |[<img alt="timodwhit" src="https://avatars.githubusercontent.com/u/2761203?v=4&s=117" width="117">](https://github.com/timodwhit) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[mokutsu-coursera](https://github.com/mokutsu-coursera) |[dschmidt](https://github.com/dschmidt) |[DJWassink](https://github.com/DJWassink) |[timodwhit](https://github.com/timodwhit) |[taoqf](https://github.com/taoqf) |[mrbatista](https://github.com/mrbatista) |
|
||||
[mokutsu-coursera](https://github.com/mokutsu-coursera) |[dschmidt](https://github.com/dschmidt) |[DJWassink](https://github.com/DJWassink) |[mrbatista](https://github.com/mrbatista) |[taoqf](https://github.com/taoqf) |[timodwhit](https://github.com/timodwhit) |
|
||||
|
||||
[<img alt="tim-kos" src="https://avatars.githubusercontent.com/u/15005?v=4&s=117" width="117">](https://github.com/tim-kos) |[<img alt="eltociear" src="https://avatars.githubusercontent.com/u/22633385?v=4&s=117" width="117">](https://github.com/eltociear) |[<img alt="tuoxiansp" src="https://avatars.githubusercontent.com/u/3960056?v=4&s=117" width="117">](https://github.com/tuoxiansp) |[<img alt="dominiceden" src="https://avatars.githubusercontent.com/u/6367692?v=4&s=117" width="117">](https://github.com/dominiceden) |[<img alt="elenalape" src="https://avatars.githubusercontent.com/u/22844059?v=4&s=117" width="117">](https://github.com/elenalape) |[<img alt="mejiaej" src="https://avatars.githubusercontent.com/u/4699893?v=4&s=117" width="117">](https://github.com/mejiaej) |
|
||||
[<img alt="tim-kos" src="https://avatars.githubusercontent.com/u/15005?v=4&s=117" width="117">](https://github.com/tim-kos) |[<img alt="eltociear" src="https://avatars.githubusercontent.com/u/22633385?v=4&s=117" width="117">](https://github.com/eltociear) |[<img alt="tuoxiansp" src="https://avatars.githubusercontent.com/u/3960056?v=4&s=117" width="117">](https://github.com/tuoxiansp) |[<img alt="pauln" src="https://avatars.githubusercontent.com/u/574359?v=4&s=117" width="117">](https://github.com/pauln) |[<img alt="MikeKovarik" src="https://avatars.githubusercontent.com/u/3995401?v=4&s=117" width="117">](https://github.com/MikeKovarik) |[<img alt="toadkicker" src="https://avatars.githubusercontent.com/u/523330?v=4&s=117" width="117">](https://github.com/toadkicker) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[tim-kos](https://github.com/tim-kos) |[eltociear](https://github.com/eltociear) |[tuoxiansp](https://github.com/tuoxiansp) |[dominiceden](https://github.com/dominiceden) |[elenalape](https://github.com/elenalape) |[mejiaej](https://github.com/mejiaej) |
|
||||
[tim-kos](https://github.com/tim-kos) |[eltociear](https://github.com/eltociear) |[tuoxiansp](https://github.com/tuoxiansp) |[pauln](https://github.com/pauln) |[MikeKovarik](https://github.com/MikeKovarik) |[toadkicker](https://github.com/toadkicker) |
|
||||
|
||||
[<img alt="gavboulton" src="https://avatars.githubusercontent.com/u/3900826?v=4&s=117" width="117">](https://github.com/gavboulton) |[<img alt="Hawxy" src="https://avatars.githubusercontent.com/u/975824?v=4&s=117" width="117">](https://github.com/Hawxy) |[<img alt="juliangruber" src="https://avatars.githubusercontent.com/u/10247?v=4&s=117" width="117">](https://github.com/juliangruber) |[<img alt="bertho-zero" src="https://avatars.githubusercontent.com/u/8525267?v=4&s=117" width="117">](https://github.com/bertho-zero) |[<img alt="LiviaMedeiros" src="https://avatars.githubusercontent.com/u/74449973?v=4&s=117" width="117">](https://github.com/LiviaMedeiros) |[<img alt="tranvansang" src="https://avatars.githubusercontent.com/u/13043196?v=4&s=117" width="117">](https://github.com/tranvansang) |
|
||||
[<img alt="ap--" src="https://avatars.githubusercontent.com/u/1463443?v=4&s=117" width="117">](https://github.com/ap--) |[<img alt="tranvansang" src="https://avatars.githubusercontent.com/u/13043196?v=4&s=117" width="117">](https://github.com/tranvansang) |[<img alt="LiviaMedeiros" src="https://avatars.githubusercontent.com/u/74449973?v=4&s=117" width="117">](https://github.com/LiviaMedeiros) |[<img alt="bertho-zero" src="https://avatars.githubusercontent.com/u/8525267?v=4&s=117" width="117">](https://github.com/bertho-zero) |[<img alt="juliangruber" src="https://avatars.githubusercontent.com/u/10247?v=4&s=117" width="117">](https://github.com/juliangruber) |[<img alt="Hawxy" src="https://avatars.githubusercontent.com/u/975824?v=4&s=117" width="117">](https://github.com/Hawxy) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[gavboulton](https://github.com/gavboulton) |[Hawxy](https://github.com/Hawxy) |[juliangruber](https://github.com/juliangruber) |[bertho-zero](https://github.com/bertho-zero) |[LiviaMedeiros](https://github.com/LiviaMedeiros) |[tranvansang](https://github.com/tranvansang) |
|
||||
[ap--](https://github.com/ap--) |[tranvansang](https://github.com/tranvansang) |[LiviaMedeiros](https://github.com/LiviaMedeiros) |[bertho-zero](https://github.com/bertho-zero) |[juliangruber](https://github.com/juliangruber) |[Hawxy](https://github.com/Hawxy) |
|
||||
|
||||
[<img alt="ap--" src="https://avatars.githubusercontent.com/u/1463443?v=4&s=117" width="117">](https://github.com/ap--) |[<img alt="MikeKovarik" src="https://avatars.githubusercontent.com/u/3995401?v=4&s=117" width="117">](https://github.com/MikeKovarik) |[<img alt="pauln" src="https://avatars.githubusercontent.com/u/574359?v=4&s=117" width="117">](https://github.com/pauln) |[<img alt="toadkicker" src="https://avatars.githubusercontent.com/u/523330?v=4&s=117" width="117">](https://github.com/toadkicker) |[<img alt="ofhope" src="https://avatars.githubusercontent.com/u/1826459?v=4&s=117" width="117">](https://github.com/ofhope) |[<img alt="johnnyperkins" src="https://avatars.githubusercontent.com/u/16482282?v=4&s=117" width="117">](https://github.com/johnnyperkins) |
|
||||
[<img alt="gavboulton" src="https://avatars.githubusercontent.com/u/3900826?v=4&s=117" width="117">](https://github.com/gavboulton) |[<img alt="mejiaej" src="https://avatars.githubusercontent.com/u/4699893?v=4&s=117" width="117">](https://github.com/mejiaej) |[<img alt="elenalape" src="https://avatars.githubusercontent.com/u/22844059?v=4&s=117" width="117">](https://github.com/elenalape) |[<img alt="dominiceden" src="https://avatars.githubusercontent.com/u/6367692?v=4&s=117" width="117">](https://github.com/dominiceden) |[<img alt="Acconut" src="https://avatars.githubusercontent.com/u/1375043?v=4&s=117" width="117">](https://github.com/Acconut) |[<img alt="jhen0409" src="https://avatars.githubusercontent.com/u/3001525?v=4&s=117" width="117">](https://github.com/jhen0409) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[ap--](https://github.com/ap--) |[MikeKovarik](https://github.com/MikeKovarik) |[pauln](https://github.com/pauln) |[toadkicker](https://github.com/toadkicker) |[ofhope](https://github.com/ofhope) |[johnnyperkins](https://github.com/johnnyperkins) |
|
||||
[gavboulton](https://github.com/gavboulton) |[mejiaej](https://github.com/mejiaej) |[elenalape](https://github.com/elenalape) |[dominiceden](https://github.com/dominiceden) |[Acconut](https://github.com/Acconut) |[jhen0409](https://github.com/jhen0409) |
|
||||
|
||||
[<img alt="dargmuesli" src="https://avatars.githubusercontent.com/u/4778485?v=4&s=117" width="117">](https://github.com/dargmuesli) |[<img alt="manuelkiessling" src="https://avatars.githubusercontent.com/u/206592?v=4&s=117" width="117">](https://github.com/manuelkiessling) |[<img alt="MatthiasKunnen" src="https://avatars.githubusercontent.com/u/16807587?v=4&s=117" width="117">](https://github.com/MatthiasKunnen) |[<img alt="nndevstudio" src="https://avatars.githubusercontent.com/u/22050968?v=4&s=117" width="117">](https://github.com/nndevstudio) |[<img alt="ogtfaber" src="https://avatars.githubusercontent.com/u/320955?v=4&s=117" width="117">](https://github.com/ogtfaber) |[<img alt="sksavant" src="https://avatars.githubusercontent.com/u/1040701?v=4&s=117" width="117">](https://github.com/sksavant) |
|
||||
[<img alt="stephentuso" src="https://avatars.githubusercontent.com/u/11889560?v=4&s=117" width="117">](https://github.com/stephentuso) |[<img alt="bencergazda" src="https://avatars.githubusercontent.com/u/5767697?v=4&s=117" width="117">](https://github.com/bencergazda) |[<img alt="a-kriya" src="https://avatars.githubusercontent.com/u/26761352?v=4&s=117" width="117">](https://github.com/a-kriya) |[<img alt="yonahforst" src="https://avatars.githubusercontent.com/u/1440796?v=4&s=117" width="117">](https://github.com/yonahforst) |[<img alt="suchoproduction" src="https://avatars.githubusercontent.com/u/6931349?v=4&s=117" width="117">](https://github.com/suchoproduction) |[<img alt="sksavant" src="https://avatars.githubusercontent.com/u/1040701?v=4&s=117" width="117">](https://github.com/sksavant) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[dargmuesli](https://github.com/dargmuesli) |[manuelkiessling](https://github.com/manuelkiessling) |[MatthiasKunnen](https://github.com/MatthiasKunnen) |[nndevstudio](https://github.com/nndevstudio) |[ogtfaber](https://github.com/ogtfaber) |[sksavant](https://github.com/sksavant) |
|
||||
[stephentuso](https://github.com/stephentuso) |[bencergazda](https://github.com/bencergazda) |[a-kriya](https://github.com/a-kriya) |[yonahforst](https://github.com/yonahforst) |[suchoproduction](https://github.com/suchoproduction) |[sksavant](https://github.com/sksavant) |
|
||||
|
||||
[<img alt="suchoproduction" src="https://avatars.githubusercontent.com/u/6931349?v=4&s=117" width="117">](https://github.com/suchoproduction) |[<img alt="yonahforst" src="https://avatars.githubusercontent.com/u/1440796?v=4&s=117" width="117">](https://github.com/yonahforst) |[<img alt="a-kriya" src="https://avatars.githubusercontent.com/u/26761352?v=4&s=117" width="117">](https://github.com/a-kriya) |[<img alt="bencergazda" src="https://avatars.githubusercontent.com/u/5767697?v=4&s=117" width="117">](https://github.com/bencergazda) |[<img alt="stephentuso" src="https://avatars.githubusercontent.com/u/11889560?v=4&s=117" width="117">](https://github.com/stephentuso) |[<img alt="jhen0409" src="https://avatars.githubusercontent.com/u/3001525?v=4&s=117" width="117">](https://github.com/jhen0409) |
|
||||
[<img alt="ogtfaber" src="https://avatars.githubusercontent.com/u/320955?v=4&s=117" width="117">](https://github.com/ogtfaber) |[<img alt="nndevstudio" src="https://avatars.githubusercontent.com/u/22050968?v=4&s=117" width="117">](https://github.com/nndevstudio) |[<img alt="MatthiasKunnen" src="https://avatars.githubusercontent.com/u/16807587?v=4&s=117" width="117">](https://github.com/MatthiasKunnen) |[<img alt="manuelkiessling" src="https://avatars.githubusercontent.com/u/206592?v=4&s=117" width="117">](https://github.com/manuelkiessling) |[<img alt="dargmuesli" src="https://avatars.githubusercontent.com/u/4778485?v=4&s=117" width="117">](https://github.com/dargmuesli) |[<img alt="johnnyperkins" src="https://avatars.githubusercontent.com/u/16482282?v=4&s=117" width="117">](https://github.com/johnnyperkins) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[suchoproduction](https://github.com/suchoproduction) |[yonahforst](https://github.com/yonahforst) |[a-kriya](https://github.com/a-kriya) |[bencergazda](https://github.com/bencergazda) |[stephentuso](https://github.com/stephentuso) |[jhen0409](https://github.com/jhen0409) |
|
||||
[ogtfaber](https://github.com/ogtfaber) |[nndevstudio](https://github.com/nndevstudio) |[MatthiasKunnen](https://github.com/MatthiasKunnen) |[manuelkiessling](https://github.com/manuelkiessling) |[dargmuesli](https://github.com/dargmuesli) |[johnnyperkins](https://github.com/johnnyperkins) |
|
||||
|
||||
[<img alt="5idereal" src="https://avatars.githubusercontent.com/u/30827929?v=4&s=117" width="117">](https://github.com/5idereal) |[<img alt="ahmedkandel" src="https://avatars.githubusercontent.com/u/28398523?v=4&s=117" width="117">](https://github.com/ahmedkandel) |[<img alt="btrice" src="https://avatars.githubusercontent.com/u/4358225?v=4&s=117" width="117">](https://github.com/btrice) |[<img alt="AndrwM" src="https://avatars.githubusercontent.com/u/565743?v=4&s=117" width="117">](https://github.com/AndrwM) |[<img alt="behnammodi" src="https://avatars.githubusercontent.com/u/1549069?v=4&s=117" width="117">](https://github.com/behnammodi) |[<img alt="BePo65" src="https://avatars.githubusercontent.com/u/6582465?v=4&s=117" width="117">](https://github.com/BePo65) |
|
||||
[<img alt="ofhope" src="https://avatars.githubusercontent.com/u/1826459?v=4&s=117" width="117">](https://github.com/ofhope) |[<img alt="yaegor" src="https://avatars.githubusercontent.com/u/3315?v=4&s=117" width="117">](https://github.com/yaegor) |[<img alt="zhuangya" src="https://avatars.githubusercontent.com/u/499038?v=4&s=117" width="117">](https://github.com/zhuangya) |[<img alt="sparanoid" src="https://avatars.githubusercontent.com/u/96356?v=4&s=117" width="117">](https://github.com/sparanoid) |[<img alt="ThomasG77" src="https://avatars.githubusercontent.com/u/642120?v=4&s=117" width="117">](https://github.com/ThomasG77) |[<img alt="subha1206" src="https://avatars.githubusercontent.com/u/36275153?v=4&s=117" width="117">](https://github.com/subha1206) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[5idereal](https://github.com/5idereal) |[ahmedkandel](https://github.com/ahmedkandel) |[btrice](https://github.com/btrice) |[AndrwM](https://github.com/AndrwM) |[behnammodi](https://github.com/behnammodi) |[BePo65](https://github.com/BePo65) |
|
||||
[ofhope](https://github.com/ofhope) |[yaegor](https://github.com/yaegor) |[zhuangya](https://github.com/zhuangya) |[sparanoid](https://github.com/sparanoid) |[ThomasG77](https://github.com/ThomasG77) |[subha1206](https://github.com/subha1206) |
|
||||
|
||||
[<img alt="bradedelman" src="https://avatars.githubusercontent.com/u/124367?v=4&s=117" width="117">](https://github.com/bradedelman) |[<img alt="camiloforero" src="https://avatars.githubusercontent.com/u/6606686?v=4&s=117" width="117">](https://github.com/camiloforero) |[<img alt="command-tab" src="https://avatars.githubusercontent.com/u/3069?v=4&s=117" width="117">](https://github.com/command-tab) |[<img alt="craigjennings11" src="https://avatars.githubusercontent.com/u/1683368?v=4&s=117" width="117">](https://github.com/craigjennings11) |[<img alt="davekiss" src="https://avatars.githubusercontent.com/u/1256071?v=4&s=117" width="117">](https://github.com/davekiss) |[<img alt="denysdesign" src="https://avatars.githubusercontent.com/u/1041797?v=4&s=117" width="117">](https://github.com/denysdesign) |
|
||||
[<img alt="schonert" src="https://avatars.githubusercontent.com/u/2185697?v=4&s=117" width="117">](https://github.com/schonert) |[<img alt="SlavikTraktor" src="https://avatars.githubusercontent.com/u/11923751?v=4&s=117" width="117">](https://github.com/SlavikTraktor) |[<img alt="scottbessler" src="https://avatars.githubusercontent.com/u/293802?v=4&s=117" width="117">](https://github.com/scottbessler) |[<img alt="jrschumacher" src="https://avatars.githubusercontent.com/u/46549?v=4&s=117" width="117">](https://github.com/jrschumacher) |[<img alt="rosenfeld" src="https://avatars.githubusercontent.com/u/32246?v=4&s=117" width="117">](https://github.com/rosenfeld) |[<img alt="rdimartino" src="https://avatars.githubusercontent.com/u/11539300?v=4&s=117" width="117">](https://github.com/rdimartino) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[bradedelman](https://github.com/bradedelman) |[camiloforero](https://github.com/camiloforero) |[command-tab](https://github.com/command-tab) |[craigjennings11](https://github.com/craigjennings11) |[davekiss](https://github.com/davekiss) |[denysdesign](https://github.com/denysdesign) |
|
||||
[schonert](https://github.com/schonert) |[SlavikTraktor](https://github.com/SlavikTraktor) |[scottbessler](https://github.com/scottbessler) |[jrschumacher](https://github.com/jrschumacher) |[rosenfeld](https://github.com/rosenfeld) |[rdimartino](https://github.com/rdimartino) |
|
||||
|
||||
[<img alt="ethanwillis" src="https://avatars.githubusercontent.com/u/182492?v=4&s=117" width="117">](https://github.com/ethanwillis) |[<img alt="frobinsonj" src="https://avatars.githubusercontent.com/u/16726902?v=4&s=117" width="117">](https://github.com/frobinsonj) |[<img alt="geertclerx" src="https://avatars.githubusercontent.com/u/1381327?v=4&s=117" width="117">](https://github.com/geertclerx) |[<img alt="ghasrfakhri" src="https://avatars.githubusercontent.com/u/4945963?v=4&s=117" width="117">](https://github.com/ghasrfakhri) |[<img alt="jasonbosco" src="https://avatars.githubusercontent.com/u/458383?v=4&s=117" width="117">](https://github.com/jasonbosco) |[<img alt="jedwood" src="https://avatars.githubusercontent.com/u/369060?v=4&s=117" width="117">](https://github.com/jedwood) |
|
||||
[<img alt="richmeij" src="https://avatars.githubusercontent.com/u/9741858?v=4&s=117" width="117">](https://github.com/richmeij) |[<img alt="Youssef1313" src="https://avatars.githubusercontent.com/u/31348972?v=4&s=117" width="117">](https://github.com/Youssef1313) |[<img alt="allenfantasy" src="https://avatars.githubusercontent.com/u/1009294?v=4&s=117" width="117">](https://github.com/allenfantasy) |[<img alt="Zyclotrop-j" src="https://avatars.githubusercontent.com/u/4939546?v=4&s=117" width="117">](https://github.com/Zyclotrop-j) |[<img alt="anark" src="https://avatars.githubusercontent.com/u/101184?v=4&s=117" width="117">](https://github.com/anark) |[<img alt="bdirito" src="https://avatars.githubusercontent.com/u/8117238?v=4&s=117" width="117">](https://github.com/bdirito) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[ethanwillis](https://github.com/ethanwillis) |[frobinsonj](https://github.com/frobinsonj) |[geertclerx](https://github.com/geertclerx) |[ghasrfakhri](https://github.com/ghasrfakhri) |[jasonbosco](https://github.com/jasonbosco) |[jedwood](https://github.com/jedwood) |
|
||||
[richmeij](https://github.com/richmeij) |[Youssef1313](https://github.com/Youssef1313) |[allenfantasy](https://github.com/allenfantasy) |[Zyclotrop-j](https://github.com/Zyclotrop-j) |[anark](https://github.com/anark) |[bdirito](https://github.com/bdirito) |
|
||||
|
||||
[<img alt="dogrocker" src="https://avatars.githubusercontent.com/u/8379027?v=4&s=117" width="117">](https://github.com/dogrocker) |[<img alt="lafe" src="https://avatars.githubusercontent.com/u/4070008?v=4&s=117" width="117">](https://github.com/lafe) |[<img alt="mactavishz" src="https://avatars.githubusercontent.com/u/12948083?v=4&s=117" width="117">](https://github.com/mactavishz) |[<img alt="maferland" src="https://avatars.githubusercontent.com/u/5889721?v=4&s=117" width="117">](https://github.com/maferland) |[<img alt="mskelton" src="https://avatars.githubusercontent.com/u/25914066?v=4&s=117" width="117">](https://github.com/mskelton) |[<img alt="Martin005" src="https://avatars.githubusercontent.com/u/10096404?v=4&s=117" width="117">](https://github.com/Martin005) |
|
||||
[<img alt="darthf1" src="https://avatars.githubusercontent.com/u/17253332?v=4&s=117" width="117">](https://github.com/darthf1) |[<img alt="fortrieb" src="https://avatars.githubusercontent.com/u/4126707?v=4&s=117" width="117">](https://github.com/fortrieb) |[<img alt="heocoi" src="https://avatars.githubusercontent.com/u/13751011?v=4&s=117" width="117">](https://github.com/heocoi) |[<img alt="jarey" src="https://avatars.githubusercontent.com/u/5025224?v=4&s=117" width="117">](https://github.com/jarey) |[<img alt="muhammadInam" src="https://avatars.githubusercontent.com/u/7801708?v=4&s=117" width="117">](https://github.com/muhammadInam) |[<img alt="rettgerst" src="https://avatars.githubusercontent.com/u/11684948?v=4&s=117" width="117">](https://github.com/rettgerst) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[dogrocker](https://github.com/dogrocker) |[lafe](https://github.com/lafe) |[mactavishz](https://github.com/mactavishz) |[maferland](https://github.com/maferland) |[mskelton](https://github.com/mskelton) |[Martin005](https://github.com/Martin005) |
|
||||
[darthf1](https://github.com/darthf1) |[fortrieb](https://github.com/fortrieb) |[heocoi](https://github.com/heocoi) |[jarey](https://github.com/jarey) |[muhammadInam](https://github.com/muhammadInam) |[rettgerst](https://github.com/rettgerst) |
|
||||
|
||||
[<img alt="martiuslim" src="https://avatars.githubusercontent.com/u/17944339?v=4&s=117" width="117">](https://github.com/martiuslim) |[<img alt="msand" src="https://avatars.githubusercontent.com/u/1131362?v=4&s=117" width="117">](https://github.com/msand) |[<img alt="paescuj" src="https://avatars.githubusercontent.com/u/5363448?v=4&s=117" width="117">](https://github.com/paescuj) |[<img alt="richartkeil" src="https://avatars.githubusercontent.com/u/8680858?v=4&s=117" width="117">](https://github.com/richartkeil) |[<img alt="richmeij" src="https://avatars.githubusercontent.com/u/9741858?v=4&s=117" width="117">](https://github.com/richmeij) |[<img alt="rdimartino" src="https://avatars.githubusercontent.com/u/11539300?v=4&s=117" width="117">](https://github.com/rdimartino) |
|
||||
[<img alt="mkabatek" src="https://avatars.githubusercontent.com/u/1764486?v=4&s=117" width="117">](https://github.com/mkabatek) |[<img alt="jukakoski" src="https://avatars.githubusercontent.com/u/52720967?v=4&s=117" width="117">](https://github.com/jukakoski) |[<img alt="olemoign" src="https://avatars.githubusercontent.com/u/11632871?v=4&s=117" width="117">](https://github.com/olemoign) |[<img alt="ahmedkandel" src="https://avatars.githubusercontent.com/u/28398523?v=4&s=117" width="117">](https://github.com/ahmedkandel) |[<img alt="btrice" src="https://avatars.githubusercontent.com/u/4358225?v=4&s=117" width="117">](https://github.com/btrice) |[<img alt="5idereal" src="https://avatars.githubusercontent.com/u/30827929?v=4&s=117" width="117">](https://github.com/5idereal) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[martiuslim](https://github.com/martiuslim) |[msand](https://github.com/msand) |[paescuj](https://github.com/paescuj) |[richartkeil](https://github.com/richartkeil) |[richmeij](https://github.com/richmeij) |[rdimartino](https://github.com/rdimartino) |
|
||||
[mkabatek](https://github.com/mkabatek) |[jukakoski](https://github.com/jukakoski) |[olemoign](https://github.com/olemoign) |[ahmedkandel](https://github.com/ahmedkandel) |[btrice](https://github.com/btrice) |[5idereal](https://github.com/5idereal) |
|
||||
|
||||
[<img alt="rosenfeld" src="https://avatars.githubusercontent.com/u/32246?v=4&s=117" width="117">](https://github.com/rosenfeld) |[<img alt="jrschumacher" src="https://avatars.githubusercontent.com/u/46549?v=4&s=117" width="117">](https://github.com/jrschumacher) |[<img alt="scottbessler" src="https://avatars.githubusercontent.com/u/293802?v=4&s=117" width="117">](https://github.com/scottbessler) |[<img alt="SlavikTraktor" src="https://avatars.githubusercontent.com/u/11923751?v=4&s=117" width="117">](https://github.com/SlavikTraktor) |[<img alt="schonert" src="https://avatars.githubusercontent.com/u/2185697?v=4&s=117" width="117">](https://github.com/schonert) |[<img alt="subha1206" src="https://avatars.githubusercontent.com/u/36275153?v=4&s=117" width="117">](https://github.com/subha1206) |
|
||||
[<img alt="AndrwM" src="https://avatars.githubusercontent.com/u/565743?v=4&s=117" width="117">](https://github.com/AndrwM) |[<img alt="behnammodi" src="https://avatars.githubusercontent.com/u/1549069?v=4&s=117" width="117">](https://github.com/behnammodi) |[<img alt="BePo65" src="https://avatars.githubusercontent.com/u/6582465?v=4&s=117" width="117">](https://github.com/BePo65) |[<img alt="bradedelman" src="https://avatars.githubusercontent.com/u/124367?v=4&s=117" width="117">](https://github.com/bradedelman) |[<img alt="camiloforero" src="https://avatars.githubusercontent.com/u/6606686?v=4&s=117" width="117">](https://github.com/camiloforero) |[<img alt="command-tab" src="https://avatars.githubusercontent.com/u/3069?v=4&s=117" width="117">](https://github.com/command-tab) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[rosenfeld](https://github.com/rosenfeld) |[jrschumacher](https://github.com/jrschumacher) |[scottbessler](https://github.com/scottbessler) |[SlavikTraktor](https://github.com/SlavikTraktor) |[schonert](https://github.com/schonert) |[subha1206](https://github.com/subha1206) |
|
||||
[AndrwM](https://github.com/AndrwM) |[behnammodi](https://github.com/behnammodi) |[BePo65](https://github.com/BePo65) |[bradedelman](https://github.com/bradedelman) |[camiloforero](https://github.com/camiloforero) |[command-tab](https://github.com/command-tab) |
|
||||
|
||||
[<img alt="ThomasG77" src="https://avatars.githubusercontent.com/u/642120?v=4&s=117" width="117">](https://github.com/ThomasG77) |[<img alt="sparanoid" src="https://avatars.githubusercontent.com/u/96356?v=4&s=117" width="117">](https://github.com/sparanoid) |[<img alt="zhuangya" src="https://avatars.githubusercontent.com/u/499038?v=4&s=117" width="117">](https://github.com/zhuangya) |[<img alt="yaegor" src="https://avatars.githubusercontent.com/u/3315?v=4&s=117" width="117">](https://github.com/yaegor) |[<img alt="Youssef1313" src="https://avatars.githubusercontent.com/u/31348972?v=4&s=117" width="117">](https://github.com/Youssef1313) |[<img alt="allenfantasy" src="https://avatars.githubusercontent.com/u/1009294?v=4&s=117" width="117">](https://github.com/allenfantasy) |
|
||||
[<img alt="craig-jennings" src="https://avatars.githubusercontent.com/u/1683368?v=4&s=117" width="117">](https://github.com/craig-jennings) |[<img alt="davekiss" src="https://avatars.githubusercontent.com/u/1256071?v=4&s=117" width="117">](https://github.com/davekiss) |[<img alt="denysdesign" src="https://avatars.githubusercontent.com/u/1041797?v=4&s=117" width="117">](https://github.com/denysdesign) |[<img alt="ethanwillis" src="https://avatars.githubusercontent.com/u/182492?v=4&s=117" width="117">](https://github.com/ethanwillis) |[<img alt="richartkeil" src="https://avatars.githubusercontent.com/u/8680858?v=4&s=117" width="117">](https://github.com/richartkeil) |[<img alt="paescuj" src="https://avatars.githubusercontent.com/u/5363448?v=4&s=117" width="117">](https://github.com/paescuj) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[ThomasG77](https://github.com/ThomasG77) |[sparanoid](https://github.com/sparanoid) |[zhuangya](https://github.com/zhuangya) |[yaegor](https://github.com/yaegor) |[Youssef1313](https://github.com/Youssef1313) |[allenfantasy](https://github.com/allenfantasy) |
|
||||
[craig-jennings](https://github.com/craig-jennings) |[davekiss](https://github.com/davekiss) |[denysdesign](https://github.com/denysdesign) |[ethanwillis](https://github.com/ethanwillis) |[richartkeil](https://github.com/richartkeil) |[paescuj](https://github.com/paescuj) |
|
||||
|
||||
[<img alt="Zyclotrop-j" src="https://avatars.githubusercontent.com/u/4939546?v=4&s=117" width="117">](https://github.com/Zyclotrop-j) |[<img alt="anark" src="https://avatars.githubusercontent.com/u/101184?v=4&s=117" width="117">](https://github.com/anark) |[<img alt="bdirito" src="https://avatars.githubusercontent.com/u/8117238?v=4&s=117" width="117">](https://github.com/bdirito) |[<img alt="fortrieb" src="https://avatars.githubusercontent.com/u/4126707?v=4&s=117" width="117">](https://github.com/fortrieb) |[<img alt="heocoi" src="https://avatars.githubusercontent.com/u/13751011?v=4&s=117" width="117">](https://github.com/heocoi) |[<img alt="jarey" src="https://avatars.githubusercontent.com/u/5025224?v=4&s=117" width="117">](https://github.com/jarey) |
|
||||
[<img alt="msand" src="https://avatars.githubusercontent.com/u/1131362?v=4&s=117" width="117">](https://github.com/msand) |[<img alt="martiuslim" src="https://avatars.githubusercontent.com/u/17944339?v=4&s=117" width="117">](https://github.com/martiuslim) |[<img alt="Martin005" src="https://avatars.githubusercontent.com/u/10096404?v=4&s=117" width="117">](https://github.com/Martin005) |[<img alt="mskelton" src="https://avatars.githubusercontent.com/u/25914066?v=4&s=117" width="117">](https://github.com/mskelton) |[<img alt="mactavishz" src="https://avatars.githubusercontent.com/u/12948083?v=4&s=117" width="117">](https://github.com/mactavishz) |[<img alt="lafe" src="https://avatars.githubusercontent.com/u/4070008?v=4&s=117" width="117">](https://github.com/lafe) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[Zyclotrop-j](https://github.com/Zyclotrop-j) |[anark](https://github.com/anark) |[bdirito](https://github.com/bdirito) |[fortrieb](https://github.com/fortrieb) |[heocoi](https://github.com/heocoi) |[jarey](https://github.com/jarey) |
|
||||
[msand](https://github.com/msand) |[martiuslim](https://github.com/martiuslim) |[Martin005](https://github.com/Martin005) |[mskelton](https://github.com/mskelton) |[mactavishz](https://github.com/mactavishz) |[lafe](https://github.com/lafe) |
|
||||
|
||||
[<img alt="muhammadInam" src="https://avatars.githubusercontent.com/u/7801708?v=4&s=117" width="117">](https://github.com/muhammadInam) |[<img alt="rettgerst" src="https://avatars.githubusercontent.com/u/11684948?v=4&s=117" width="117">](https://github.com/rettgerst) |[<img alt="Acconut" src="https://avatars.githubusercontent.com/u/1375043?v=4&s=117" width="117">](https://github.com/Acconut) |[<img alt="mkabatek" src="https://avatars.githubusercontent.com/u/1764486?v=4&s=117" width="117">](https://github.com/mkabatek) |[<img alt="jukakoski" src="https://avatars.githubusercontent.com/u/52720967?v=4&s=117" width="117">](https://github.com/jukakoski) |[<img alt="olemoign" src="https://avatars.githubusercontent.com/u/11632871?v=4&s=117" width="117">](https://github.com/olemoign) |
|
||||
[<img alt="dogrocker" src="https://avatars.githubusercontent.com/u/8379027?v=4&s=117" width="117">](https://github.com/dogrocker) |[<img alt="jedwood" src="https://avatars.githubusercontent.com/u/369060?v=4&s=117" width="117">](https://github.com/jedwood) |[<img alt="jasonbosco" src="https://avatars.githubusercontent.com/u/458383?v=4&s=117" width="117">](https://github.com/jasonbosco) |[<img alt="frobinsonj" src="https://avatars.githubusercontent.com/u/16726902?v=4&s=117" width="117">](https://github.com/frobinsonj) |[<img alt="ghasrfakhri" src="https://avatars.githubusercontent.com/u/4945963?v=4&s=117" width="117">](https://github.com/ghasrfakhri) |[<img alt="geertclerx" src="https://avatars.githubusercontent.com/u/1381327?v=4&s=117" width="117">](https://github.com/geertclerx) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[muhammadInam](https://github.com/muhammadInam) |[rettgerst](https://github.com/rettgerst) |[Acconut](https://github.com/Acconut) |[mkabatek](https://github.com/mkabatek) |[jukakoski](https://github.com/jukakoski) |[olemoign](https://github.com/olemoign) |
|
||||
[dogrocker](https://github.com/dogrocker) |[jedwood](https://github.com/jedwood) |[jasonbosco](https://github.com/jasonbosco) |[frobinsonj](https://github.com/frobinsonj) |[ghasrfakhri](https://github.com/ghasrfakhri) |[geertclerx](https://github.com/geertclerx) |
|
||||
|
||||
[<img alt="ajschmidt8" src="https://avatars.githubusercontent.com/u/7400326?v=4&s=117" width="117">](https://github.com/ajschmidt8) |[<img alt="superhawk610" src="https://avatars.githubusercontent.com/u/18172185?v=4&s=117" width="117">](https://github.com/superhawk610) |[<img alt="abannach" src="https://avatars.githubusercontent.com/u/43150303?v=4&s=117" width="117">](https://github.com/abannach) |[<img alt="adamdottv" src="https://avatars.githubusercontent.com/u/2363879?v=4&s=117" width="117">](https://github.com/adamdottv) |[<img alt="ajh-sr" src="https://avatars.githubusercontent.com/u/71472057?v=4&s=117" width="117">](https://github.com/ajh-sr) |[<img alt="adamvigneault" src="https://avatars.githubusercontent.com/u/18236120?v=4&s=117" width="117">](https://github.com/adamvigneault) |
|
||||
[<img alt="Tashows" src="https://avatars.githubusercontent.com/u/16656928?v=4&s=117" width="117">](https://github.com/Tashows) |[<img alt="scherroman" src="https://avatars.githubusercontent.com/u/7923938?v=4&s=117" width="117">](https://github.com/scherroman) |[<img alt="robwilson1" src="https://avatars.githubusercontent.com/u/7114944?v=4&s=117" width="117">](https://github.com/robwilson1) |[<img alt="SxDx" src="https://avatars.githubusercontent.com/u/2004247?v=4&s=117" width="117">](https://github.com/SxDx) |[<img alt="refo" src="https://avatars.githubusercontent.com/u/1114116?v=4&s=117" width="117">](https://github.com/refo) |[<img alt="raulibanez" src="https://avatars.githubusercontent.com/u/1070825?v=4&s=117" width="117">](https://github.com/raulibanez) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[ajschmidt8](https://github.com/ajschmidt8) |[superhawk610](https://github.com/superhawk610) |[abannach](https://github.com/abannach) |[adamdottv](https://github.com/adamdottv) |[ajh-sr](https://github.com/ajh-sr) |[adamvigneault](https://github.com/adamvigneault) |
|
||||
[Tashows](https://github.com/Tashows) |[scherroman](https://github.com/scherroman) |[robwilson1](https://github.com/robwilson1) |[SxDx](https://github.com/SxDx) |[refo](https://github.com/refo) |[raulibanez](https://github.com/raulibanez) |
|
||||
|
||||
[<img alt="Adrrei" src="https://avatars.githubusercontent.com/u/22191685?v=4&s=117" width="117">](https://github.com/Adrrei) |[<img alt="adritasharma" src="https://avatars.githubusercontent.com/u/29271635?v=4&s=117" width="117">](https://github.com/adritasharma) |[<img alt="ahmadissa" src="https://avatars.githubusercontent.com/u/9936573?v=4&s=117" width="117">](https://github.com/ahmadissa) |[<img alt="asmt3" src="https://avatars.githubusercontent.com/u/1777709?v=4&s=117" width="117">](https://github.com/asmt3) |[<img alt="alexnj" src="https://avatars.githubusercontent.com/u/683500?v=4&s=117" width="117">](https://github.com/alexnj) |[<img alt="aalepis" src="https://avatars.githubusercontent.com/u/35684834?v=4&s=117" width="117">](https://github.com/aalepis) |
|
||||
[<img alt="luarmr" src="https://avatars.githubusercontent.com/u/817416?v=4&s=117" width="117">](https://github.com/luarmr) |[<img alt="eman8519" src="https://avatars.githubusercontent.com/u/2380804?v=4&s=117" width="117">](https://github.com/eman8519) |[<img alt="Pzoco" src="https://avatars.githubusercontent.com/u/3101348?v=4&s=117" width="117">](https://github.com/Pzoco) |[<img alt="ppadmavilasom" src="https://avatars.githubusercontent.com/u/11167452?v=4&s=117" width="117">](https://github.com/ppadmavilasom) |[<img alt="phillipalexander" src="https://avatars.githubusercontent.com/u/1577682?v=4&s=117" width="117">](https://github.com/phillipalexander) |[<img alt="pmusaraj" src="https://avatars.githubusercontent.com/u/368961?v=4&s=117" width="117">](https://github.com/pmusaraj) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[Adrrei](https://github.com/Adrrei) |[adritasharma](https://github.com/adritasharma) |[ahmadissa](https://github.com/ahmadissa) |[asmt3](https://github.com/asmt3) |[alexnj](https://github.com/alexnj) |[aalepis](https://github.com/aalepis) |
|
||||
[luarmr](https://github.com/luarmr) |[eman8519](https://github.com/eman8519) |[Pzoco](https://github.com/Pzoco) |[ppadmavilasom](https://github.com/ppadmavilasom) |[phillipalexander](https://github.com/phillipalexander) |[pmusaraj](https://github.com/pmusaraj) |
|
||||
|
||||
[<img alt="Dogfalo" src="https://avatars.githubusercontent.com/u/2775751?v=4&s=117" width="117">](https://github.com/Dogfalo) |[<img alt="tekacs" src="https://avatars.githubusercontent.com/u/63247?v=4&s=117" width="117">](https://github.com/tekacs) |[<img alt="amitport" src="https://avatars.githubusercontent.com/u/1131991?v=4&s=117" width="117">](https://github.com/amitport) |[<img alt="functino" src="https://avatars.githubusercontent.com/u/415498?v=4&s=117" width="117">](https://github.com/functino) |[<img alt="radarhere" src="https://avatars.githubusercontent.com/u/3112309?v=4&s=117" width="117">](https://github.com/radarhere) |[<img alt="superandrew213" src="https://avatars.githubusercontent.com/u/13059204?v=4&s=117" width="117">](https://github.com/superandrew213) |
|
||||
[<img alt="pedrofs" src="https://avatars.githubusercontent.com/u/56484?v=4&s=117" width="117">](https://github.com/pedrofs) |[<img alt="plneto" src="https://avatars.githubusercontent.com/u/5697434?v=4&s=117" width="117">](https://github.com/plneto) |[<img alt="patricklindsay" src="https://avatars.githubusercontent.com/u/7923681?v=4&s=117" width="117">](https://github.com/patricklindsay) |[<img alt="pascalwengerter" src="https://avatars.githubusercontent.com/u/16822008?v=4&s=117" width="117">](https://github.com/pascalwengerter) |[<img alt="ken-kuro" src="https://avatars.githubusercontent.com/u/47441476?v=4&s=117" width="117">](https://github.com/ken-kuro) |[<img alt="taj" src="https://avatars.githubusercontent.com/u/16062635?v=4&s=117" width="117">](https://github.com/taj) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[Dogfalo](https://github.com/Dogfalo) |[tekacs](https://github.com/tekacs) |[amitport](https://github.com/amitport) |[functino](https://github.com/functino) |[radarhere](https://github.com/radarhere) |[superandrew213](https://github.com/superandrew213) |
|
||||
[pedrofs](https://github.com/pedrofs) |[plneto](https://github.com/plneto) |[patricklindsay](https://github.com/patricklindsay) |[pascalwengerter](https://github.com/pascalwengerter) |[ken-kuro](https://github.com/ken-kuro) |[taj](https://github.com/taj) |
|
||||
|
||||
[<img alt="andrii-bodnar" src="https://avatars.githubusercontent.com/u/29282228?v=4&s=117" width="117">](https://github.com/andrii-bodnar) |[<img alt="andychongyz" src="https://avatars.githubusercontent.com/u/12697240?v=4&s=117" width="117">](https://github.com/andychongyz) |[<img alt="anthony0030" src="https://avatars.githubusercontent.com/u/13033263?v=4&s=117" width="117">](https://github.com/anthony0030) |[<img alt="tyndria" src="https://avatars.githubusercontent.com/u/17138916?v=4&s=117" width="117">](https://github.com/tyndria) |[<img alt="Abourass" src="https://avatars.githubusercontent.com/u/39917231?v=4&s=117" width="117">](https://github.com/Abourass) |[<img alt="arthurdenner" src="https://avatars.githubusercontent.com/u/13774309?v=4&s=117" width="117">](https://github.com/arthurdenner) |
|
||||
[<img alt="strayer" src="https://avatars.githubusercontent.com/u/310624?v=4&s=117" width="117">](https://github.com/strayer) |[<img alt="sjauld" src="https://avatars.githubusercontent.com/u/8232503?v=4&s=117" width="117">](https://github.com/sjauld) |[<img alt="steverob" src="https://avatars.githubusercontent.com/u/1220480?v=4&s=117" width="117">](https://github.com/steverob) |[<img alt="amaitu" src="https://avatars.githubusercontent.com/u/15688439?v=4&s=117" width="117">](https://github.com/amaitu) |[<img alt="quigebo" src="https://avatars.githubusercontent.com/u/741?v=4&s=117" width="117">](https://github.com/quigebo) |[<img alt="waptik" src="https://avatars.githubusercontent.com/u/1687551?v=4&s=117" width="117">](https://github.com/waptik) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[andrii-bodnar](https://github.com/andrii-bodnar) |[andychongyz](https://github.com/andychongyz) |[anthony0030](https://github.com/anthony0030) |[tyndria](https://github.com/tyndria) |[Abourass](https://github.com/Abourass) |[arthurdenner](https://github.com/arthurdenner) |
|
||||
[strayer](https://github.com/strayer) |[sjauld](https://github.com/sjauld) |[steverob](https://github.com/steverob) |[amaitu](https://github.com/amaitu) |[quigebo](https://github.com/quigebo) |[waptik](https://github.com/waptik) |
|
||||
|
||||
[<img alt="apuyou" src="https://avatars.githubusercontent.com/u/520053?v=4&s=117" width="117">](https://github.com/apuyou) |[<img alt="ash-jc-allen" src="https://avatars.githubusercontent.com/u/39652331?v=4&s=117" width="117">](https://github.com/ash-jc-allen) |[<img alt="atsawin" src="https://avatars.githubusercontent.com/u/666663?v=4&s=117" width="117">](https://github.com/atsawin) |[<img alt="ayhankesicioglu" src="https://avatars.githubusercontent.com/u/36304312?v=4&s=117" width="117">](https://github.com/ayhankesicioglu) |[<img alt="azeemba" src="https://avatars.githubusercontent.com/u/2160795?v=4&s=117" width="117">](https://github.com/azeemba) |[<img alt="azizk" src="https://avatars.githubusercontent.com/u/37282?v=4&s=117" width="117">](https://github.com/azizk) |
|
||||
[<img alt="SpazzMarticus" src="https://avatars.githubusercontent.com/u/5716457?v=4&s=117" width="117">](https://github.com/SpazzMarticus) |[<img alt="szh" src="https://avatars.githubusercontent.com/u/546965?v=4&s=117" width="117">](https://github.com/szh) |[<img alt="sergei-zelinsky" src="https://avatars.githubusercontent.com/u/19428086?v=4&s=117" width="117">](https://github.com/sergei-zelinsky) |[<img alt="sebasegovia01" src="https://avatars.githubusercontent.com/u/35777287?v=4&s=117" width="117">](https://github.com/sebasegovia01) |[<img alt="sdebacker" src="https://avatars.githubusercontent.com/u/134503?v=4&s=117" width="117">](https://github.com/sdebacker) |[<img alt="samuelcolburn" src="https://avatars.githubusercontent.com/u/9741902?v=4&s=117" width="117">](https://github.com/samuelcolburn) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[apuyou](https://github.com/apuyou) |[ash-jc-allen](https://github.com/ash-jc-allen) |[atsawin](https://github.com/atsawin) |[ayhankesicioglu](https://github.com/ayhankesicioglu) |[azeemba](https://github.com/azeemba) |[azizk](https://github.com/azizk) |
|
||||
[SpazzMarticus](https://github.com/SpazzMarticus) |[szh](https://github.com/szh) |[sergei-zelinsky](https://github.com/sergei-zelinsky) |[sebasegovia01](https://github.com/sebasegovia01) |[sdebacker](https://github.com/sdebacker) |[samuelcolburn](https://github.com/samuelcolburn) |
|
||||
|
||||
[<img alt="bducharme" src="https://avatars.githubusercontent.com/u/4173569?v=4&s=117" width="117">](https://github.com/bducharme) |[<img alt="Quorafind" src="https://avatars.githubusercontent.com/u/13215013?v=4&s=117" width="117">](https://github.com/Quorafind) |[<img alt="wbaaron" src="https://avatars.githubusercontent.com/u/1048988?v=4&s=117" width="117">](https://github.com/wbaaron) |[<img alt="bedgerotto" src="https://avatars.githubusercontent.com/u/4459657?v=4&s=117" width="117">](https://github.com/bedgerotto) |[<img alt="bryanjswift" src="https://avatars.githubusercontent.com/u/9911?v=4&s=117" width="117">](https://github.com/bryanjswift) |[<img alt="cyu" src="https://avatars.githubusercontent.com/u/2431?v=4&s=117" width="117">](https://github.com/cyu) |
|
||||
[<img alt="fortunto2" src="https://avatars.githubusercontent.com/u/1236751?v=4&s=117" width="117">](https://github.com/fortunto2) |[<img alt="GNURub" src="https://avatars.githubusercontent.com/u/1318648?v=4&s=117" width="117">](https://github.com/GNURub) |[<img alt="rart" src="https://avatars.githubusercontent.com/u/3928341?v=4&s=117" width="117">](https://github.com/rart) |[<img alt="rossng" src="https://avatars.githubusercontent.com/u/565371?v=4&s=117" width="117">](https://github.com/rossng) |[<img alt="mkopinsky" src="https://avatars.githubusercontent.com/u/591435?v=4&s=117" width="117">](https://github.com/mkopinsky) |[<img alt="mhulet" src="https://avatars.githubusercontent.com/u/293355?v=4&s=117" width="117">](https://github.com/mhulet) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[bducharme](https://github.com/bducharme) |[Quorafind](https://github.com/Quorafind) |[wbaaron](https://github.com/wbaaron) |[bedgerotto](https://github.com/bedgerotto) |[bryanjswift](https://github.com/bryanjswift) |[cyu](https://github.com/cyu) |
|
||||
[fortunto2](https://github.com/fortunto2) |[GNURub](https://github.com/GNURub) |[rart](https://github.com/rart) |[rossng](https://github.com/rossng) |[mkopinsky](https://github.com/mkopinsky) |[mhulet](https://github.com/mhulet) |
|
||||
|
||||
[<img alt="cartfisk" src="https://avatars.githubusercontent.com/u/8764375?v=4&s=117" width="117">](https://github.com/cartfisk) |[<img alt="cellvinchung" src="https://avatars.githubusercontent.com/u/5347394?v=4&s=117" width="117">](https://github.com/cellvinchung) |[<img alt="chao" src="https://avatars.githubusercontent.com/u/55872?v=4&s=117" width="117">](https://github.com/chao) |[<img alt="Cretezy" src="https://avatars.githubusercontent.com/u/2672503?v=4&s=117" width="117">](https://github.com/Cretezy) |[<img alt="charlybillaud" src="https://avatars.githubusercontent.com/u/31970410?v=4&s=117" width="117">](https://github.com/charlybillaud) |[<img alt="prattcmp" src="https://avatars.githubusercontent.com/u/1497950?v=4&s=117" width="117">](https://github.com/prattcmp) |
|
||||
[<img alt="hrsh" src="https://avatars.githubusercontent.com/u/1929359?v=4&s=117" width="117">](https://github.com/hrsh) |[<img alt="mauricioribeiro" src="https://avatars.githubusercontent.com/u/2589856?v=4&s=117" width="117">](https://github.com/mauricioribeiro) |[<img alt="matthewhartstonge" src="https://avatars.githubusercontent.com/u/6119549?v=4&s=117" width="117">](https://github.com/matthewhartstonge) |[<img alt="mjesuele" src="https://avatars.githubusercontent.com/u/871117?v=4&s=117" width="117">](https://github.com/mjesuele) |[<img alt="mattfik" src="https://avatars.githubusercontent.com/u/1638028?v=4&s=117" width="117">](https://github.com/mattfik) |[<img alt="mateuscruz" src="https://avatars.githubusercontent.com/u/8962842?v=4&s=117" width="117">](https://github.com/mateuscruz) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[cartfisk](https://github.com/cartfisk) |[cellvinchung](https://github.com/cellvinchung) |[chao](https://github.com/chao) |[Cretezy](https://github.com/Cretezy) |[charlybillaud](https://github.com/charlybillaud) |[prattcmp](https://github.com/prattcmp) |
|
||||
[hrsh](https://github.com/hrsh) |[mauricioribeiro](https://github.com/mauricioribeiro) |[matthewhartstonge](https://github.com/matthewhartstonge) |[mjesuele](https://github.com/mjesuele) |[mattfik](https://github.com/mattfik) |[mateuscruz](https://github.com/mateuscruz) |
|
||||
|
||||
[<img alt="csprance" src="https://avatars.githubusercontent.com/u/7902617?v=4&s=117" width="117">](https://github.com/csprance) |[<img alt="cfra" src="https://avatars.githubusercontent.com/u/1347051?v=4&s=117" width="117">](https://github.com/cfra) |[<img alt="Aarbel" src="https://avatars.githubusercontent.com/u/25119847?v=4&s=117" width="117">](https://github.com/Aarbel) |[<img alt="cbush06" src="https://avatars.githubusercontent.com/u/15720146?v=4&s=117" width="117">](https://github.com/cbush06) |[<img alt="czj" src="https://avatars.githubusercontent.com/u/14306?v=4&s=117" width="117">](https://github.com/czj) |[<img alt="CommanderRoot" src="https://avatars.githubusercontent.com/u/4395417?v=4&s=117" width="117">](https://github.com/CommanderRoot) |
|
||||
[<img alt="masumulu28" src="https://avatars.githubusercontent.com/u/49063256?v=4&s=117" width="117">](https://github.com/masumulu28) |[<img alt="masaok" src="https://avatars.githubusercontent.com/u/1320083?v=4&s=117" width="117">](https://github.com/masaok) |[<img alt="martin-brennan" src="https://avatars.githubusercontent.com/u/920448?v=4&s=117" width="117">](https://github.com/martin-brennan) |[<img alt="marcusforsberg" src="https://avatars.githubusercontent.com/u/1009069?v=4&s=117" width="117">](https://github.com/marcusforsberg) |[<img alt="marcosthejew" src="https://avatars.githubusercontent.com/u/1500967?v=4&s=117" width="117">](https://github.com/marcosthejew) |[<img alt="mperrando" src="https://avatars.githubusercontent.com/u/525572?v=4&s=117" width="117">](https://github.com/mperrando) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[csprance](https://github.com/csprance) |[cfra](https://github.com/cfra) |[Aarbel](https://github.com/Aarbel) |[cbush06](https://github.com/cbush06) |[czj](https://github.com/czj) |[CommanderRoot](https://github.com/CommanderRoot) |
|
||||
[masumulu28](https://github.com/masumulu28) |[masaok](https://github.com/masaok) |[martin-brennan](https://github.com/martin-brennan) |[marcusforsberg](https://github.com/marcusforsberg) |[marcosthejew](https://github.com/marcosthejew) |[mperrando](https://github.com/mperrando) |
|
||||
|
||||
[<img alt="ardeois" src="https://avatars.githubusercontent.com/u/1867939?v=4&s=117" width="117">](https://github.com/ardeois) |[<img alt="sercraig" src="https://avatars.githubusercontent.com/u/24261518?v=4&s=117" width="117">](https://github.com/sercraig) |[<img alt="Cruaier" src="https://avatars.githubusercontent.com/u/5204940?v=4&s=117" width="117">](https://github.com/Cruaier) |[<img alt="danmichaelo" src="https://avatars.githubusercontent.com/u/434495?v=4&s=117" width="117">](https://github.com/danmichaelo) |[<img alt="danschalow" src="https://avatars.githubusercontent.com/u/3527437?v=4&s=117" width="117">](https://github.com/danschalow) |[<img alt="danilat" src="https://avatars.githubusercontent.com/u/22763?v=4&s=117" width="117">](https://github.com/danilat) |
|
||||
[<img alt="onhate" src="https://avatars.githubusercontent.com/u/980905?v=4&s=117" width="117">](https://github.com/onhate) |[<img alt="elliotdickison" src="https://avatars.githubusercontent.com/u/2523678?v=4&s=117" width="117">](https://github.com/elliotdickison) |[<img alt="ParsaArvanehPA" src="https://avatars.githubusercontent.com/u/62149413?v=4&s=117" width="117">](https://github.com/ParsaArvanehPA) |[<img alt="cryptic022" src="https://avatars.githubusercontent.com/u/18145703?v=4&s=117" width="117">](https://github.com/cryptic022) |[<img alt="Ozodbek1405" src="https://avatars.githubusercontent.com/u/86141593?v=4&s=117" width="117">](https://github.com/Ozodbek1405) |[<img alt="leftdevel" src="https://avatars.githubusercontent.com/u/843337?v=4&s=117" width="117">](https://github.com/leftdevel) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[ardeois](https://github.com/ardeois) |[sercraig](https://github.com/sercraig) |[Cruaier](https://github.com/Cruaier) |[danmichaelo](https://github.com/danmichaelo) |[danschalow](https://github.com/danschalow) |[danilat](https://github.com/danilat) |
|
||||
[onhate](https://github.com/onhate) |[elliotdickison](https://github.com/elliotdickison) |[ParsaArvanehPA](https://github.com/ParsaArvanehPA) |[cryptic022](https://github.com/cryptic022) |[Ozodbek1405](https://github.com/Ozodbek1405) |[leftdevel](https://github.com/leftdevel) |
|
||||
|
||||
[<img alt="mrboomer" src="https://avatars.githubusercontent.com/u/5942912?v=4&s=117" width="117">](https://github.com/mrboomer) |[<img alt="Cantabar" src="https://avatars.githubusercontent.com/u/6812207?v=4&s=117" width="117">](https://github.com/Cantabar) |[<img alt="KaminskiDaniell" src="https://avatars.githubusercontent.com/u/27357868?v=4&s=117" width="117">](https://github.com/KaminskiDaniell) |[<img alt="akizor" src="https://avatars.githubusercontent.com/u/1052439?v=4&s=117" width="117">](https://github.com/akizor) |[<img alt="davilima6" src="https://avatars.githubusercontent.com/u/422130?v=4&s=117" width="117">](https://github.com/davilima6) |[<img alt="DennisKofflard" src="https://avatars.githubusercontent.com/u/8669129?v=4&s=117" width="117">](https://github.com/DennisKofflard) |
|
||||
[<img alt="nil1511" src="https://avatars.githubusercontent.com/u/2058170?v=4&s=117" width="117">](https://github.com/nil1511) |[<img alt="coreprocess" src="https://avatars.githubusercontent.com/u/1226918?v=4&s=117" width="117">](https://github.com/coreprocess) |[<img alt="nicojones" src="https://avatars.githubusercontent.com/u/6078915?v=4&s=117" width="117">](https://github.com/nicojones) |[<img alt="trungcva10a6tn" src="https://avatars.githubusercontent.com/u/18293783?v=4&s=117" width="117">](https://github.com/trungcva10a6tn) |[<img alt="naveed-ahmad" src="https://avatars.githubusercontent.com/u/701567?v=4&s=117" width="117">](https://github.com/naveed-ahmad) |[<img alt="pleasespammelater" src="https://avatars.githubusercontent.com/u/11870394?v=4&s=117" width="117">](https://github.com/pleasespammelater) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[mrboomer](https://github.com/mrboomer) |[Cantabar](https://github.com/Cantabar) |[KaminskiDaniell](https://github.com/KaminskiDaniell) |[akizor](https://github.com/akizor) |[davilima6](https://github.com/davilima6) |[DennisKofflard](https://github.com/DennisKofflard) |
|
||||
[nil1511](https://github.com/nil1511) |[coreprocess](https://github.com/coreprocess) |[nicojones](https://github.com/nicojones) |[trungcva10a6tn](https://github.com/trungcva10a6tn) |[naveed-ahmad](https://github.com/naveed-ahmad) |[pleasespammelater](https://github.com/pleasespammelater) |
|
||||
|
||||
[<img alt="jeetiss" src="https://avatars.githubusercontent.com/u/6726016?v=4&s=117" width="117">](https://github.com/jeetiss) |[<img alt="sweetro" src="https://avatars.githubusercontent.com/u/6228717?v=4&s=117" width="117">](https://github.com/sweetro) |[<img alt="EdgarSantiago93" src="https://avatars.githubusercontent.com/u/14806877?v=4&s=117" width="117">](https://github.com/EdgarSantiago93) |[<img alt="emuell" src="https://avatars.githubusercontent.com/u/11521600?v=4&s=117" width="117">](https://github.com/emuell) |[<img alt="efbautista" src="https://avatars.githubusercontent.com/u/35430671?v=4&s=117" width="117">](https://github.com/efbautista) |[<img alt="yoldar" src="https://avatars.githubusercontent.com/u/1597578?v=4&s=117" width="117">](https://github.com/yoldar) |
|
||||
[<img alt="marton-laszlo-attila" src="https://avatars.githubusercontent.com/u/73295321?v=4&s=117" width="117">](https://github.com/marton-laszlo-attila) |[<img alt="navruzm" src="https://avatars.githubusercontent.com/u/168341?v=4&s=117" width="117">](https://github.com/navruzm) |[<img alt="mogzol" src="https://avatars.githubusercontent.com/u/11789801?v=4&s=117" width="117">](https://github.com/mogzol) |[<img alt="shahimclt" src="https://avatars.githubusercontent.com/u/8318002?v=4&s=117" width="117">](https://github.com/shahimclt) |[<img alt="mnafees" src="https://avatars.githubusercontent.com/u/1763885?v=4&s=117" width="117">](https://github.com/mnafees) |[<img alt="boudra" src="https://avatars.githubusercontent.com/u/711886?v=4&s=117" width="117">](https://github.com/boudra) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[jeetiss](https://github.com/jeetiss) |[sweetro](https://github.com/sweetro) |[EdgarSantiago93](https://github.com/EdgarSantiago93) |[emuell](https://github.com/emuell) |[efbautista](https://github.com/efbautista) |[yoldar](https://github.com/yoldar) |
|
||||
[marton-laszlo-attila](https://github.com/marton-laszlo-attila) |[navruzm](https://github.com/navruzm) |[mogzol](https://github.com/mogzol) |[shahimclt](https://github.com/shahimclt) |[mnafees](https://github.com/mnafees) |[boudra](https://github.com/boudra) |
|
||||
|
||||
[<img alt="eliOcs" src="https://avatars.githubusercontent.com/u/1283954?v=4&s=117" width="117">](https://github.com/eliOcs) |[<img alt="elliotdickison" src="https://avatars.githubusercontent.com/u/2523678?v=4&s=117" width="117">](https://github.com/elliotdickison) |[<img alt="EnricoSottile" src="https://avatars.githubusercontent.com/u/10349653?v=4&s=117" width="117">](https://github.com/EnricoSottile) |[<img alt="epexa" src="https://avatars.githubusercontent.com/u/2198826?v=4&s=117" width="117">](https://github.com/epexa) |[<img alt="Gkleinereva" src="https://avatars.githubusercontent.com/u/23621633?v=4&s=117" width="117">](https://github.com/Gkleinereva) |[<img alt="fgallinari" src="https://avatars.githubusercontent.com/u/6473638?v=4&s=117" width="117">](https://github.com/fgallinari) |
|
||||
[<img alt="achmiral" src="https://avatars.githubusercontent.com/u/10906059?v=4&s=117" width="117">](https://github.com/achmiral) |[<img alt="JimmyLv" src="https://avatars.githubusercontent.com/u/4997466?v=4&s=117" width="117">](https://github.com/JimmyLv) |[<img alt="neuronet77" src="https://avatars.githubusercontent.com/u/4220037?v=4&s=117" width="117">](https://github.com/neuronet77) |[<img alt="mosi-kha" src="https://avatars.githubusercontent.com/u/35611016?v=4&s=117" width="117">](https://github.com/mosi-kha) |[<img alt="maddy-jo" src="https://avatars.githubusercontent.com/u/3241493?v=4&s=117" width="117">](https://github.com/maddy-jo) |[<img alt="mdxiaohu" src="https://avatars.githubusercontent.com/u/42248614?v=4&s=117" width="117">](https://github.com/mdxiaohu) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[eliOcs](https://github.com/eliOcs) |[elliotdickison](https://github.com/elliotdickison) |[EnricoSottile](https://github.com/EnricoSottile) |[epexa](https://github.com/epexa) |[Gkleinereva](https://github.com/Gkleinereva) |[fgallinari](https://github.com/fgallinari) |
|
||||
[achmiral](https://github.com/achmiral) |[JimmyLv](https://github.com/JimmyLv) |[neuronet77](https://github.com/neuronet77) |[mosi-kha](https://github.com/mosi-kha) |[maddy-jo](https://github.com/maddy-jo) |[mdxiaohu](https://github.com/mdxiaohu) |
|
||||
|
||||
[<img alt="ferdiusa" src="https://avatars.githubusercontent.com/u/1997982?v=4&s=117" width="117">](https://github.com/ferdiusa) |[<img alt="dtrucs" src="https://avatars.githubusercontent.com/u/1926041?v=4&s=117" width="117">](https://github.com/dtrucs) |[<img alt="fuadscodes" src="https://avatars.githubusercontent.com/u/60370584?v=4&s=117" width="117">](https://github.com/fuadscodes) |[<img alt="gabiganam" src="https://avatars.githubusercontent.com/u/28859646?v=4&s=117" width="117">](https://github.com/gabiganam) |[<img alt="geoffappleford" src="https://avatars.githubusercontent.com/u/731678?v=4&s=117" width="117">](https://github.com/geoffappleford) |[<img alt="gjungb" src="https://avatars.githubusercontent.com/u/3391068?v=4&s=117" width="117">](https://github.com/gjungb) |
|
||||
[<img alt="magumbo" src="https://avatars.githubusercontent.com/u/6683765?v=4&s=117" width="117">](https://github.com/magumbo) |[<img alt="jx-zyf" src="https://avatars.githubusercontent.com/u/26456842?v=4&s=117" width="117">](https://github.com/jx-zyf) |[<img alt="kode-ninja" src="https://avatars.githubusercontent.com/u/7857611?v=4&s=117" width="117">](https://github.com/kode-ninja) |[<img alt="sontixyou" src="https://avatars.githubusercontent.com/u/19817196?v=4&s=117" width="117">](https://github.com/sontixyou) |[<img alt="jur-ng" src="https://avatars.githubusercontent.com/u/111122756?v=4&s=117" width="117">](https://github.com/jur-ng) |[<img alt="johnmanjiro13" src="https://avatars.githubusercontent.com/u/28798279?v=4&s=117" width="117">](https://github.com/johnmanjiro13) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[ferdiusa](https://github.com/ferdiusa) |[dtrucs](https://github.com/dtrucs) |[fuadscodes](https://github.com/fuadscodes) |[gabiganam](https://github.com/gabiganam) |[geoffappleford](https://github.com/geoffappleford) |[gjungb](https://github.com/gjungb) |
|
||||
[magumbo](https://github.com/magumbo) |[jx-zyf](https://github.com/jx-zyf) |[kode-ninja](https://github.com/kode-ninja) |[sontixyou](https://github.com/sontixyou) |[jur-ng](https://github.com/jur-ng) |[johnmanjiro13](https://github.com/johnmanjiro13) |
|
||||
|
||||
[<img alt="roenschg" src="https://avatars.githubusercontent.com/u/9590236?v=4&s=117" width="117">](https://github.com/roenschg) |[<img alt="giacomocerquone" src="https://avatars.githubusercontent.com/u/9303791?v=4&s=117" width="117">](https://github.com/giacomocerquone) |[<img alt="HughbertD" src="https://avatars.githubusercontent.com/u/1580021?v=4&s=117" width="117">](https://github.com/HughbertD) |[<img alt="HussainAlkhalifah" src="https://avatars.githubusercontent.com/u/43642162?v=4&s=117" width="117">](https://github.com/HussainAlkhalifah) |[<img alt="huydod" src="https://avatars.githubusercontent.com/u/37580530?v=4&s=117" width="117">](https://github.com/huydod) |[<img alt="IanVS" src="https://avatars.githubusercontent.com/u/4616705?v=4&s=117" width="117">](https://github.com/IanVS) |
|
||||
[<img alt="hxgf" src="https://avatars.githubusercontent.com/u/56104?v=4&s=117" width="117">](https://github.com/hxgf) |[<img alt="green-mike" src="https://avatars.githubusercontent.com/u/5584225?v=4&s=117" width="117">](https://github.com/green-mike) |[<img alt="gaelicwinter" src="https://avatars.githubusercontent.com/u/6510266?v=4&s=117" width="117">](https://github.com/gaelicwinter) |[<img alt="frederikhors" src="https://avatars.githubusercontent.com/u/41120635?v=4&s=117" width="117">](https://github.com/frederikhors) |[<img alt="franckl" src="https://avatars.githubusercontent.com/u/3875803?v=4&s=117" width="117">](https://github.com/franckl) |[<img alt="fingul" src="https://avatars.githubusercontent.com/u/894739?v=4&s=117" width="117">](https://github.com/fingul) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[roenschg](https://github.com/roenschg) |[giacomocerquone](https://github.com/giacomocerquone) |[HughbertD](https://github.com/HughbertD) |[HussainAlkhalifah](https://github.com/HussainAlkhalifah) |[huydod](https://github.com/huydod) |[IanVS](https://github.com/IanVS) |
|
||||
[hxgf](https://github.com/hxgf) |[green-mike](https://github.com/green-mike) |[gaelicwinter](https://github.com/gaelicwinter) |[frederikhors](https://github.com/frederikhors) |[franckl](https://github.com/franckl) |[fingul](https://github.com/fingul) |
|
||||
|
||||
[<img alt="ishendyweb" src="https://avatars.githubusercontent.com/u/10582418?v=4&s=117" width="117">](https://github.com/ishendyweb) |[<img alt="NaxYo" src="https://avatars.githubusercontent.com/u/1963876?v=4&s=117" width="117">](https://github.com/NaxYo) |[<img alt="intenzive" src="https://avatars.githubusercontent.com/u/11055931?v=4&s=117" width="117">](https://github.com/intenzive) |[<img alt="GreenJimmy" src="https://avatars.githubusercontent.com/u/39386?v=4&s=117" width="117">](https://github.com/GreenJimmy) |[<img alt="mazoruss" src="https://avatars.githubusercontent.com/u/17625190?v=4&s=117" width="117">](https://github.com/mazoruss) |[<img alt="JacobMGEvans" src="https://avatars.githubusercontent.com/u/27247160?v=4&s=117" width="117">](https://github.com/JacobMGEvans) |
|
||||
[<img alt="elliotsayes" src="https://avatars.githubusercontent.com/u/7699058?v=4&s=117" width="117">](https://github.com/elliotsayes) |[<img alt="zanzlender" src="https://avatars.githubusercontent.com/u/44570474?v=4&s=117" width="117">](https://github.com/zanzlender) |[<img alt="olitomas" src="https://avatars.githubusercontent.com/u/6918659?v=4&s=117" width="117">](https://github.com/olitomas) |[<img alt="yoann-hellopret" src="https://avatars.githubusercontent.com/u/46525558?v=4&s=117" width="117">](https://github.com/yoann-hellopret) |[<img alt="vedran555" src="https://avatars.githubusercontent.com/u/38395951?v=4&s=117" width="117">](https://github.com/vedran555) |[<img alt="tusharjkhunt" src="https://avatars.githubusercontent.com/u/31904234?v=4&s=117" width="117">](https://github.com/tusharjkhunt) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[ishendyweb](https://github.com/ishendyweb) |[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) |
|
||||
[elliotsayes](https://github.com/elliotsayes) |[zanzlender](https://github.com/zanzlender) |[olitomas](https://github.com/olitomas) |[yoann-hellopret](https://github.com/yoann-hellopret) |[vedran555](https://github.com/vedran555) |[tusharjkhunt](https://github.com/tusharjkhunt) |
|
||||
|
||||
[<img alt="gaejabong" src="https://avatars.githubusercontent.com/u/978944?v=4&s=117" width="117">](https://github.com/gaejabong) |[<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="thanhthot" src="https://avatars.githubusercontent.com/u/50633205?v=4&s=117" width="117">](https://github.com/thanhthot) |[<img alt="stduhpf" src="https://avatars.githubusercontent.com/u/28208228?v=4&s=117" width="117">](https://github.com/stduhpf) |[<img alt="slawexxx44" src="https://avatars.githubusercontent.com/u/11180644?v=4&s=117" width="117">](https://github.com/slawexxx44) |[<img alt="rtaieb" src="https://avatars.githubusercontent.com/u/35224301?v=4&s=117" width="117">](https://github.com/rtaieb) |[<img alt="rmoura-92" src="https://avatars.githubusercontent.com/u/419044?v=4&s=117" width="117">](https://github.com/rmoura-92) |[<img alt="rlebosse" src="https://avatars.githubusercontent.com/u/2794137?v=4&s=117" width="117">](https://github.com/rlebosse) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[gaejabong](https://github.com/gaejabong) |[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) |
|
||||
[thanhthot](https://github.com/thanhthot) |[stduhpf](https://github.com/stduhpf) |[slawexxx44](https://github.com/slawexxx44) |[rtaieb](https://github.com/rtaieb) |[rmoura-92](https://github.com/rmoura-92) |[rlebosse](https://github.com/rlebosse) |
|
||||
|
||||
[<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) |[<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="rhymes" src="https://avatars.githubusercontent.com/u/146201?v=4&s=117" width="117">](https://github.com/rhymes) |[<img alt="luntta" src="https://avatars.githubusercontent.com/u/14221637?v=4&s=117" width="117">](https://github.com/luntta) |[<img alt="phil714" src="https://avatars.githubusercontent.com/u/7584581?v=4&s=117" width="117">](https://github.com/phil714) |[<img alt="ordago" src="https://avatars.githubusercontent.com/u/6376814?v=4&s=117" width="117">](https://github.com/ordago) |[<img alt="odselsevier" src="https://avatars.githubusercontent.com/u/95745934?v=4&s=117" width="117">](https://github.com/odselsevier) |[<img alt="ninesalt" src="https://avatars.githubusercontent.com/u/7952255?v=4&s=117" width="117">](https://github.com/ninesalt) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[janwilts](https://github.com/janwilts) |[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) |
|
||||
[rhymes](https://github.com/rhymes) |[luntta](https://github.com/luntta) |[phil714](https://github.com/phil714) |[ordago](https://github.com/ordago) |[odselsevier](https://github.com/odselsevier) |[ninesalt](https://github.com/ninesalt) |
|
||||
|
||||
[<img alt="chromacoma" src="https://avatars.githubusercontent.com/u/1535623?v=4&s=117" width="117">](https://github.com/chromacoma) |[<img alt="Jokcy" src="https://avatars.githubusercontent.com/u/2088642?v=4&s=117" width="117">](https://github.com/Jokcy) |[<img alt="jsanchez034" src="https://avatars.githubusercontent.com/u/761087?v=4&s=117" width="117">](https://github.com/jsanchez034) |[<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="dzcpy" src="https://avatars.githubusercontent.com/u/203980?v=4&s=117" width="117">](https://github.com/dzcpy) |[<img alt="xhocquet" src="https://avatars.githubusercontent.com/u/8116516?v=4&s=117" width="117">](https://github.com/xhocquet) |[<img alt="willycamargo" src="https://avatars.githubusercontent.com/u/5041887?v=4&s=117" width="117">](https://github.com/willycamargo) |[<img alt="weston-sankey-mark43" src="https://avatars.githubusercontent.com/u/97678695?v=4&s=117" width="117">](https://github.com/weston-sankey-mark43) |[<img alt="dwnste" src="https://avatars.githubusercontent.com/u/17119722?v=4&s=117" width="117">](https://github.com/dwnste) |[<img alt="nagyv" src="https://avatars.githubusercontent.com/u/126671?v=4&s=117" width="117">](https://github.com/nagyv) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[chromacoma](https://github.com/chromacoma) |[Jokcy](https://github.com/Jokcy) |[jsanchez034](https://github.com/jsanchez034) |[jonathanarbely](https://github.com/jonathanarbely) |[jderrough](https://github.com/jderrough) |[jorgeepc](https://github.com/jorgeepc) |
|
||||
[dzcpy](https://github.com/dzcpy) |[xhocquet](https://github.com/xhocquet) |[willycamargo](https://github.com/willycamargo) |[weston-sankey-mark43](https://github.com/weston-sankey-mark43) |[dwnste](https://github.com/dwnste) |[nagyv](https://github.com/nagyv) |
|
||||
|
||||
[<img alt="jszobody" src="https://avatars.githubusercontent.com/u/203749?v=4&s=117" width="117">](https://github.com/jszobody) |[<img alt="jbelej" src="https://avatars.githubusercontent.com/u/2229202?v=4&s=117" width="117">](https://github.com/jbelej) |[<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="mellow-fellow" src="https://avatars.githubusercontent.com/u/19280122?v=4&s=117" width="117">](https://github.com/mellow-fellow) |[<img alt="jvelten" src="https://avatars.githubusercontent.com/u/48118068?v=4&s=117" width="117">](https://github.com/jvelten) |
|
||||
[<img alt="stiig" src="https://avatars.githubusercontent.com/u/8639922?v=4&s=117" width="117">](https://github.com/stiig) |[<img alt="valentinoli" src="https://avatars.githubusercontent.com/u/23453691?v=4&s=117" width="117">](https://github.com/valentinoli) |[<img alt="vially" src="https://avatars.githubusercontent.com/u/433598?v=4&s=117" width="117">](https://github.com/vially) |[<img alt="trivikr" src="https://avatars.githubusercontent.com/u/16024985?v=4&s=117" width="117">](https://github.com/trivikr) |[<img alt="top-master" src="https://avatars.githubusercontent.com/u/31405473?v=4&s=117" width="117">](https://github.com/top-master) |[<img alt="tvaliasek" src="https://avatars.githubusercontent.com/u/8644946?v=4&s=117" width="117">](https://github.com/tvaliasek) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[jszobody](https://github.com/jszobody) |[jbelej](https://github.com/jbelej) |[jcalonso](https://github.com/jcalonso) |[jmontoyaa](https://github.com/jmontoyaa) |[mellow-fellow](https://github.com/mellow-fellow) |[jvelten](https://github.com/jvelten) |
|
||||
[stiig](https://github.com/stiig) |[valentinoli](https://github.com/valentinoli) |[vially](https://github.com/vially) |[trivikr](https://github.com/trivikr) |[top-master](https://github.com/top-master) |[tvaliasek](https://github.com/tvaliasek) |
|
||||
|
||||
[<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="kergekacsa" src="https://avatars.githubusercontent.com/u/16637320?v=4&s=117" width="117">](https://github.com/kergekacsa) |[<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="kidonng" src="https://avatars.githubusercontent.com/u/44045911?v=4&s=117" width="117">](https://github.com/kidonng) |
|
||||
[<img alt="tomekp" src="https://avatars.githubusercontent.com/u/1856393?v=4&s=117" width="117">](https://github.com/tomekp) |[<img alt="tomsaleeba" src="https://avatars.githubusercontent.com/u/1773838?v=4&s=117" width="117">](https://github.com/tomsaleeba) |[<img alt="WIStudent" src="https://avatars.githubusercontent.com/u/2707930?v=4&s=117" width="117">](https://github.com/WIStudent) |[<img alt="tmaier" src="https://avatars.githubusercontent.com/u/350038?v=4&s=117" width="117">](https://github.com/tmaier) |[<img alt="twarlop" src="https://avatars.githubusercontent.com/u/2856082?v=4&s=117" width="117">](https://github.com/twarlop) |[<img alt="tcgj" src="https://avatars.githubusercontent.com/u/7994529?v=4&s=117" width="117">](https://github.com/tcgj) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[tykarol](https://github.com/tykarol) |[kaspermeinema](https://github.com/kaspermeinema) |[firesharkstudios](https://github.com/firesharkstudios) |[kergekacsa](https://github.com/kergekacsa) |[kevin-west-10x](https://github.com/kevin-west-10x) |[kidonng](https://github.com/kidonng) |
|
||||
[tomekp](https://github.com/tomekp) |[tomsaleeba](https://github.com/tomsaleeba) |[WIStudent](https://github.com/WIStudent) |[tmaier](https://github.com/tmaier) |[twarlop](https://github.com/twarlop) |[tcgj](https://github.com/tcgj) |
|
||||
|
||||
[<img alt="elkebab" src="https://avatars.githubusercontent.com/u/6313468?v=4&s=117" width="117">](https://github.com/elkebab) |[<img alt="kyleparisi" src="https://avatars.githubusercontent.com/u/1286753?v=4&s=117" width="117">](https://github.com/kyleparisi) |[<img alt="labohkip81" src="https://avatars.githubusercontent.com/u/36964869?v=4&s=117" width="117">](https://github.com/labohkip81) |[<img alt="hoangbits" src="https://avatars.githubusercontent.com/u/7990827?v=4&s=117" width="117">](https://github.com/hoangbits) |[<img alt="leaanthony" src="https://avatars.githubusercontent.com/u/1943904?v=4&s=117" width="117">](https://github.com/leaanthony) |[<img alt="larowlan" src="https://avatars.githubusercontent.com/u/555254?v=4&s=117" width="117">](https://github.com/larowlan) |
|
||||
[<img alt="dkisic" src="https://avatars.githubusercontent.com/u/32257921?v=4&s=117" width="117">](https://github.com/dkisic) |[<img alt="craigcbrunner" src="https://avatars.githubusercontent.com/u/2780521?v=4&s=117" width="117">](https://github.com/craigcbrunner) |[<img alt="codehero7386" src="https://avatars.githubusercontent.com/u/56253286?v=4&s=117" width="117">](https://github.com/codehero7386) |[<img alt="christianwengert" src="https://avatars.githubusercontent.com/u/12936057?v=4&s=117" width="117">](https://github.com/christianwengert) |[<img alt="cgoinglove" src="https://avatars.githubusercontent.com/u/86150470?v=4&s=117" width="117">](https://github.com/cgoinglove) |[<img alt="canvasbh" src="https://avatars.githubusercontent.com/u/44477734?v=4&s=117" width="117">](https://github.com/canvasbh) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[elkebab](https://github.com/elkebab) |[kyleparisi](https://github.com/kyleparisi) |[labohkip81](https://github.com/labohkip81) |[hoangbits](https://github.com/hoangbits) |[leaanthony](https://github.com/leaanthony) |[larowlan](https://github.com/larowlan) |
|
||||
[dkisic](https://github.com/dkisic) |[craigcbrunner](https://github.com/craigcbrunner) |[codehero7386](https://github.com/codehero7386) |[christianwengert](https://github.com/christianwengert) |[cgoinglove](https://github.com/cgoinglove) |[canvasbh](https://github.com/canvasbh) |
|
||||
|
||||
[<img alt="dviry" src="https://avatars.githubusercontent.com/u/1230260?v=4&s=117" width="117">](https://github.com/dviry) |[<img alt="galli-leo" src="https://avatars.githubusercontent.com/u/5339762?v=4&s=117" width="117">](https://github.com/galli-leo) |[<img alt="leods92" src="https://avatars.githubusercontent.com/u/879395?v=4&s=117" width="117">](https://github.com/leods92) |[<img alt="leomelzer" src="https://avatars.githubusercontent.com/u/23313?v=4&s=117" width="117">](https://github.com/leomelzer) |[<img alt="dolphinigle" src="https://avatars.githubusercontent.com/u/7020472?v=4&s=117" width="117">](https://github.com/dolphinigle) |[<img alt="louim" src="https://avatars.githubusercontent.com/u/923718?v=4&s=117" width="117">](https://github.com/louim) |
|
||||
[<img alt="c0b41" src="https://avatars.githubusercontent.com/u/2834954?v=4&s=117" width="117">](https://github.com/c0b41) |[<img alt="avalla" src="https://avatars.githubusercontent.com/u/986614?v=4&s=117" width="117">](https://github.com/avalla) |[<img alt="arggh" src="https://avatars.githubusercontent.com/u/17210302?v=4&s=117" width="117">](https://github.com/arggh) |[<img alt="alfatv" src="https://avatars.githubusercontent.com/u/62238673?v=4&s=117" width="117">](https://github.com/alfatv) |[<img alt="agreene-coursera" src="https://avatars.githubusercontent.com/u/30501355?v=4&s=117" width="117">](https://github.com/agreene-coursera) |[<img alt="aduh95-test-account" src="https://avatars.githubusercontent.com/u/93441190?v=4&s=117" width="117">](https://github.com/aduh95-test-account) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[dviry](https://github.com/dviry) |[galli-leo](https://github.com/galli-leo) |[leods92](https://github.com/leods92) |[leomelzer](https://github.com/leomelzer) |[dolphinigle](https://github.com/dolphinigle) |[louim](https://github.com/louim) |
|
||||
[c0b41](https://github.com/c0b41) |[avalla](https://github.com/avalla) |[arggh](https://github.com/arggh) |[alfatv](https://github.com/alfatv) |[agreene-coursera](https://github.com/agreene-coursera) |[aduh95-test-account](https://github.com/aduh95-test-account) |
|
||||
|
||||
[<img alt="ombr" src="https://avatars.githubusercontent.com/u/857339?v=4&s=117" width="117">](https://github.com/ombr) |[<img alt="lucaperret" src="https://avatars.githubusercontent.com/u/1887122?v=4&s=117" width="117">](https://github.com/lucaperret) |[<img alt="lucax88x" src="https://avatars.githubusercontent.com/u/6294464?v=4&s=117" width="117">](https://github.com/lucax88x) |[<img alt="Lucklj521" src="https://avatars.githubusercontent.com/u/93632042?v=4&s=117" width="117">](https://github.com/Lucklj521) |[<img alt="marc-mabe" src="https://avatars.githubusercontent.com/u/302689?v=4&s=117" width="117">](https://github.com/marc-mabe) |[<img alt="onhate" src="https://avatars.githubusercontent.com/u/980905?v=4&s=117" width="117">](https://github.com/onhate) |
|
||||
[<img alt="sartoshi-foot-dao" src="https://avatars.githubusercontent.com/u/99770068?v=4&s=117" width="117">](https://github.com/sartoshi-foot-dao) |[<img alt="zackbloom" src="https://avatars.githubusercontent.com/u/55347?v=4&s=117" width="117">](https://github.com/zackbloom) |[<img alt="zlawson-ut" src="https://avatars.githubusercontent.com/u/7375444?v=4&s=117" width="117">](https://github.com/zlawson-ut) |[<img alt="zachconner" src="https://avatars.githubusercontent.com/u/11339326?v=4&s=117" width="117">](https://github.com/zachconner) |[<img alt="YehudaKremer" src="https://avatars.githubusercontent.com/u/946652?v=4&s=117" width="117">](https://github.com/YehudaKremer) |[<img alt="Cruaier" src="https://avatars.githubusercontent.com/u/5204940?v=4&s=117" width="117">](https://github.com/Cruaier) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[ombr](https://github.com/ombr) |[lucaperret](https://github.com/lucaperret) |[lucax88x](https://github.com/lucax88x) |[Lucklj521](https://github.com/Lucklj521) |[marc-mabe](https://github.com/marc-mabe) |[onhate](https://github.com/onhate) |
|
||||
[sartoshi-foot-dao](https://github.com/sartoshi-foot-dao) |[zackbloom](https://github.com/zackbloom) |[zlawson-ut](https://github.com/zlawson-ut) |[zachconner](https://github.com/zachconner) |[YehudaKremer](https://github.com/YehudaKremer) |[Cruaier](https://github.com/Cruaier) |
|
||||
|
||||
[<img alt="mperrando" src="https://avatars.githubusercontent.com/u/525572?v=4&s=117" width="117">](https://github.com/mperrando) |[<img alt="marcosthejew" src="https://avatars.githubusercontent.com/u/1500967?v=4&s=117" width="117">](https://github.com/marcosthejew) |[<img alt="marcusforsberg" src="https://avatars.githubusercontent.com/u/1009069?v=4&s=117" width="117">](https://github.com/marcusforsberg) |[<img alt="martin-brennan" src="https://avatars.githubusercontent.com/u/920448?v=4&s=117" width="117">](https://github.com/martin-brennan) |[<img alt="masaok" src="https://avatars.githubusercontent.com/u/1320083?v=4&s=117" width="117">](https://github.com/masaok) |[<img alt="masumulu28" src="https://avatars.githubusercontent.com/u/49063256?v=4&s=117" width="117">](https://github.com/masumulu28) |
|
||||
[<img alt="sercraig" src="https://avatars.githubusercontent.com/u/24261518?v=4&s=117" width="117">](https://github.com/sercraig) |[<img alt="ardeois" src="https://avatars.githubusercontent.com/u/1867939?v=4&s=117" width="117">](https://github.com/ardeois) |[<img alt="CommanderRoot" src="https://avatars.githubusercontent.com/u/4395417?v=4&s=117" width="117">](https://github.com/CommanderRoot) |[<img alt="czj" src="https://avatars.githubusercontent.com/u/14306?v=4&s=117" width="117">](https://github.com/czj) |[<img alt="cbush06" src="https://avatars.githubusercontent.com/u/15720146?v=4&s=117" width="117">](https://github.com/cbush06) |[<img alt="Aarbel" src="https://avatars.githubusercontent.com/u/25119847?v=4&s=117" width="117">](https://github.com/Aarbel) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[mperrando](https://github.com/mperrando) |[marcosthejew](https://github.com/marcosthejew) |[marcusforsberg](https://github.com/marcusforsberg) |[martin-brennan](https://github.com/martin-brennan) |[masaok](https://github.com/masaok) |[masumulu28](https://github.com/masumulu28) |
|
||||
[sercraig](https://github.com/sercraig) |[ardeois](https://github.com/ardeois) |[CommanderRoot](https://github.com/CommanderRoot) |[czj](https://github.com/czj) |[cbush06](https://github.com/cbush06) |[Aarbel](https://github.com/Aarbel) |
|
||||
|
||||
[<img alt="mateuscruz" src="https://avatars.githubusercontent.com/u/8962842?v=4&s=117" width="117">](https://github.com/mateuscruz) |[<img alt="mattfik" src="https://avatars.githubusercontent.com/u/1638028?v=4&s=117" width="117">](https://github.com/mattfik) |[<img alt="mjesuele" src="https://avatars.githubusercontent.com/u/871117?v=4&s=117" width="117">](https://github.com/mjesuele) |[<img alt="matthewhartstonge" src="https://avatars.githubusercontent.com/u/6119549?v=4&s=117" width="117">](https://github.com/matthewhartstonge) |[<img alt="mauricioribeiro" src="https://avatars.githubusercontent.com/u/2589856?v=4&s=117" width="117">](https://github.com/mauricioribeiro) |[<img alt="hrsh" src="https://avatars.githubusercontent.com/u/1929359?v=4&s=117" width="117">](https://github.com/hrsh) |
|
||||
[<img alt="cfra" src="https://avatars.githubusercontent.com/u/1347051?v=4&s=117" width="117">](https://github.com/cfra) |[<img alt="csprance" src="https://avatars.githubusercontent.com/u/7902617?v=4&s=117" width="117">](https://github.com/csprance) |[<img alt="prattcmp" src="https://avatars.githubusercontent.com/u/1497950?v=4&s=117" width="117">](https://github.com/prattcmp) |[<img alt="charlybillaud" src="https://avatars.githubusercontent.com/u/31970410?v=4&s=117" width="117">](https://github.com/charlybillaud) |[<img alt="Cretezy" src="https://avatars.githubusercontent.com/u/2672503?v=4&s=117" width="117">](https://github.com/Cretezy) |[<img alt="chao" src="https://avatars.githubusercontent.com/u/55872?v=4&s=117" width="117">](https://github.com/chao) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[mateuscruz](https://github.com/mateuscruz) |[mattfik](https://github.com/mattfik) |[mjesuele](https://github.com/mjesuele) |[matthewhartstonge](https://github.com/matthewhartstonge) |[mauricioribeiro](https://github.com/mauricioribeiro) |[hrsh](https://github.com/hrsh) |
|
||||
[cfra](https://github.com/cfra) |[csprance](https://github.com/csprance) |[prattcmp](https://github.com/prattcmp) |[charlybillaud](https://github.com/charlybillaud) |[Cretezy](https://github.com/Cretezy) |[chao](https://github.com/chao) |
|
||||
|
||||
[<img alt="mhulet" src="https://avatars.githubusercontent.com/u/293355?v=4&s=117" width="117">](https://github.com/mhulet) |[<img alt="mkopinsky" src="https://avatars.githubusercontent.com/u/591435?v=4&s=117" width="117">](https://github.com/mkopinsky) |[<img alt="ken-kuro" src="https://avatars.githubusercontent.com/u/47441476?v=4&s=117" width="117">](https://github.com/ken-kuro) |[<img alt="achmiral" src="https://avatars.githubusercontent.com/u/10906059?v=4&s=117" width="117">](https://github.com/achmiral) |[<img alt="boudra" src="https://avatars.githubusercontent.com/u/711886?v=4&s=117" width="117">](https://github.com/boudra) |[<img alt="mnafees" src="https://avatars.githubusercontent.com/u/1763885?v=4&s=117" width="117">](https://github.com/mnafees) |
|
||||
[<img alt="cellvinchung" src="https://avatars.githubusercontent.com/u/5347394?v=4&s=117" width="117">](https://github.com/cellvinchung) |[<img alt="cartfisk" src="https://avatars.githubusercontent.com/u/8764375?v=4&s=117" width="117">](https://github.com/cartfisk) |[<img alt="cyu" src="https://avatars.githubusercontent.com/u/2431?v=4&s=117" width="117">](https://github.com/cyu) |[<img alt="bryanjswift" src="https://avatars.githubusercontent.com/u/9911?v=4&s=117" width="117">](https://github.com/bryanjswift) |[<img alt="eliOcs" src="https://avatars.githubusercontent.com/u/1283954?v=4&s=117" width="117">](https://github.com/eliOcs) |[<img alt="yoldar" src="https://avatars.githubusercontent.com/u/1597578?v=4&s=117" width="117">](https://github.com/yoldar) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[mhulet](https://github.com/mhulet) |[mkopinsky](https://github.com/mkopinsky) |[ken-kuro](https://github.com/ken-kuro) |[achmiral](https://github.com/achmiral) |[boudra](https://github.com/boudra) |[mnafees](https://github.com/mnafees) |
|
||||
[cellvinchung](https://github.com/cellvinchung) |[cartfisk](https://github.com/cartfisk) |[cyu](https://github.com/cyu) |[bryanjswift](https://github.com/bryanjswift) |[eliOcs](https://github.com/eliOcs) |[yoldar](https://github.com/yoldar) |
|
||||
|
||||
[<img alt="shahimclt" src="https://avatars.githubusercontent.com/u/8318002?v=4&s=117" width="117">](https://github.com/shahimclt) |[<img alt="mogzol" src="https://avatars.githubusercontent.com/u/11789801?v=4&s=117" width="117">](https://github.com/mogzol) |[<img alt="navruzm" src="https://avatars.githubusercontent.com/u/168341?v=4&s=117" width="117">](https://github.com/navruzm) |[<img alt="marton-laszlo-attila" src="https://avatars.githubusercontent.com/u/73295321?v=4&s=117" width="117">](https://github.com/marton-laszlo-attila) |[<img alt="pleasespammelater" src="https://avatars.githubusercontent.com/u/11870394?v=4&s=117" width="117">](https://github.com/pleasespammelater) |[<img alt="naveed-ahmad" src="https://avatars.githubusercontent.com/u/701567?v=4&s=117" width="117">](https://github.com/naveed-ahmad) |
|
||||
[<img alt="efbautista" src="https://avatars.githubusercontent.com/u/35430671?v=4&s=117" width="117">](https://github.com/efbautista) |[<img alt="emuell" src="https://avatars.githubusercontent.com/u/11521600?v=4&s=117" width="117">](https://github.com/emuell) |[<img alt="EdgarSantiago93" src="https://avatars.githubusercontent.com/u/14806877?v=4&s=117" width="117">](https://github.com/EdgarSantiago93) |[<img alt="sweetro" src="https://avatars.githubusercontent.com/u/6228717?v=4&s=117" width="117">](https://github.com/sweetro) |[<img alt="jeetiss" src="https://avatars.githubusercontent.com/u/6726016?v=4&s=117" width="117">](https://github.com/jeetiss) |[<img alt="DennisKofflard" src="https://avatars.githubusercontent.com/u/8669129?v=4&s=117" width="117">](https://github.com/DennisKofflard) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[shahimclt](https://github.com/shahimclt) |[mogzol](https://github.com/mogzol) |[navruzm](https://github.com/navruzm) |[marton-laszlo-attila](https://github.com/marton-laszlo-attila) |[pleasespammelater](https://github.com/pleasespammelater) |[naveed-ahmad](https://github.com/naveed-ahmad) |
|
||||
[efbautista](https://github.com/efbautista) |[emuell](https://github.com/emuell) |[EdgarSantiago93](https://github.com/EdgarSantiago93) |[sweetro](https://github.com/sweetro) |[jeetiss](https://github.com/jeetiss) |[DennisKofflard](https://github.com/DennisKofflard) |
|
||||
|
||||
[<img alt="trungcva10a6tn" src="https://avatars.githubusercontent.com/u/18293783?v=4&s=117" width="117">](https://github.com/trungcva10a6tn) |[<img alt="nicojones" src="https://avatars.githubusercontent.com/u/6078915?v=4&s=117" width="117">](https://github.com/nicojones) |[<img alt="coreprocess" src="https://avatars.githubusercontent.com/u/1226918?v=4&s=117" width="117">](https://github.com/coreprocess) |[<img alt="nil1511" src="https://avatars.githubusercontent.com/u/2058170?v=4&s=117" width="117">](https://github.com/nil1511) |[<img alt="leftdevel" src="https://avatars.githubusercontent.com/u/843337?v=4&s=117" width="117">](https://github.com/leftdevel) |[<img alt="Ozodbek1405" src="https://avatars.githubusercontent.com/u/86141593?v=4&s=117" width="117">](https://github.com/Ozodbek1405) |
|
||||
[<img alt="hoangsvit" src="https://avatars.githubusercontent.com/u/11882322?v=4&s=117" width="117">](https://github.com/hoangsvit) |[<img alt="davilima6" src="https://avatars.githubusercontent.com/u/422130?v=4&s=117" width="117">](https://github.com/davilima6) |[<img alt="akizor" src="https://avatars.githubusercontent.com/u/1052439?v=4&s=117" width="117">](https://github.com/akizor) |[<img alt="KaminskiDaniell" src="https://avatars.githubusercontent.com/u/27357868?v=4&s=117" width="117">](https://github.com/KaminskiDaniell) |[<img alt="Cantabar" src="https://avatars.githubusercontent.com/u/6812207?v=4&s=117" width="117">](https://github.com/Cantabar) |[<img alt="mrboomer" src="https://avatars.githubusercontent.com/u/5942912?v=4&s=117" width="117">](https://github.com/mrboomer) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[trungcva10a6tn](https://github.com/trungcva10a6tn) |[nicojones](https://github.com/nicojones) |[coreprocess](https://github.com/coreprocess) |[nil1511](https://github.com/nil1511) |[leftdevel](https://github.com/leftdevel) |[Ozodbek1405](https://github.com/Ozodbek1405) |
|
||||
[hoangsvit](https://github.com/hoangsvit) |[davilima6](https://github.com/davilima6) |[akizor](https://github.com/akizor) |[KaminskiDaniell](https://github.com/KaminskiDaniell) |[Cantabar](https://github.com/Cantabar) |[mrboomer](https://github.com/mrboomer) |
|
||||
|
||||
[<img alt="cryptic022" src="https://avatars.githubusercontent.com/u/18145703?v=4&s=117" width="117">](https://github.com/cryptic022) |[<img alt="ParsaArvanehPA" src="https://avatars.githubusercontent.com/u/62149413?v=4&s=117" width="117">](https://github.com/ParsaArvanehPA) |[<img alt="pascalwengerter" src="https://avatars.githubusercontent.com/u/16822008?v=4&s=117" width="117">](https://github.com/pascalwengerter) |[<img alt="patricklindsay" src="https://avatars.githubusercontent.com/u/7923681?v=4&s=117" width="117">](https://github.com/patricklindsay) |[<img alt="plneto" src="https://avatars.githubusercontent.com/u/5697434?v=4&s=117" width="117">](https://github.com/plneto) |[<img alt="pedrofs" src="https://avatars.githubusercontent.com/u/56484?v=4&s=117" width="117">](https://github.com/pedrofs) |
|
||||
[<img alt="danilat" src="https://avatars.githubusercontent.com/u/22763?v=4&s=117" width="117">](https://github.com/danilat) |[<img alt="danschalow" src="https://avatars.githubusercontent.com/u/3527437?v=4&s=117" width="117">](https://github.com/danschalow) |[<img alt="danmichaelo" src="https://avatars.githubusercontent.com/u/434495?v=4&s=117" width="117">](https://github.com/danmichaelo) |[<img alt="bedgerotto" src="https://avatars.githubusercontent.com/u/4459657?v=4&s=117" width="117">](https://github.com/bedgerotto) |[<img alt="functino" src="https://avatars.githubusercontent.com/u/415498?v=4&s=117" width="117">](https://github.com/functino) |[<img alt="amitport" src="https://avatars.githubusercontent.com/u/1131991?v=4&s=117" width="117">](https://github.com/amitport) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[cryptic022](https://github.com/cryptic022) |[ParsaArvanehPA](https://github.com/ParsaArvanehPA) |[pascalwengerter](https://github.com/pascalwengerter) |[patricklindsay](https://github.com/patricklindsay) |[plneto](https://github.com/plneto) |[pedrofs](https://github.com/pedrofs) |
|
||||
[danilat](https://github.com/danilat) |[danschalow](https://github.com/danschalow) |[danmichaelo](https://github.com/danmichaelo) |[bedgerotto](https://github.com/bedgerotto) |[functino](https://github.com/functino) |[amitport](https://github.com/amitport) |
|
||||
|
||||
[<img alt="pmusaraj" src="https://avatars.githubusercontent.com/u/368961?v=4&s=117" width="117">](https://github.com/pmusaraj) |[<img alt="phillipalexander" src="https://avatars.githubusercontent.com/u/1577682?v=4&s=117" width="117">](https://github.com/phillipalexander) |[<img alt="ppadmavilasom" src="https://avatars.githubusercontent.com/u/11167452?v=4&s=117" width="117">](https://github.com/ppadmavilasom) |[<img alt="Pzoco" src="https://avatars.githubusercontent.com/u/3101348?v=4&s=117" width="117">](https://github.com/Pzoco) |[<img alt="eman8519" src="https://avatars.githubusercontent.com/u/2380804?v=4&s=117" width="117">](https://github.com/eman8519) |[<img alt="luarmr" src="https://avatars.githubusercontent.com/u/817416?v=4&s=117" width="117">](https://github.com/luarmr) |
|
||||
[<img alt="tekacs" src="https://avatars.githubusercontent.com/u/63247?v=4&s=117" width="117">](https://github.com/tekacs) |[<img alt="Dogfalo" src="https://avatars.githubusercontent.com/u/2775751?v=4&s=117" width="117">](https://github.com/Dogfalo) |[<img alt="aalepis" src="https://avatars.githubusercontent.com/u/35684834?v=4&s=117" width="117">](https://github.com/aalepis) |[<img alt="alexnj" src="https://avatars.githubusercontent.com/u/683500?v=4&s=117" width="117">](https://github.com/alexnj) |[<img alt="asmt3" src="https://avatars.githubusercontent.com/u/1777709?v=4&s=117" width="117">](https://github.com/asmt3) |[<img alt="ahmadissa" src="https://avatars.githubusercontent.com/u/9936573?v=4&s=117" width="117">](https://github.com/ahmadissa) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[pmusaraj](https://github.com/pmusaraj) |[phillipalexander](https://github.com/phillipalexander) |[ppadmavilasom](https://github.com/ppadmavilasom) |[Pzoco](https://github.com/Pzoco) |[eman8519](https://github.com/eman8519) |[luarmr](https://github.com/luarmr) |
|
||||
[tekacs](https://github.com/tekacs) |[Dogfalo](https://github.com/Dogfalo) |[aalepis](https://github.com/aalepis) |[alexnj](https://github.com/alexnj) |[asmt3](https://github.com/asmt3) |[ahmadissa](https://github.com/ahmadissa) |
|
||||
|
||||
[<img alt="raulibanez" src="https://avatars.githubusercontent.com/u/1070825?v=4&s=117" width="117">](https://github.com/raulibanez) |[<img alt="refo" src="https://avatars.githubusercontent.com/u/1114116?v=4&s=117" width="117">](https://github.com/refo) |[<img alt="SxDx" src="https://avatars.githubusercontent.com/u/2004247?v=4&s=117" width="117">](https://github.com/SxDx) |[<img alt="robwilson1" src="https://avatars.githubusercontent.com/u/7114944?v=4&s=117" width="117">](https://github.com/robwilson1) |[<img alt="scherroman" src="https://avatars.githubusercontent.com/u/7923938?v=4&s=117" width="117">](https://github.com/scherroman) |[<img alt="rossng" src="https://avatars.githubusercontent.com/u/565371?v=4&s=117" width="117">](https://github.com/rossng) |
|
||||
[<img alt="adritasharma" src="https://avatars.githubusercontent.com/u/29271635?v=4&s=117" width="117">](https://github.com/adritasharma) |[<img alt="Adrrei" src="https://avatars.githubusercontent.com/u/22191685?v=4&s=117" width="117">](https://github.com/Adrrei) |[<img alt="adityapatadia" src="https://avatars.githubusercontent.com/u/1086617?v=4&s=117" width="117">](https://github.com/adityapatadia) |[<img alt="adamvigneault" src="https://avatars.githubusercontent.com/u/18236120?v=4&s=117" width="117">](https://github.com/adamvigneault) |[<img alt="ajh-sr" src="https://avatars.githubusercontent.com/u/71472057?v=4&s=117" width="117">](https://github.com/ajh-sr) |[<img alt="adamdottv" src="https://avatars.githubusercontent.com/u/2363879?v=4&s=117" width="117">](https://github.com/adamdottv) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[raulibanez](https://github.com/raulibanez) |[refo](https://github.com/refo) |[SxDx](https://github.com/SxDx) |[robwilson1](https://github.com/robwilson1) |[scherroman](https://github.com/scherroman) |[rossng](https://github.com/rossng) |
|
||||
[adritasharma](https://github.com/adritasharma) |[Adrrei](https://github.com/Adrrei) |[adityapatadia](https://github.com/adityapatadia) |[adamvigneault](https://github.com/adamvigneault) |[ajh-sr](https://github.com/ajh-sr) |[adamdottv](https://github.com/adamdottv) |
|
||||
|
||||
[<img alt="rart" src="https://avatars.githubusercontent.com/u/3928341?v=4&s=117" width="117">](https://github.com/rart) |[<img alt="GNURub" src="https://avatars.githubusercontent.com/u/1318648?v=4&s=117" width="117">](https://github.com/GNURub) |[<img alt="fortunto2" src="https://avatars.githubusercontent.com/u/1236751?v=4&s=117" width="117">](https://github.com/fortunto2) |[<img alt="samuelcolburn" src="https://avatars.githubusercontent.com/u/9741902?v=4&s=117" width="117">](https://github.com/samuelcolburn) |[<img alt="sdebacker" src="https://avatars.githubusercontent.com/u/134503?v=4&s=117" width="117">](https://github.com/sdebacker) |[<img alt="sebasegovia01" src="https://avatars.githubusercontent.com/u/35777287?v=4&s=117" width="117">](https://github.com/sebasegovia01) |
|
||||
[<img alt="abannach" src="https://avatars.githubusercontent.com/u/43150303?v=4&s=117" width="117">](https://github.com/abannach) |[<img alt="superhawk610" src="https://avatars.githubusercontent.com/u/18172185?v=4&s=117" width="117">](https://github.com/superhawk610) |[<img alt="ajschmidt8" src="https://avatars.githubusercontent.com/u/7400326?v=4&s=117" width="117">](https://github.com/ajschmidt8) |[<img alt="wbaaron" src="https://avatars.githubusercontent.com/u/1048988?v=4&s=117" width="117">](https://github.com/wbaaron) |[<img alt="Quorafind" src="https://avatars.githubusercontent.com/u/13215013?v=4&s=117" width="117">](https://github.com/Quorafind) |[<img alt="bducharme" src="https://avatars.githubusercontent.com/u/4173569?v=4&s=117" width="117">](https://github.com/bducharme) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[rart](https://github.com/rart) |[GNURub](https://github.com/GNURub) |[fortunto2](https://github.com/fortunto2) |[samuelcolburn](https://github.com/samuelcolburn) |[sdebacker](https://github.com/sdebacker) |[sebasegovia01](https://github.com/sebasegovia01) |
|
||||
[abannach](https://github.com/abannach) |[superhawk610](https://github.com/superhawk610) |[ajschmidt8](https://github.com/ajschmidt8) |[wbaaron](https://github.com/wbaaron) |[Quorafind](https://github.com/Quorafind) |[bducharme](https://github.com/bducharme) |
|
||||
|
||||
[<img alt="sergei-zelinsky" src="https://avatars.githubusercontent.com/u/19428086?v=4&s=117" width="117">](https://github.com/sergei-zelinsky) |[<img alt="szh" src="https://avatars.githubusercontent.com/u/546965?v=4&s=117" width="117">](https://github.com/szh) |[<img alt="SpazzMarticus" src="https://avatars.githubusercontent.com/u/5716457?v=4&s=117" width="117">](https://github.com/SpazzMarticus) |[<img alt="waptik" src="https://avatars.githubusercontent.com/u/1687551?v=4&s=117" width="117">](https://github.com/waptik) |[<img alt="quigebo" src="https://avatars.githubusercontent.com/u/741?v=4&s=117" width="117">](https://github.com/quigebo) |[<img alt="amaitu" src="https://avatars.githubusercontent.com/u/15688439?v=4&s=117" width="117">](https://github.com/amaitu) |
|
||||
[<img alt="azizk" src="https://avatars.githubusercontent.com/u/37282?v=4&s=117" width="117">](https://github.com/azizk) |[<img alt="azeemba" src="https://avatars.githubusercontent.com/u/2160795?v=4&s=117" width="117">](https://github.com/azeemba) |[<img alt="ayhankesicioglu" src="https://avatars.githubusercontent.com/u/36304312?v=4&s=117" width="117">](https://github.com/ayhankesicioglu) |[<img alt="atsawin" src="https://avatars.githubusercontent.com/u/666663?v=4&s=117" width="117">](https://github.com/atsawin) |[<img alt="ash-jc-allen" src="https://avatars.githubusercontent.com/u/39652331?v=4&s=117" width="117">](https://github.com/ash-jc-allen) |[<img alt="apuyou" src="https://avatars.githubusercontent.com/u/520053?v=4&s=117" width="117">](https://github.com/apuyou) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[sergei-zelinsky](https://github.com/sergei-zelinsky) |[szh](https://github.com/szh) |[SpazzMarticus](https://github.com/SpazzMarticus) |[waptik](https://github.com/waptik) |[quigebo](https://github.com/quigebo) |[amaitu](https://github.com/amaitu) |
|
||||
[azizk](https://github.com/azizk) |[azeemba](https://github.com/azeemba) |[ayhankesicioglu](https://github.com/ayhankesicioglu) |[atsawin](https://github.com/atsawin) |[ash-jc-allen](https://github.com/ash-jc-allen) |[apuyou](https://github.com/apuyou) |
|
||||
|
||||
[<img alt="steverob" src="https://avatars.githubusercontent.com/u/1220480?v=4&s=117" width="117">](https://github.com/steverob) |[<img alt="sjauld" src="https://avatars.githubusercontent.com/u/8232503?v=4&s=117" width="117">](https://github.com/sjauld) |[<img alt="strayer" src="https://avatars.githubusercontent.com/u/310624?v=4&s=117" width="117">](https://github.com/strayer) |[<img alt="taj" src="https://avatars.githubusercontent.com/u/16062635?v=4&s=117" width="117">](https://github.com/taj) |[<img alt="Tashows" src="https://avatars.githubusercontent.com/u/16656928?v=4&s=117" width="117">](https://github.com/Tashows) |[<img alt="tcgj" src="https://avatars.githubusercontent.com/u/7994529?v=4&s=117" width="117">](https://github.com/tcgj) |
|
||||
[<img alt="arthurdenner" src="https://avatars.githubusercontent.com/u/13774309?v=4&s=117" width="117">](https://github.com/arthurdenner) |[<img alt="Abourass" src="https://avatars.githubusercontent.com/u/39917231?v=4&s=117" width="117">](https://github.com/Abourass) |[<img alt="tyndria" src="https://avatars.githubusercontent.com/u/17138916?v=4&s=117" width="117">](https://github.com/tyndria) |[<img alt="anthony0030" src="https://avatars.githubusercontent.com/u/13033263?v=4&s=117" width="117">](https://github.com/anthony0030) |[<img alt="andychongyz" src="https://avatars.githubusercontent.com/u/12697240?v=4&s=117" width="117">](https://github.com/andychongyz) |[<img alt="andrii-bodnar" src="https://avatars.githubusercontent.com/u/29282228?v=4&s=117" width="117">](https://github.com/andrii-bodnar) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[steverob](https://github.com/steverob) |[sjauld](https://github.com/sjauld) |[strayer](https://github.com/strayer) |[taj](https://github.com/taj) |[Tashows](https://github.com/Tashows) |[tcgj](https://github.com/tcgj) |
|
||||
[arthurdenner](https://github.com/arthurdenner) |[Abourass](https://github.com/Abourass) |[tyndria](https://github.com/tyndria) |[anthony0030](https://github.com/anthony0030) |[andychongyz](https://github.com/andychongyz) |[andrii-bodnar](https://github.com/andrii-bodnar) |
|
||||
|
||||
[<img alt="twarlop" src="https://avatars.githubusercontent.com/u/2856082?v=4&s=117" width="117">](https://github.com/twarlop) |[<img alt="tmaier" src="https://avatars.githubusercontent.com/u/350038?v=4&s=117" width="117">](https://github.com/tmaier) |[<img alt="WIStudent" src="https://avatars.githubusercontent.com/u/2707930?v=4&s=117" width="117">](https://github.com/WIStudent) |[<img alt="tomsaleeba" src="https://avatars.githubusercontent.com/u/1773838?v=4&s=117" width="117">](https://github.com/tomsaleeba) |[<img alt="tomekp" src="https://avatars.githubusercontent.com/u/1856393?v=4&s=117" width="117">](https://github.com/tomekp) |[<img alt="tvaliasek" src="https://avatars.githubusercontent.com/u/8644946?v=4&s=117" width="117">](https://github.com/tvaliasek) |
|
||||
[<img alt="superandrew213" src="https://avatars.githubusercontent.com/u/13059204?v=4&s=117" width="117">](https://github.com/superandrew213) |[<img alt="radarhere" src="https://avatars.githubusercontent.com/u/3112309?v=4&s=117" width="117">](https://github.com/radarhere) |[<img alt="marc-mabe" src="https://avatars.githubusercontent.com/u/302689?v=4&s=117" width="117">](https://github.com/marc-mabe) |[<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="kergekacsa" src="https://avatars.githubusercontent.com/u/16637320?v=4&s=117" width="117">](https://github.com/kergekacsa) |[<img alt="firesharkstudios" src="https://avatars.githubusercontent.com/u/17069637?v=4&s=117" width="117">](https://github.com/firesharkstudios) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[twarlop](https://github.com/twarlop) |[tmaier](https://github.com/tmaier) |[WIStudent](https://github.com/WIStudent) |[tomsaleeba](https://github.com/tomsaleeba) |[tomekp](https://github.com/tomekp) |[tvaliasek](https://github.com/tvaliasek) |
|
||||
[superandrew213](https://github.com/superandrew213) |[radarhere](https://github.com/radarhere) |[marc-mabe](https://github.com/marc-mabe) |[kevin-west-10x](https://github.com/kevin-west-10x) |[kergekacsa](https://github.com/kergekacsa) |[firesharkstudios](https://github.com/firesharkstudios) |
|
||||
|
||||
[<img alt="top-master" src="https://avatars.githubusercontent.com/u/31405473?v=4&s=117" width="117">](https://github.com/top-master) |[<img alt="trivikr" src="https://avatars.githubusercontent.com/u/16024985?v=4&s=117" width="117">](https://github.com/trivikr) |[<img alt="vially" src="https://avatars.githubusercontent.com/u/433598?v=4&s=117" width="117">](https://github.com/vially) |[<img alt="valentinoli" src="https://avatars.githubusercontent.com/u/23453691?v=4&s=117" width="117">](https://github.com/valentinoli) |[<img alt="stiig" src="https://avatars.githubusercontent.com/u/8639922?v=4&s=117" width="117">](https://github.com/stiig) |[<img alt="nagyv" src="https://avatars.githubusercontent.com/u/126671?v=4&s=117" width="117">](https://github.com/nagyv) |
|
||||
[<img alt="kaspermeinema" src="https://avatars.githubusercontent.com/u/73821331?v=4&s=117" width="117">](https://github.com/kaspermeinema) |[<img alt="tykarol" src="https://avatars.githubusercontent.com/u/9386320?v=4&s=117" width="117">](https://github.com/tykarol) |[<img alt="jvelten" src="https://avatars.githubusercontent.com/u/48118068?v=4&s=117" width="117">](https://github.com/jvelten) |[<img alt="mellow-fellow" src="https://avatars.githubusercontent.com/u/19280122?v=4&s=117" width="117">](https://github.com/mellow-fellow) |[<img alt="jmontoyaa" src="https://avatars.githubusercontent.com/u/158935?v=4&s=117" width="117">](https://github.com/jmontoyaa) |[<img alt="jcalonso" src="https://avatars.githubusercontent.com/u/664474?v=4&s=117" width="117">](https://github.com/jcalonso) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[top-master](https://github.com/top-master) |[trivikr](https://github.com/trivikr) |[vially](https://github.com/vially) |[valentinoli](https://github.com/valentinoli) |[stiig](https://github.com/stiig) |[nagyv](https://github.com/nagyv) |
|
||||
[kaspermeinema](https://github.com/kaspermeinema) |[tykarol](https://github.com/tykarol) |[jvelten](https://github.com/jvelten) |[mellow-fellow](https://github.com/mellow-fellow) |[jmontoyaa](https://github.com/jmontoyaa) |[jcalonso](https://github.com/jcalonso) |
|
||||
|
||||
[<img alt="dwnste" src="https://avatars.githubusercontent.com/u/17119722?v=4&s=117" width="117">](https://github.com/dwnste) |[<img alt="weston-sankey-mark43" src="https://avatars.githubusercontent.com/u/97678695?v=4&s=117" width="117">](https://github.com/weston-sankey-mark43) |[<img alt="willycamargo" src="https://avatars.githubusercontent.com/u/5041887?v=4&s=117" width="117">](https://github.com/willycamargo) |[<img alt="xhocquet" src="https://avatars.githubusercontent.com/u/8116516?v=4&s=117" width="117">](https://github.com/xhocquet) |[<img alt="YehudaKremer" src="https://avatars.githubusercontent.com/u/946652?v=4&s=117" width="117">](https://github.com/YehudaKremer) |[<img alt="zachconner" src="https://avatars.githubusercontent.com/u/11339326?v=4&s=117" width="117">](https://github.com/zachconner) |
|
||||
[<img alt="jbelej" src="https://avatars.githubusercontent.com/u/2229202?v=4&s=117" width="117">](https://github.com/jbelej) |[<img alt="jszobody" src="https://avatars.githubusercontent.com/u/203749?v=4&s=117" width="117">](https://github.com/jszobody) |[<img alt="jorgeepc" src="https://avatars.githubusercontent.com/u/3879892?v=4&s=117" width="117">](https://github.com/jorgeepc) |[<img alt="jderrough" src="https://avatars.githubusercontent.com/u/1108358?v=4&s=117" width="117">](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="jsanchez034" src="https://avatars.githubusercontent.com/u/761087?v=4&s=117" width="117">](https://github.com/jsanchez034) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[dwnste](https://github.com/dwnste) |[weston-sankey-mark43](https://github.com/weston-sankey-mark43) |[willycamargo](https://github.com/willycamargo) |[xhocquet](https://github.com/xhocquet) |[YehudaKremer](https://github.com/YehudaKremer) |[zachconner](https://github.com/zachconner) |
|
||||
[jbelej](https://github.com/jbelej) |[jszobody](https://github.com/jszobody) |[jorgeepc](https://github.com/jorgeepc) |[jderrough](https://github.com/jderrough) |[jonathanarbely](https://github.com/jonathanarbely) |[jsanchez034](https://github.com/jsanchez034) |
|
||||
|
||||
[<img alt="zlawson-ut" src="https://avatars.githubusercontent.com/u/7375444?v=4&s=117" width="117">](https://github.com/zlawson-ut) |[<img alt="zackbloom" src="https://avatars.githubusercontent.com/u/55347?v=4&s=117" width="117">](https://github.com/zackbloom) |[<img alt="sartoshi-foot-dao" src="https://avatars.githubusercontent.com/u/99770068?v=4&s=117" width="117">](https://github.com/sartoshi-foot-dao) |[<img alt="aduh95-test-account" src="https://avatars.githubusercontent.com/u/93441190?v=4&s=117" width="117">](https://github.com/aduh95-test-account) |[<img alt="agreene-coursera" src="https://avatars.githubusercontent.com/u/30501355?v=4&s=117" width="117">](https://github.com/agreene-coursera) |[<img alt="alfatv" src="https://avatars.githubusercontent.com/u/62238673?v=4&s=117" width="117">](https://github.com/alfatv) |
|
||||
[<img alt="Jokcy" src="https://avatars.githubusercontent.com/u/2088642?v=4&s=117" width="117">](https://github.com/Jokcy) |[<img alt="chromacoma" src="https://avatars.githubusercontent.com/u/1535623?v=4&s=117" width="117">](https://github.com/chromacoma) |[<img alt="Lucklj521" src="https://avatars.githubusercontent.com/u/93632042?v=4&s=117" width="117">](https://github.com/Lucklj521) |[<img alt="lucax88x" src="https://avatars.githubusercontent.com/u/6294464?v=4&s=117" width="117">](https://github.com/lucax88x) |[<img alt="lucaperret" src="https://avatars.githubusercontent.com/u/1887122?v=4&s=117" width="117">](https://github.com/lucaperret) |[<img alt="ombr" src="https://avatars.githubusercontent.com/u/857339?v=4&s=117" width="117">](https://github.com/ombr) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[zlawson-ut](https://github.com/zlawson-ut) |[zackbloom](https://github.com/zackbloom) |[sartoshi-foot-dao](https://github.com/sartoshi-foot-dao) |[aduh95-test-account](https://github.com/aduh95-test-account) |[agreene-coursera](https://github.com/agreene-coursera) |[alfatv](https://github.com/alfatv) |
|
||||
[Jokcy](https://github.com/Jokcy) |[chromacoma](https://github.com/chromacoma) |[Lucklj521](https://github.com/Lucklj521) |[lucax88x](https://github.com/lucax88x) |[lucaperret](https://github.com/lucaperret) |[ombr](https://github.com/ombr) |
|
||||
|
||||
[<img alt="arggh" src="https://avatars.githubusercontent.com/u/17210302?v=4&s=117" width="117">](https://github.com/arggh) |[<img alt="avalla" src="https://avatars.githubusercontent.com/u/986614?v=4&s=117" width="117">](https://github.com/avalla) |[<img alt="c0b41" src="https://avatars.githubusercontent.com/u/2834954?v=4&s=117" width="117">](https://github.com/c0b41) |[<img alt="canvasbh" src="https://avatars.githubusercontent.com/u/44477734?v=4&s=117" width="117">](https://github.com/canvasbh) |[<img alt="cgoinglove" src="https://avatars.githubusercontent.com/u/86150470?v=4&s=117" width="117">](https://github.com/cgoinglove) |[<img alt="christianwengert" src="https://avatars.githubusercontent.com/u/12936057?v=4&s=117" width="117">](https://github.com/christianwengert) |
|
||||
[<img alt="louim" src="https://avatars.githubusercontent.com/u/923718?v=4&s=117" width="117">](https://github.com/louim) |[<img alt="dolphinigle" src="https://avatars.githubusercontent.com/u/7020472?v=4&s=117" width="117">](https://github.com/dolphinigle) |[<img alt="leomelzer" src="https://avatars.githubusercontent.com/u/23313?v=4&s=117" width="117">](https://github.com/leomelzer) |[<img alt="leods92" src="https://avatars.githubusercontent.com/u/879395?v=4&s=117" width="117">](https://github.com/leods92) |[<img alt="galli-leo" src="https://avatars.githubusercontent.com/u/5339762?v=4&s=117" width="117">](https://github.com/galli-leo) |[<img alt="dviry" src="https://avatars.githubusercontent.com/u/1230260?v=4&s=117" width="117">](https://github.com/dviry) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[arggh](https://github.com/arggh) |[avalla](https://github.com/avalla) |[c0b41](https://github.com/c0b41) |[canvasbh](https://github.com/canvasbh) |[cgoinglove](https://github.com/cgoinglove) |[christianwengert](https://github.com/christianwengert) |
|
||||
[louim](https://github.com/louim) |[dolphinigle](https://github.com/dolphinigle) |[leomelzer](https://github.com/leomelzer) |[leods92](https://github.com/leods92) |[galli-leo](https://github.com/galli-leo) |[dviry](https://github.com/dviry) |
|
||||
|
||||
[<img alt="codehero7386" src="https://avatars.githubusercontent.com/u/56253286?v=4&s=117" width="117">](https://github.com/codehero7386) |[<img alt="craigcbrunner" src="https://avatars.githubusercontent.com/u/2780521?v=4&s=117" width="117">](https://github.com/craigcbrunner) |[<img alt="darthf1" src="https://avatars.githubusercontent.com/u/17253332?v=4&s=117" width="117">](https://github.com/darthf1) |[<img alt="dkisic" src="https://avatars.githubusercontent.com/u/32257921?v=4&s=117" width="117">](https://github.com/dkisic) |[<img alt="dzcpy" src="https://avatars.githubusercontent.com/u/203980?v=4&s=117" width="117">](https://github.com/dzcpy) |[<img alt="elliotsayes" src="https://avatars.githubusercontent.com/u/7699058?v=4&s=117" width="117">](https://github.com/elliotsayes) |
|
||||
[<img alt="larowlan" src="https://avatars.githubusercontent.com/u/555254?v=4&s=117" width="117">](https://github.com/larowlan) |[<img alt="leaanthony" src="https://avatars.githubusercontent.com/u/1943904?v=4&s=117" width="117">](https://github.com/leaanthony) |[<img alt="hoangbits" src="https://avatars.githubusercontent.com/u/7990827?v=4&s=117" width="117">](https://github.com/hoangbits) |[<img alt="labohkip81" src="https://avatars.githubusercontent.com/u/36964869?v=4&s=117" width="117">](https://github.com/labohkip81) |[<img alt="kyleparisi" src="https://avatars.githubusercontent.com/u/1286753?v=4&s=117" width="117">](https://github.com/kyleparisi) |[<img alt="elkebab" src="https://avatars.githubusercontent.com/u/6313468?v=4&s=117" width="117">](https://github.com/elkebab) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[codehero7386](https://github.com/codehero7386) |[craigcbrunner](https://github.com/craigcbrunner) |[darthf1](https://github.com/darthf1) |[dkisic](https://github.com/dkisic) |[dzcpy](https://github.com/dzcpy) |[elliotsayes](https://github.com/elliotsayes) |
|
||||
[larowlan](https://github.com/larowlan) |[leaanthony](https://github.com/leaanthony) |[hoangbits](https://github.com/hoangbits) |[labohkip81](https://github.com/labohkip81) |[kyleparisi](https://github.com/kyleparisi) |[elkebab](https://github.com/elkebab) |
|
||||
|
||||
[<img alt="fingul" src="https://avatars.githubusercontent.com/u/894739?v=4&s=117" width="117">](https://github.com/fingul) |[<img alt="franckl" src="https://avatars.githubusercontent.com/u/3875803?v=4&s=117" width="117">](https://github.com/franckl) |[<img alt="frederikhors" src="https://avatars.githubusercontent.com/u/41120635?v=4&s=117" width="117">](https://github.com/frederikhors) |[<img alt="gaelicwinter" src="https://avatars.githubusercontent.com/u/6510266?v=4&s=117" width="117">](https://github.com/gaelicwinter) |[<img alt="green-mike" src="https://avatars.githubusercontent.com/u/5584225?v=4&s=117" width="117">](https://github.com/green-mike) |[<img alt="hxgf" src="https://avatars.githubusercontent.com/u/56104?v=4&s=117" width="117">](https://github.com/hxgf) |
|
||||
[<img alt="kidonng" src="https://avatars.githubusercontent.com/u/44045911?v=4&s=117" width="117">](https://github.com/kidonng) |[<img alt="profsmallpine" src="https://avatars.githubusercontent.com/u/7328006?v=4&s=117" width="117">](https://github.com/profsmallpine) |[<img alt="ishendyweb" src="https://avatars.githubusercontent.com/u/10582418?v=4&s=117" width="117">](https://github.com/ishendyweb) |[<img alt="IanVS" src="https://avatars.githubusercontent.com/u/4616705?v=4&s=117" width="117">](https://github.com/IanVS) |[<img alt="huydod" src="https://avatars.githubusercontent.com/u/37580530?v=4&s=117" width="117">](https://github.com/huydod) |[<img alt="HussainAlkhalifah" src="https://avatars.githubusercontent.com/u/43642162?v=4&s=117" width="117">](https://github.com/HussainAlkhalifah) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[fingul](https://github.com/fingul) |[franckl](https://github.com/franckl) |[frederikhors](https://github.com/frederikhors) |[gaelicwinter](https://github.com/gaelicwinter) |[green-mike](https://github.com/green-mike) |[hxgf](https://github.com/hxgf) |
|
||||
[kidonng](https://github.com/kidonng) |[profsmallpine](https://github.com/profsmallpine) |[ishendyweb](https://github.com/ishendyweb) |[IanVS](https://github.com/IanVS) |[huydod](https://github.com/huydod) |[HussainAlkhalifah](https://github.com/HussainAlkhalifah) |
|
||||
|
||||
[<img alt="johnmanjiro13" src="https://avatars.githubusercontent.com/u/28798279?v=4&s=117" width="117">](https://github.com/johnmanjiro13) |[<img alt="jur-ng" src="https://avatars.githubusercontent.com/u/111122756?v=4&s=117" width="117">](https://github.com/jur-ng) |[<img alt="sontixyou" src="https://avatars.githubusercontent.com/u/19817196?v=4&s=117" width="117">](https://github.com/sontixyou) |[<img alt="kode-ninja" src="https://avatars.githubusercontent.com/u/7857611?v=4&s=117" width="117">](https://github.com/kode-ninja) |[<img alt="jx-zyf" src="https://avatars.githubusercontent.com/u/26456842?v=4&s=117" width="117">](https://github.com/jx-zyf) |[<img alt="magumbo" src="https://avatars.githubusercontent.com/u/6683765?v=4&s=117" width="117">](https://github.com/magumbo) |
|
||||
[<img alt="HughbertD" src="https://avatars.githubusercontent.com/u/1580021?v=4&s=117" width="117">](https://github.com/HughbertD) |[<img alt="giacomocerquone" src="https://avatars.githubusercontent.com/u/9303791?v=4&s=117" width="117">](https://github.com/giacomocerquone) |[<img alt="roenschg" src="https://avatars.githubusercontent.com/u/9590236?v=4&s=117" width="117">](https://github.com/roenschg) |[<img alt="gjungb" src="https://avatars.githubusercontent.com/u/3391068?v=4&s=117" width="117">](https://github.com/gjungb) |[<img alt="geoffappleford" src="https://avatars.githubusercontent.com/u/731678?v=4&s=117" width="117">](https://github.com/geoffappleford) |[<img alt="gabiganam" src="https://avatars.githubusercontent.com/u/28859646?v=4&s=117" width="117">](https://github.com/gabiganam) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[johnmanjiro13](https://github.com/johnmanjiro13) |[jur-ng](https://github.com/jur-ng) |[sontixyou](https://github.com/sontixyou) |[kode-ninja](https://github.com/kode-ninja) |[jx-zyf](https://github.com/jx-zyf) |[magumbo](https://github.com/magumbo) |
|
||||
[HughbertD](https://github.com/HughbertD) |[giacomocerquone](https://github.com/giacomocerquone) |[roenschg](https://github.com/roenschg) |[gjungb](https://github.com/gjungb) |[geoffappleford](https://github.com/geoffappleford) |[gabiganam](https://github.com/gabiganam) |
|
||||
|
||||
[<img alt="mdxiaohu" src="https://avatars.githubusercontent.com/u/42248614?v=4&s=117" width="117">](https://github.com/mdxiaohu) |[<img alt="maddy-jo" src="https://avatars.githubusercontent.com/u/3241493?v=4&s=117" width="117">](https://github.com/maddy-jo) |[<img alt="mosi-kha" src="https://avatars.githubusercontent.com/u/35611016?v=4&s=117" width="117">](https://github.com/mosi-kha) |[<img alt="neuronet77" src="https://avatars.githubusercontent.com/u/4220037?v=4&s=117" width="117">](https://github.com/neuronet77) |[<img alt="ninesalt" src="https://avatars.githubusercontent.com/u/7952255?v=4&s=117" width="117">](https://github.com/ninesalt) |[<img alt="odselsevier" src="https://avatars.githubusercontent.com/u/95745934?v=4&s=117" width="117">](https://github.com/odselsevier) |
|
||||
[<img alt="fuadscodes" src="https://avatars.githubusercontent.com/u/60370584?v=4&s=117" width="117">](https://github.com/fuadscodes) |[<img alt="dtrucs" src="https://avatars.githubusercontent.com/u/1926041?v=4&s=117" width="117">](https://github.com/dtrucs) |[<img alt="ferdiusa" src="https://avatars.githubusercontent.com/u/1997982?v=4&s=117" width="117">](https://github.com/ferdiusa) |[<img alt="fgallinari" src="https://avatars.githubusercontent.com/u/6473638?v=4&s=117" width="117">](https://github.com/fgallinari) |[<img alt="Gkleinereva" src="https://avatars.githubusercontent.com/u/23621633?v=4&s=117" width="117">](https://github.com/Gkleinereva) |[<img alt="epexa" src="https://avatars.githubusercontent.com/u/2198826?v=4&s=117" width="117">](https://github.com/epexa) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[mdxiaohu](https://github.com/mdxiaohu) |[maddy-jo](https://github.com/maddy-jo) |[mosi-kha](https://github.com/mosi-kha) |[neuronet77](https://github.com/neuronet77) |[ninesalt](https://github.com/ninesalt) |[odselsevier](https://github.com/odselsevier) |
|
||||
[fuadscodes](https://github.com/fuadscodes) |[dtrucs](https://github.com/dtrucs) |[ferdiusa](https://github.com/ferdiusa) |[fgallinari](https://github.com/fgallinari) |[Gkleinereva](https://github.com/Gkleinereva) |[epexa](https://github.com/epexa) |
|
||||
|
||||
[<img alt="ordago" src="https://avatars.githubusercontent.com/u/6376814?v=4&s=117" width="117">](https://github.com/ordago) |[<img alt="phil714" src="https://avatars.githubusercontent.com/u/7584581?v=4&s=117" width="117">](https://github.com/phil714) |[<img alt="luntta" src="https://avatars.githubusercontent.com/u/14221637?v=4&s=117" width="117">](https://github.com/luntta) |[<img alt="rhymes" src="https://avatars.githubusercontent.com/u/146201?v=4&s=117" width="117">](https://github.com/rhymes) |[<img alt="rlebosse" src="https://avatars.githubusercontent.com/u/2794137?v=4&s=117" width="117">](https://github.com/rlebosse) |[<img alt="rmoura-92" src="https://avatars.githubusercontent.com/u/419044?v=4&s=117" width="117">](https://github.com/rmoura-92) |
|
||||
[<img alt="EnricoSottile" src="https://avatars.githubusercontent.com/u/10349653?v=4&s=117" width="117">](https://github.com/EnricoSottile) |[<img alt="theJoeBiz" src="https://avatars.githubusercontent.com/u/189589?v=4&s=117" width="117">](https://github.com/theJoeBiz) |[<img alt="Jmales" src="https://avatars.githubusercontent.com/u/22914881?v=4&s=117" width="117">](https://github.com/Jmales) |[<img alt="jessica-coursera" src="https://avatars.githubusercontent.com/u/35155465?v=4&s=117" width="117">](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="janwilts" src="https://avatars.githubusercontent.com/u/16721581?v=4&s=117" width="117">](https://github.com/janwilts) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[ordago](https://github.com/ordago) |[phil714](https://github.com/phil714) |[luntta](https://github.com/luntta) |[rhymes](https://github.com/rhymes) |[rlebosse](https://github.com/rlebosse) |[rmoura-92](https://github.com/rmoura-92) |
|
||||
[EnricoSottile](https://github.com/EnricoSottile) |[theJoeBiz](https://github.com/theJoeBiz) |[Jmales](https://github.com/Jmales) |[jessica-coursera](https://github.com/jessica-coursera) |[vith](https://github.com/vith) |[janwilts](https://github.com/janwilts) |
|
||||
|
||||
[<img alt="rtaieb" src="https://avatars.githubusercontent.com/u/35224301?v=4&s=117" width="117">](https://github.com/rtaieb) |[<img alt="slawexxx44" src="https://avatars.githubusercontent.com/u/11180644?v=4&s=117" width="117">](https://github.com/slawexxx44) |[<img alt="stduhpf" src="https://avatars.githubusercontent.com/u/28208228?v=4&s=117" width="117">](https://github.com/stduhpf) |[<img alt="thanhthot" src="https://avatars.githubusercontent.com/u/50633205?v=4&s=117" width="117">](https://github.com/thanhthot) |[<img alt="tusharjkhunt" src="https://avatars.githubusercontent.com/u/31904234?v=4&s=117" width="117">](https://github.com/tusharjkhunt) |[<img alt="vedran555" src="https://avatars.githubusercontent.com/u/38395951?v=4&s=117" width="117">](https://github.com/vedran555) |
|
||||
[<img alt="janklimo" src="https://avatars.githubusercontent.com/u/7811733?v=4&s=117" width="117">](https://github.com/janklimo) |[<img alt="jamestiotio" src="https://avatars.githubusercontent.com/u/18364745?v=4&s=117" width="117">](https://github.com/jamestiotio) |[<img alt="jcjmcclean" src="https://avatars.githubusercontent.com/u/1822574?v=4&s=117" width="117">](https://github.com/jcjmcclean) |[<img alt="Jbithell" src="https://avatars.githubusercontent.com/u/8408967?v=4&s=117" width="117">](https://github.com/Jbithell) |[<img alt="JakubHaladej" src="https://avatars.githubusercontent.com/u/77832677?v=4&s=117" width="117">](https://github.com/JakubHaladej) |[<img alt="jakemcallister" src="https://avatars.githubusercontent.com/u/1185699?v=4&s=117" width="117">](https://github.com/jakemcallister) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[rtaieb](https://github.com/rtaieb) |[slawexxx44](https://github.com/slawexxx44) |[stduhpf](https://github.com/stduhpf) |[thanhthot](https://github.com/thanhthot) |[tusharjkhunt](https://github.com/tusharjkhunt) |[vedran555](https://github.com/vedran555) |
|
||||
[janklimo](https://github.com/janklimo) |[jamestiotio](https://github.com/jamestiotio) |[jcjmcclean](https://github.com/jcjmcclean) |[Jbithell](https://github.com/Jbithell) |[JakubHaladej](https://github.com/JakubHaladej) |[jakemcallister](https://github.com/jakemcallister) |
|
||||
|
||||
[<img alt="yoann-hellopret" src="https://avatars.githubusercontent.com/u/46525558?v=4&s=117" width="117">](https://github.com/yoann-hellopret) |[<img alt="olitomas" src="https://avatars.githubusercontent.com/u/6918659?v=4&s=117" width="117">](https://github.com/olitomas) |[<img alt="JimmyLv" src="https://avatars.githubusercontent.com/u/4997466?v=4&s=117" width="117">](https://github.com/JimmyLv) |
|
||||
:---: |:---: |:---: |
|
||||
[yoann-hellopret](https://github.com/yoann-hellopret) |[olitomas](https://github.com/olitomas) |[JimmyLv](https://github.com/JimmyLv) |
|
||||
[<img alt="gaejabong" src="https://avatars.githubusercontent.com/u/978944?v=4&s=117" width="117">](https://github.com/gaejabong) |[<img alt="JacobMGEvans" src="https://avatars.githubusercontent.com/u/27247160?v=4&s=117" width="117">](https://github.com/JacobMGEvans) |[<img alt="mazoruss" src="https://avatars.githubusercontent.com/u/17625190?v=4&s=117" width="117">](https://github.com/mazoruss) |[<img alt="GreenJimmy" src="https://avatars.githubusercontent.com/u/39386?v=4&s=117" width="117">](https://github.com/GreenJimmy) |[<img alt="intenzive" src="https://avatars.githubusercontent.com/u/11055931?v=4&s=117" width="117">](https://github.com/intenzive) |[<img alt="NaxYo" src="https://avatars.githubusercontent.com/u/1963876?v=4&s=117" width="117">](https://github.com/NaxYo) |
|
||||
:---: |:---: |:---: |:---: |:---: |:---: |
|
||||
[gaejabong](https://github.com/gaejabong) |[JacobMGEvans](https://github.com/JacobMGEvans) |[mazoruss](https://github.com/mazoruss) |[GreenJimmy](https://github.com/GreenJimmy) |[intenzive](https://github.com/intenzive) |[NaxYo](https://github.com/NaxYo) |
|
||||
|
||||
<!--/contributors-->
|
||||
|
||||
|
|
@ -468,3 +468,4 @@ We use Browserstack for manual testing <a href="https://www.browserstack.com" ta
|
|||
## License
|
||||
|
||||
[The MIT License](LICENSE).
|
||||
E).
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ module.exports = (api) => {
|
|||
}],
|
||||
],
|
||||
plugins: [
|
||||
['@babel/plugin-transform-react-jsx', { pragma: 'h' }],
|
||||
['@babel/plugin-transform-react-jsx', { pragma: 'h', pragmaFrag: 'Fragment' }],
|
||||
process.env.NODE_ENV !== 'dev' && 'babel-plugin-inline-package-json',
|
||||
].filter(Boolean),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,9 @@ function buildBundle (srcFile, bundleFile, { minify = true, standalone = '', plu
|
|||
outfile: bundleFile,
|
||||
platform: 'browser',
|
||||
minify,
|
||||
keepNames: true,
|
||||
keepNames: target !== 'es5',
|
||||
plugins,
|
||||
tsconfigRaw: '{}',
|
||||
target,
|
||||
format,
|
||||
}).then(() => {
|
||||
|
|
|
|||
|
|
@ -114,7 +114,14 @@ async function buildLib () {
|
|||
},
|
||||
}]
|
||||
const isTSX = file.endsWith('.tsx')
|
||||
if (isTSX || file.endsWith('.ts')) { plugins.push(['@babel/plugin-transform-typescript', { disallowAmbiguousJSXLike: true, isTSX, jsxPragma: 'h' }]) }
|
||||
if (isTSX || file.endsWith('.ts')) {
|
||||
plugins.push(['@babel/plugin-transform-typescript', {
|
||||
disallowAmbiguousJSXLike: true,
|
||||
isTSX,
|
||||
jsxPragma: 'h',
|
||||
jsxPragmaFrag: 'Fragment',
|
||||
}])
|
||||
}
|
||||
|
||||
const { code, map } = await babel.transformFileAsync(file, {
|
||||
sourceMaps: true,
|
||||
|
|
|
|||
|
|
@ -176,22 +176,22 @@ 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('.uppy-StatusBar-actionBtn--upload').click()
|
||||
|
||||
cy.window().then(({ uppy }) => {
|
||||
cy.get('@file-input').selectFile(
|
||||
[
|
||||
'cypress/fixtures/images/cat.jpg',
|
||||
'cypress/fixtures/images/traffic.jpg',
|
||||
],
|
||||
{ force: true },
|
||||
)
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore fix me
|
||||
expect(
|
||||
Object.values(uppy.getPlugin('Transloadit').activeAssemblies).length,
|
||||
).to.equal(0)
|
||||
|
||||
cy.get('.uppy-StatusBar-actionBtn--upload').click()
|
||||
|
||||
const { files } = uppy.getState()
|
||||
// eslint-disable-next-line
|
||||
// @ts-ignore fix me
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"noEmit": true,
|
||||
"target": "es2020",
|
||||
"lib": ["es2020", "dom"],
|
||||
"types": ["cypress"]
|
||||
"types": ["cypress"],
|
||||
},
|
||||
"include": ["cypress/**/*.ts"]
|
||||
"include": ["cypress/**/*.ts"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -717,8 +717,8 @@
|
|||
<!-- Terminal -->
|
||||
<div class="terminal" [ngSwitch]="selection.value">
|
||||
<pre *ngSwitchDefault>ng generate component xyz</pre>
|
||||
<pre *ngSwitchCase="'material'">ng add @angular/material</pre>
|
||||
<pre *ngSwitchCase="'pwa'">ng add @angular/pwa</pre>
|
||||
<pre *ngSwitchCase="'material'">ng add @angular/material</pre>
|
||||
<pre *ngSwitchCase="'pwa'">ng add @angular/pwa</pre>
|
||||
<pre *ngSwitchCase="'dependency'">ng add _____</pre>
|
||||
<pre *ngSwitchCase="'test'">ng test</pre>
|
||||
<pre *ngSwitchCase="'build'">ng build</pre>
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@
|
|||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"useDefineForClassFields": false,
|
||||
"lib": ["ES2022", "dom"]
|
||||
"lib": ["ES2022", "dom"],
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
"strictTemplates": true,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<title>Uppy</title>
|
||||
<link
|
||||
href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
|
||||
href="https://releases.transloadit.com/uppy/v3.23.0/uppy.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
DragDrop,
|
||||
ProgressBar,
|
||||
AwsS3,
|
||||
} from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
|
||||
} from 'https://releases.transloadit.com/uppy/v3.23.0/uppy.min.mjs'
|
||||
|
||||
// Function for displaying uploaded files
|
||||
const onUploadSuccess = (elForUploadedFiles) => (file, response) => {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<title>Uppy – AWS upload example</title>
|
||||
<link
|
||||
href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
|
||||
href="https://releases.transloadit.com/uppy/v3.23.0/uppy.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
Uppy,
|
||||
Dashboard,
|
||||
AwsS3,
|
||||
} from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
|
||||
} from 'https://releases.transloadit.com/uppy/v3.23.0/uppy.min.mjs'
|
||||
/**
|
||||
* This generator transforms a deep object into URL-encodable pairs
|
||||
* to work with `URLSearchParams` on the client and `body-parser` on the server.
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link
|
||||
href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
|
||||
href="https://releases.transloadit.com/uppy/v3.23.0/uppy.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
Dashboard,
|
||||
Webcam,
|
||||
Tus,
|
||||
} from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
|
||||
} from 'https://releases.transloadit.com/uppy/v3.23.0/uppy.min.mjs'
|
||||
|
||||
const uppy = new Uppy({ debug: true, autoProceed: false })
|
||||
.use(Dashboard, { trigger: '#uppyModalOpener' })
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
"compilerOptions": {},
|
||||
"esModuleInterop": true,
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"]
|
||||
"exclude": ["node_modules/*", "__sapper__/*", "public/*"],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link
|
||||
href="https://releases.transloadit.com/uppy/v3.21.0/uppy.min.css"
|
||||
href="https://releases.transloadit.com/uppy/v3.23.0/uppy.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
Instagram,
|
||||
GoogleDrive,
|
||||
Tus,
|
||||
} from 'https://releases.transloadit.com/uppy/v3.21.0/uppy.min.mjs'
|
||||
} from 'https://releases.transloadit.com/uppy/v3.23.0/uppy.min.mjs'
|
||||
|
||||
const uppy = new Uppy({ debug: true, autoProceed: false })
|
||||
.use(Dashboard, { trigger: '#uppyModalOpener' })
|
||||
|
|
|
|||
15
package.json
15
package.json
|
|
@ -27,7 +27,7 @@
|
|||
"node": "^16.15.0 || >=18.0.0",
|
||||
"yarn": "3.6.1"
|
||||
},
|
||||
"packageManager": "yarn@3.6.1+sha224.679d48a4db29f6beed7fe901a71e56b5e0619cdd615e140d9f33ce92",
|
||||
"packageManager": "yarn@3.8.0+sha256.a1a53a88823c9f5bcd36e465791b7e490837ebf94dc6ef96282ab558a00c0811",
|
||||
"workspaces": [
|
||||
"examples/*",
|
||||
"packages/@uppy/*",
|
||||
|
|
@ -63,7 +63,7 @@
|
|||
"concat-stream": "^2.0.0",
|
||||
"cssnano": "^5.0.6",
|
||||
"dotenv": "^16.0.0",
|
||||
"esbuild": "^0.17.1",
|
||||
"esbuild": "^0.20.1",
|
||||
"esbuild-plugin-babel": "^0.2.3",
|
||||
"eslint": "^8.0.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
|
|
@ -134,9 +134,9 @@
|
|||
"lint:css:fix": "stylelint ./packages/**/*.scss --fix",
|
||||
"lint": "eslint . --cache",
|
||||
"format:show-diff": "git diff --quiet || (echo 'Unable to show a diff because there are unstaged changes'; false) && (prettier . -w --loglevel silent && git --no-pager diff; git restore .)",
|
||||
"format:check": "prettier -c .",
|
||||
"format:check": "prettier -c .",
|
||||
"format:check-diff": "yarn format:check || (yarn format:show-diff && false)",
|
||||
"format": "prettier -w .",
|
||||
"format": "prettier -w .",
|
||||
"release": "PACKAGES=$(yarn workspaces list --json) yarn workspace @uppy-dev/release interactive",
|
||||
"size": "echo 'JS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.js | gzip | wc -c && echo 'CSS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.css | gzip | wc -c",
|
||||
"e2e": "yarn build:clean && yarn build && yarn e2e:skip-build",
|
||||
|
|
@ -162,6 +162,9 @@
|
|||
"watch:js": "npm-run-all --parallel watch:js:bundle watch:js:lib",
|
||||
"watch": "npm-run-all --parallel watch:css watch:js"
|
||||
},
|
||||
"alias": {
|
||||
"preact/jsx-dev-runtime": "preact/jsx-runtime"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/eslint@^7.2.13": "^8.2.0",
|
||||
"@types/react": "^17",
|
||||
|
|
@ -172,6 +175,8 @@
|
|||
"preact": "patch:preact@npm:10.10.0#.yarn/patches/preact-npm-10.10.0-dd04de05e8.patch",
|
||||
"start-server-and-test": "patch:start-server-and-test@npm:1.14.0#.yarn/patches/start-server-and-test-npm-1.14.0-841aa34fdf.patch",
|
||||
"stylelint-config-rational-order": "patch:stylelint-config-rational-order@npm%3A0.1.2#./.yarn/patches/stylelint-config-rational-order-npm-0.1.2-d8336e84ed.patch",
|
||||
"uuid@^8.3.2": "patch:uuid@npm:8.3.2#.yarn/patches/uuid-npm-8.3.2-eca0baba53.patch"
|
||||
"uuid@^8.3.2": "patch:uuid@npm:8.3.2#.yarn/patches/uuid-npm-8.3.2-eca0baba53.patch",
|
||||
"tus-js-client": "patch:tus-js-client@npm%3A3.1.3#./.yarn/patches/tus-js-client-npm-3.1.3-dc57874d23.patch",
|
||||
"resize-observer-polyfill": "patch:resize-observer-polyfill@npm%3A1.5.1#./.yarn/patches/resize-observer-polyfill-npm-1.5.1-603120e8a0.patch"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,5 +33,8 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@uppy/angular": ["dist/uppy/angular"]
|
||||
"@uppy/angular": ["dist/uppy/angular"],
|
||||
},
|
||||
"baseUrl": "./",
|
||||
"outDir": "./dist/out-tsc",
|
||||
|
|
@ -22,12 +22,12 @@
|
|||
"target": "ES2022",
|
||||
"module": "ES2022",
|
||||
"useDefineForClassFields": false,
|
||||
"lib": ["ES2022", "dom"]
|
||||
"lib": ["ES2022", "dom"],
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
"strictTemplates": true,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
1
packages/@uppy/audio/.npmignore
Normal file
1
packages/@uppy/audio/.npmignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
tsconfig.*
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
# @uppy/audio
|
||||
|
||||
## 1.1.5
|
||||
|
||||
Released: 2024-02-19
|
||||
Included in: Uppy v3.22.0
|
||||
|
||||
- @uppy/audio: fix `audiooptions` (antoine du hamel / #4884)
|
||||
- @uppy/audio: refactor to typescript (antoine du hamel / #4860)
|
||||
|
||||
## 1.0.4
|
||||
|
||||
Released: 2023-02-13
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@uppy/audio",
|
||||
"description": "Uppy plugin that records audio using the device’s microphone.",
|
||||
"version": "1.1.4",
|
||||
"version": "1.1.7",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"style": "dist/style.min.css",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import { h } from 'preact'
|
||||
|
||||
import { UIPlugin, type UIPluginOptions } from '@uppy/core'
|
||||
import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
|
||||
import type { Uppy, MinimalRequiredUppyFile } from '@uppy/core/lib/Uppy.ts'
|
||||
import type {
|
||||
Body,
|
||||
Meta,
|
||||
MinimalRequiredUppyFile,
|
||||
} from '@uppy/utils/lib/UppyFile'
|
||||
import type { Uppy } from '@uppy/core/lib/Uppy.ts'
|
||||
|
||||
import getFileTypeExtension from '@uppy/utils/lib/getFileTypeExtension'
|
||||
import supportsMediaRecorder from './supportsMediaRecorder.ts'
|
||||
|
|
@ -14,7 +18,6 @@ import locale from './locale.ts'
|
|||
import packageJson from '../package.json'
|
||||
|
||||
interface AudioOptions extends UIPluginOptions {
|
||||
target?: HTMLElement | string
|
||||
showAudioSourceDropdown?: boolean
|
||||
}
|
||||
interface AudioState {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export default ({
|
|||
<select
|
||||
className="uppy-u-reset uppy-Audio-audioSource-select"
|
||||
onChange={(event) => {
|
||||
onChangeSource(event.target.value)
|
||||
onChangeSource((event.target as HTMLSelectElement).value)
|
||||
}}
|
||||
>
|
||||
{audioSources.map((audioSource) => (
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ export default (props: PermissionsScreenProps): JSX.Element => {
|
|||
{hasAudio ? i18n('allowAudioAccessTitle') : i18n('noAudioTitle')}
|
||||
</h1>
|
||||
<p>
|
||||
{hasAudio
|
||||
? i18n('allowAudioAccessDescription')
|
||||
: i18n('noAudioDescription')}
|
||||
{hasAudio ?
|
||||
i18n('allowAudioAccessDescription')
|
||||
: i18n('noAudioDescription')}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -91,11 +91,9 @@ export default function RecordingScreen(
|
|||
return (
|
||||
<div className="uppy-Audio-container">
|
||||
<div className="uppy-Audio-audioContainer">
|
||||
{hasRecordedAudio ? (
|
||||
{hasRecordedAudio ?
|
||||
<audio className="uppy-Audio-player" controls src={recordedAudio} />
|
||||
) : (
|
||||
<canvas ref={canvasEl} className="uppy-Audio-canvas" />
|
||||
)}
|
||||
: <canvas ref={canvasEl} className="uppy-Audio-canvas" />}
|
||||
</div>
|
||||
<div className="uppy-Audio-footer">
|
||||
<div className="uppy-Audio-audioSourceContainer">
|
||||
|
|
|
|||
|
|
@ -75,9 +75,8 @@ export default class AudioOscilloscope {
|
|||
this.canvasContext.strokeStyle =
|
||||
result(canvasContextOptions.strokeStyle) || 'rgb(0, 0, 0)'
|
||||
this.canvasContext.lineWidth = result(canvasContextOptions.lineWidth) || 1
|
||||
this.onDrawFrame = isFunction(options.onDrawFrame)
|
||||
? options.onDrawFrame
|
||||
: () => {} // eslint-disable-line @typescript-eslint/no-empty-function
|
||||
this.onDrawFrame =
|
||||
isFunction(options.onDrawFrame) ? options.onDrawFrame : () => {}
|
||||
}
|
||||
|
||||
addSource(streamSource: MediaStreamAudioSourceNode): void {
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@
|
|||
"paths": {
|
||||
"@uppy/utils/lib/*": ["../utils/src/*"],
|
||||
"@uppy/core": ["../core/src/index.js"],
|
||||
"@uppy/core/lib/*": ["../core/src/*"]
|
||||
}
|
||||
"@uppy/core/lib/*": ["../core/src/*"],
|
||||
},
|
||||
},
|
||||
"include": ["./package.json", "./src/**/*.*"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../utils/tsconfig.build.json"
|
||||
"path": "../utils/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../core/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
"path": "../core/tsconfig.build.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@uppy/aws-s3-multipart",
|
||||
"description": "Upload to Amazon S3 with Uppy and S3's Multipart upload strategy",
|
||||
"version": "3.10.0",
|
||||
"version": "3.10.2",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ async function hash (key, data) {
|
|||
return subtle.sign(algorithm, await generateHmacKey(key), ec.encode(data))
|
||||
}
|
||||
|
||||
function percentEncode(c) {
|
||||
return `%${c.charCodeAt(0).toString(16).toUpperCase()}`
|
||||
}
|
||||
|
||||
/**
|
||||
* @see https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
|
||||
* @param {Record<string,string>} param0
|
||||
|
|
@ -90,7 +94,13 @@ export default async function createSignedURL ({
|
|||
}) {
|
||||
const Service = 's3'
|
||||
const host = `${bucketName}.${Service}.${Region}.amazonaws.com`
|
||||
const CanonicalUri = `/${encodeURI(Key)}`
|
||||
/**
|
||||
* List of char out of `encodeURI()` is taken from ECMAScript spec.
|
||||
* Note that the `/` character is purposefully not included in list below.
|
||||
*
|
||||
* @see https://tc39.es/ecma262/#sec-encodeuri-uri
|
||||
*/
|
||||
const CanonicalUri = `/${encodeURI(Key).replace(/[;?:@&=+$,#!'()*]/g, percentEncode)}`
|
||||
const payload = 'UNSIGNED-PAYLOAD'
|
||||
|
||||
const requestDateTime = new Date().toISOString().replace(/[-:]|\.\d+/g, '') // YYYYMMDDTHHMMSSZ
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ describe('createSignedURL', () => {
|
|||
Bucket: bucketName,
|
||||
Fields: {},
|
||||
Key: 'some/key',
|
||||
}, { expiresIn: 900 }))).searchParams.get('X-Amz-Signature'),
|
||||
}), { expiresIn: 900 })).searchParams.get('X-Amz-Signature'),
|
||||
)
|
||||
})
|
||||
it('should be able to sign multipart upload', async () => {
|
||||
|
|
@ -71,7 +71,43 @@ describe('createSignedURL', () => {
|
|||
UploadId: uploadId,
|
||||
PartNumber: partNumber,
|
||||
Key: 'some/key',
|
||||
}, { expiresIn: 900 }))).searchParams.get('X-Amz-Signature'),
|
||||
}), { expiresIn: 900 })).searchParams.get('X-Amz-Signature'),
|
||||
)
|
||||
})
|
||||
it('should escape path and query as restricted to RFC 3986', async () => {
|
||||
const client = new S3Client(s3ClientOptions)
|
||||
const partNumber = 99
|
||||
const specialChars = ';?:@&=+$,#!\'()'
|
||||
const uploadId = `Upload${specialChars}Id`
|
||||
// '.*' chars of path should be encoded
|
||||
const Key = `${specialChars}.*/${specialChars}.*.ext`
|
||||
const implResult =
|
||||
await createSignedURL({
|
||||
accountKey: s3ClientOptions.credentials.accessKeyId,
|
||||
accountSecret: s3ClientOptions.credentials.secretAccessKey,
|
||||
sessionToken: s3ClientOptions.credentials.sessionToken,
|
||||
uploadId,
|
||||
partNumber,
|
||||
bucketName,
|
||||
Key,
|
||||
Region: s3ClientOptions.region,
|
||||
expires: 900,
|
||||
})
|
||||
const sdkResult =
|
||||
new URL(
|
||||
await getSignedUrl(client, new UploadPartCommand({
|
||||
Bucket: bucketName,
|
||||
UploadId: uploadId,
|
||||
PartNumber: partNumber,
|
||||
Key,
|
||||
}), { expiresIn: 900 }
|
||||
)
|
||||
)
|
||||
assert.strictEqual(implResult.pathname, sdkResult.pathname)
|
||||
|
||||
const extractUploadId = /([?&])uploadId=([^&]+?)(&|$)/
|
||||
const extractSignature = /([?&])X-Amz-Signature=([^&]+?)(&|$)/
|
||||
assert.strictEqual(implResult.search.match(extractUploadId)[2], sdkResult.search.match(extractUploadId)[2])
|
||||
assert.strictEqual(implResult.search.match(extractSignature)[2], sdkResult.search.match(extractSignature)[2])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -754,7 +754,7 @@ export default class AwsS3Multipart extends BasePlugin {
|
|||
#uploadLocalFile (file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onProgress = (bytesUploaded, bytesTotal) => {
|
||||
this.uppy.emit('upload-progress', file, {
|
||||
this.uppy.emit('upload-progress', this.uppy.getFile(file.id), {
|
||||
uploader: this,
|
||||
bytesUploaded,
|
||||
bytesTotal,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
# @uppy/aws-s3
|
||||
|
||||
## 3.6.1
|
||||
|
||||
Released: 2024-02-19
|
||||
Included in: Uppy v3.22.0
|
||||
|
||||
- @uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/companion-client,@uppy/tus,@uppy/xhr-upload: update `uppyfile` objects before emitting events (antoine du hamel / #4928)
|
||||
|
||||
## 3.6.0
|
||||
|
||||
Released: 2023-12-12
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@uppy/aws-s3",
|
||||
"description": "Upload to Amazon S3 with Uppy",
|
||||
"version": "3.6.0",
|
||||
"version": "3.6.2",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ export default class MiniXHRUpload {
|
|||
timer.progress()
|
||||
|
||||
if (ev.lengthComputable) {
|
||||
this.uppy.emit('upload-progress', file, {
|
||||
this.uppy.emit('upload-progress', this.uppy.getFile(file.id), {
|
||||
uploader: this,
|
||||
bytesUploaded: ev.loaded,
|
||||
bytesTotal: ev.total,
|
||||
|
|
@ -162,7 +162,7 @@ export default class MiniXHRUpload {
|
|||
uploadURL,
|
||||
}
|
||||
|
||||
this.uppy.emit('upload-success', file, uploadResp)
|
||||
this.uppy.emit('upload-success', this.uppy.getFile(file.id), uploadResp)
|
||||
|
||||
if (uploadURL) {
|
||||
this.uppy.log(`Download ${file.name} from ${uploadURL}`)
|
||||
|
|
|
|||
1
packages/@uppy/box/.npmignore
Normal file
1
packages/@uppy/box/.npmignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
tsconfig.*
|
||||
|
|
@ -1,5 +1,12 @@
|
|||
# @uppy/box
|
||||
|
||||
## 2.2.1
|
||||
|
||||
Released: 2024-02-28
|
||||
Included in: Uppy v3.23.0
|
||||
|
||||
- @uppy/box: fetchPreAuthToken in box too (Mikael Finstad / #4969)
|
||||
|
||||
## 2.1.2
|
||||
|
||||
Released: 2023-07-13
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@uppy/box",
|
||||
"description": "Import files from Box, into Uppy.",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.1",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"type": "module",
|
||||
|
|
|
|||
|
|
@ -1,21 +1,55 @@
|
|||
import { UIPlugin } from '@uppy/core'
|
||||
import { Provider } from '@uppy/companion-client'
|
||||
import {
|
||||
Provider,
|
||||
getAllowedHosts,
|
||||
tokenStorage,
|
||||
type CompanionPluginOptions,
|
||||
} from '@uppy/companion-client'
|
||||
import { UIPlugin, Uppy } from '@uppy/core'
|
||||
import { ProviderViews } from '@uppy/provider-views'
|
||||
import { h } from 'preact'
|
||||
import { h, type ComponentChild } from 'preact'
|
||||
|
||||
import locale from './locale.js'
|
||||
import type { UppyFile, Body, Meta } from '@uppy/utils/lib/UppyFile'
|
||||
import type { UnknownProviderPluginState } from '@uppy/core/lib/Uppy.ts'
|
||||
import locale from './locale.ts'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore We don't want TS to generate types for the package.json
|
||||
import packageJson from '../package.json'
|
||||
|
||||
export default class Box extends UIPlugin {
|
||||
export type BoxOptions = CompanionPluginOptions
|
||||
|
||||
export default class Box<M extends Meta, B extends Body> extends UIPlugin<
|
||||
BoxOptions,
|
||||
M,
|
||||
B,
|
||||
UnknownProviderPluginState
|
||||
> {
|
||||
static VERSION = packageJson.version
|
||||
|
||||
constructor (uppy, opts) {
|
||||
icon: () => JSX.Element
|
||||
|
||||
provider: Provider<M, B>
|
||||
|
||||
view: ProviderViews<M, B>
|
||||
|
||||
storage: typeof tokenStorage
|
||||
|
||||
files: UppyFile<M, B>[]
|
||||
|
||||
constructor(uppy: Uppy<M, B>, opts: BoxOptions) {
|
||||
super(uppy, opts)
|
||||
this.id = this.opts.id || 'Box'
|
||||
Provider.initPlugin(this, opts)
|
||||
this.title = this.opts.title || 'Box'
|
||||
this.type = 'acquirer'
|
||||
this.storage = this.opts.storage || tokenStorage
|
||||
this.files = []
|
||||
this.icon = () => (
|
||||
<svg className="uppy-DashboardTab-iconBox" aria-hidden="true" focusable="false" width="32" height="32" viewBox="0 0 32 32">
|
||||
<svg
|
||||
className="uppy-DashboardTab-iconBox"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 32 32"
|
||||
>
|
||||
<g fill="currentcolor" fillRule="nonzero">
|
||||
<path d="m16.4 13.5c-1.6 0-3 0.9-3.7 2.2-0.7-1.3-2.1-2.2-3.7-2.2-1 0-1.8 0.3-2.5 0.8v-3.6c-0.1-0.3-0.5-0.7-1-0.7s-0.8 0.4-0.8 0.8v7c0 2.3 1.9 4.2 4.2 4.2 1.6 0 3-0.9 3.7-2.2 0.7 1.3 2.1 2.2 3.7 2.2 2.3 0 4.2-1.9 4.2-4.2 0.1-2.4-1.8-4.3-4.1-4.3m-7.5 6.8c-1.4 0-2.5-1.1-2.5-2.5s1.1-2.5 2.5-2.5 2.5 1.1 2.5 2.5-1.1 2.5-2.5 2.5m7.5 0c-1.4 0-2.5-1.1-2.5-2.5s1.1-2.5 2.5-2.5 2.5 1.1 2.5 2.5-1.1 2.5-2.5 2.5" />
|
||||
<path d="m27.2 20.6l-2.3-2.8 2.3-2.8c0.3-0.4 0.2-0.9-0.2-1.2s-1-0.2-1.3 0.2l-2 2.4-2-2.4c-0.3-0.4-0.9-0.4-1.3-0.2-0.4 0.3-0.5 0.8-0.2 1.2l2.3 2.8-2.3 2.8c-0.3 0.4-0.2 0.9 0.2 1.2s1 0.2 1.3-0.2l2-2.4 2 2.4c0.3 0.4 0.9 0.4 1.3 0.2 0.4-0.3 0.4-0.8 0.2-1.2" />
|
||||
|
|
@ -23,6 +57,10 @@ export default class Box extends UIPlugin {
|
|||
</svg>
|
||||
)
|
||||
|
||||
this.opts.companionAllowedHosts = getAllowedHosts(
|
||||
this.opts.companionAllowedHosts,
|
||||
this.opts.companionUrl,
|
||||
)
|
||||
this.provider = new Provider(uppy, {
|
||||
companionUrl: this.opts.companionUrl,
|
||||
companionHeaders: this.opts.companionHeaders,
|
||||
|
|
@ -42,7 +80,7 @@ export default class Box extends UIPlugin {
|
|||
this.render = this.render.bind(this)
|
||||
}
|
||||
|
||||
install () {
|
||||
install(): void {
|
||||
this.view = new ProviderViews(this, {
|
||||
provider: this.provider,
|
||||
loadAllFiles: true,
|
||||
|
|
@ -54,16 +92,19 @@ export default class Box extends UIPlugin {
|
|||
}
|
||||
}
|
||||
|
||||
uninstall () {
|
||||
uninstall(): void {
|
||||
this.view.tearDown()
|
||||
this.unmount()
|
||||
}
|
||||
|
||||
onFirstRender () {
|
||||
return this.view.getFolder()
|
||||
async onFirstRender(): Promise<void> {
|
||||
await Promise.all([
|
||||
this.provider.fetchPreAuthToken(),
|
||||
this.view.getFolder(),
|
||||
])
|
||||
}
|
||||
|
||||
render (state) {
|
||||
render(state: unknown): ComponentChild {
|
||||
return this.view.render(state)
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { default } from './Box.jsx'
|
||||
1
packages/@uppy/box/src/index.ts
Normal file
1
packages/@uppy/box/src/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default } from './Box.tsx'
|
||||
35
packages/@uppy/box/tsconfig.build.json
Normal file
35
packages/@uppy/box/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"outDir": "./lib",
|
||||
"paths": {
|
||||
"@uppy/companion-client": ["../companion-client/src/index.js"],
|
||||
"@uppy/companion-client/lib/*": ["../companion-client/src/*"],
|
||||
"@uppy/provider-views": ["../provider-views/src/index.js"],
|
||||
"@uppy/provider-views/lib/*": ["../provider-views/src/*"],
|
||||
"@uppy/utils/lib/*": ["../utils/src/*"],
|
||||
"@uppy/core": ["../core/src/index.js"],
|
||||
"@uppy/core/lib/*": ["../core/src/*"]
|
||||
},
|
||||
"resolveJsonModule": false,
|
||||
"rootDir": "./src",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./src/**/*.*"],
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../companion-client/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../provider-views/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../utils/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../core/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
31
packages/@uppy/box/tsconfig.json
Normal file
31
packages/@uppy/box/tsconfig.json
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@uppy/companion-client": ["../companion-client/src/index.js"],
|
||||
"@uppy/companion-client/lib/*": ["../companion-client/src/*"],
|
||||
"@uppy/provider-views": ["../provider-views/src/index.js"],
|
||||
"@uppy/provider-views/lib/*": ["../provider-views/src/*"],
|
||||
"@uppy/utils/lib/*": ["../utils/src/*"],
|
||||
"@uppy/core": ["../core/src/index.js"],
|
||||
"@uppy/core/lib/*": ["../core/src/*"],
|
||||
},
|
||||
},
|
||||
"include": ["./package.json", "./src/**/*.*"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../companion-client/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../provider-views/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../utils/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../core/tsconfig.build.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
1
packages/@uppy/companion-client/.npmignore
Normal file
1
packages/@uppy/companion-client/.npmignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
tsconfig.*
|
||||
|
|
@ -1,5 +1,32 @@
|
|||
# @uppy/companion-client
|
||||
|
||||
## 3.7.4
|
||||
|
||||
Released: 2024-02-28
|
||||
Included in: Uppy v3.23.0
|
||||
|
||||
- @uppy/companion-client,@uppy/utils,@uppy/xhr-upload: improvements for #4922 (Mikael Finstad / #4960)
|
||||
|
||||
## 3.7.3
|
||||
|
||||
Released: 2024-02-22
|
||||
Included in: Uppy v3.22.2
|
||||
|
||||
- @uppy/companion-client: fix body/url on upload-success (Merlijn Vos / #4922)
|
||||
- @uppy/companion-client: remove unnecessary `'use strict'` directives (Antoine du Hamel / #4943)
|
||||
- @uppy/companion-client: type changes for provider-views (Antoine du Hamel / #4938)
|
||||
- @uppy/companion-client: update types (Antoine du Hamel / #4927)
|
||||
|
||||
## 3.7.1
|
||||
|
||||
Released: 2024-02-19
|
||||
Included in: Uppy v3.22.0
|
||||
|
||||
- @uppy/aws-s3-multipart,@uppy/aws-s3,@uppy/companion-client,@uppy/tus,@uppy/xhr-upload: update `uppyfile` objects before emitting events (antoine du hamel / #4928)
|
||||
- @uppy/companion-client: fix tests and linter (antoine du hamel / #4890)
|
||||
- @uppy/companion-client: migrate to ts (merlijn vos / #4864)
|
||||
- @uppy/companion-client: fix `typeerror` (antoine du hamel)
|
||||
|
||||
## 3.7.0
|
||||
|
||||
Released: 2023-12-12
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@uppy/companion-client",
|
||||
"description": "Client library for communication with Companion. Intended for use in Uppy plugins.",
|
||||
"version": "3.7.0",
|
||||
"version": "3.7.4",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"type": "module",
|
||||
|
|
@ -27,5 +27,8 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^1.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@uppy/core": "workspace:^"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
'use strict'
|
||||
|
||||
class AuthError extends Error {
|
||||
isAuthError: boolean
|
||||
|
||||
constructor() {
|
||||
super('Authorization required')
|
||||
this.name = 'AuthError'
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import type { UIPluginOptions } from '@uppy/core'
|
||||
import type { tokenStorage } from '.'
|
||||
|
||||
export interface CompanionPluginOptions extends UIPluginOptions {
|
||||
title?: string
|
||||
storage?: typeof tokenStorage
|
||||
companionUrl: string
|
||||
companionHeaders?: Record<string, string>
|
||||
companionKeysParams?: Record<string, string>
|
||||
companionCookiesRule?: 'same-origin' | 'include'
|
||||
companionAllowedHosts?: string | RegExp | (string | RegExp)[]
|
||||
}
|
||||
|
|
@ -1,11 +1,31 @@
|
|||
'use strict'
|
||||
import type { Uppy } from '@uppy/core'
|
||||
import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
|
||||
import type { PluginOpts } from '@uppy/core/lib/BasePlugin.ts'
|
||||
import type {
|
||||
RequestOptions,
|
||||
CompanionClientProvider,
|
||||
} from '@uppy/utils/lib/CompanionClientProvider'
|
||||
import type { UnknownProviderPlugin } from '@uppy/core/lib/Uppy.ts'
|
||||
import RequestClient, { authErrorStatusCode } from './RequestClient.ts'
|
||||
import type { CompanionPluginOptions } from '.'
|
||||
|
||||
import RequestClient, { authErrorStatusCode } from './RequestClient.js'
|
||||
import * as tokenStorage from './tokenStorage.js'
|
||||
// TODO: remove deprecated options in next major release
|
||||
export interface Opts extends PluginOpts, CompanionPluginOptions {
|
||||
/** @deprecated */
|
||||
serverUrl?: string
|
||||
/** @deprecated */
|
||||
serverPattern?: string
|
||||
pluginId: string
|
||||
name?: string
|
||||
supportsRefreshToken?: boolean
|
||||
provider: string
|
||||
}
|
||||
|
||||
|
||||
const getName = (id) => {
|
||||
return id.split('-').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(' ')
|
||||
const getName = (id: string) => {
|
||||
return id
|
||||
.split('-')
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
function getOrigin() {
|
||||
|
|
@ -13,25 +33,52 @@ function getOrigin() {
|
|||
return location.origin
|
||||
}
|
||||
|
||||
function getRegex(value) {
|
||||
function getRegex(value?: string | RegExp) {
|
||||
if (typeof value === 'string') {
|
||||
return new RegExp(`^${value}$`)
|
||||
} if (value instanceof RegExp) {
|
||||
}
|
||||
if (value instanceof RegExp) {
|
||||
return value
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function isOriginAllowed(origin, allowedOrigin) {
|
||||
const patterns = Array.isArray(allowedOrigin) ? allowedOrigin.map(getRegex) : [getRegex(allowedOrigin)]
|
||||
return patterns
|
||||
.some((pattern) => pattern?.test(origin) || pattern?.test(`${origin}/`)) // allowing for trailing '/'
|
||||
function isOriginAllowed(
|
||||
origin: string,
|
||||
allowedOrigin: string | RegExp | Array<string | RegExp> | undefined,
|
||||
) {
|
||||
const patterns =
|
||||
Array.isArray(allowedOrigin) ?
|
||||
allowedOrigin.map(getRegex)
|
||||
: [getRegex(allowedOrigin)]
|
||||
return patterns.some(
|
||||
(pattern) => pattern?.test(origin) || pattern?.test(`${origin}/`),
|
||||
) // allowing for trailing '/'
|
||||
}
|
||||
|
||||
export default class Provider extends RequestClient {
|
||||
#refreshingTokenPromise
|
||||
export default class Provider<M extends Meta, B extends Body>
|
||||
extends RequestClient<M, B>
|
||||
implements CompanionClientProvider
|
||||
{
|
||||
#refreshingTokenPromise: Promise<void> | undefined
|
||||
|
||||
constructor(uppy, opts) {
|
||||
provider: string
|
||||
|
||||
id: string
|
||||
|
||||
name: string
|
||||
|
||||
pluginId: string
|
||||
|
||||
tokenKey: string
|
||||
|
||||
companionKeysParams?: Record<string, string>
|
||||
|
||||
preAuthToken: string | null
|
||||
|
||||
supportsRefreshToken: boolean
|
||||
|
||||
constructor(uppy: Uppy<M, B>, opts: Opts) {
|
||||
super(uppy, opts)
|
||||
this.provider = opts.provider
|
||||
this.id = this.provider
|
||||
|
|
@ -43,9 +90,12 @@ export default class Provider extends RequestClient {
|
|||
this.supportsRefreshToken = opts.supportsRefreshToken ?? true // todo false in next major
|
||||
}
|
||||
|
||||
async headers() {
|
||||
const [headers, token] = await Promise.all([super.headers(), this.#getAuthToken()])
|
||||
const authHeaders = {}
|
||||
async headers(): Promise<Record<string, string>> {
|
||||
const [headers, token] = await Promise.all([
|
||||
super.headers(),
|
||||
this.#getAuthToken(),
|
||||
])
|
||||
const authHeaders: Record<string, string> = {}
|
||||
if (token) {
|
||||
authHeaders['uppy-auth-token'] = token
|
||||
}
|
||||
|
|
@ -58,48 +108,67 @@ export default class Provider extends RequestClient {
|
|||
return { ...headers, ...authHeaders }
|
||||
}
|
||||
|
||||
onReceiveResponse(response) {
|
||||
onReceiveResponse(response: Response): Response {
|
||||
super.onReceiveResponse(response)
|
||||
const plugin = this.uppy.getPlugin(this.pluginId)
|
||||
const plugin = this.#getPlugin()
|
||||
const oldAuthenticated = plugin.getPluginState().authenticated
|
||||
const authenticated = oldAuthenticated ? response.status !== authErrorStatusCode : response.status < 400
|
||||
const authenticated =
|
||||
oldAuthenticated ?
|
||||
response.status !== authErrorStatusCode
|
||||
: response.status < 400
|
||||
plugin.setPluginState({ authenticated })
|
||||
return response
|
||||
}
|
||||
|
||||
async setAuthToken(token) {
|
||||
return this.uppy.getPlugin(this.pluginId).storage.setItem(this.tokenKey, token)
|
||||
async setAuthToken(token: string): Promise<void> {
|
||||
return this.#getPlugin().storage.setItem(this.tokenKey, token)
|
||||
}
|
||||
|
||||
async #getAuthToken() {
|
||||
return this.uppy.getPlugin(this.pluginId).storage.getItem(this.tokenKey)
|
||||
async #getAuthToken(): Promise<string | null> {
|
||||
return this.#getPlugin().storage.getItem(this.tokenKey)
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
async removeAuthToken() {
|
||||
return this.uppy.getPlugin(this.pluginId).storage.removeItem(this.tokenKey)
|
||||
protected async removeAuthToken(): Promise<void> {
|
||||
return this.#getPlugin().storage.removeItem(this.tokenKey)
|
||||
}
|
||||
|
||||
#getPlugin() {
|
||||
const plugin = this.uppy.getPlugin(this.pluginId) as UnknownProviderPlugin<
|
||||
M,
|
||||
B
|
||||
>
|
||||
if (plugin == null) throw new Error('Plugin was nullish')
|
||||
return plugin
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure we have a preauth token if necessary. Attempts to fetch one if we don't,
|
||||
* or rejects if loading one fails.
|
||||
*/
|
||||
async ensurePreAuth() {
|
||||
async ensurePreAuth(): Promise<void> {
|
||||
if (this.companionKeysParams && !this.preAuthToken) {
|
||||
await this.fetchPreAuthToken()
|
||||
|
||||
if (!this.preAuthToken) {
|
||||
throw new Error('Could not load authentication data required for third-party login. Please try again later.')
|
||||
throw new Error(
|
||||
'Could not load authentication data required for third-party login. Please try again later.',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
authQuery() {
|
||||
// eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-unused-vars
|
||||
authQuery(data: unknown): Record<string, string> {
|
||||
return {}
|
||||
}
|
||||
|
||||
authUrl({ authFormData, query } = {}) {
|
||||
authUrl({
|
||||
authFormData,
|
||||
query,
|
||||
}: {
|
||||
authFormData: unknown
|
||||
query: Record<string, string>
|
||||
}): string {
|
||||
const params = new URLSearchParams({
|
||||
...query,
|
||||
state: btoa(JSON.stringify({ origin: getOrigin() })),
|
||||
|
|
@ -113,14 +182,33 @@ export default class Provider extends RequestClient {
|
|||
return `${this.hostname}/${this.id}/connect?${params}`
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
async loginSimpleAuth({ uppyVersions, authFormData, signal }) {
|
||||
const response = await this.post(`${this.id}/simple-auth`, { form: authFormData }, { qs: { uppyVersions }, signal })
|
||||
protected async loginSimpleAuth({
|
||||
uppyVersions,
|
||||
authFormData,
|
||||
signal,
|
||||
}: {
|
||||
uppyVersions: string
|
||||
authFormData: unknown
|
||||
signal: AbortSignal
|
||||
}): Promise<void> {
|
||||
type Res = { uppyAuthToken: string }
|
||||
const response = await this.post<Res>(
|
||||
`${this.id}/simple-auth`,
|
||||
{ form: authFormData },
|
||||
{ qs: { uppyVersions }, signal },
|
||||
)
|
||||
this.setAuthToken(response.uppyAuthToken)
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
async loginOAuth({ uppyVersions, authFormData, signal }) {
|
||||
protected async loginOAuth({
|
||||
uppyVersions,
|
||||
authFormData,
|
||||
signal,
|
||||
}: {
|
||||
uppyVersions: string
|
||||
authFormData: unknown
|
||||
signal: AbortSignal
|
||||
}): Promise<void> {
|
||||
await this.ensurePreAuth()
|
||||
|
||||
signal.throwIfAborted()
|
||||
|
|
@ -129,9 +217,9 @@ export default class Provider extends RequestClient {
|
|||
const link = this.authUrl({ query: { uppyVersions }, authFormData })
|
||||
const authWindow = window.open(link, '_blank')
|
||||
|
||||
let cleanup
|
||||
let cleanup: () => void
|
||||
|
||||
const handleToken = (e) => {
|
||||
const handleToken = (e: MessageEvent<any>) => {
|
||||
if (e.source !== authWindow) {
|
||||
let jsonData = ''
|
||||
try {
|
||||
|
|
@ -143,13 +231,20 @@ export default class Provider extends RequestClient {
|
|||
} catch (err) {
|
||||
// in case JSON.stringify fails (ignored)
|
||||
}
|
||||
this.uppy.log(`ignoring event from unknown source ${jsonData}`, 'warning')
|
||||
this.uppy.log(
|
||||
`ignoring event from unknown source ${jsonData}`,
|
||||
'warning',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const { companionAllowedHosts } = this.uppy.getPlugin(this.pluginId).opts
|
||||
const { companionAllowedHosts } = this.#getPlugin().opts
|
||||
if (!isOriginAllowed(e.origin, companionAllowedHosts)) {
|
||||
reject(new Error(`rejecting event from ${e.origin} vs allowed pattern ${companionAllowedHosts}`))
|
||||
reject(
|
||||
new Error(
|
||||
`rejecting event from ${e.origin} vs allowed pattern ${companionAllowedHosts}`,
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -175,7 +270,7 @@ export default class Provider extends RequestClient {
|
|||
}
|
||||
|
||||
cleanup = () => {
|
||||
authWindow.close()
|
||||
authWindow?.close()
|
||||
window.removeEventListener('message', handleToken)
|
||||
signal.removeEventListener('abort', cleanup)
|
||||
}
|
||||
|
|
@ -185,20 +280,29 @@ export default class Provider extends RequestClient {
|
|||
})
|
||||
}
|
||||
|
||||
async login({ uppyVersions, authFormData, signal }) {
|
||||
async login({
|
||||
uppyVersions,
|
||||
authFormData,
|
||||
signal,
|
||||
}: {
|
||||
uppyVersions: string
|
||||
authFormData: unknown
|
||||
signal: AbortSignal
|
||||
}): Promise<void> {
|
||||
return this.loginOAuth({ uppyVersions, authFormData, signal })
|
||||
}
|
||||
|
||||
refreshTokenUrl() {
|
||||
refreshTokenUrl(): string {
|
||||
return `${this.hostname}/${this.id}/refresh-token`
|
||||
}
|
||||
|
||||
fileUrl(id) {
|
||||
fileUrl(id: string): string {
|
||||
return `${this.hostname}/${this.id}/get/${id}`
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
async request(...args) {
|
||||
protected async request<ResBody>(
|
||||
...args: Parameters<RequestClient<M, B>['request']>
|
||||
): Promise<ResBody> {
|
||||
await this.#refreshingTokenPromise
|
||||
|
||||
try {
|
||||
|
|
@ -208,7 +312,7 @@ export default class Provider extends RequestClient {
|
|||
// While uploading, go to your google account settings,
|
||||
// "Third-party apps & services", then click "Companion" and "Remove access".
|
||||
|
||||
return await super.request(...args)
|
||||
return await super.request<ResBody>(...args)
|
||||
} catch (err) {
|
||||
if (!this.supportsRefreshToken) throw err
|
||||
// only handle auth errors (401 from provider), and only handle them if we have a (refresh) token
|
||||
|
|
@ -220,8 +324,14 @@ export default class Provider extends RequestClient {
|
|||
// Once a refresh token operation has started, we need all other request to wait for this operation (atomically)
|
||||
this.#refreshingTokenPromise = (async () => {
|
||||
try {
|
||||
this.uppy.log(`[CompanionClient] Refreshing expired auth token`, 'info')
|
||||
const response = await super.request({ path: this.refreshTokenUrl(), method: 'POST' })
|
||||
this.uppy.log(
|
||||
`[CompanionClient] Refreshing expired auth token`,
|
||||
'info',
|
||||
)
|
||||
const response = await super.request<{ uppyAuthToken: string }>({
|
||||
path: this.refreshTokenUrl(),
|
||||
method: 'POST',
|
||||
})
|
||||
await this.setAuthToken(response.uppyAuthToken)
|
||||
} catch (refreshTokenErr) {
|
||||
if (refreshTokenErr.isAuthError) {
|
||||
|
|
@ -242,56 +352,34 @@ export default class Provider extends RequestClient {
|
|||
}
|
||||
}
|
||||
|
||||
async fetchPreAuthToken() {
|
||||
async fetchPreAuthToken(): Promise<void> {
|
||||
if (!this.companionKeysParams) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await this.post(`${this.id}/preauth/`, { params: this.companionKeysParams })
|
||||
const res = await this.post<{ token: string }>(`${this.id}/preauth/`, {
|
||||
params: this.companionKeysParams,
|
||||
})
|
||||
this.preAuthToken = res.token
|
||||
} catch (err) {
|
||||
this.uppy.log(`[CompanionClient] unable to fetch preAuthToken ${err}`, 'warning')
|
||||
this.uppy.log(
|
||||
`[CompanionClient] unable to fetch preAuthToken ${err}`,
|
||||
'warning',
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
list(directory, options) {
|
||||
return this.get(`${this.id}/list/${directory || ''}`, options)
|
||||
list<ResBody>(
|
||||
directory: string | undefined,
|
||||
options: RequestOptions,
|
||||
): Promise<ResBody> {
|
||||
return this.get<ResBody>(`${this.id}/list/${directory || ''}`, options)
|
||||
}
|
||||
|
||||
async logout(options) {
|
||||
const response = await this.get(`${this.id}/logout`, options)
|
||||
async logout<ResBody>(options?: RequestOptions): Promise<ResBody> {
|
||||
const response = await this.get<ResBody>(`${this.id}/logout`, options)
|
||||
await this.removeAuthToken()
|
||||
return response
|
||||
}
|
||||
|
||||
static initPlugin(plugin, opts, defaultOpts) {
|
||||
/* eslint-disable no-param-reassign */
|
||||
plugin.type = 'acquirer'
|
||||
plugin.files = []
|
||||
if (defaultOpts) {
|
||||
plugin.opts = { ...defaultOpts, ...opts }
|
||||
}
|
||||
|
||||
if (opts.serverUrl || opts.serverPattern) {
|
||||
throw new Error('`serverUrl` and `serverPattern` have been renamed to `companionUrl` and `companionAllowedHosts` respectively in the 0.30.5 release. Please consult the docs (for example, https://uppy.io/docs/instagram/ for the Instagram plugin) and use the updated options.`')
|
||||
}
|
||||
|
||||
if (opts.companionAllowedHosts) {
|
||||
const pattern = opts.companionAllowedHosts
|
||||
// validate companionAllowedHosts param
|
||||
if (typeof pattern !== 'string' && !Array.isArray(pattern) && !(pattern instanceof RegExp)) {
|
||||
throw new TypeError(`${plugin.id}: the option "companionAllowedHosts" must be one of string, Array, RegExp`)
|
||||
}
|
||||
plugin.opts.companionAllowedHosts = pattern
|
||||
} else if (/^(?!https?:\/\/).*$/i.test(opts.companionUrl)) {
|
||||
// does not start with https://
|
||||
plugin.opts.companionAllowedHosts = `https://${opts.companionUrl?.replace(/^\/\//, '')}`
|
||||
} else {
|
||||
plugin.opts.companionAllowedHosts = new URL(opts.companionUrl).origin
|
||||
}
|
||||
|
||||
plugin.storage = plugin.opts.storage || tokenStorage
|
||||
/* eslint-enable no-param-reassign */
|
||||
}
|
||||
}
|
||||
|
|
@ -1,461 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
import UserFacingApiError from '@uppy/utils/lib/UserFacingApiError'
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import pRetry, { AbortError } from 'p-retry'
|
||||
|
||||
import fetchWithNetworkError from '@uppy/utils/lib/fetchWithNetworkError'
|
||||
import ErrorWithCause from '@uppy/utils/lib/ErrorWithCause'
|
||||
import emitSocketProgress from '@uppy/utils/lib/emitSocketProgress'
|
||||
import getSocketHost from '@uppy/utils/lib/getSocketHost'
|
||||
|
||||
import AuthError from './AuthError.js'
|
||||
|
||||
import packageJson from '../package.json'
|
||||
|
||||
// Remove the trailing slash so we can always safely append /xyz.
|
||||
function stripSlash(url) {
|
||||
return url.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
const retryCount = 10 // set to a low number, like 2 to test manual user retries
|
||||
const socketActivityTimeoutMs = 5 * 60 * 1000 // set to a low number like 10000 to test this
|
||||
|
||||
export const authErrorStatusCode = 401
|
||||
|
||||
class HttpError extends Error {
|
||||
statusCode
|
||||
|
||||
constructor({ statusCode, message }) {
|
||||
super(message)
|
||||
this.name = 'HttpError'
|
||||
this.statusCode = statusCode
|
||||
}
|
||||
}
|
||||
|
||||
async function handleJSONResponse(res) {
|
||||
if (res.status === authErrorStatusCode) {
|
||||
throw new AuthError()
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
}
|
||||
|
||||
let errMsg = `Failed request with status: ${res.status}. ${res.statusText}`
|
||||
let errData
|
||||
try {
|
||||
errData = await res.json()
|
||||
|
||||
if (errData.message) errMsg = `${errMsg} message: ${errData.message}`
|
||||
if (errData.requestId) errMsg = `${errMsg} request-Id: ${errData.requestId}`
|
||||
} catch (cause) {
|
||||
// if the response contains invalid JSON, let's ignore the error data
|
||||
throw new Error(errMsg, { cause })
|
||||
}
|
||||
|
||||
if (res.status >= 400 && res.status <= 499 && errData.message) {
|
||||
throw new UserFacingApiError(errData.message)
|
||||
}
|
||||
|
||||
throw new HttpError({ statusCode: res.status, message: errMsg })
|
||||
}
|
||||
|
||||
export default class RequestClient {
|
||||
static VERSION = packageJson.version
|
||||
|
||||
#companionHeaders
|
||||
|
||||
constructor(uppy, opts) {
|
||||
this.uppy = uppy
|
||||
this.opts = opts
|
||||
this.onReceiveResponse = this.onReceiveResponse.bind(this)
|
||||
this.#companionHeaders = opts?.companionHeaders
|
||||
}
|
||||
|
||||
setCompanionHeaders(headers) {
|
||||
this.#companionHeaders = headers
|
||||
}
|
||||
|
||||
[Symbol.for('uppy test: getCompanionHeaders')]() {
|
||||
return this.#companionHeaders
|
||||
}
|
||||
|
||||
get hostname() {
|
||||
const { companion } = this.uppy.getState()
|
||||
const host = this.opts.companionUrl
|
||||
return stripSlash(companion && companion[host] ? companion[host] : host)
|
||||
}
|
||||
|
||||
async headers (emptyBody = false) {
|
||||
const defaultHeaders = {
|
||||
Accept: 'application/json',
|
||||
...(emptyBody ? undefined : {
|
||||
// Passing those headers on requests with no data forces browsers to first make a preflight request.
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultHeaders,
|
||||
...this.#companionHeaders,
|
||||
}
|
||||
}
|
||||
|
||||
onReceiveResponse({ headers }) {
|
||||
const state = this.uppy.getState()
|
||||
const companion = state.companion || {}
|
||||
const host = this.opts.companionUrl
|
||||
|
||||
// Store the self-identified domain name for the Companion instance we just hit.
|
||||
if (headers.has('i-am') && headers.get('i-am') !== companion[host]) {
|
||||
this.uppy.setState({
|
||||
companion: { ...companion, [host]: headers.get('i-am') },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#getUrl(url) {
|
||||
if (/^(https?:|)\/\//.test(url)) {
|
||||
return url
|
||||
}
|
||||
return `${this.hostname}/${url}`
|
||||
}
|
||||
|
||||
/** @protected */
|
||||
async request({ path, method = 'GET', data, skipPostResponse, signal }) {
|
||||
try {
|
||||
const headers = await this.headers(!data)
|
||||
const response = await fetchWithNetworkError(this.#getUrl(path), {
|
||||
method,
|
||||
signal,
|
||||
headers,
|
||||
credentials: this.opts.companionCookiesRule || 'same-origin',
|
||||
body: data ? JSON.stringify(data) : null,
|
||||
})
|
||||
if (!skipPostResponse) this.onReceiveResponse(response)
|
||||
|
||||
return await handleJSONResponse(response)
|
||||
} catch (err) {
|
||||
// pass these through
|
||||
if (err.isAuthError || err.name === 'UserFacingApiError' || err.name === 'AbortError') throw err
|
||||
|
||||
throw new ErrorWithCause(`Could not ${method} ${this.#getUrl(path)}`, {
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async get(path, options = undefined) {
|
||||
// TODO: remove boolean support for options that was added for backward compatibility.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (typeof options === 'boolean') options = { skipPostResponse: options }
|
||||
return this.request({ ...options, path })
|
||||
}
|
||||
|
||||
async post(path, data, options = undefined) {
|
||||
// TODO: remove boolean support for options that was added for backward compatibility.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (typeof options === 'boolean') options = { skipPostResponse: options }
|
||||
return this.request({ ...options, path, method: 'POST', data })
|
||||
}
|
||||
|
||||
async delete(path, data = undefined, options) {
|
||||
// TODO: remove boolean support for options that was added for backward compatibility.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (typeof options === 'boolean') options = { skipPostResponse: options }
|
||||
return this.request({ ...options, path, method: 'DELETE', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote uploading consists of two steps:
|
||||
* 1. #requestSocketToken which starts the download/upload in companion and returns a unique token for the upload.
|
||||
* Then companion will halt the upload until:
|
||||
* 2. #awaitRemoteFileUpload is called, which will open/ensure a websocket connection towards companion, with the
|
||||
* previously generated token provided. It returns a promise that will resolve/reject once the file has finished
|
||||
* uploading or is otherwise done (failed, canceled)
|
||||
*
|
||||
* @param {*} file
|
||||
* @param {*} reqBody
|
||||
* @param {*} options
|
||||
* @returns
|
||||
*/
|
||||
async uploadRemoteFile(file, reqBody, options = {}) {
|
||||
try {
|
||||
const { signal, getQueue } = options
|
||||
|
||||
return await pRetry(async () => {
|
||||
// if we already have a serverToken, assume that we are resuming the existing server upload id
|
||||
const existingServerToken = this.uppy.getFile(file.id)?.serverToken;
|
||||
if (existingServerToken != null) {
|
||||
this.uppy.log(`Connecting to exiting websocket ${existingServerToken}`)
|
||||
return this.#awaitRemoteFileUpload({ file, queue: getQueue(), signal })
|
||||
}
|
||||
|
||||
const queueRequestSocketToken = getQueue().wrapPromiseFunction(async (...args) => {
|
||||
try {
|
||||
return await this.#requestSocketToken(...args)
|
||||
} catch (outerErr) {
|
||||
// throwing AbortError will cause p-retry to stop retrying
|
||||
if (outerErr.isAuthError) throw new AbortError(outerErr)
|
||||
|
||||
if (outerErr.cause == null) throw outerErr
|
||||
const err = outerErr.cause
|
||||
|
||||
const isRetryableHttpError = () => (
|
||||
[408, 409, 429, 418, 423].includes(err.statusCode)
|
||||
|| (err.statusCode >= 500 && err.statusCode <= 599 && ![501, 505].includes(err.statusCode))
|
||||
)
|
||||
if (err.name === 'HttpError' && !isRetryableHttpError()) throw new AbortError(err);
|
||||
|
||||
// p-retry will retry most other errors,
|
||||
// but it will not retry TypeError (except network error TypeErrors)
|
||||
throw err
|
||||
}
|
||||
}, { priority: -1 })
|
||||
|
||||
const serverToken = await queueRequestSocketToken({ file, postBody: reqBody, signal }).abortOn(signal)
|
||||
|
||||
if (!this.uppy.getFile(file.id)) return undefined // has file since been removed?
|
||||
|
||||
this.uppy.setFileState(file.id, { serverToken })
|
||||
|
||||
return this.#awaitRemoteFileUpload({
|
||||
file: this.uppy.getFile(file.id), // re-fetching file because it might have changed in the meantime
|
||||
queue: getQueue(),
|
||||
signal
|
||||
})
|
||||
}, { retries: retryCount, signal, onFailedAttempt: (err) => this.uppy.log(`Retrying upload due to: ${err.message}`, 'warning') });
|
||||
} catch (err) {
|
||||
// this is a bit confusing, but note that an error with the `name` prop set to 'AbortError' (from AbortController)
|
||||
// is not the same as `p-retry` `AbortError`
|
||||
if (err.name === 'AbortError') {
|
||||
// The file upload was aborted, it’s not an error
|
||||
return undefined
|
||||
}
|
||||
|
||||
this.uppy.emit('upload-error', file, err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
#requestSocketToken = async ({ file, postBody, signal }) => {
|
||||
if (file.remote.url == null) {
|
||||
throw new Error('Cannot connect to an undefined URL')
|
||||
}
|
||||
|
||||
const res = await this.post(file.remote.url, {
|
||||
...file.remote.body,
|
||||
...postBody,
|
||||
}, signal)
|
||||
|
||||
return res.token
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will ensure a websocket for the specified file and returns a promise that resolves
|
||||
* when the file has finished downloading, or rejects if it fails.
|
||||
* It will retry if the websocket gets disconnected
|
||||
*
|
||||
* @param {{ file: UppyFile, queue: RateLimitedQueue, signal: AbortSignal }} file
|
||||
*/
|
||||
async #awaitRemoteFileUpload({ file, queue, signal }) {
|
||||
let removeEventHandlers
|
||||
|
||||
const { capabilities } = this.uppy.getState()
|
||||
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const token = file.serverToken
|
||||
const host = getSocketHost(file.remote.companionUrl)
|
||||
|
||||
/** @type {WebSocket} */
|
||||
let socket
|
||||
/** @type {AbortController?} */
|
||||
let socketAbortController
|
||||
let activityTimeout
|
||||
|
||||
let { isPaused } = file
|
||||
|
||||
const socketSend = (action, payload) => {
|
||||
if (socket == null || socket.readyState !== socket.OPEN) {
|
||||
this.uppy.log(`Cannot send "${action}" to socket ${file.id} because the socket state was ${String(socket?.readyState)}`, 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
socket.send(JSON.stringify({
|
||||
action,
|
||||
payload: payload ?? {},
|
||||
}))
|
||||
};
|
||||
|
||||
function sendState() {
|
||||
if (!capabilities.resumableUploads) return;
|
||||
|
||||
if (isPaused) socketSend('pause')
|
||||
else socketSend('resume')
|
||||
}
|
||||
|
||||
const createWebsocket = async () => {
|
||||
if (socketAbortController) socketAbortController.abort()
|
||||
socketAbortController = new AbortController()
|
||||
|
||||
const onFatalError = (err) => {
|
||||
// Remove the serverToken so that a new one will be created for the retry.
|
||||
this.uppy.setFileState(file.id, { serverToken: null })
|
||||
socketAbortController?.abort?.()
|
||||
reject(err)
|
||||
}
|
||||
|
||||
// todo instead implement the ability for users to cancel / retry *currently uploading files* in the UI
|
||||
function resetActivityTimeout() {
|
||||
clearTimeout(activityTimeout)
|
||||
if (isPaused) return
|
||||
activityTimeout = setTimeout(() => onFatalError(new Error('Timeout waiting for message from Companion socket')), socketActivityTimeoutMs)
|
||||
}
|
||||
|
||||
try {
|
||||
await queue.wrapPromiseFunction(async () => {
|
||||
// eslint-disable-next-line promise/param-names
|
||||
const reconnectWebsocket = async () => new Promise((resolveSocket, rejectSocket) => {
|
||||
socket = new WebSocket(`${host}/api/${token}`)
|
||||
|
||||
resetActivityTimeout()
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
socket = undefined
|
||||
rejectSocket(new Error('Socket closed unexpectedly'))
|
||||
})
|
||||
|
||||
socket.addEventListener('error', (error) => {
|
||||
this.uppy.log(`Companion socket error ${JSON.stringify(error)}, closing socket`, 'warning')
|
||||
socket.close() // will 'close' event to be emitted
|
||||
})
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
sendState()
|
||||
})
|
||||
|
||||
socket.addEventListener('message', (e) => {
|
||||
resetActivityTimeout()
|
||||
|
||||
try {
|
||||
const { action, payload } = JSON.parse(e.data)
|
||||
|
||||
switch (action) {
|
||||
case 'progress': {
|
||||
emitSocketProgress(this, payload, file)
|
||||
break;
|
||||
}
|
||||
case 'success': {
|
||||
this.uppy.emit('upload-success', file, { uploadURL: payload.url })
|
||||
socketAbortController?.abort?.()
|
||||
resolve()
|
||||
break;
|
||||
}
|
||||
case 'error': {
|
||||
const { message } = payload.error
|
||||
throw Object.assign(new Error(message), { cause: payload.error })
|
||||
}
|
||||
default:
|
||||
this.uppy.log(`Companion socket unknown action ${action}`, 'warning')
|
||||
}
|
||||
} catch (err) {
|
||||
onFatalError(err)
|
||||
}
|
||||
})
|
||||
|
||||
const closeSocket = () => {
|
||||
this.uppy.log(`Closing socket ${file.id}`, 'info')
|
||||
clearTimeout(activityTimeout)
|
||||
if (socket) socket.close()
|
||||
socket = undefined
|
||||
}
|
||||
|
||||
socketAbortController.signal.addEventListener('abort', () => {
|
||||
closeSocket()
|
||||
})
|
||||
})
|
||||
|
||||
await pRetry(reconnectWebsocket, {
|
||||
retries: retryCount,
|
||||
signal: socketAbortController.signal,
|
||||
onFailedAttempt: () => {
|
||||
if (socketAbortController.signal.aborted) return // don't log in this case
|
||||
this.uppy.log(`Retrying websocket ${file.id}`, 'info')
|
||||
},
|
||||
});
|
||||
})().abortOn(socketAbortController.signal);
|
||||
} catch (err) {
|
||||
if (socketAbortController.signal.aborted) return
|
||||
onFatalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
const pause = (newPausedState) => {
|
||||
if (!capabilities.resumableUploads) return;
|
||||
|
||||
isPaused = newPausedState
|
||||
if (socket) sendState()
|
||||
|
||||
if (newPausedState) {
|
||||
// Remove this file from the queue so another file can start in its place.
|
||||
socketAbortController?.abort?.() // close socket to free up the request for other uploads
|
||||
} else {
|
||||
// Resuming an upload should be queued, else you could pause and then
|
||||
// resume a queued upload to make it skip the queue.
|
||||
createWebsocket()
|
||||
}
|
||||
}
|
||||
|
||||
const onFileRemove = (targetFile) => {
|
||||
if (!capabilities.individualCancellation) return
|
||||
if (targetFile.id !== file.id) return
|
||||
socketSend('cancel')
|
||||
socketAbortController?.abort?.()
|
||||
this.uppy.log(`upload ${file.id} was removed`, 'info')
|
||||
resolve()
|
||||
}
|
||||
|
||||
const onCancelAll = ({ reason }) => {
|
||||
if (reason === 'user') {
|
||||
socketSend('cancel')
|
||||
}
|
||||
socketAbortController?.abort?.()
|
||||
this.uppy.log(`upload ${file.id} was canceled`, 'info')
|
||||
resolve()
|
||||
};
|
||||
|
||||
const onFilePausedChange = (targetFileId, newPausedState) => {
|
||||
if (targetFileId !== file.id) return
|
||||
pause(newPausedState)
|
||||
}
|
||||
|
||||
const onPauseAll = () => pause(true)
|
||||
const onResumeAll = () => pause(false)
|
||||
|
||||
this.uppy.on('file-removed', onFileRemove)
|
||||
this.uppy.on('cancel-all', onCancelAll)
|
||||
this.uppy.on('upload-pause', onFilePausedChange)
|
||||
this.uppy.on('pause-all', onPauseAll)
|
||||
this.uppy.on('resume-all', onResumeAll)
|
||||
|
||||
removeEventHandlers = () => {
|
||||
this.uppy.off('file-removed', onFileRemove)
|
||||
this.uppy.off('cancel-all', onCancelAll)
|
||||
this.uppy.off('upload-pause', onFilePausedChange)
|
||||
this.uppy.off('pause-all', onPauseAll)
|
||||
this.uppy.off('resume-all', onResumeAll)
|
||||
}
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
socketAbortController?.abort();
|
||||
})
|
||||
|
||||
createWebsocket()
|
||||
})
|
||||
} finally {
|
||||
removeEventHandlers?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import RequestClient from './RequestClient.js'
|
||||
|
||||
describe('RequestClient', () => {
|
||||
it('has a hostname without trailing slash', () => {
|
||||
const mockCore = { getState: () => ({}) }
|
||||
const a = new RequestClient(mockCore, { companionUrl: 'http://companion.uppy.io' })
|
||||
const b = new RequestClient(mockCore, { companionUrl: 'http://companion.uppy.io/' })
|
||||
|
||||
expect(a.hostname).toBe('http://companion.uppy.io')
|
||||
expect(b.hostname).toBe('http://companion.uppy.io')
|
||||
})
|
||||
})
|
||||
21
packages/@uppy/companion-client/src/RequestClient.test.ts
Normal file
21
packages/@uppy/companion-client/src/RequestClient.test.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import RequestClient from './RequestClient.ts'
|
||||
|
||||
describe('RequestClient', () => {
|
||||
it('has a hostname without trailing slash', () => {
|
||||
const mockCore = { getState: () => ({}) } as any
|
||||
const a = new RequestClient(mockCore, {
|
||||
pluginId: 'test',
|
||||
provider: 'test',
|
||||
companionUrl: 'http://companion.uppy.io',
|
||||
})
|
||||
const b = new RequestClient(mockCore, {
|
||||
pluginId: 'test2',
|
||||
provider: 'test2',
|
||||
companionUrl: 'http://companion.uppy.io/',
|
||||
})
|
||||
|
||||
expect(a.hostname).toBe('http://companion.uppy.io')
|
||||
expect(b.hostname).toBe('http://companion.uppy.io')
|
||||
})
|
||||
})
|
||||
622
packages/@uppy/companion-client/src/RequestClient.ts
Normal file
622
packages/@uppy/companion-client/src/RequestClient.ts
Normal file
|
|
@ -0,0 +1,622 @@
|
|||
import UserFacingApiError from '@uppy/utils/lib/UserFacingApiError'
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import pRetry, { AbortError } from 'p-retry'
|
||||
|
||||
import fetchWithNetworkError from '@uppy/utils/lib/fetchWithNetworkError'
|
||||
import ErrorWithCause from '@uppy/utils/lib/ErrorWithCause'
|
||||
import emitSocketProgress from '@uppy/utils/lib/emitSocketProgress'
|
||||
import getSocketHost from '@uppy/utils/lib/getSocketHost'
|
||||
|
||||
import type Uppy from '@uppy/core'
|
||||
import type { UppyFile, Meta, Body } from '@uppy/utils/lib/UppyFile'
|
||||
import type { RequestOptions } from '@uppy/utils/lib/CompanionClientProvider.ts'
|
||||
import AuthError from './AuthError.ts'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore We don't want TS to generate types for the package.json
|
||||
import packageJson from '../package.json'
|
||||
|
||||
type CompanionHeaders = Record<string, string> | undefined
|
||||
|
||||
export type Opts = {
|
||||
name?: string
|
||||
provider: string
|
||||
pluginId: string
|
||||
companionUrl: string
|
||||
companionCookiesRule?: 'same-origin' | 'include' | 'omit'
|
||||
companionHeaders?: CompanionHeaders
|
||||
companionKeysParams?: Record<string, string>
|
||||
}
|
||||
|
||||
type _RequestOptions =
|
||||
| boolean // TODO: remove this on the next major
|
||||
| RequestOptions
|
||||
|
||||
// Remove the trailing slash so we can always safely append /xyz.
|
||||
function stripSlash(url: string) {
|
||||
return url.replace(/\/$/, '')
|
||||
}
|
||||
|
||||
const retryCount = 10 // set to a low number, like 2 to test manual user retries
|
||||
const socketActivityTimeoutMs = 5 * 60 * 1000 // set to a low number like 10000 to test this
|
||||
|
||||
export const authErrorStatusCode = 401
|
||||
|
||||
class HttpError extends Error {
|
||||
statusCode: number
|
||||
|
||||
constructor({
|
||||
statusCode,
|
||||
message,
|
||||
}: {
|
||||
statusCode: number
|
||||
message: string
|
||||
}) {
|
||||
super(message)
|
||||
this.name = 'HttpError'
|
||||
this.statusCode = statusCode
|
||||
}
|
||||
}
|
||||
|
||||
async function handleJSONResponse<ResJson>(res: Response): Promise<ResJson> {
|
||||
if (res.status === authErrorStatusCode) {
|
||||
throw new AuthError()
|
||||
}
|
||||
|
||||
if (res.ok) {
|
||||
return res.json()
|
||||
}
|
||||
|
||||
let errMsg = `Failed request with status: ${res.status}. ${res.statusText}`
|
||||
let errData
|
||||
try {
|
||||
errData = await res.json()
|
||||
|
||||
if (errData.message) errMsg = `${errMsg} message: ${errData.message}`
|
||||
if (errData.requestId) errMsg = `${errMsg} request-Id: ${errData.requestId}`
|
||||
} catch (cause) {
|
||||
// if the response contains invalid JSON, let's ignore the error data
|
||||
throw new Error(errMsg, { cause })
|
||||
}
|
||||
|
||||
if (res.status >= 400 && res.status <= 499 && errData.message) {
|
||||
throw new UserFacingApiError(errData.message)
|
||||
}
|
||||
|
||||
throw new HttpError({ statusCode: res.status, message: errMsg })
|
||||
}
|
||||
|
||||
export default class RequestClient<M extends Meta, B extends Body> {
|
||||
static VERSION = packageJson.version
|
||||
|
||||
#companionHeaders: CompanionHeaders
|
||||
|
||||
uppy: Uppy<M, B>
|
||||
|
||||
opts: Opts
|
||||
|
||||
constructor(uppy: Uppy<M, B>, opts: Opts) {
|
||||
this.uppy = uppy
|
||||
this.opts = opts
|
||||
this.onReceiveResponse = this.onReceiveResponse.bind(this)
|
||||
// TODO: Remove optional chaining
|
||||
this.#companionHeaders = opts?.companionHeaders
|
||||
}
|
||||
|
||||
setCompanionHeaders(headers: Record<string, string>): void {
|
||||
this.#companionHeaders = headers
|
||||
}
|
||||
|
||||
private [Symbol.for('uppy test: getCompanionHeaders')](): CompanionHeaders {
|
||||
return this.#companionHeaders
|
||||
}
|
||||
|
||||
get hostname(): string {
|
||||
const { companion } = this.uppy.getState()
|
||||
const host = this.opts.companionUrl
|
||||
return stripSlash(companion && companion[host] ? companion[host] : host)
|
||||
}
|
||||
|
||||
async headers(emptyBody = false): Promise<Record<string, string>> {
|
||||
const defaultHeaders = {
|
||||
Accept: 'application/json',
|
||||
...(emptyBody ? undefined : (
|
||||
{
|
||||
// Passing those headers on requests with no data forces browsers to first make a preflight request.
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
)),
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultHeaders,
|
||||
...this.#companionHeaders,
|
||||
}
|
||||
}
|
||||
|
||||
onReceiveResponse(res: Response): void {
|
||||
const { headers } = res
|
||||
const state = this.uppy.getState()
|
||||
const companion = state.companion || {}
|
||||
const host = this.opts.companionUrl
|
||||
|
||||
// Store the self-identified domain name for the Companion instance we just hit.
|
||||
if (headers.has('i-am') && headers.get('i-am') !== companion[host]) {
|
||||
this.uppy.setState({
|
||||
companion: { ...companion, [host]: headers.get('i-am') as string },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#getUrl(url: string) {
|
||||
if (/^(https?:|)\/\//.test(url)) {
|
||||
return url
|
||||
}
|
||||
return `${this.hostname}/${url}`
|
||||
}
|
||||
|
||||
protected async request<ResBody>({
|
||||
path,
|
||||
method = 'GET',
|
||||
data,
|
||||
skipPostResponse,
|
||||
signal,
|
||||
}: {
|
||||
path: string
|
||||
method?: string
|
||||
data?: Record<string, unknown>
|
||||
skipPostResponse?: boolean
|
||||
signal?: AbortSignal
|
||||
}): Promise<ResBody> {
|
||||
try {
|
||||
const headers = await this.headers(!data)
|
||||
const response = await fetchWithNetworkError(this.#getUrl(path), {
|
||||
method,
|
||||
signal,
|
||||
headers,
|
||||
credentials: this.opts.companionCookiesRule || 'same-origin',
|
||||
body: data ? JSON.stringify(data) : null,
|
||||
})
|
||||
if (!skipPostResponse) this.onReceiveResponse(response)
|
||||
|
||||
return await handleJSONResponse<ResBody>(response)
|
||||
} catch (err) {
|
||||
// pass these through
|
||||
if (
|
||||
err.isAuthError ||
|
||||
err.name === 'UserFacingApiError' ||
|
||||
err.name === 'AbortError'
|
||||
)
|
||||
throw err
|
||||
|
||||
throw new ErrorWithCause(`Could not ${method} ${this.#getUrl(path)}`, {
|
||||
cause: err,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async get<PostBody>(
|
||||
path: string,
|
||||
options?: _RequestOptions,
|
||||
): Promise<PostBody> {
|
||||
// TODO: remove boolean support for options that was added for backward compatibility.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (typeof options === 'boolean') options = { skipPostResponse: options }
|
||||
return this.request({ ...options, path })
|
||||
}
|
||||
|
||||
async post<PostBody>(
|
||||
path: string,
|
||||
data: Record<string, unknown>,
|
||||
options?: _RequestOptions,
|
||||
): Promise<PostBody> {
|
||||
// TODO: remove boolean support for options that was added for backward compatibility.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (typeof options === 'boolean') options = { skipPostResponse: options }
|
||||
return this.request<PostBody>({ ...options, path, method: 'POST', data })
|
||||
}
|
||||
|
||||
async delete<T>(
|
||||
path: string,
|
||||
data?: Record<string, unknown>,
|
||||
options?: _RequestOptions,
|
||||
): Promise<T> {
|
||||
// TODO: remove boolean support for options that was added for backward compatibility.
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
if (typeof options === 'boolean') options = { skipPostResponse: options }
|
||||
return this.request({ ...options, path, method: 'DELETE', data })
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote uploading consists of two steps:
|
||||
* 1. #requestSocketToken which starts the download/upload in companion and returns a unique token for the upload.
|
||||
* Then companion will halt the upload until:
|
||||
* 2. #awaitRemoteFileUpload is called, which will open/ensure a websocket connection towards companion, with the
|
||||
* previously generated token provided. It returns a promise that will resolve/reject once the file has finished
|
||||
* uploading or is otherwise done (failed, canceled)
|
||||
*/
|
||||
async uploadRemoteFile(
|
||||
file: UppyFile<M, B>,
|
||||
reqBody: Record<string, unknown>,
|
||||
options: { signal: AbortSignal; getQueue: () => any },
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { signal, getQueue } = options || {}
|
||||
|
||||
return await pRetry(
|
||||
async () => {
|
||||
// if we already have a serverToken, assume that we are resuming the existing server upload id
|
||||
const existingServerToken = this.uppy.getFile(file.id)?.serverToken
|
||||
if (existingServerToken != null) {
|
||||
this.uppy.log(
|
||||
`Connecting to exiting websocket ${existingServerToken}`,
|
||||
)
|
||||
return this.#awaitRemoteFileUpload({
|
||||
file,
|
||||
queue: getQueue(),
|
||||
signal,
|
||||
})
|
||||
}
|
||||
|
||||
const queueRequestSocketToken = getQueue().wrapPromiseFunction(
|
||||
async (
|
||||
...args: [
|
||||
{
|
||||
file: UppyFile<M, B>
|
||||
postBody: Record<string, unknown>
|
||||
signal: AbortSignal
|
||||
},
|
||||
]
|
||||
) => {
|
||||
try {
|
||||
return await this.#requestSocketToken(...args)
|
||||
} catch (outerErr) {
|
||||
// throwing AbortError will cause p-retry to stop retrying
|
||||
if (outerErr.isAuthError) throw new AbortError(outerErr)
|
||||
|
||||
if (outerErr.cause == null) throw outerErr
|
||||
const err = outerErr.cause
|
||||
|
||||
const isRetryableHttpError = () =>
|
||||
[408, 409, 429, 418, 423].includes(err.statusCode) ||
|
||||
(err.statusCode >= 500 &&
|
||||
err.statusCode <= 599 &&
|
||||
![501, 505].includes(err.statusCode))
|
||||
if (err.name === 'HttpError' && !isRetryableHttpError())
|
||||
throw new AbortError(err)
|
||||
|
||||
// p-retry will retry most other errors,
|
||||
// but it will not retry TypeError (except network error TypeErrors)
|
||||
throw err
|
||||
}
|
||||
},
|
||||
{ priority: -1 },
|
||||
)
|
||||
|
||||
const serverToken = await queueRequestSocketToken({
|
||||
file,
|
||||
postBody: reqBody,
|
||||
signal,
|
||||
}).abortOn(signal)
|
||||
|
||||
if (!this.uppy.getFile(file.id)) return undefined // has file since been removed?
|
||||
|
||||
this.uppy.setFileState(file.id, { serverToken })
|
||||
|
||||
return this.#awaitRemoteFileUpload({
|
||||
file: this.uppy.getFile(file.id), // re-fetching file because it might have changed in the meantime
|
||||
queue: getQueue(),
|
||||
signal,
|
||||
})
|
||||
},
|
||||
{
|
||||
retries: retryCount,
|
||||
signal,
|
||||
onFailedAttempt: (err) =>
|
||||
this.uppy.log(`Retrying upload due to: ${err.message}`, 'warning'),
|
||||
},
|
||||
)
|
||||
} catch (err) {
|
||||
// this is a bit confusing, but note that an error with the `name` prop set to 'AbortError' (from AbortController)
|
||||
// is not the same as `p-retry` `AbortError`
|
||||
if (err.name === 'AbortError') {
|
||||
// The file upload was aborted, it’s not an error
|
||||
return undefined
|
||||
}
|
||||
|
||||
this.uppy.emit('upload-error', file, err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
#requestSocketToken = async ({
|
||||
file,
|
||||
postBody,
|
||||
signal,
|
||||
}: {
|
||||
file: UppyFile<M, B>
|
||||
postBody: Record<string, unknown>
|
||||
signal: AbortSignal
|
||||
}): Promise<string> => {
|
||||
if (file.remote?.url == null) {
|
||||
throw new Error('Cannot connect to an undefined URL')
|
||||
}
|
||||
|
||||
const res = await this.post<{ token: string }>(
|
||||
file.remote.url,
|
||||
{
|
||||
...file.remote.body,
|
||||
...postBody,
|
||||
},
|
||||
{ signal },
|
||||
)
|
||||
|
||||
return res.token
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will ensure a websocket for the specified file and returns a promise that resolves
|
||||
* when the file has finished downloading, or rejects if it fails.
|
||||
* It will retry if the websocket gets disconnected
|
||||
*/
|
||||
async #awaitRemoteFileUpload({
|
||||
file,
|
||||
queue,
|
||||
signal,
|
||||
}: {
|
||||
file: UppyFile<M, B>
|
||||
queue: any
|
||||
signal: AbortSignal
|
||||
}): Promise<void> {
|
||||
let removeEventHandlers: () => void
|
||||
|
||||
const { capabilities } = this.uppy.getState()
|
||||
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const token = file.serverToken
|
||||
const host = getSocketHost(file.remote!.companionUrl)
|
||||
|
||||
let socket: WebSocket | undefined
|
||||
let socketAbortController: AbortController
|
||||
let activityTimeout: ReturnType<typeof setTimeout>
|
||||
|
||||
let { isPaused } = file
|
||||
|
||||
const socketSend = (action: string, payload?: unknown) => {
|
||||
if (socket == null || socket.readyState !== socket.OPEN) {
|
||||
this.uppy.log(
|
||||
`Cannot send "${action}" to socket ${
|
||||
file.id
|
||||
} because the socket state was ${String(socket?.readyState)}`,
|
||||
'warning',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
socket.send(
|
||||
JSON.stringify({
|
||||
action,
|
||||
payload: payload ?? {},
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
function sendState() {
|
||||
if (!capabilities.resumableUploads) return
|
||||
|
||||
if (isPaused) socketSend('pause')
|
||||
else socketSend('resume')
|
||||
}
|
||||
|
||||
const createWebsocket = async () => {
|
||||
if (socketAbortController) socketAbortController.abort()
|
||||
socketAbortController = new AbortController()
|
||||
|
||||
const onFatalError = (err: Error) => {
|
||||
// Remove the serverToken so that a new one will be created for the retry.
|
||||
this.uppy.setFileState(file.id, { serverToken: null })
|
||||
socketAbortController?.abort?.()
|
||||
reject(err)
|
||||
}
|
||||
|
||||
// todo instead implement the ability for users to cancel / retry *currently uploading files* in the UI
|
||||
function resetActivityTimeout() {
|
||||
clearTimeout(activityTimeout)
|
||||
if (isPaused) return
|
||||
activityTimeout = setTimeout(
|
||||
() =>
|
||||
onFatalError(
|
||||
new Error(
|
||||
'Timeout waiting for message from Companion socket',
|
||||
),
|
||||
),
|
||||
socketActivityTimeoutMs,
|
||||
)
|
||||
}
|
||||
|
||||
try {
|
||||
await queue
|
||||
.wrapPromiseFunction(async () => {
|
||||
const reconnectWebsocket = async () =>
|
||||
// eslint-disable-next-line promise/param-names
|
||||
new Promise((_, rejectSocket) => {
|
||||
socket = new WebSocket(`${host}/api/${token}`)
|
||||
|
||||
resetActivityTimeout()
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
socket = undefined
|
||||
rejectSocket(new Error('Socket closed unexpectedly'))
|
||||
})
|
||||
|
||||
socket.addEventListener('error', (error) => {
|
||||
this.uppy.log(
|
||||
`Companion socket error ${JSON.stringify(
|
||||
error,
|
||||
)}, closing socket`,
|
||||
'warning',
|
||||
)
|
||||
socket?.close() // will 'close' event to be emitted
|
||||
})
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
sendState()
|
||||
})
|
||||
|
||||
socket.addEventListener('message', (e) => {
|
||||
resetActivityTimeout()
|
||||
|
||||
try {
|
||||
const { action, payload } = JSON.parse(e.data)
|
||||
|
||||
switch (action) {
|
||||
case 'progress': {
|
||||
emitSocketProgress(
|
||||
this,
|
||||
payload,
|
||||
this.uppy.getFile(file.id),
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'success': {
|
||||
// payload.response is sent from companion for xhr-upload (aka uploadMultipart in companion) and
|
||||
// s3 multipart (aka uploadS3Multipart)
|
||||
// but not for tus/transloadit (aka uploadTus)
|
||||
// responseText is a string which may or may not be in JSON format
|
||||
// this means that an upload destination of xhr or s3 multipart MUST respond with valid JSON
|
||||
// to companion, or the JSON.parse will crash
|
||||
const text = payload.response?.responseText
|
||||
|
||||
this.uppy.emit(
|
||||
'upload-success',
|
||||
this.uppy.getFile(file.id),
|
||||
{
|
||||
uploadURL: payload.url,
|
||||
status: payload.response?.status ?? 200,
|
||||
body:
|
||||
text ? (JSON.parse(text) as B) : undefined,
|
||||
},
|
||||
)
|
||||
socketAbortController?.abort?.()
|
||||
resolve()
|
||||
break
|
||||
}
|
||||
case 'error': {
|
||||
const { message } = payload.error
|
||||
throw Object.assign(new Error(message), {
|
||||
cause: payload.error,
|
||||
})
|
||||
}
|
||||
default:
|
||||
this.uppy.log(
|
||||
`Companion socket unknown action ${action}`,
|
||||
'warning',
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
onFatalError(err)
|
||||
}
|
||||
})
|
||||
|
||||
const closeSocket = () => {
|
||||
this.uppy.log(`Closing socket ${file.id}`, 'info')
|
||||
clearTimeout(activityTimeout)
|
||||
if (socket) socket.close()
|
||||
socket = undefined
|
||||
}
|
||||
|
||||
socketAbortController.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
closeSocket()
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
await pRetry(reconnectWebsocket, {
|
||||
retries: retryCount,
|
||||
signal: socketAbortController.signal,
|
||||
onFailedAttempt: () => {
|
||||
if (socketAbortController.signal.aborted) return // don't log in this case
|
||||
this.uppy.log(`Retrying websocket ${file.id}`, 'info')
|
||||
},
|
||||
})
|
||||
})()
|
||||
.abortOn(socketAbortController.signal)
|
||||
} catch (err) {
|
||||
if (socketAbortController.signal.aborted) return
|
||||
onFatalError(err)
|
||||
}
|
||||
}
|
||||
|
||||
const pause = (newPausedState: boolean) => {
|
||||
if (!capabilities.resumableUploads) return
|
||||
|
||||
isPaused = newPausedState
|
||||
if (socket) sendState()
|
||||
|
||||
if (newPausedState) {
|
||||
// Remove this file from the queue so another file can start in its place.
|
||||
socketAbortController?.abort?.() // close socket to free up the request for other uploads
|
||||
} else {
|
||||
// Resuming an upload should be queued, else you could pause and then
|
||||
// resume a queued upload to make it skip the queue.
|
||||
createWebsocket()
|
||||
}
|
||||
}
|
||||
|
||||
const onFileRemove = (targetFile: UppyFile<M, B>) => {
|
||||
if (!capabilities.individualCancellation) return
|
||||
if (targetFile.id !== file.id) return
|
||||
socketSend('cancel')
|
||||
socketAbortController?.abort?.()
|
||||
this.uppy.log(`upload ${file.id} was removed`, 'info')
|
||||
resolve()
|
||||
}
|
||||
|
||||
const onCancelAll = ({ reason }: { reason?: string }) => {
|
||||
if (reason === 'user') {
|
||||
socketSend('cancel')
|
||||
}
|
||||
socketAbortController?.abort?.()
|
||||
this.uppy.log(`upload ${file.id} was canceled`, 'info')
|
||||
resolve()
|
||||
}
|
||||
|
||||
const onFilePausedChange = (
|
||||
targetFileId: string | undefined,
|
||||
newPausedState: boolean,
|
||||
) => {
|
||||
if (targetFileId !== file.id) return
|
||||
pause(newPausedState)
|
||||
}
|
||||
|
||||
const onPauseAll = () => pause(true)
|
||||
const onResumeAll = () => pause(false)
|
||||
|
||||
this.uppy.on('file-removed', onFileRemove)
|
||||
this.uppy.on('cancel-all', onCancelAll)
|
||||
this.uppy.on('upload-pause', onFilePausedChange)
|
||||
this.uppy.on('pause-all', onPauseAll)
|
||||
this.uppy.on('resume-all', onResumeAll)
|
||||
|
||||
removeEventHandlers = () => {
|
||||
this.uppy.off('file-removed', onFileRemove)
|
||||
this.uppy.off('cancel-all', onCancelAll)
|
||||
this.uppy.off('upload-pause', onFilePausedChange)
|
||||
this.uppy.off('pause-all', onPauseAll)
|
||||
this.uppy.off('resume-all', onResumeAll)
|
||||
}
|
||||
|
||||
signal.addEventListener('abort', () => {
|
||||
socketAbortController?.abort()
|
||||
})
|
||||
|
||||
createWebsocket()
|
||||
})
|
||||
} finally {
|
||||
// @ts-expect-error used before defined
|
||||
removeEventHandlers?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
import RequestClient from './RequestClient.js'
|
||||
|
||||
const getName = (id) => {
|
||||
return id.split('-').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(' ')
|
||||
}
|
||||
|
||||
export default class SearchProvider extends RequestClient {
|
||||
constructor (uppy, opts) {
|
||||
super(uppy, opts)
|
||||
this.provider = opts.provider
|
||||
this.id = this.provider
|
||||
this.name = this.opts.name || getName(this.id)
|
||||
this.pluginId = this.opts.pluginId
|
||||
}
|
||||
|
||||
fileUrl (id) {
|
||||
return `${this.hostname}/search/${this.id}/get/${id}`
|
||||
}
|
||||
|
||||
search (text, queries) {
|
||||
return this.get(`search/${this.id}/list?q=${encodeURIComponent(text)}${queries ? `&${queries}` : ''}`)
|
||||
}
|
||||
}
|
||||
44
packages/@uppy/companion-client/src/SearchProvider.ts
Normal file
44
packages/@uppy/companion-client/src/SearchProvider.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import type { Body, Meta } from '@uppy/utils/lib/UppyFile.ts'
|
||||
import type { Uppy } from '@uppy/core'
|
||||
import type { CompanionClientSearchProvider } from '@uppy/utils/lib/CompanionClientProvider'
|
||||
import RequestClient, { type Opts } from './RequestClient.ts'
|
||||
|
||||
const getName = (id: string): string => {
|
||||
return id
|
||||
.split('-')
|
||||
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
||||
.join(' ')
|
||||
}
|
||||
|
||||
export default class SearchProvider<M extends Meta, B extends Body>
|
||||
extends RequestClient<M, B>
|
||||
implements CompanionClientSearchProvider
|
||||
{
|
||||
provider: string
|
||||
|
||||
id: string
|
||||
|
||||
name: string
|
||||
|
||||
pluginId: string
|
||||
|
||||
constructor(uppy: Uppy<M, B>, opts: Opts) {
|
||||
super(uppy, opts)
|
||||
this.provider = opts.provider
|
||||
this.id = this.provider
|
||||
this.name = this.opts.name || getName(this.id)
|
||||
this.pluginId = this.opts.pluginId
|
||||
}
|
||||
|
||||
fileUrl(id: string): string {
|
||||
return `${this.hostname}/search/${this.id}/get/${id}`
|
||||
}
|
||||
|
||||
search<ResBody>(text: string, queries?: string): Promise<ResBody> {
|
||||
return this.get<ResBody>(
|
||||
`search/${this.id}/list?q=${encodeURIComponent(text)}${
|
||||
queries ? `&${queries}` : ''
|
||||
}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,53 @@
|
|||
import { afterEach, beforeEach, vi, describe, it, expect } from 'vitest'
|
||||
import UppySocket from './Socket.js'
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
vi,
|
||||
describe,
|
||||
it,
|
||||
expect,
|
||||
type Mock,
|
||||
} from 'vitest'
|
||||
import UppySocket from './Socket.ts'
|
||||
|
||||
describe('Socket', () => {
|
||||
let webSocketConstructorSpy
|
||||
let webSocketCloseSpy
|
||||
let webSocketSendSpy
|
||||
let webSocketConstructorSpy: Mock
|
||||
let webSocketCloseSpy: Mock
|
||||
let webSocketSendSpy: Mock
|
||||
|
||||
beforeEach(() => {
|
||||
webSocketConstructorSpy = vi.fn()
|
||||
webSocketCloseSpy = vi.fn()
|
||||
webSocketSendSpy = vi.fn()
|
||||
|
||||
// @ts-expect-error WebSocket expects a lot more to be present but we don't care for this test
|
||||
globalThis.WebSocket = class WebSocket {
|
||||
constructor (target) {
|
||||
constructor(target: string) {
|
||||
webSocketConstructorSpy(target)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
close (args) {
|
||||
close(args: any) {
|
||||
webSocketCloseSpy(args)
|
||||
}
|
||||
|
||||
// eslint-disable-next-line class-methods-use-this
|
||||
send (json) {
|
||||
send(json: any) {
|
||||
webSocketSendSpy(json)
|
||||
}
|
||||
|
||||
triggerOpen () {
|
||||
triggerOpen() {
|
||||
// @ts-expect-error exist
|
||||
this.onopen()
|
||||
}
|
||||
|
||||
triggerClose () {
|
||||
triggerClose() {
|
||||
// @ts-expect-error exist
|
||||
this.onclose()
|
||||
}
|
||||
}
|
||||
})
|
||||
afterEach(() => {
|
||||
// @ts-expect-error not allowed but needed for test
|
||||
globalThis.WebSocket = undefined
|
||||
})
|
||||
|
||||
|
|
@ -55,6 +67,7 @@ describe('Socket', () => {
|
|||
|
||||
it('should send a message via the websocket if the connection is open', () => {
|
||||
const uppySocket = new UppySocket({ target: 'foo' })
|
||||
// @ts-expect-error not allowed but needed for test
|
||||
const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
|
||||
webSocketInstance.triggerOpen()
|
||||
|
||||
|
|
@ -69,16 +82,21 @@ describe('Socket', () => {
|
|||
const uppySocket = new UppySocket({ target: 'foo' })
|
||||
|
||||
uppySocket.send('bar', 'boo')
|
||||
expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([{ action: 'bar', payload: 'boo' }])
|
||||
// @ts-expect-error not allowed but needed for test
|
||||
expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([
|
||||
{ action: 'bar', payload: 'boo' },
|
||||
])
|
||||
expect(webSocketSendSpy.mock.calls.length).toEqual(0)
|
||||
})
|
||||
|
||||
it('should queue any messages for the websocket if the connection is not open, then send them when the connection is open', () => {
|
||||
const uppySocket = new UppySocket({ target: 'foo' })
|
||||
// @ts-expect-error not allowed but needed for test
|
||||
const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
|
||||
|
||||
uppySocket.send('bar', 'boo')
|
||||
uppySocket.send('moo', 'baa')
|
||||
// @ts-expect-error not allowed but needed for test
|
||||
expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([
|
||||
{ action: 'bar', payload: 'boo' },
|
||||
{ action: 'moo', payload: 'baa' },
|
||||
|
|
@ -87,6 +105,7 @@ describe('Socket', () => {
|
|||
|
||||
webSocketInstance.triggerOpen()
|
||||
|
||||
// @ts-expect-error not allowed but needed for test
|
||||
expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([])
|
||||
expect(webSocketSendSpy.mock.calls.length).toEqual(2)
|
||||
expect(webSocketSendSpy.mock.calls[0]).toEqual([
|
||||
|
|
@ -99,18 +118,24 @@ describe('Socket', () => {
|
|||
|
||||
it('should start queuing any messages when the websocket connection is closed', () => {
|
||||
const uppySocket = new UppySocket({ target: 'foo' })
|
||||
// @ts-expect-error not allowed but needed for test
|
||||
const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
|
||||
webSocketInstance.triggerOpen()
|
||||
uppySocket.send('bar', 'boo')
|
||||
// @ts-expect-error not allowed but needed for test
|
||||
expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([])
|
||||
|
||||
webSocketInstance.triggerClose()
|
||||
uppySocket.send('bar', 'boo')
|
||||
expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([{ action: 'bar', payload: 'boo' }])
|
||||
// @ts-expect-error not allowed but needed for test
|
||||
expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([
|
||||
{ action: 'bar', payload: 'boo' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should close the websocket when it is force closed', () => {
|
||||
const uppySocket = new UppySocket({ target: 'foo' })
|
||||
// @ts-expect-error not allowed but needed for test
|
||||
const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
|
||||
webSocketInstance.triggerOpen()
|
||||
|
||||
|
|
@ -120,6 +145,7 @@ describe('Socket', () => {
|
|||
|
||||
it('should be able to subscribe to messages received on the websocket', () => {
|
||||
const uppySocket = new UppySocket({ target: 'foo' })
|
||||
// @ts-expect-error not allowed but needed for test
|
||||
const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
|
||||
|
||||
const emitterListenerMock = vi.fn()
|
||||
|
|
@ -1,15 +1,24 @@
|
|||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore no types
|
||||
import ee from 'namespace-emitter'
|
||||
|
||||
type Opts = {
|
||||
autoOpen?: boolean
|
||||
target: string
|
||||
}
|
||||
|
||||
export default class UppySocket {
|
||||
#queued = []
|
||||
#queued: Array<{ action: string; payload: unknown }> = []
|
||||
|
||||
#emitter = ee()
|
||||
|
||||
#isOpen = false
|
||||
|
||||
#socket
|
||||
#socket: WebSocket | null
|
||||
|
||||
constructor (opts) {
|
||||
opts: Opts
|
||||
|
||||
constructor(opts: Opts) {
|
||||
this.opts = opts
|
||||
|
||||
if (!opts || opts.autoOpen !== false) {
|
||||
|
|
@ -17,13 +26,22 @@ export default class UppySocket {
|
|||
}
|
||||
}
|
||||
|
||||
get isOpen () { return this.#isOpen }
|
||||
get isOpen(): boolean {
|
||||
return this.#isOpen
|
||||
}
|
||||
|
||||
[Symbol.for('uppy test: getSocket')] () { return this.#socket }
|
||||
private [Symbol.for('uppy test: getSocket')](): WebSocket | null {
|
||||
return this.#socket
|
||||
}
|
||||
|
||||
[Symbol.for('uppy test: getQueued')] () { return this.#queued }
|
||||
private [Symbol.for('uppy test: getQueued')](): Array<{
|
||||
action: string
|
||||
payload: unknown
|
||||
}> {
|
||||
return this.#queued
|
||||
}
|
||||
|
||||
open () {
|
||||
open(): void {
|
||||
if (this.#socket != null) return
|
||||
|
||||
this.#socket = new WebSocket(this.opts.target)
|
||||
|
|
@ -32,7 +50,7 @@ export default class UppySocket {
|
|||
this.#isOpen = true
|
||||
|
||||
while (this.#queued.length > 0 && this.#isOpen) {
|
||||
const first = this.#queued.shift()
|
||||
const first = this.#queued.shift()!
|
||||
this.send(first.action, first.payload)
|
||||
}
|
||||
}
|
||||
|
|
@ -45,11 +63,11 @@ export default class UppySocket {
|
|||
this.#socket.onmessage = this.#handleMessage
|
||||
}
|
||||
|
||||
close () {
|
||||
close(): void {
|
||||
this.#socket?.close()
|
||||
}
|
||||
|
||||
send (action, payload) {
|
||||
send(action: string, payload: unknown): void {
|
||||
// attach uuid
|
||||
|
||||
if (!this.#isOpen) {
|
||||
|
|
@ -57,25 +75,27 @@ export default class UppySocket {
|
|||
return
|
||||
}
|
||||
|
||||
this.#socket.send(JSON.stringify({
|
||||
action,
|
||||
payload,
|
||||
}))
|
||||
this.#socket!.send(
|
||||
JSON.stringify({
|
||||
action,
|
||||
payload,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
on (action, handler) {
|
||||
on(action: string, handler: () => void): void {
|
||||
this.#emitter.on(action, handler)
|
||||
}
|
||||
|
||||
emit (action, payload) {
|
||||
emit(action: string, payload: unknown): void {
|
||||
this.#emitter.emit(action, payload)
|
||||
}
|
||||
|
||||
once (action, handler) {
|
||||
once(action: string, handler: () => void): void {
|
||||
this.#emitter.once(action, handler)
|
||||
}
|
||||
|
||||
#handleMessage = (e) => {
|
||||
#handleMessage = (e: MessageEvent<any>) => {
|
||||
try {
|
||||
const message = JSON.parse(e.data)
|
||||
this.emit(message.action, message.payload)
|
||||
22
packages/@uppy/companion-client/src/getAllowedHosts.ts
Normal file
22
packages/@uppy/companion-client/src/getAllowedHosts.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export default function getAllowedHosts(
|
||||
hosts: string | RegExp | Array<string | RegExp> | undefined,
|
||||
url: string,
|
||||
): string | RegExp | Array<string | RegExp> {
|
||||
if (hosts) {
|
||||
if (
|
||||
typeof hosts !== 'string' &&
|
||||
!Array.isArray(hosts) &&
|
||||
!(hosts instanceof RegExp)
|
||||
) {
|
||||
throw new TypeError(
|
||||
`The option "companionAllowedHosts" must be one of string, Array, RegExp`,
|
||||
)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
// does not start with https://
|
||||
if (/^(?!https?:\/\/).*$/i.test(url)) {
|
||||
return `https://${url.replace(/^\/\//, '')}`
|
||||
}
|
||||
return new URL(url).origin
|
||||
}
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
'use strict'
|
||||
|
||||
/**
|
||||
* Manages communications with Companion
|
||||
*/
|
||||
|
||||
export { default as RequestClient } from './RequestClient.js'
|
||||
export { default as Provider } from './Provider.js'
|
||||
export { default as SearchProvider } from './SearchProvider.js'
|
||||
|
||||
// TODO: remove in the next major
|
||||
export { default as Socket } from './Socket.js'
|
||||
16
packages/@uppy/companion-client/src/index.ts
Normal file
16
packages/@uppy/companion-client/src/index.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Manages communications with Companion
|
||||
*/
|
||||
|
||||
export { default as RequestClient } from './RequestClient.ts'
|
||||
export { default as Provider } from './Provider.ts'
|
||||
export { default as SearchProvider } from './SearchProvider.ts'
|
||||
|
||||
export { default as getAllowedHosts } from './getAllowedHosts.ts'
|
||||
|
||||
export * as tokenStorage from './tokenStorage.ts'
|
||||
|
||||
export type { CompanionPluginOptions } from './CompanionPluginOptions.ts'
|
||||
|
||||
// TODO: remove in the next major
|
||||
export { default as Socket } from './Socket.ts'
|
||||
|
|
@ -1,20 +1,18 @@
|
|||
'use strict'
|
||||
|
||||
/**
|
||||
* This module serves as an Async wrapper for LocalStorage
|
||||
*/
|
||||
export function setItem (key, value) {
|
||||
export function setItem(key: string, value: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
localStorage.setItem(key, value)
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
export function getItem (key) {
|
||||
export function getItem(key: string): Promise<string | null> {
|
||||
return Promise.resolve(localStorage.getItem(key))
|
||||
}
|
||||
|
||||
export function removeItem (key) {
|
||||
export function removeItem(key: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
localStorage.removeItem(key)
|
||||
resolve()
|
||||
25
packages/@uppy/companion-client/tsconfig.build.json
Normal file
25
packages/@uppy/companion-client/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"outDir": "./lib",
|
||||
"paths": {
|
||||
"@uppy/utils/lib/*": ["../utils/src/*"],
|
||||
"@uppy/core": ["../core/src/index.js"],
|
||||
"@uppy/core/lib/*": ["../core/src/*"]
|
||||
},
|
||||
"resolveJsonModule": false,
|
||||
"rootDir": "./src",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./src/**/*.*"],
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../utils/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../core/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
packages/@uppy/companion-client/tsconfig.json
Normal file
21
packages/@uppy/companion-client/tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@uppy/utils/lib/*": ["../utils/src/*"],
|
||||
"@uppy/core": ["../core/src/index.js"],
|
||||
"@uppy/core/lib/*": ["../core/src/*"],
|
||||
},
|
||||
},
|
||||
"include": ["./package.json", "./src/**/*.*"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../utils/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../core/tsconfig.build.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
@ -1,5 +1,13 @@
|
|||
# @uppy/companion
|
||||
|
||||
## 4.12.1
|
||||
|
||||
Released: 2024-02-19
|
||||
Included in: Uppy v3.22.0
|
||||
|
||||
- @uppy/companion: fix companion dns and allow redirects from http->https again (mikael finstad / #4895)
|
||||
- @uppy/companion,@uppy/tus: bump `tus-js-client` version range (merlijn vos / #4848)
|
||||
|
||||
## 4.12.0
|
||||
|
||||
Released: 2023-12-12
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@uppy/companion",
|
||||
"version": "4.12.0",
|
||||
"version": "4.12.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",
|
||||
|
|
|
|||
|
|
@ -86,7 +86,13 @@ const validateConfig = (companionOptions) => {
|
|||
)
|
||||
}
|
||||
|
||||
const { providerOptions, periodicPingUrls } = companionOptions
|
||||
const { providerOptions, periodicPingUrls, server } = companionOptions
|
||||
|
||||
if (server && server.path) {
|
||||
// see https://github.com/transloadit/uppy/issues/4271
|
||||
// todo fix the code so we can allow `/`
|
||||
if (server.path === '/') throw new Error('server.path cannot be set to /')
|
||||
}
|
||||
|
||||
if (providerOptions) {
|
||||
const deprecatedOptions = { microsoft: 'providerOptions.onedrive', google: 'providerOptions.drive', s3: 's3' }
|
||||
|
|
|
|||
|
|
@ -642,8 +642,16 @@ class Uploader {
|
|||
throw new Error(errMsg)
|
||||
}
|
||||
|
||||
let bodyURL = null
|
||||
try {
|
||||
bodyURL = JSON.parse(response.body)?.url
|
||||
} catch {
|
||||
// response.body can be undefined or an empty string
|
||||
// in that case we ignore and continue.
|
||||
}
|
||||
|
||||
return {
|
||||
url: null,
|
||||
url: bodyURL,
|
||||
extraData: { response: getRespObj(response), bytesUploaded },
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const downloadURL = async (url, blockLocalIPs, traceId) => {
|
|||
// TODO in next major, rename all blockLocalIPs to allowLocalUrls and invert the bool, to make it consistent
|
||||
// see discussion https://github.com/transloadit/uppy/pull/4554/files#r1268677162
|
||||
try {
|
||||
const protectedGot = getProtectedGot({ url, blockLocalIPs })
|
||||
const protectedGot = getProtectedGot({ blockLocalIPs })
|
||||
const stream = protectedGot.stream.get(url, { responseType: 'json' })
|
||||
await prepareStream(stream)
|
||||
return stream
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
// eslint-disable-next-line max-classes-per-file
|
||||
const http = require('node:http')
|
||||
const https = require('node:https')
|
||||
const { URL } = require('node:url')
|
||||
const dns = require('node:dns')
|
||||
const ipaddr = require('ipaddr.js')
|
||||
const got = require('got').default
|
||||
|
|
@ -9,10 +8,7 @@ const path = require('node:path')
|
|||
const contentDisposition = require('content-disposition')
|
||||
const validator = require('validator')
|
||||
|
||||
const logger = require('../logger')
|
||||
|
||||
const FORBIDDEN_IP_ADDRESS = 'Forbidden IP address'
|
||||
const FORBIDDEN_RESOLVED_IP_ADDRESS = 'Forbidden resolved IP address'
|
||||
|
||||
// Example scary IPs that should return false (ipv6-to-ipv4 mapped):
|
||||
// ::FFFF:127.0.0.1
|
||||
|
|
@ -20,31 +16,6 @@ const FORBIDDEN_RESOLVED_IP_ADDRESS = 'Forbidden resolved IP address'
|
|||
const isDisallowedIP = (ipAddress) => ipaddr.parse(ipAddress).range() !== 'unicast'
|
||||
|
||||
module.exports.FORBIDDEN_IP_ADDRESS = FORBIDDEN_IP_ADDRESS
|
||||
module.exports.FORBIDDEN_RESOLVED_IP_ADDRESS = FORBIDDEN_RESOLVED_IP_ADDRESS
|
||||
|
||||
module.exports.getRedirectEvaluator = (rawRequestURL, isEnabled) => {
|
||||
const requestURL = new URL(rawRequestURL)
|
||||
|
||||
return ({ headers }) => {
|
||||
if (!isEnabled) return true
|
||||
|
||||
let redirectURL = null
|
||||
try {
|
||||
redirectURL = new URL(headers.location, requestURL)
|
||||
} catch (err) {
|
||||
return false
|
||||
}
|
||||
|
||||
const shouldRedirect = redirectURL.protocol === requestURL.protocol
|
||||
if (!shouldRedirect) {
|
||||
logger.info(
|
||||
`blocking redirect from ${requestURL} to ${redirectURL}`, 'redirect.protection',
|
||||
)
|
||||
}
|
||||
|
||||
return shouldRedirect
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the download URL is secure
|
||||
|
|
@ -83,14 +54,19 @@ const getProtectedHttpAgent = ({ protocol, blockLocalIPs }) => {
|
|||
}
|
||||
|
||||
const toValidate = Array.isArray(addresses) ? addresses : [{ address: addresses }]
|
||||
for (const record of toValidate) {
|
||||
if (blockLocalIPs && isDisallowedIP(record.address)) {
|
||||
callback(new Error(FORBIDDEN_RESOLVED_IP_ADDRESS), addresses, maybeFamily)
|
||||
return
|
||||
}
|
||||
// because dns.lookup seems to be called with option `all: true`, if we are on an ipv6 system,
|
||||
// `addresses` could contain a list of ipv4 addresses as well as ipv6 mapped addresses (rfc6052) which we cannot allow
|
||||
// however we should still allow any valid ipv4 addresses, so we filter out the invalid addresses
|
||||
const validAddresses = !blockLocalIPs ? toValidate : toValidate.filter(({ address }) => !isDisallowedIP(address))
|
||||
|
||||
// and check if there's anything left after we filtered:
|
||||
if (validAddresses.length === 0) {
|
||||
callback(new Error(`Forbidden resolved IP address ${hostname} -> ${toValidate.map(({ address }) => address).join(', ')}`), addresses, maybeFamily)
|
||||
return
|
||||
}
|
||||
|
||||
callback(err, addresses, maybeFamily)
|
||||
const ret = Array.isArray(addresses) ? validAddresses : validAddresses[0].address;
|
||||
callback(err, ret, maybeFamily)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -108,23 +84,15 @@ const getProtectedHttpAgent = ({ protocol, blockLocalIPs }) => {
|
|||
|
||||
module.exports.getProtectedHttpAgent = getProtectedHttpAgent
|
||||
|
||||
function getProtectedGot ({ url, blockLocalIPs }) {
|
||||
function getProtectedGot ({ blockLocalIPs }) {
|
||||
const HttpAgent = getProtectedHttpAgent({ protocol: 'http', blockLocalIPs })
|
||||
const HttpsAgent = getProtectedHttpAgent({ protocol: 'https', blockLocalIPs })
|
||||
const httpAgent = new HttpAgent()
|
||||
const httpsAgent = new HttpsAgent()
|
||||
|
||||
const redirectEvaluator = module.exports.getRedirectEvaluator(url, blockLocalIPs)
|
||||
|
||||
const beforeRedirect = (options, response) => {
|
||||
const allowRedirect = redirectEvaluator(response)
|
||||
if (!allowRedirect) {
|
||||
throw new Error(`Redirect evaluator does not allow the redirect to ${response.headers.location}`)
|
||||
}
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return got.extend({ hooks: { beforeRedirect: [beforeRedirect] }, agent: { http: httpAgent, https: httpsAgent } })
|
||||
return got.extend({ agent: { http: httpAgent, https: httpsAgent } })
|
||||
}
|
||||
|
||||
module.exports.getProtectedGot = getProtectedGot
|
||||
|
|
@ -138,7 +106,7 @@ module.exports.getProtectedGot = getProtectedGot
|
|||
*/
|
||||
exports.getURLMeta = async (url, blockLocalIPs = false) => {
|
||||
async function requestWithMethod (method) {
|
||||
const protectedGot = getProtectedGot({ url, blockLocalIPs })
|
||||
const protectedGot = getProtectedGot({ blockLocalIPs })
|
||||
const stream = protectedGot.stream(url, { method, throwHttpErrors: false })
|
||||
|
||||
return new Promise((resolve, reject) => (
|
||||
|
|
|
|||
|
|
@ -1,37 +1,7 @@
|
|||
const nock = require('nock')
|
||||
const { getRedirectEvaluator, FORBIDDEN_IP_ADDRESS, FORBIDDEN_RESOLVED_IP_ADDRESS } = require('../../src/server/helpers/request')
|
||||
const { FORBIDDEN_IP_ADDRESS } = require('../../src/server/helpers/request')
|
||||
const { getProtectedGot } = require('../../src/server/helpers/request')
|
||||
|
||||
describe('test getRedirectEvaluator', () => {
|
||||
const httpURL = 'http://uppy.io'
|
||||
const httpsURL = 'https://uppy.io'
|
||||
const httpRedirectResp = {
|
||||
headers: {
|
||||
location: 'http://transloadit.com',
|
||||
},
|
||||
}
|
||||
|
||||
const httpsRedirectResp = {
|
||||
headers: {
|
||||
location: 'https://transloadit.com',
|
||||
},
|
||||
}
|
||||
|
||||
test('when original URL has "https:" as protocol', (done) => {
|
||||
const shouldRedirectHttps = getRedirectEvaluator(httpsURL, true)
|
||||
expect(shouldRedirectHttps(httpsRedirectResp)).toEqual(true)
|
||||
expect(shouldRedirectHttps(httpRedirectResp)).toEqual(false)
|
||||
done()
|
||||
})
|
||||
|
||||
test('when original URL has "http:" as protocol', (done) => {
|
||||
const shouldRedirectHttp = getRedirectEvaluator(httpURL, true)
|
||||
expect(shouldRedirectHttp(httpRedirectResp)).toEqual(true)
|
||||
expect(shouldRedirectHttp(httpsRedirectResp)).toEqual(false)
|
||||
done()
|
||||
})
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
nock.cleanAll()
|
||||
nock.restore()
|
||||
|
|
@ -41,24 +11,24 @@ describe('test protected request Agent', () => {
|
|||
test('allows URLs without IP addresses', async () => {
|
||||
nock('https://transloadit.com').get('/').reply(200)
|
||||
const url = 'https://transloadit.com'
|
||||
await getProtectedGot({ url, blockLocalIPs: true }).get(url)
|
||||
await getProtectedGot({ blockLocalIPs: true }).get(url)
|
||||
})
|
||||
|
||||
test('blocks url that resolves to forbidden IP', async () => {
|
||||
const url = 'https://localhost'
|
||||
const promise = getProtectedGot({ url, blockLocalIPs: true }).get(url)
|
||||
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_RESOLVED_IP_ADDRESS))
|
||||
const promise = getProtectedGot({ blockLocalIPs: true }).get(url)
|
||||
await expect(promise).rejects.toThrow(/^Forbidden resolved IP address/)
|
||||
})
|
||||
|
||||
test('blocks private http IP address', async () => {
|
||||
const url = 'http://172.20.10.4:8090'
|
||||
const promise = getProtectedGot({ url, blockLocalIPs: true }).get(url)
|
||||
const promise = getProtectedGot({ blockLocalIPs: true }).get(url)
|
||||
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
|
||||
})
|
||||
|
||||
test('blocks private https IP address', async () => {
|
||||
const url = 'https://172.20.10.4:8090'
|
||||
const promise = getProtectedGot({ url, blockLocalIPs: true }).get(url)
|
||||
const promise = getProtectedGot({ blockLocalIPs: true }).get(url)
|
||||
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
|
||||
})
|
||||
|
||||
|
|
@ -87,12 +57,12 @@ describe('test protected request Agent', () => {
|
|||
|
||||
for (const ip of ipv4s) {
|
||||
const url = `http://${ip}:8090`
|
||||
const promise = getProtectedGot({ url, blockLocalIPs: true }).get(url)
|
||||
const promise = getProtectedGot({ blockLocalIPs: true }).get(url)
|
||||
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
|
||||
}
|
||||
for (const ip of ipv6s) {
|
||||
const url = `http://[${ip}]:8090`
|
||||
const promise = getProtectedGot({ url, blockLocalIPs: true }).get(url)
|
||||
const promise = getProtectedGot({ blockLocalIPs: true }).get(url)
|
||||
await expect(promise).rejects.toThrow(new Error(FORBIDDEN_IP_ADDRESS))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"sourceMap": false,
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"noEmitOnError": true
|
||||
"noEmitOnError": true,
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
"include": ["src/**/*"],
|
||||
}
|
||||
|
|
|
|||
1
packages/@uppy/compressor/.npmignore
Normal file
1
packages/@uppy/compressor/.npmignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
tsconfig.*
|
||||
|
|
@ -1,5 +1,20 @@
|
|||
# @uppy/compressor
|
||||
|
||||
## 1.1.1
|
||||
|
||||
Released: 2024-02-20
|
||||
Included in: Uppy v3.22.1
|
||||
|
||||
- @uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/status-bar: bump `@transloadit/prettier-bytes` (Antoine du Hamel / #4933)
|
||||
|
||||
## 1.1.0
|
||||
|
||||
Released: 2024-02-19
|
||||
Included in: Uppy v3.22.0
|
||||
|
||||
- @uppy/compressor: upgrade compressorjs (merlijn vos / #4924)
|
||||
- @uppy/compressor: migrate to ts (mikael finstad / #4907)
|
||||
|
||||
## 1.0.3
|
||||
|
||||
Released: 2023-09-18
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@uppy/compressor",
|
||||
"description": "Uppy plugin that compresses images before upload, saving up to 60% in size",
|
||||
"version": "1.0.5",
|
||||
"version": "1.1.1",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"style": "dist/style.min.css",
|
||||
|
|
@ -22,9 +22,9 @@
|
|||
"url": "git+https://github.com/transloadit/uppy.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@transloadit/prettier-bytes": "^0.2.0",
|
||||
"@transloadit/prettier-bytes": "^0.3.0",
|
||||
"@uppy/utils": "workspace:^",
|
||||
"compressorjs": "^1.1.1",
|
||||
"compressorjs": "^1.2.1",
|
||||
"preact": "^10.5.13",
|
||||
"promise-queue": "^2.2.5"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,10 +3,11 @@ import Core from '@uppy/core'
|
|||
import getFileNameAndExtension from '@uppy/utils/lib/getFileNameAndExtension'
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import CompressorPlugin from './index.js'
|
||||
import CompressorPlugin from './index.ts'
|
||||
|
||||
// Compressor uses browser canvas API, so need to mock compress()
|
||||
CompressorPlugin.prototype.compress = (blob) => {
|
||||
// @ts-expect-error mocked
|
||||
CompressorPlugin.prototype.compress = async (blob: Blob) => {
|
||||
return {
|
||||
name: `${getFileNameAndExtension(blob.name).name}.webp`,
|
||||
type: 'image/webp',
|
||||
|
|
@ -16,11 +17,28 @@ CompressorPlugin.prototype.compress = (blob) => {
|
|||
}
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const sampleImage = fs.readFileSync(path.join(__dirname, '../../../../e2e/cypress/fixtures/images/image.jpg'))
|
||||
const sampleImage = fs.readFileSync(
|
||||
path.join(__dirname, '../../../../e2e/cypress/fixtures/images/image.jpg'),
|
||||
)
|
||||
|
||||
const file1 = { source: 'jest', name: 'image-1.jpeg', type: 'image/jpeg', data: new File([sampleImage], 'image-1.jpeg', { type: 'image/jpeg' }) }
|
||||
const file2 = { source: 'jest', name: 'yolo', type: 'image/jpeg', data: new File([sampleImage], 'yolo', { type: 'image/jpeg' }) }
|
||||
const file3 = { source: 'jest', name: 'my.file.is.weird.png', type: 'image/png', data: new File([sampleImage], 'my.file.is.weird.png', { type: 'image/png' }) }
|
||||
const file1 = {
|
||||
source: 'jest',
|
||||
name: 'image-1.jpeg',
|
||||
type: 'image/jpeg',
|
||||
data: new File([sampleImage], 'image-1.jpeg', { type: 'image/jpeg' }),
|
||||
}
|
||||
const file2 = {
|
||||
source: 'jest',
|
||||
name: 'yolo',
|
||||
type: 'image/jpeg',
|
||||
data: new File([sampleImage], 'yolo', { type: 'image/jpeg' }),
|
||||
}
|
||||
const file3 = {
|
||||
source: 'jest',
|
||||
name: 'my.file.is.weird.png',
|
||||
type: 'image/png',
|
||||
data: new File([sampleImage], 'my.file.is.weird.png', { type: 'image/png' }),
|
||||
}
|
||||
|
||||
describe('CompressorPlugin', () => {
|
||||
it('should change update extension in file.name and file.meta.name', () => {
|
||||
|
|
@ -1,14 +1,34 @@
|
|||
import { BasePlugin } from '@uppy/core'
|
||||
import { BasePlugin, Uppy } from '@uppy/core'
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
import { RateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue'
|
||||
import getFileNameAndExtension from '@uppy/utils/lib/getFileNameAndExtension'
|
||||
import prettierBytes from '@transloadit/prettier-bytes'
|
||||
import CompressorJS from 'compressorjs'
|
||||
import locale from './locale.js'
|
||||
|
||||
export default class Compressor extends BasePlugin {
|
||||
import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
|
||||
import type { PluginOpts } from '@uppy/core/lib/BasePlugin.ts'
|
||||
|
||||
import locale from './locale.ts'
|
||||
|
||||
declare module '@uppy/core' {
|
||||
export interface UppyEventMap<M extends Meta, B extends Body> {
|
||||
'compressor:complete': (file: UppyFile<M, B>[]) => void
|
||||
}
|
||||
}
|
||||
|
||||
export interface CompressorOpts extends PluginOpts, CompressorJS.Options {
|
||||
quality: number
|
||||
limit?: number
|
||||
}
|
||||
|
||||
export default class Compressor<
|
||||
M extends Meta,
|
||||
B extends Body,
|
||||
> extends BasePlugin<CompressorOpts, M, B> {
|
||||
#RateLimitedQueue
|
||||
|
||||
constructor (uppy, opts) {
|
||||
constructor(uppy: Uppy<M, B>, opts: CompressorOpts) {
|
||||
super(uppy, opts)
|
||||
this.id = this.opts.id || 'Compressor'
|
||||
this.type = 'modifier'
|
||||
|
|
@ -30,7 +50,7 @@ export default class Compressor extends BasePlugin {
|
|||
this.compress = this.compress.bind(this)
|
||||
}
|
||||
|
||||
compress (blob) {
|
||||
compress(blob: Blob): Promise<Blob | File> {
|
||||
return new Promise((resolve, reject) => {
|
||||
/* eslint-disable no-new */
|
||||
new CompressorJS(blob, {
|
||||
|
|
@ -41,15 +61,17 @@ export default class Compressor extends BasePlugin {
|
|||
})
|
||||
}
|
||||
|
||||
async prepareUpload (fileIDs) {
|
||||
async prepareUpload(fileIDs: string[]): Promise<void> {
|
||||
let totalCompressedSize = 0
|
||||
const compressedFiles = []
|
||||
const compressedFiles: UppyFile<M, B>[] = []
|
||||
const compressAndApplyResult = this.#RateLimitedQueue.wrapPromiseFunction(
|
||||
async (file) => {
|
||||
async (file: UppyFile<M, B>) => {
|
||||
try {
|
||||
const compressedBlob = await this.compress(file.data)
|
||||
const compressedSavingsSize = file.data.size - compressedBlob.size
|
||||
this.uppy.log(`[Image Compressor] Image ${file.id} compressed by ${prettierBytes(compressedSavingsSize)}`)
|
||||
this.uppy.log(
|
||||
`[Image Compressor] Image ${file.id} compressed by ${prettierBytes(compressedSavingsSize)}`,
|
||||
)
|
||||
totalCompressedSize += compressedSavingsSize
|
||||
const { name, type, size } = compressedBlob
|
||||
|
||||
|
|
@ -61,7 +83,9 @@ export default class Compressor extends BasePlugin {
|
|||
|
||||
this.uppy.setFileState(file.id, {
|
||||
...(name && { name }),
|
||||
...(compressedFileName.extension && { extension: compressedFileName.extension }),
|
||||
...(compressedFileName.extension && {
|
||||
extension: compressedFileName.extension,
|
||||
}),
|
||||
...(type && { type }),
|
||||
...(size && { size }),
|
||||
data: compressedBlob,
|
||||
|
|
@ -73,7 +97,10 @@ export default class Compressor extends BasePlugin {
|
|||
})
|
||||
compressedFiles.push(file)
|
||||
} catch (err) {
|
||||
this.uppy.log(`[Image Compressor] Failed to compress ${file.id}:`, 'warning')
|
||||
this.uppy.log(
|
||||
`[Image Compressor] Failed to compress ${file.id}:`,
|
||||
'warning',
|
||||
)
|
||||
this.uppy.log(err, 'warning')
|
||||
}
|
||||
},
|
||||
|
|
@ -97,7 +124,7 @@ export default class Compressor extends BasePlugin {
|
|||
file.data = file.data.slice(0, file.data.size, file.type)
|
||||
}
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
if (!file.type?.startsWith('image/')) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
|
|
@ -128,11 +155,11 @@ export default class Compressor extends BasePlugin {
|
|||
}
|
||||
}
|
||||
|
||||
install () {
|
||||
install(): void {
|
||||
this.uppy.addPreProcessor(this.prepareUpload)
|
||||
}
|
||||
|
||||
uninstall () {
|
||||
uninstall(): void {
|
||||
this.uppy.removePreProcessor(this.prepareUpload)
|
||||
}
|
||||
}
|
||||
25
packages/@uppy/compressor/tsconfig.build.json
Normal file
25
packages/@uppy/compressor/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"noImplicitAny": false,
|
||||
"outDir": "./lib",
|
||||
"paths": {
|
||||
"@uppy/utils/lib/*": ["../utils/src/*"],
|
||||
"@uppy/core": ["../core/src/index.js"],
|
||||
"@uppy/core/lib/*": ["../core/src/*"]
|
||||
},
|
||||
"resolveJsonModule": false,
|
||||
"rootDir": "./src",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./src/**/*.*"],
|
||||
"exclude": ["./src/**/*.test.ts"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../utils/tsconfig.build.json"
|
||||
},
|
||||
{
|
||||
"path": "../core/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
packages/@uppy/compressor/tsconfig.json
Normal file
21
packages/@uppy/compressor/tsconfig.json
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true,
|
||||
"paths": {
|
||||
"@uppy/utils/lib/*": ["../utils/src/*"],
|
||||
"@uppy/core": ["../core/src/index.js"],
|
||||
"@uppy/core/lib/*": ["../core/src/*"],
|
||||
},
|
||||
},
|
||||
"include": ["./package.json", "./src/**/*.*"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../utils/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../core/tsconfig.build.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
1
packages/@uppy/core/.npmignore
Normal file
1
packages/@uppy/core/.npmignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
tsconfig.*
|
||||
|
|
@ -1,5 +1,41 @@
|
|||
# @uppy/core
|
||||
|
||||
## 3.9.3
|
||||
|
||||
Released: 2024-02-28
|
||||
Included in: Uppy v3.23.0
|
||||
|
||||
- @uppy/core: remove unused import (Antoine du Hamel / #4972)
|
||||
|
||||
## 3.9.2
|
||||
|
||||
Released: 2024-02-22
|
||||
Included in: Uppy v3.22.2
|
||||
|
||||
- @uppy/core: fix plugin detection (Antoine du Hamel / #4951)
|
||||
- @uppy/core,@uppy/utils: Introduce `ValidateableFile` & move `MinimalRequiredUppyFile` into utils (Antoine du Hamel / #4944)
|
||||
- @uppy/core: improve `UIPluginOptions` types (Merlijn Vos / #4946)
|
||||
|
||||
## 3.9.1
|
||||
|
||||
Released: 2024-02-20
|
||||
Included in: Uppy v3.22.1
|
||||
|
||||
- @uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/status-bar: bump `@transloadit/prettier-bytes` (Antoine du Hamel / #4933)
|
||||
|
||||
## 3.9.0
|
||||
|
||||
Released: 2024-02-19
|
||||
Included in: Uppy v3.22.0
|
||||
|
||||
- @uppy/core: add utility type to help define plugin option types (antoine du hamel / #4885)
|
||||
- @uppy/core: improve types of .use() (merlijn vos / #4882)
|
||||
- @uppy/core: add `plugintarget` type and mark options as optional (antoine du hamel / #4874)
|
||||
- @uppy/core: add `debuglogger` as export in manual types (antoine du hamel / #4831)
|
||||
- @uppy/core: add missing requiredmetafields key in restrictions (darthf1 / #4819)
|
||||
- @uppy/core: fix types (antoine du hamel / #4842)
|
||||
- @uppy/core: refactor to ts (murderlon)
|
||||
|
||||
## 3.8.0
|
||||
|
||||
Released: 2023-12-12
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@uppy/core",
|
||||
"description": "Core module for the extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
|
||||
"version": "3.8.0",
|
||||
"version": "3.9.3",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"style": "dist/style.min.css",
|
||||
|
|
@ -23,7 +23,7 @@
|
|||
"url": "git+https://github.com/transloadit/uppy.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@transloadit/prettier-bytes": "^0.2.0",
|
||||
"@transloadit/prettier-bytes": "^0.3.0",
|
||||
"@uppy/store-default": "workspace:^",
|
||||
"@uppy/utils": "workspace:^",
|
||||
"lodash": "^4.17.21",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
|
||||
/**
|
||||
* Core plugin logic that all plugins share.
|
||||
|
|
@ -11,19 +10,33 @@
|
|||
*/
|
||||
|
||||
import Translator from '@uppy/utils/lib/Translator'
|
||||
import type { I18n, Locale } from '@uppy/utils/lib/Translator'
|
||||
import type {
|
||||
I18n,
|
||||
Locale,
|
||||
OptionalPluralizeLocale,
|
||||
} from '@uppy/utils/lib/Translator'
|
||||
import type { Body, Meta } from '@uppy/utils/lib/UppyFile'
|
||||
import type { State, Uppy } from './Uppy'
|
||||
import type { State, UnknownPlugin, Uppy } from './Uppy'
|
||||
|
||||
export type PluginOpts = {
|
||||
locale?: Locale
|
||||
id?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type OnlyOptionals<T> = Pick<
|
||||
T,
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? K : never
|
||||
}[keyof T]
|
||||
>
|
||||
|
||||
/**
|
||||
* DefinePluginOpts marks all of the passed AlwaysDefinedKeys as “required” or “always defined”.
|
||||
*/
|
||||
export type DefinePluginOpts<
|
||||
Opts extends PluginOpts,
|
||||
AlwaysDefinedKeys extends string,
|
||||
Opts,
|
||||
AlwaysDefinedKeys extends keyof OnlyOptionals<Opts>,
|
||||
> = Opts & Required<Pick<Opts, AlwaysDefinedKeys>>
|
||||
|
||||
export default class BasePlugin<
|
||||
|
|
@ -38,7 +51,7 @@ export default class BasePlugin<
|
|||
|
||||
id: string
|
||||
|
||||
defaultLocale: Locale
|
||||
defaultLocale: OptionalPluralizeLocale
|
||||
|
||||
i18n: I18n
|
||||
|
||||
|
|
@ -98,7 +111,7 @@ export default class BasePlugin<
|
|||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
addTarget(plugin: unknown): HTMLElement {
|
||||
addTarget(plugin: UnknownPlugin<M, B>): HTMLElement | null {
|
||||
throw new Error(
|
||||
"Extend the addTarget method to add your plugin to another plugin's target",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,16 @@ export type Restrictions = {
|
|||
requiredMetaFields: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The minimal required properties to be present from UppyFile in order to validate it.
|
||||
*/
|
||||
export type ValidateableFile<M extends Meta, B extends Body> = Pick<
|
||||
UppyFile<M, B>,
|
||||
'type' | 'extension' | 'size' | 'name'
|
||||
// Both UppyFile and CompanionFile need to be passable as a ValidateableFile
|
||||
// CompanionFile's do not have `isGhost`, so we mark it optional.
|
||||
> & { isGhost?: boolean }
|
||||
|
||||
const defaultOptions = {
|
||||
maxFileSize: null,
|
||||
minFileSize: null,
|
||||
|
|
@ -69,8 +79,8 @@ class Restricter<M extends Meta, B extends Body> {
|
|||
|
||||
// Because these operations are slow, we cannot run them for every file (if we are adding multiple files)
|
||||
validateAggregateRestrictions(
|
||||
existingFiles: UppyFile<M, B>[],
|
||||
addingFiles: UppyFile<M, B>[],
|
||||
existingFiles: ValidateableFile<M, B>[],
|
||||
addingFiles: ValidateableFile<M, B>[],
|
||||
): void {
|
||||
const { maxTotalFileSize, maxNumberOfFiles } = this.getOpts().restrictions
|
||||
|
||||
|
|
@ -109,7 +119,7 @@ class Restricter<M extends Meta, B extends Body> {
|
|||
}
|
||||
}
|
||||
|
||||
validateSingleFile(file: UppyFile<M, B>): void {
|
||||
validateSingleFile(file: ValidateableFile<M, B>): void {
|
||||
const { maxFileSize, minFileSize, allowedFileTypes } =
|
||||
this.getOpts().restrictions
|
||||
|
||||
|
|
@ -134,7 +144,7 @@ class Restricter<M extends Meta, B extends Body> {
|
|||
this.i18n('youCanOnlyUploadFileTypes', {
|
||||
types: allowedFileTypesString,
|
||||
}),
|
||||
{ file },
|
||||
{ file } as { file: UppyFile<M, B> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -146,7 +156,7 @@ class Restricter<M extends Meta, B extends Body> {
|
|||
size: prettierBytes(maxFileSize),
|
||||
file: file.name,
|
||||
}),
|
||||
{ file },
|
||||
{ file } as { file: UppyFile<M, B> },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -156,14 +166,14 @@ class Restricter<M extends Meta, B extends Body> {
|
|||
this.i18n('inferiorSize', {
|
||||
size: prettierBytes(minFileSize),
|
||||
}),
|
||||
{ file },
|
||||
{ file } as { file: UppyFile<M, B> },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
validate(
|
||||
existingFiles: UppyFile<M, B>[],
|
||||
addingFiles: UppyFile<M, B>[],
|
||||
existingFiles: ValidateableFile<M, B>[],
|
||||
addingFiles: ValidateableFile<M, B>[],
|
||||
): void {
|
||||
addingFiles.forEach((addingFile) => {
|
||||
this.validateSingleFile(addingFile)
|
||||
|
|
@ -180,7 +190,7 @@ class Restricter<M extends Meta, B extends Body> {
|
|||
}
|
||||
}
|
||||
|
||||
getMissingRequiredMetaFields(file: UppyFile<M, B>): {
|
||||
getMissingRequiredMetaFields(file: ValidateableFile<M, B> & { meta: M }): {
|
||||
missingFields: string[]
|
||||
error: RestrictionError<M, B>
|
||||
} {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { render, type ComponentChild } from 'preact'
|
||||
import findDOMElement from '@uppy/utils/lib/findDOMElement'
|
||||
import getTextDirection from '@uppy/utils/lib/getTextDirection'
|
||||
|
|
@ -33,11 +32,6 @@ function debounce<T extends (...args: any[]) => any>(
|
|||
}
|
||||
}
|
||||
|
||||
export interface UIPluginOptions extends PluginOpts {
|
||||
replaceTargetContent?: boolean
|
||||
direction?: 'ltr' | 'rtl'
|
||||
}
|
||||
|
||||
/**
|
||||
* UIPlugin is the extended version of BasePlugin to incorporate rendering with Preact.
|
||||
* Use this for plugins that need a user interface.
|
||||
|
|
@ -45,6 +39,7 @@ export interface UIPluginOptions extends PluginOpts {
|
|||
* For plugins without an user interface, see BasePlugin.
|
||||
*/
|
||||
class UIPlugin<
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
Opts extends UIPluginOptions,
|
||||
M extends Meta,
|
||||
B extends Body,
|
||||
|
|
@ -64,9 +59,18 @@ class UIPlugin<
|
|||
target: PluginTarget<Me, Bo>, // eslint-disable-line no-use-before-define
|
||||
): UIPlugin<any, Me, Bo> | undefined {
|
||||
let targetPlugin
|
||||
if (typeof target === 'object' && target instanceof UIPlugin) {
|
||||
if (typeof (target as UIPlugin<any, any, any>)?.addTarget === 'function') {
|
||||
// Targeting a plugin *instance*
|
||||
targetPlugin = target
|
||||
targetPlugin = target as UIPlugin<any, any, any>
|
||||
if (!(targetPlugin instanceof UIPlugin)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
new Error(
|
||||
'The provided plugin is not an instance of UIPlugin. This is an indication of a bug with the way Uppy is bundled.',
|
||||
{ cause: { targetPlugin, UIPlugin } },
|
||||
),
|
||||
)
|
||||
}
|
||||
} else if (typeof target === 'function') {
|
||||
// Targeting a plugin type
|
||||
const Target = target
|
||||
|
|
@ -133,7 +137,7 @@ class UIPlugin<
|
|||
|
||||
this.onMount()
|
||||
|
||||
return this.el
|
||||
return this.el!
|
||||
}
|
||||
|
||||
const targetPlugin = this.getTargetPlugin(target)
|
||||
|
|
@ -144,7 +148,7 @@ class UIPlugin<
|
|||
this.el = targetPlugin.addTarget(plugin)
|
||||
|
||||
this.onMount()
|
||||
return this.el
|
||||
return this.el!
|
||||
}
|
||||
|
||||
this.uppy.log(`Not installing ${callerPluginName}`)
|
||||
|
|
@ -205,3 +209,9 @@ export type PluginTarget<M extends Meta, B extends Body> =
|
|||
| typeof BasePlugin
|
||||
| typeof UIPlugin
|
||||
| BasePlugin<any, M, B>
|
||||
|
||||
export interface UIPluginOptions extends PluginOpts {
|
||||
target?: PluginTarget<any, any>
|
||||
replaceTargetContent?: boolean
|
||||
direction?: 'ltr' | 'rtl'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
/* eslint no-console: "off", no-restricted-syntax: "off" */
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
|
@ -166,7 +165,6 @@ describe('src/Core', () => {
|
|||
|
||||
core.use(AcquirerPlugin1)
|
||||
const plugin = core.getPlugin('TestSelector1')
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
expect(plugin!.id).toEqual('TestSelector1')
|
||||
expect(plugin instanceof UIPlugin)
|
||||
})
|
||||
|
|
@ -611,7 +609,6 @@ describe('src/Core', () => {
|
|||
describe('preprocessors', () => {
|
||||
it('should add and remove preprocessor', () => {
|
||||
const core = new Core()
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const preprocessor = () => {}
|
||||
expect(core.removePreProcessor(preprocessor)).toBe(false)
|
||||
core.addPreProcessor(preprocessor)
|
||||
|
|
@ -731,7 +728,6 @@ describe('src/Core', () => {
|
|||
describe('postprocessors', () => {
|
||||
it('should add and remove postprocessor', () => {
|
||||
const core = new Core()
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const postprocessor = () => {}
|
||||
expect(core.removePostProcessor(postprocessor)).toBe(false)
|
||||
core.addPostProcessor(postprocessor)
|
||||
|
|
@ -849,7 +845,6 @@ describe('src/Core', () => {
|
|||
describe('uploaders', () => {
|
||||
it('should add and remove uploader', () => {
|
||||
const core = new Core()
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const uploader = () => {}
|
||||
expect(core.removeUploader(uploader)).toBe(false)
|
||||
core.addUploader(uploader)
|
||||
|
|
@ -1273,9 +1268,9 @@ describe('src/Core', () => {
|
|||
core
|
||||
.upload()
|
||||
.then((r) =>
|
||||
typeof r!.uploadID === 'string' && r!.uploadID.length === 21
|
||||
? { ...r, uploadID: 'cjd09qwxb000dlql4tp4doz8h' }
|
||||
: r,
|
||||
typeof r!.uploadID === 'string' && r!.uploadID.length === 21 ?
|
||||
{ ...r, uploadID: 'cjd09qwxb000dlql4tp4doz8h' }
|
||||
: r,
|
||||
),
|
||||
).resolves.toMatchSnapshot()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -11,7 +11,17 @@ import DefaultStore from '@uppy/store-default'
|
|||
import getFileType from '@uppy/utils/lib/getFileType'
|
||||
import getFileNameAndExtension from '@uppy/utils/lib/getFileNameAndExtension'
|
||||
import { getSafeFileId } from '@uppy/utils/lib/generateFileID'
|
||||
import type { UppyFile, Meta, Body } from '@uppy/utils/lib/UppyFile'
|
||||
import type {
|
||||
UppyFile,
|
||||
Meta,
|
||||
Body,
|
||||
MinimalRequiredUppyFile,
|
||||
} from '@uppy/utils/lib/UppyFile'
|
||||
import type { CompanionFile } from '@uppy/utils/lib/CompanionFile'
|
||||
import type {
|
||||
CompanionClientProvider,
|
||||
CompanionClientSearchProvider,
|
||||
} from '@uppy/utils/lib/CompanionClientProvider'
|
||||
import type {
|
||||
FileProgressNotStarted,
|
||||
FileProgressStarted,
|
||||
|
|
@ -35,8 +45,7 @@ import packageJson from '../package.json'
|
|||
import locale from './locale.ts'
|
||||
|
||||
import type BasePlugin from './BasePlugin.ts'
|
||||
import type UIPlugin from './UIPlugin.ts'
|
||||
import type { Restrictions } from './Restricter.ts'
|
||||
import type { Restrictions, ValidateableFile } from './Restricter.ts'
|
||||
|
||||
type Processor = (fileIDs: string[], uploadID: string) => Promise<void> | void
|
||||
|
||||
|
|
@ -44,31 +53,84 @@ type FileRemoveReason = 'user' | 'cancel-all'
|
|||
|
||||
type LogLevel = 'info' | 'warning' | 'error' | 'success'
|
||||
|
||||
type UnknownPlugin<M extends Meta, B extends Body> = InstanceType<
|
||||
typeof BasePlugin<any, M, B> | typeof UIPlugin<any, M, B>
|
||||
>
|
||||
export type UnknownPlugin<
|
||||
M extends Meta,
|
||||
B extends Body,
|
||||
PluginState extends Record<string, unknown> = Record<string, unknown>,
|
||||
> = BasePlugin<any, M, B, PluginState>
|
||||
|
||||
type UnknownProviderPlugin<M extends Meta, B extends Body> = UnknownPlugin<
|
||||
M,
|
||||
B
|
||||
> & {
|
||||
provider: {
|
||||
logout: () => void
|
||||
export type UnknownProviderPluginState = {
|
||||
authenticated: boolean | undefined
|
||||
breadcrumbs: {
|
||||
requestPath?: string
|
||||
name?: string
|
||||
id?: string
|
||||
}[]
|
||||
didFirstRender: boolean
|
||||
currentSelection: CompanionFile[]
|
||||
filterInput: string
|
||||
loading: boolean | string
|
||||
folders: CompanionFile[]
|
||||
files: CompanionFile[]
|
||||
isSearchVisible: boolean
|
||||
}
|
||||
/*
|
||||
* UnknownProviderPlugin can be any Companion plugin (such as Google Drive).
|
||||
* As the plugins are passed around throughout Uppy we need a generic type for this.
|
||||
* It may seems like duplication, but this type safe. Changing the type of `storage`
|
||||
* will error in the `Provider` class of @uppy/companion-client and vice versa.
|
||||
*
|
||||
* Note that this is the *plugin* class, not a version of the `Provider` class.
|
||||
* `Provider` does operate on Companion plugins with `uppy.getPlugin()`.
|
||||
*/
|
||||
export type UnknownProviderPlugin<
|
||||
M extends Meta,
|
||||
B extends Body,
|
||||
> = UnknownPlugin<M, B, UnknownProviderPluginState> & {
|
||||
onFirstRender: () => void
|
||||
title: string
|
||||
files: UppyFile<M, B>[]
|
||||
icon: () => JSX.Element
|
||||
provider: CompanionClientProvider
|
||||
storage: {
|
||||
getItem: (key: string) => Promise<string | null>
|
||||
setItem: (key: string, value: string) => Promise<void>
|
||||
removeItem: (key: string) => Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
// The user facing type for UppyFile used in uppy.addFile() and uppy.setOptions()
|
||||
export type MinimalRequiredUppyFile<M extends Meta, B extends Body> = Required<
|
||||
Pick<UppyFile<M, B>, 'name' | 'data' | 'type' | 'source'>
|
||||
> &
|
||||
Partial<
|
||||
Omit<UppyFile<M, B>, 'name' | 'data' | 'type' | 'source' | 'meta'>
|
||||
// We want to omit the 'meta' from UppyFile because of internal metadata
|
||||
// (see InternalMetadata in `UppyFile.ts`), as when adding a new file
|
||||
// that is not required.
|
||||
> & { meta?: M }
|
||||
/*
|
||||
* UnknownSearchProviderPlugin can be any search Companion plugin (such as Unsplash).
|
||||
* As the plugins are passed around throughout Uppy we need a generic type for this.
|
||||
* It may seems like duplication, but this type safe. Changing the type of `title`
|
||||
* will error in the `SearchProvider` class of @uppy/companion-client and vice versa.
|
||||
*
|
||||
* Note that this is the *plugin* class, not a version of the `SearchProvider` class.
|
||||
* `SearchProvider` does operate on Companion plugins with `uppy.getPlugin()`.
|
||||
*/
|
||||
export type UnknownSearchProviderPluginState = {
|
||||
isInputMode?: boolean
|
||||
searchTerm?: string | null
|
||||
} & Pick<
|
||||
UnknownProviderPluginState,
|
||||
| 'loading'
|
||||
| 'files'
|
||||
| 'folders'
|
||||
| 'currentSelection'
|
||||
| 'filterInput'
|
||||
| 'didFirstRender'
|
||||
>
|
||||
export type UnknownSearchProviderPlugin<
|
||||
M extends Meta,
|
||||
B extends Body,
|
||||
> = UnknownPlugin<M, B, UnknownSearchProviderPluginState> & {
|
||||
onFirstRender: () => void
|
||||
title: string
|
||||
icon: () => JSX.Element
|
||||
provider: CompanionClientSearchProvider
|
||||
}
|
||||
|
||||
interface UploadResult<M extends Meta, B extends Body> {
|
||||
export interface UploadResult<M extends Meta, B extends Body> {
|
||||
successful?: UppyFile<M, B>[]
|
||||
failed?: UppyFile<M, B>[]
|
||||
uploadID?: string
|
||||
|
|
@ -92,10 +154,12 @@ export interface State<M extends Meta, B extends Body>
|
|||
uploadProgress: boolean
|
||||
individualCancellation: boolean
|
||||
resumableUploads: boolean
|
||||
isMobileDevice?: boolean
|
||||
darkMode?: boolean
|
||||
}
|
||||
currentUploads: Record<string, CurrentUpload<M, B>>
|
||||
allowNewUpload: boolean
|
||||
recoveredState: null | State<M, B>
|
||||
recoveredState: null | Required<Pick<State<M, B>, 'files' | 'currentUploads'>>
|
||||
error: string | null
|
||||
files: {
|
||||
[key: string]: UppyFile<M, B>
|
||||
|
|
@ -108,6 +172,7 @@ export interface State<M extends Meta, B extends Body>
|
|||
}>
|
||||
plugins: Plugins
|
||||
totalProgress: number
|
||||
companion?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface UppyOptions<M extends Meta, B extends Body> {
|
||||
|
|
@ -201,14 +266,16 @@ type UploadCompleteCallback<M extends Meta, B extends Body> = (
|
|||
result: UploadResult<M, B>,
|
||||
) => void
|
||||
type ErrorCallback<M extends Meta, B extends Body> = (
|
||||
error: { message?: string; details?: string },
|
||||
error: { name: string; message: string; details?: string },
|
||||
file?: UppyFile<M, B>,
|
||||
response?: UppyFile<M, B>['response'],
|
||||
) => void
|
||||
type UploadErrorCallback<M extends Meta, B extends Body> = (
|
||||
file: UppyFile<M, B> | undefined,
|
||||
error: { message: string; details?: string },
|
||||
response: UppyFile<M, B>['response'] | undefined,
|
||||
error: { name: string; message: string; details?: string },
|
||||
response?:
|
||||
| Omit<NonNullable<UppyFile<M, B>['response']>, 'uploadURL'>
|
||||
| undefined,
|
||||
) => void
|
||||
type UploadStalledCallback<M extends Meta, B extends Body> = (
|
||||
error: { message: string; details?: string },
|
||||
|
|
@ -249,8 +316,9 @@ export interface _UppyEventMap<M extends Meta, B extends Body> {
|
|||
'preprocess-progress': PreProcessProgressCallback<M, B>
|
||||
progress: ProgressCallback
|
||||
'reset-progress': GenericEventCallback
|
||||
restored: GenericEventCallback
|
||||
restored: (pluginData: any) => void
|
||||
'restore-confirmed': GenericEventCallback
|
||||
'restore-canceled': GenericEventCallback
|
||||
'restriction-failed': RestrictionFailedCallback<M, B>
|
||||
'resume-all': GenericEventCallback
|
||||
'retry-all': RetryAllCallback
|
||||
|
|
@ -411,17 +479,6 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
this.#emitter.emit(event, ...args)
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
on<K extends keyof DeprecatedUppyEventMap<M, B>>(
|
||||
event: K,
|
||||
callback: DeprecatedUppyEventMap<M, B>[K],
|
||||
): this
|
||||
|
||||
on<K extends keyof _UppyEventMap<M, B>>(
|
||||
event: K,
|
||||
callback: _UppyEventMap<M, B>[K],
|
||||
): this
|
||||
|
||||
on<K extends keyof UppyEventMap<M, B>>(
|
||||
event: K,
|
||||
callback: UppyEventMap<M, B>[K],
|
||||
|
|
@ -570,7 +627,7 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
|
||||
// @todo next major: rename to `clear()`, make it also cancel ongoing uploads
|
||||
// or throw and say you need to cancel manually
|
||||
protected clearUploadedFiles(): void {
|
||||
clearUploadedFiles(): void {
|
||||
this.setState({ ...defaultUploadState, files: {} })
|
||||
}
|
||||
|
||||
|
|
@ -721,6 +778,7 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
|
||||
#informAndEmit(
|
||||
errors: {
|
||||
name: string
|
||||
message: string
|
||||
isUserFacing?: boolean
|
||||
details?: string
|
||||
|
|
@ -761,8 +819,8 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
}
|
||||
|
||||
validateRestrictions(
|
||||
file: UppyFile<M, B>,
|
||||
files = this.getFiles(),
|
||||
file: ValidateableFile<M, B>,
|
||||
files: ValidateableFile<M, B>[] = this.getFiles(),
|
||||
): RestrictionError<M, B> | null {
|
||||
try {
|
||||
this.#restricter.validate(files, [file])
|
||||
|
|
@ -799,9 +857,12 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
const { allowNewUpload } = this.getState()
|
||||
|
||||
if (allowNewUpload === false) {
|
||||
const error = new RestrictionError(this.i18n('noMoreFilesAllowed'), {
|
||||
file,
|
||||
})
|
||||
const error = new RestrictionError<M, B>(
|
||||
this.i18n('noMoreFilesAllowed'),
|
||||
{
|
||||
file,
|
||||
},
|
||||
)
|
||||
this.#informAndEmit([error])
|
||||
throw error
|
||||
}
|
||||
|
|
@ -824,15 +885,14 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
// If the actual File object is passed from input[type=file] or drag-drop,
|
||||
// we normalize it to match Uppy file object
|
||||
const file = (
|
||||
fileDescriptorOrFile instanceof File
|
||||
? {
|
||||
name: fileDescriptorOrFile.name,
|
||||
type: fileDescriptorOrFile.type,
|
||||
size: fileDescriptorOrFile.size,
|
||||
data: fileDescriptorOrFile,
|
||||
}
|
||||
: fileDescriptorOrFile
|
||||
) as UppyFile<M, B>
|
||||
fileDescriptorOrFile instanceof File ?
|
||||
{
|
||||
name: fileDescriptorOrFile.name,
|
||||
type: fileDescriptorOrFile.type,
|
||||
size: fileDescriptorOrFile.size,
|
||||
data: fileDescriptorOrFile,
|
||||
}
|
||||
: fileDescriptorOrFile) as UppyFile<M, B>
|
||||
|
||||
const fileType = getFileType(file)
|
||||
const fileName = getFileName(fileType, file)
|
||||
|
|
@ -863,7 +923,7 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
bytesTotal: size,
|
||||
uploadComplete: false,
|
||||
uploadStarted: null,
|
||||
} as FileProgressNotStarted,
|
||||
} satisfies FileProgressNotStarted,
|
||||
size,
|
||||
isGhost: false,
|
||||
isRemote: file.isRemote || false,
|
||||
|
|
@ -1298,6 +1358,8 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
// and click 'ADD MORE FILES', - focus won't activate in Firefox.
|
||||
// - We must throttle at around >500ms to avoid performance lags.
|
||||
// [Practical Check] Firefox, try to upload a big file for a prolonged period of time. Laptop will start to heat up.
|
||||
// todo when uploading multiple files, this will cause problems because they share the same throttle,
|
||||
// meaning some files might never get their progress reported (eaten up by progress events from other files)
|
||||
calculateProgress = throttle(
|
||||
(file, data) => {
|
||||
const fileInState = this.getFile(file?.id)
|
||||
|
|
@ -1323,8 +1385,9 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
...fileInState.progress,
|
||||
bytesUploaded: data.bytesUploaded,
|
||||
bytesTotal: data.bytesTotal,
|
||||
percentage: canHavePercentage
|
||||
? Math.round((data.bytesUploaded / data.bytesTotal) * 100)
|
||||
percentage:
|
||||
canHavePercentage ?
|
||||
Math.round((data.bytesUploaded / data.bytesTotal) * 100)
|
||||
: 0,
|
||||
},
|
||||
})
|
||||
|
|
@ -1364,7 +1427,7 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
if (sizedFiles.length === 0) {
|
||||
const progressMax = inProgress.length * 100
|
||||
const currentProgress = unsizedFiles.reduce((acc, file) => {
|
||||
return acc + file.progress.percentage
|
||||
return acc + (file.progress.percentage as number)
|
||||
}, 0)
|
||||
const totalProgress = Math.round((currentProgress / progressMax) * 100)
|
||||
this.setState({ totalProgress })
|
||||
|
|
@ -1479,7 +1542,6 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
file.id,
|
||||
{
|
||||
progress: {
|
||||
progress: 0,
|
||||
uploadStarted: Date.now(),
|
||||
uploadComplete: false,
|
||||
percentage: 0,
|
||||
|
|
@ -1516,11 +1578,11 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
progress: {
|
||||
...currentProgress,
|
||||
postprocess:
|
||||
this.#postProcessors.size > 0
|
||||
? {
|
||||
mode: 'indeterminate',
|
||||
}
|
||||
: undefined,
|
||||
this.#postProcessors.size > 0 ?
|
||||
{
|
||||
mode: 'indeterminate',
|
||||
}
|
||||
: undefined,
|
||||
uploadComplete: true,
|
||||
percentage: 100,
|
||||
bytesUploaded: currentProgress.bytesTotal,
|
||||
|
|
@ -1644,7 +1706,7 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
|
||||
#updateOnlineStatus = this.updateOnlineStatus.bind(this)
|
||||
|
||||
getID(): UppyOptions<M, B>['id'] {
|
||||
getID(): string {
|
||||
return this.opts.id
|
||||
}
|
||||
|
||||
|
|
@ -1827,7 +1889,7 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
* Passes messages to a function, provided in `opts.logger`.
|
||||
* If `opts.logger: Uppy.debugLogger` or `opts.debug: true`, logs to the browser console.
|
||||
*/
|
||||
log(message: string | Record<string, unknown> | Error, type?: string): void {
|
||||
log(message: string | Record<any, any> | Error, type?: string): void {
|
||||
const { logger } = this.opts
|
||||
switch (type) {
|
||||
case 'error':
|
||||
|
|
@ -1859,7 +1921,7 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
}
|
||||
|
||||
/** @protected */
|
||||
getRequestClientForFile(file: UppyFile<M, B>): unknown {
|
||||
getRequestClientForFile<Client>(file: UppyFile<M, B>): Client {
|
||||
if (!file.remote)
|
||||
throw new Error(
|
||||
`Tried to get RequestClient for a non-remote file ${file.id}`,
|
||||
|
|
@ -1871,7 +1933,7 @@ export class Uppy<M extends Meta, B extends Body> {
|
|||
throw new Error(
|
||||
`requestClientId "${file.remote.requestClientId}" not registered for file "${file.id}"`,
|
||||
)
|
||||
return requestClient
|
||||
return requestClient as Client
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
export { default } from './Uppy.ts'
|
||||
export { default as Uppy } from './Uppy.ts'
|
||||
export {
|
||||
default as Uppy,
|
||||
type State,
|
||||
type UnknownPlugin,
|
||||
type UnknownProviderPlugin,
|
||||
type UnknownSearchProviderPlugin,
|
||||
type UploadResult,
|
||||
type UppyEventMap,
|
||||
} from './Uppy.ts'
|
||||
export { default as UIPlugin } from './UIPlugin.ts'
|
||||
export { default as BasePlugin } from './BasePlugin.ts'
|
||||
export { debugLogger } from './loggers.ts'
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
/* eslint-disable no-console */
|
||||
import getTimeStamp from '@uppy/utils/lib/getTimeStamp'
|
||||
|
||||
|
|
|
|||
|
|
@ -6,16 +6,16 @@
|
|||
"paths": {
|
||||
"@uppy/store-default": ["../store-default/src/index.js"],
|
||||
"@uppy/store-default/lib/*": ["../store-default/src/*"],
|
||||
"@uppy/utils/lib/*": ["../utils/src/*"]
|
||||
}
|
||||
"@uppy/utils/lib/*": ["../utils/src/*"],
|
||||
},
|
||||
},
|
||||
"include": ["./package.json", "./src/**/*.*"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../store-default/tsconfig.build.json"
|
||||
"path": "../store-default/tsconfig.build.json",
|
||||
},
|
||||
{
|
||||
"path": "../utils/tsconfig.build.json"
|
||||
}
|
||||
]
|
||||
"path": "../utils/tsconfig.build.json",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,28 @@
|
|||
# @uppy/dashboard
|
||||
|
||||
## 3.7.4
|
||||
|
||||
Released: 2024-02-22
|
||||
Included in: Uppy v3.22.2
|
||||
|
||||
- @uppy/dashboard: MetaEditor + ImageEditor - new state machine logic (Evgenia Karunus / #4939)
|
||||
|
||||
## 3.7.3
|
||||
|
||||
Released: 2024-02-20
|
||||
Included in: Uppy v3.22.1
|
||||
|
||||
- @uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/status-bar: bump `@transloadit/prettier-bytes` (Antoine du Hamel / #4933)
|
||||
|
||||
## 3.7.2
|
||||
|
||||
Released: 2024-02-19
|
||||
Included in: Uppy v3.22.0
|
||||
|
||||
- @uppy/dashboard: autoopenfileeditor - rename "edit file" to "edit image" (evgenia karunus / #4925)
|
||||
- @uppy/dashboard: Uncouple native camera and video buttons from the `disableLocalFiles` option (jake mcallister / #4894)
|
||||
- @uppy/dashboard: fix `typeerror` when `file.remote` is nullish (antoine du hamel / #4825)
|
||||
|
||||
## 3.7.1
|
||||
|
||||
Released: 2023-11-12
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@uppy/dashboard",
|
||||
"description": "Universal UI plugin for Uppy.",
|
||||
"version": "3.7.1",
|
||||
"version": "3.7.5",
|
||||
"license": "MIT",
|
||||
"main": "lib/index.js",
|
||||
"style": "dist/style.min.css",
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
"url": "git+https://github.com/transloadit/uppy.git"
|
||||
},
|
||||
"dependencies": {
|
||||
"@transloadit/prettier-bytes": "^0.2.0",
|
||||
"@transloadit/prettier-bytes": "^0.3.0",
|
||||
"@uppy/informer": "workspace:^",
|
||||
"@uppy/provider-views": "workspace:^",
|
||||
"@uppy/status-bar": "workspace:^",
|
||||
|
|
|
|||
|
|
@ -208,6 +208,24 @@ export default class Dashboard extends UIPlugin {
|
|||
})
|
||||
}
|
||||
|
||||
closeFileEditor = () => {
|
||||
const { metaFields } = this.getPluginState()
|
||||
const isMetaEditorEnabled = metaFields && metaFields.length > 0
|
||||
|
||||
if (isMetaEditorEnabled) {
|
||||
this.setPluginState({
|
||||
showFileEditor: false,
|
||||
activeOverlayType: 'FileCard'
|
||||
})
|
||||
} else {
|
||||
this.setPluginState({
|
||||
showFileEditor: false,
|
||||
fileCardFor: null,
|
||||
activeOverlayType: 'AddFiles'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
saveFileEditor = () => {
|
||||
const { targets } = this.getPluginState()
|
||||
const editors = this.#getEditors(targets)
|
||||
|
|
@ -216,7 +234,7 @@ export default class Dashboard extends UIPlugin {
|
|||
this.uppy.getPlugin(editor.id).save()
|
||||
})
|
||||
|
||||
this.hideAllPanels()
|
||||
this.closeFileEditor()
|
||||
}
|
||||
|
||||
openModal = () => {
|
||||
|
|
@ -730,7 +748,14 @@ export default class Dashboard extends UIPlugin {
|
|||
|
||||
#openFileEditorWhenFilesAdded = (files) => {
|
||||
const firstFile = files[0]
|
||||
if (this.canEditFile(firstFile)) {
|
||||
|
||||
const {metaFields} = this.getPluginState()
|
||||
const isMetaEditorEnabled = metaFields && metaFields.length > 0
|
||||
const isFileEditorEnabled = this.canEditFile(firstFile)
|
||||
|
||||
if (isMetaEditorEnabled) {
|
||||
this.toggleFileCard(true, firstFile.id)
|
||||
} else if (isFileEditorEnabled) {
|
||||
this.openFileEditor(firstFile)
|
||||
}
|
||||
}
|
||||
|
|
@ -753,7 +778,6 @@ export default class Dashboard extends UIPlugin {
|
|||
this.uppy.on('plugin-remove', this.removeTarget)
|
||||
this.uppy.on('file-added', this.hideAllPanels)
|
||||
this.uppy.on('dashboard:modal-closed', this.hideAllPanels)
|
||||
this.uppy.on('file-editor:complete', this.hideAllPanels)
|
||||
this.uppy.on('complete', this.handleComplete)
|
||||
|
||||
this.uppy.on('files-added', this.#generateLargeThumbnailIfSingleFile)
|
||||
|
|
@ -787,7 +811,6 @@ export default class Dashboard extends UIPlugin {
|
|||
this.uppy.off('plugin-remove', this.removeTarget)
|
||||
this.uppy.off('file-added', this.hideAllPanels)
|
||||
this.uppy.off('dashboard:modal-closed', this.hideAllPanels)
|
||||
this.uppy.off('file-editor:complete', this.hideAllPanels)
|
||||
this.uppy.off('complete', this.handleComplete)
|
||||
|
||||
this.uppy.off('files-added', this.#generateLargeThumbnailIfSingleFile)
|
||||
|
|
@ -954,6 +977,7 @@ export default class Dashboard extends UIPlugin {
|
|||
activePickerPanel: pluginState.activePickerPanel,
|
||||
showFileEditor: pluginState.showFileEditor,
|
||||
saveFileEditor: this.saveFileEditor,
|
||||
closeFileEditor: this.closeFileEditor,
|
||||
disableInteractiveElements: this.disableInteractiveElements,
|
||||
animateOpenClose: this.opts.animateOpenClose,
|
||||
isClosing: pluginState.isClosing,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue