Merge branch 'main' into ai-image-generator

* main: (328 commits)
  Remove temporary migration.md
  @uppy/examples: fix .stackblitzrc install command (#5919)
  [ci] release (#5918)
  Fix changeset for @uppy/utils major
  Fix changeset
  Update paths-ignore in ci.yml (#5914)
  @uppy/examples: fix workspace dependencies for StackBlitz environment (#5910)
  Ignore angular & @uppy-dev/dev for changesets
  fixup! Fix changesets
  Fix changesets
  Remove example-redux from .changesets/config
  Fix Companion build output and Dockerfile (#5917)
  Remove example-react-native-expo from .changeset/config.json
  Hopefully fix playwright in CI
  update use external sync store , fix yarn warnings
  @uppy/svelte: rename "check" script to "typecheck"
  remove chooseFiles from all the locales
  remove dropHereOr from all the locales
  update yarnrc
  lint fix
  ...
This commit is contained in:
Murderlon 2025-08-25 15:17:37 +02:00
commit d6fa6836e4
No known key found for this signature in database
GPG key ID: 1FF861FF1DDBB953
1301 changed files with 36347 additions and 51080 deletions

8
.changeset/README.md Normal file
View file

@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

33
.changeset/config.json Normal file
View file

@ -0,0 +1,33 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
"onlyUpdatePeerDependentsWhenOutOfRange": true
},
"ignore": [
"angular",
"@uppy-dev/dev",
"example-angular",
"example-aws-companion",
"example-aws-nodejs",
"example-aws-php",
"example-cdn",
"example-companion",
"example-companion-custom-provider",
"example-companion-digitalocean-spaces",
"example-react",
"example-sveltekit",
"example-transloadit",
"example-vue",
"example-xhr-bundle",
"example-xhr-node",
"example-xhr-php",
"example-xhr-python"
]
}

View file

@ -0,0 +1,114 @@
---
description:
globs: packages/@uppy/components/src/**
alwaysApply: false
---
# Headless components
You are an expert at making headless UI components in Preact, similar to libraries like shadcn, except we don't rely on packages like radix.
## Goal
Making headless components in Preact for the open source Uppy file uploader and framework specific hooks. We want to give flexbility to users of this library by rendering sensible UI defaults and hooks that abstract Uppy functionality to completely build your own UI.
Another way to give flexibility is to add data attributes selectively to some HTML elements. These can be used with CSS selectors by users of this library to conditionally apply styles:
```html
<button
type="button"
data-uppy-element="upload-button"
data-state={ctx.status}
></button>
```
## How to build these components
It's important to understand that an automated build script (bin/build-components.mjs) generates framework-specific wrappers for these Preact components.
Here is an example from the React wrapper created by the script.
```tsx
import { useEffect, useRef, useContext, createElement as h } from 'react'
import {
Dropzone as PreactDropzone,
type DropzoneProps,
} from '@uppy/components'
import { h as preactH } from 'preact'
import { render as preactRender } from 'preact/compat'
import { UppyContext } from './UppyContextProvider.js'
export default function Dropzone(props: Omit<DropzoneProps, 'ctx' | 'render'>) {
const ref = useRef(null)
const ctx = useContext(UppyContext)
useEffect(() => {
if (ref.current) {
preactRender(
preactH(PreactDropzone, {
...props,
ctx,
} satisfies DropzoneProps),
ref.current,
)
}
}, [ctx, props])
return <div ref={ref} />
}
```
You don't have to worry about how these wrappers are created but it's important to know when building the Preact components that you will always receive a `ctx` prop to work with. This is its type:
```ts
import type Uppy from '@uppy/core'
export type UploadStatus =
| 'init'
| 'ready'
| 'uploading'
| 'paused'
| 'error'
| 'complete'
export type UppyContext = {
uppy: Uppy | undefined
status: UploadStatus
progress: number
}
```
## Styling
Styling is done with Tailwind CSS 4.x. There is no Tailwind config file.
**IMPORTANT**: all classes have the `uppy:` prefix. Example: `bg-red-500` should become `uppy:bg-red-500`.
Use `clsx` for conditional styles.
```js
import clsx from 'clsx';
// or
import { clsx } from 'clsx';
// Strings (variadic)
clsx('foo', true && 'bar', 'baz');
//=> 'foo bar baz'
// Objects
clsx({ foo:true, bar:false, baz:isTrue() });
//=> 'foo baz'
// Objects (variadic)
clsx({ foo:true }, { bar:false }, null, { '--foobar':'hello' });
//=> 'foo --foobar'
// Arrays
clsx(['foo', 0, false, 'bar']);
//=> 'foo bar'
// Arrays (variadic)
clsx(['foo'], ['', 0, false, 'bar'], [['baz', [['hello'], 'there']]]);
//=> 'foo bar baz hello there'
// Kitchen sink (with nesting)
clsx('foo', [1 && 'bar', { baz:false, bat:null }, ['hello', ['world']]], 'cya');
//=> 'foo bar hello world cya'
```

78
.cursor/rules/svelte.mdc Normal file
View file

@ -0,0 +1,78 @@
---
description:
globs: packages/@uppy/svelte/**,examples/sveltekit/**
alwaysApply: false
---
I'm using svelte 5 instead of svelte 4 here is an overview of the changes.
## Overview of Changes
Svelte 5 introduces runes, a set of advanced primitives for controlling reactivity. The runes replace certain non-runes features and provide more explicit control over state and effects.
Snippets, along with render tags, help create reusable chunks of markup inside your components, reducing duplication and enhancing maintainability.
## Event Handlers in Svelte 5
In Svelte 5, event handlers are treated as standard HTML properties rather than Svelte-specific directives, simplifying their use and integrating them more closely with the rest of the properties in the component.
### Svelte 4 vs. Svelte 5:
**Before (Svelte 4):**
```html
<script>
let count = 0;
$: double = count * 2;
$: {
if (count > 10) alert('Too high!');
}
</script>
<button on:click={() => count++}> {count} / {double}</button>
```
**After (Svelte 5):**
```html
<script>
import { $state, $effect, $derived } from 'svelte';
// Define state with runes
let count = $state(0);
// Option 1: Using $derived for computed values
let double = $derived(count * 2);
// Reactive effects using runes
$effect(() => {
if (count > 10) alert('Too high!');
});
</script>
<!-- Standard HTML event attributes instead of Svelte directives -->
<button onclick={() => count++}>
{count} / {double}
</button>
<!-- Alternatively, you can compute values inline -->
<!-- <button onclick={() => count++}>
{count} / {count * 2}
</button> -->
```
## Key Differences:
1. **Reactivity is Explicit**:
- Svelte 5 uses `$state()` to explicitly mark reactive variables
- `$derived()` replaces `$:` for computed values
- `$effect()` replaces `$: {}` blocks for side effects
2. **Event Handling is Standardized**:
- Svelte 4: `on:click={handler}`
- Svelte 5: `onclick={handler}`
3. **Import Runes**:
- All runes must be imported from 'svelte': `import { $state, $effect, $derived, $props, $slots } from 'svelte';`
4. **No More Event Modifiers**:
- Svelte 4: `on:click|preventDefault={handler}`
- Svelte 5: `onclick={e => { e.preventDefault(); handler(e); }}`
This creates clearer, more maintainable components compared to Svelte 4's previous syntax by making reactivity explicit and using standardized web platform features.

149
.cursor/rules/uppy-core.mdc Normal file
View file

@ -0,0 +1,149 @@
---
description: Read this when you are about to use the Uppy class from @uppy/core
globs:
alwaysApply: false
---
# Uppy Core Summary
If you need to reference or work with the `Uppy` class from `@uppy/core`, here is an high-level overview of the class.
## `Uppy<M extends Meta, B extends Body>` Class
A modular file uploader. Manages plugins, state, events, and file handling.
**Generics:**
* `M`: Metadata object shape associated with files (`Meta`).
* `B`: Body object shape associated with files (`Body`).
**Key Public Properties:**
* `opts: NonNullableUppyOptions<M, B>`: The fully resolved Uppy options.
* `store: Store<State<M, B>>`: The state management store (defaults to `DefaultStore`).
* `i18n: I18n`: The translation function.
* `i18nArray: Translator['translateArray']`: The array translation function.
* `locale: Locale`: The active locale object.
**Constructor:**
* `constructor(opts?: UppyOptionsWithOptionalRestrictions<M, B>)`
**Core Public Methods:**
* `use<T extends typeof BasePlugin<any, M, B>>(Plugin: T, ...args: OmitFirstArg<ConstructorParameters<T>>): this`: Adds a plugin instance.
* `getPlugin<T extends UnknownPlugin<M, B> = UnknownPlugin<M, B>>(id: string): T | undefined`: Retrieves a plugin instance by ID.
* `iteratePlugins(method: (plugin: UnknownPlugin<M, B>) => void): void`: Executes a function on all installed plugins.
* `removePlugin(instance: UnknownPlugin<M, B>): void`: Removes a plugin instance.
* `addFile(file: File | MinimalRequiredUppyFile<M, B>): UppyFile<M, B>['id']`: Adds a single file.
* `addFiles(fileDescriptors: MinimalRequiredUppyFile<M, B>[]): void`: Adds multiple files.
* `removeFile(fileID: string): void`: Removes a file by ID.
* `removeFiles(fileIDs: string[]): void`: Removes multiple files by ID.
* `getFile(fileID: string): UppyFile<M, B>`: Retrieves a file object by ID.
* `getFiles(): UppyFile<M, B>[]`: Retrieves all file objects as an array.
* `setOptions(newOpts: MinimalRequiredOptions<M, B>): void`: Updates Uppy options.
* `setState(patch?: Partial<State<M, B>>): void`: Updates the Uppy state.
* `getState(): State<M, B>`: Retrieves the current Uppy state.
* `setFileState(fileID: string, state: Partial<UppyFile<M, B>>): void`: Updates the state for a specific file.
* `setMeta(data: Partial<M>): void`: Merges metadata into the global `state.meta` and all file `meta` objects.
* `setFileMeta(fileID: string, data: Partial<M>): void`: Merges metadata into a specific file's `meta` object.
* `upload(): Promise<NonNullable<UploadResult<M, B>> | undefined>`: Starts the upload process for all new files.
* `retryUpload(fileID: string): Promise<UploadResult<M, B> | undefined>`: Retries a failed upload for a specific file.
* `retryAll(): Promise<UploadResult<M, B> | undefined>`: Retries all failed uploads.
* `cancelAll(): void`: Cancels all uploads and removes all files.
* `pauseResume(fileID: string): boolean | undefined`: Toggles pause/resume state for a resumable upload.
* `pauseAll(): void`: Pauses all resumable uploads.
* `resumeAll(): void`: Resumes all paused uploads.
* `info(message: string | { message: string; details?: string | Record<string, string> }, type?: LogLevel, duration?: number): void`: Displays an informational message via UI plugins.
* `hideInfo(): void`: Hides the oldest info message.
* `log(message: unknown, type?: 'error' | 'warning'): void`: Logs a message using the configured logger.
* `on<K extends keyof UppyEventMap<M, B>>(event: K, callback: UppyEventMap<M, B>[K]): this`: Registers an event listener.
* `off<K extends keyof UppyEventMap<M, B>>(event: K, callback: UppyEventMap<M, B>[K]): this`: Unregisters an event listener.
* `emit<T extends keyof UppyEventMap<M, B>>(event: T, ...args: Parameters<UppyEventMap<M, B>[T]>): void`: Emits an event.
* `destroy(): void`: Uninstalls all plugins and cleans up the Uppy instance.
**Internal Lifecycle Methods (Called by Uppy):**
* `addPreProcessor(fn: Processor): void`
* `removePreProcessor(fn: Processor): boolean`
* `addPostProcessor(fn: Processor): void`
* `removePostProcessor(fn: Processor): boolean`
* `addUploader(fn: Processor): void`
* `removeUploader(fn: Processor): boolean`
---
## Key Associated Types
* **`UppyFile<M extends Meta, B extends Body>`**: Represents a file within Uppy.
* `id: string`: Unique file ID.
* `source?: string`: Source plugin ID.
* `name: string`: File name.
* `type?: string`: MIME type.
* `data: Blob | File`: The actual file data.
* `meta: M & { name: string, type: string }`: User and Uppy metadata.
* `size: number | null`: File size in bytes.
* `isRemote: boolean`: If the file is from a remote source (Companion).
* `remote?: { requestClientId: string, ... }`: Remote source details.
* `progress: FileProgressStarted | FileProgressNotStarted`: Upload progress state.
* `error?: string`: Error message if upload failed.
* `isPaused?: boolean`: If upload is paused (for resumable uploads).
* `isGhost?: boolean`: If file is restored without data (Golden Retriever).
* `response?: { status: number, body: B, uploadURL?: string, ... }`: Upload response details.
* `preview?: string`: URL to a preview image.
* `[key: string]: any`: Extensible with plugin-specific state.
* **`State<M extends Meta, B extends Body>`**: The main Uppy state object.
* `files: { [fileID: string]: UppyFile<M, B> }`: Object map of files by ID.
* `currentUploads: { [uploadID: string]: CurrentUpload<M, B> }`: Map of active uploads.
* `capabilities: { uploadProgress: boolean, individualCancellation: boolean, resumableUploads: boolean, ... }`: Detected/configured capabilities.
* `totalProgress: number`: Overall upload progress (0-100).
* `meta: M`: Global metadata.
* `info: Array<{ type: LogLevel, message: string, details?: string | Record<string, string> }>`: Info messages for UI display.
* `error: string | null`: Global error message.
* `allowNewUpload: boolean`: Whether new uploads can be started.
* `plugins: { [pluginID: string]: Record<string, unknown> }`: State managed by plugins.
* `recoveredState: null | { files: ..., currentUploads: ... }`: State recovered by Golden Retriever.
* **`UppyOptions<M extends Meta, B extends Body>`**: Options for the Uppy constructor.
* `id?: string`: Instance ID (default: 'uppy').
* `autoProceed?: boolean`: Start upload automatically after files are added (default: false).
* `allowMultipleUploadBatches?: boolean`: Allow adding files while an upload is in progress (default: true).
* `debug?: boolean`: Enable debug logging (default: false).
* `restrictions: Restrictions`: File restrictions (type, size, number).
* `meta?: M`: Initial global metadata.
* `onBeforeFileAdded?: (currentFile: UppyFile<M, B>, files: { ... }) => UppyFile<M, B> | boolean | undefined`: Hook before a file is added.
* `onBeforeUpload?: (files: { ... }) => { ... } | boolean`: Hook before an upload starts.
* `locale?: Locale`: Custom locale strings.
* `store?: Store<State<M, B>>`: Custom state store.
* `logger?: { debug, warn, error }`: Custom logger.
* `infoTimeout?: number`: Duration for info messages (ms).
* **`Restrictions`**: File restriction options.
* `maxFileSize?: number | null`: Max individual file size (bytes).
* `minFileSize?: number | null`: Min individual file size (bytes).
* `maxTotalFileSize?: number | null`: Max total size of all files (bytes).
* `maxNumberOfFiles?: number | null`: Max number of files allowed.
* `minNumberOfFiles?: number | null`: Min number of files required.
* `allowedFileTypes?: string[] | null`: Allowed MIME types or extensions (e.g., `['image/*', '.pdf']`).
* `requiredMetaFields?: string[]`: Meta fields that must be present before upload.
* **`UppyEventMap<M extends Meta, B extends Body>`**: Map of event names to their callback signatures (includes events like `file-added`, `upload-progress`, `upload-success`, `complete`, `error`, `restriction-failed`, etc.).
* **`UploadResult<M extends Meta, B extends Body>`**: The object returned when an upload completes.
* `successful?: UppyFile<M, B>[]`: Array of successfully uploaded files.
* `failed?: UppyFile<M, B>[]`: Array of files that failed to upload.
* `uploadID?: string`: The ID of the upload batch.
* **`BasePlugin<...> `**: The base class for all Uppy plugins.
* `id: string`: Unique plugin ID.
* `type: string`: Plugin type ('acquirer', 'modifier', 'uploader', 'presenter', 'orchestrator', 'logger').
* `uppy: Uppy<M, B>`: Reference to the Uppy instance.
* `install(): void`: Called when the plugin is added.
* `uninstall(): void`: Called when the plugin is removed.
* `update(state: Partial<State<M, B>>): void`: Called on every Uppy state update.
* **`Processor`**: Type for pre/post/upload processor functions `(fileIDs: string[], uploadID: string) => Promise<unknown> | void`.
* **`LogLevel`**: `'info' | 'warning' | 'error' | 'success'`.
* **`PartialTree`**: Type used by Provider plugins (like Google Drive) to represent folder structures (`(PartialTreeFile | PartialTreeFolder)[]`).

View file

@ -15,6 +15,9 @@ COMPANION_PREAUTH_SECRET=development2
# NOTE: Only enable this in development. Enabling it in production is a security risk
COMPANION_ALLOW_LOCAL_URLS=true
COMPANION_ENABLE_URL_ENDPOINT=true
COMPANION_ENABLE_GOOGLE_PICKER_ENDPOINT=true
# to enable S3
COMPANION_AWS_KEY="YOUR AWS KEY"
COMPANION_AWS_SECRET="YOUR AWS SECRET"
@ -89,3 +92,10 @@ VITE_TRANSLOADIT_TEMPLATE=***
VITE_TRANSLOADIT_SERVICE_URL=https://api2.transloadit.com
# Fill in if you want requests sent to Transloadit to be signed:
# VITE_TRANSLOADIT_SECRET=***
# For Google Photos Picker and Google Drive Picker:
VITE_GOOGLE_PICKER_CLIENT_ID=***
# For Google Drive Picker
VITE_GOOGLE_PICKER_API_KEY=***
VITE_GOOGLE_PICKER_APP_ID=***

View file

@ -1,8 +0,0 @@
node_modules
lib
dist
coverage
test/lib/**
test/endtoend/*/build
examples/svelte-example/public/build/
bundle-legacy.js

View file

@ -1,523 +0,0 @@
/* eslint-disable quote-props */
'use strict'
const svgPresentationAttributes = [
'alignment-baseline', 'baseline-shift', 'class', 'clip', 'clip-path', 'clip-rule', 'color', 'color-interpolatio', 'color-interpolatio-filters', 'color-profile', 'color-rendering', 'cursor', 'direction', 'display', 'dominant-baseline', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'image-rendering', 'kerning', 'letter-spacing', 'lighting-color', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'overflow', 'pointer-events', 'shape-rendering', 'stop-color', 'stop-opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-decoration', 'text-rendering', 'transform', 'transform-origin', 'unicode-bidi', 'vector-effect', 'visibility', 'word-spacing', 'writing-mod',
]
module.exports = {
root: true,
extends: ['transloadit', 'prettier'],
env: {
es6: true,
jest: true,
node: true,
// extra:
browser: true,
},
globals: {
globalThis: true,
hexo: true,
window: true,
},
plugins: [
'@babel/eslint-plugin',
'jest',
'markdown',
'node',
'prefer-import',
'promise',
'react',
// extra:
'compat',
'jsdoc',
'no-only-tests',
'unicorn',
],
parser: '@babel/eslint-parser',
parserOptions: {
sourceType: 'script',
ecmaVersion: 2022,
ecmaFeatures: {
jsx: true,
},
},
rules: {
// transloadit rules we are actually ok with in the uppy repo
'import/extensions': 'off',
'object-shorthand': ['error', 'always'],
'strict': 'off',
'key-spacing': 'off',
'max-classes-per-file': ['error', 2],
'react/no-unknown-property': ['error', {
ignore: svgPresentationAttributes,
}],
// Special rules for CI:
...(process.env.CI && {
// Some imports are available only after a full build, which we don't do on CI.
'import/no-unresolved': 'off',
}),
// rules we want to enforce
'array-callback-return': 'error',
'func-names': 'error',
'import/no-dynamic-require': 'error',
'import/no-extraneous-dependencies': 'error',
'max-len': 'error',
'no-empty': 'error',
'no-bitwise': 'error',
'no-continue': 'error',
'no-lonely-if': 'error',
'no-nested-ternary': 'error',
'no-restricted-properties': 'error',
'no-return-assign': 'error',
'no-underscore-dangle': 'error',
'no-unused-expressions': 'error',
'no-unused-vars': 'error',
'no-useless-concat': 'error',
'no-var': 'error',
'node/handle-callback-err': 'error',
'prefer-destructuring': 'error',
'prefer-spread': 'error',
'unicorn/prefer-node-protocol': 'error',
'react/button-has-type': 'error',
'react/forbid-prop-types': 'error',
'react/no-access-state-in-setstate': 'error',
'react/no-array-index-key': 'error',
'react/no-deprecated': 'error',
'react/no-this-in-sfc': 'error',
'react/no-will-update-set-state': 'error',
'react/prefer-stateless-function': 'error',
'react/require-default-props': ['error', {
forbidDefaultForRequired: true,
functions: 'ignore',
}],
'react/sort-comp': 'error',
'react/static-property-placement': 'off',
'react/style-prop-object': 'error',
// accessibility
'jsx-a11y/alt-text': 'error',
'jsx-a11y/anchor-has-content': 'error',
'jsx-a11y/click-events-have-key-events': 'error',
'jsx-a11y/control-has-associated-label': 'error',
'jsx-a11y/label-has-associated-control': 'error',
'jsx-a11y/media-has-caption': 'error',
'jsx-a11y/mouse-events-have-key-events': 'error',
'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
'jsx-a11y/no-noninteractive-element-interactions': 'error',
'jsx-a11y/no-static-element-interactions': 'error',
// compat
'compat/compat': ['error'],
// jsdoc
'jsdoc/check-alignment': 'error',
'jsdoc/check-examples': 'off', // cannot yet be supported for ESLint 8, see https://github.com/eslint/eslint/issues/14745
'jsdoc/check-param-names': 'error',
'jsdoc/check-syntax': 'error',
'jsdoc/check-tag-names': ['error', { jsxTags: true }],
'jsdoc/check-types': 'error',
'jsdoc/valid-types': 'error',
'jsdoc/check-indentation': ['off'],
},
settings: {
'import/core-modules': ['tsd'],
react: {
pragma: 'h',
},
jsdoc: {
mode: 'typescript',
},
polyfills: [
'Promise',
'fetch',
'Object.assign',
'document.querySelector',
],
},
overrides: [
{
files: [
'*.jsx',
'*.tsx',
'packages/@uppy/react-native/**/*.js',
],
parser: 'espree',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
rules: {
'no-restricted-globals': [
'error',
{
name: '__filename',
message: 'Use import.meta.url instead',
},
{
name: '__dirname',
message: 'Not available in ESM',
},
{
name: 'exports',
message: 'Not available in ESM',
},
{
name: 'module',
message: 'Not available in ESM',
},
{
name: 'require',
message: 'Use import instead',
},
{
name: 'JSX',
message: 'Use h.JSX.Element, ComponentChild, or ComponentChildren from Preact',
},
{
name: 'React',
message: 'Import the value instead of relying on global.React.',
},
],
'import/extensions': ['error', 'ignorePackages'],
},
},
{
files: [
'*.mjs',
'e2e/clients/**/*.js',
'examples/aws-companion/*.js',
'examples/aws-php/*.js',
'examples/bundled/*.js',
'examples/custom-provider/client/*.js',
'examples/digitalocean-spaces/*.js',
'examples/multiple-instances/*.js',
'examples/node-xhr/*.js',
'examples/php-xhr/*.js',
'examples/python-xhr/*.js',
'examples/react-example/*.js',
'examples/redux/*.js',
'examples/transloadit/*.js',
'examples/transloadit-markdown-bin/*.js',
'examples/xhr-bundle/*.js',
'private/dev/*.js',
'private/release/*.js',
'private/remark-lint-uppy/*.js',
'packages/@uppy/!(companion|react-native)/**/*.js',
],
parser: 'espree',
parserOptions: {
sourceType: 'module',
ecmaFeatures: {
jsx: false,
},
},
rules: {
'import/named': 'off', // Disabled because that rule tries and fails to parse JSX dependencies.
'import/no-named-as-default': 'off', // Disabled because that rule tries and fails to parse JSX dependencies.
'import/no-named-as-default-member': 'off', // Disabled because that rule tries and fails to parse JSX dependencies.
'no-restricted-globals': [
'error',
{
name: '__filename',
message: 'Use import.meta.url instead',
},
{
name: '__dirname',
message: 'Not available in ESM',
},
{
name: 'exports',
message: 'Not available in ESM',
},
{
name: 'module',
message: 'Not available in ESM',
},
{
name: 'require',
message: 'Use import instead',
},
{
name: 'JSX',
message: 'Use h.JSX.Element, ComponentChild, or ComponentChildren from Preact',
},
{
name: 'React',
message: 'Import the value instead of relying on global.React.',
},
],
'import/extensions': ['error', 'ignorePackages'],
},
},
{
files: ['packages/uppy/*.mjs'],
rules: {
'import/first': 'off',
'import/newline-after-import': 'off',
'import/no-extraneous-dependencies': ['error', {
devDependencies: true,
}],
},
},
{
files: [
'packages/@uppy/*/types/*.d.ts',
],
rules : {
'import/no-unresolved': 'off',
'max-classes-per-file': 'off',
'no-use-before-define': 'off',
},
},
{
files: [
'packages/@uppy/dashboard/src/components/**/*.jsx',
],
rules: {
'react/destructuring-assignment': 'off',
},
},
{
files: [
// Those need looser rules, and cannot be made part of the stricter rules above.
// TODO: update those to more modern code when switch to ESM is complete
'examples/react-native-expo/*.js',
'examples/svelte-example/**/*.js',
'examples/vue/**/*.js',
'examples/vue3/**/*.js',
],
rules: {
'no-unused-vars': [
'error',
{
'varsIgnorePattern': 'React',
},
],
},
parserOptions: {
sourceType: 'module',
},
},
{
files: ['./packages/@uppy/companion/**/*.js'],
rules: {
'no-underscore-dangle': 'off',
},
},
{
files: [
'*.test.js',
'*.test.ts',
'test/endtoend/*.js',
'bin/**.js',
],
rules: {
'compat/compat': ['off'],
},
},
{
files: [
'bin/**.js',
'bin/**.mjs',
'examples/**/*.cjs',
'examples/**/*.js',
'packages/@uppy/companion/test/**/*.js',
'test/**/*.js',
'test/**/*.ts',
'*.test.js',
'*.test.ts',
'*.test-d.ts',
'*.test-d.tsx',
'postcss.config.js',
'.eslintrc.js',
'private/**/*.js',
'private/**/*.mjs',
],
rules: {
'no-console': 'off',
'import/no-extraneous-dependencies': ['error', {
devDependencies: true,
}],
},
},
{
files: [
'packages/@uppy/locales/src/*.js',
'packages/@uppy/locales/template.js',
],
rules: {
camelcase: ['off'],
'quote-props': ['error', 'as-needed', { 'numbers': true }],
},
},
{
files: ['test/endtoend/*/*.mjs', 'test/endtoend/*/*.ts'],
rules: {
// we mostly import @uppy stuff in these files.
'import/no-extraneous-dependencies': ['off'],
},
},
{
files: ['test/endtoend/*/*.js'],
env: {
mocha: true,
},
},
{
files: ['packages/@uppy/react/src/**/*.js'],
rules: {
'import/no-extraneous-dependencies': ['error', {
peerDependencies: true,
}],
},
},
{
files: ['**/*.md', '*.md'],
processor: 'markdown/markdown',
},
{
files: ['docs/**/*.md/*.js'],
parserOptions: {
sourceType: 'module',
},
},
{
files: ['**/*.md/*.js', '**/*.md/*.javascript'],
excludedFiles: ["docs/**/*"],
parserOptions: {
sourceType: 'module',
},
rules: {
'react/destructuring-assignment': 'off',
'no-restricted-globals': [
'error',
{
name: '__filename',
message: 'Use import.meta.url instead',
},
{
name: '__dirname',
message: 'Not available in ESM',
},
{
name: 'exports',
message: 'Not available in ESM',
},
{
name: 'module',
message: 'Not available in ESM',
},
{
name: 'require',
message: 'Use import instead',
},
],
},
},
{
files: ['**/*.ts', '**/*.md/*.ts', '**/*.md/*.typescript', '**/*.tsx', '**/*.md/*.tsx'],
excludedFiles: ['examples/angular-example/**/*.ts', 'packages/@uppy/angular/**/*.ts'],
parser: '@typescript-eslint/parser',
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
},
},
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'no-extra-semi': 'off',
'no-restricted-syntax': ['error', {
selector: 'ImportDeclaration[importKind="type"][source.value=/^@uppy\\x2F[a-z-0-9]+\\x2F/]:not([source.value=/^@uppy\\x2Futils\\x2F/]):not([source.value=/\\.js$/])',
message: 'Use ".js" file extension for import type declarations from a different package',
}, {
selector: 'ImportDeclaration[importKind="type"][source.value=/^\\.\\.?\\x2F.+\\.js$/]',
message: 'Do not use ".js" file extension for relative import type declarations',
}, {
selector: 'ImportDeclaration[source.value=/^@uppy\\x2Futils\\x2Flib\\x2F.+\\.[mc]?[jt]sx?$/]',
message: 'Do not use file extension when importing from @uppy/utils',
}],
'import/extensions': ['error', 'ignorePackages'],
'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',
},
},
{
files: ['packages/@uppy/*/src/**/*.ts'],
excludedFiles: ['packages/@uppy/**/*.test.ts', 'packages/@uppy/core/src/mocks/*.ts'],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 'error',
},
},
{
files: ['**/*.md/*.*'],
rules: {
'import/no-extraneous-dependencies': 'off',
'import/no-unresolved': 'off',
'no-console': 'off',
'no-undef': 'off',
'no-unused-vars': 'off',
},
},
{
files: ['**/react/*.md/*.js', '**/react.md/*.js', '**/react-*.md/*.js', '**/react/**/*.test-d.tsx'],
settings: {
react: { pragma: 'React' },
},
},
{
files: ['**/react/**/*.test-d.tsx'],
rules: {
'import/extensions': 'off',
'import/no-useless-path-segments': 'off',
'no-alert': 'off',
'no-inner-declarations': 'off',
'no-lone-blocks': 'off',
'no-unused-expressions': 'off',
'no-unused-vars': 'off',
},
},
{
files: ['e2e/**/*.ts'],
extends: ['plugin:cypress/recommended'],
},
{
files: ['e2e/**/*.ts', 'e2e/**/*.js', 'e2e/**/*.jsx', 'e2e/**/*.mjs'],
rules: {
'import/no-extraneous-dependencies': 'off',
'no-console': 'off',
'no-only-tests/no-only-tests': 'error',
'no-unused-expressions': 'off',
},
},
{
files: ["packages/@uppy/vue/**"],
rules: {
'react-hooks/rules-of-hooks': 'off',
},
},
],
}

View file

@ -7,7 +7,7 @@ instructions.
After you have successfully forked the repository, clone it locally.
```sh
git clone https://github.com/transloadit/uppy.git
git clone https://github.com/[your-username]/uppy.git
cd uppy
```
@ -24,24 +24,37 @@ corepack enable
## Development
First of all, install Uppy dependencies:
Install dependencies:
```bash
yarn install
```
### Basic
To run a basic development version of Uppy, run:
Build all packages:
```bash
yarn dev
yarn build
```
and go to http://localhost:5174 (or whatever link the yarn command outputted).
As you edit Uppy code, the browser will live reload the changes.
### Development Commands
### Companion
- `yarn dev` - Start development server at http://localhost:5174
- `yarn dev:with-companion` - Start dev server with Companion for cloud integrations
- `yarn start:companion` - Start only Companion server at http://localhost:3020
- `yarn build:watch` - Build packages in watch mode
- `yarn test` - Run tests for all packages
- `yarn test:watch` - Run tests in watch mode
- `yarn typecheck` - Run TypeScript type checking
- `yarn check` - Run Biome linting and formatting
### Headless components
When adding a new component to `@uppy/components`, you have to run `yarn migrate:components` from root
to migrate the Preact components to React, Svelte, and Vue.
This is not needed for changing existing components.
## Companion
If youd like to work on features that the basic development version of Uppy
doesnt support, such as Uppy integrations with Instagram/Google Drive/Facebook
@ -62,12 +75,6 @@ This would get the Companion instance running on `http://localhost:3020`. It
uses [nodemon](https://github.com/remy/nodemon) so it will automatically restart
when files are changed.
### Live example
An example server is running at <https://companion.uppy.io>, which is deployed
with
[Kubernetes](https://github.com/transloadit/uppy/blob/main/packages/%40uppy/companion/KUBERNETES.md)
### How the Authentication and Token mechanism works
This section describes how Authentication works between Companion and Providers.
@ -101,415 +108,6 @@ Authenticates and Uploads from Dropbox through Companion:
- Companion reports progress to Uppy, as if it were a local upload.
- Completed!
### Requiring files
- If we are `require()`ing a file from the same subpackage, we can freely use
relative imports as long as the required file is under the `src` directory
(for example to import `@uppy/dashboard/src/utils/hi.js` from
`@uppy/dashboard/src/index.js`, use `require('./utils/hi.js')`).
- But if we want to `require()` some file from another subpackage - we should
use global @uppy requires, and they should always be in the form of
`@uppy/:packageName/(lib instead of src)/(same path).js`
## Tests
### Unit tests
Unit tests are using Jest and can be run with:
```bash
yarn test:unit
```
### End-to-End tests
We use [Cypress](https://www.cypress.io/) for our e2e test suite. Be sure to
checkout
“[Writing your first test](https://docs.cypress.io/guides/getting-started/writing-your-first-test#Add-a-test-file)”
and the
“[Introduction to Cypress](https://docs.cypress.io/guides/core-concepts/introduction-to-cypress#Cypress-Can-Be-Simple-Sometimes)”.
You should also be aware of the
“[Best Practices](https://docs.cypress.io/guides/references/best-practices)”.
To get started make sure you have your `.env` set up. Copy the contents of
`.env.example` to a file named `.env` and add the values relevant for the
test(s) you are trying to run.
To start the testing suite run:
```
yarn e2e
```
This will run Cypress in watch-mode, and it will pick up and rebuild any changes
to JS files. If you need to change other files (like CSS for example), you need
to run the respective `yarn build:*` scripts.
Alternatively the following command is the same as the above, except it doesnt
run `build` first:
```
yarn e2e:skip-build
```
To generate the boilerplate for a new test run:
```
yarn e2e:generate
```
## Releases
Releases are managed by GitHub Actions, heres an overview of the process to
release a new Uppy version:
- Run `yarn release` on your local machine.
- Follow the instructions and select what packages to release. **Warning:**
skipping packages results in those changes being “lost”, meaning they wont be
picked up in the changelog automatically next release. Always try to release
all.
- Before committing, check if the generated files look good.
- When asked to edit the next CHANGELOG, only include changes related to the
package(s) you selected for release.
- Push to the Transloadit repository using the command given by the tool. Do not
open a PR yourself, the GitHub Actions will create one and assign you to it.
- Wait for all the GitHub Actions checks to pass. If one fails, try to figure
out why. Do not go ahead without consulting the rest of the team.
- Review the PR thoroughly, and if everything looks good to you, approve the PR.
Do not merge it manually!
- After the PR is automatically merged, the demos on transloadit.com should also
be updated. Check that some things work locally:
- the demos in the demo section work (try one that uses an import robot, and
one that you need to upload to)
- the demos on the homepage work and can import from Google Drive, Instagram,
Dropbox, etc.
If you dont have access to the transloadit.com source code ping @arturi or
@goto-bus-stop and well pick it up. :sparkles:
### Releasing hotfix patch
#### Companion hotfix
First checkout the tag of the version you want to patch:
```bash
git checkout @uppy/companion@x.y.z
```
Now create a branch for your hotfix:
```bash
git checkout -b x.y.z-hotfix
```
Run yarn to make sure all packages are consistent:
```bash
corepack yarn
```
Now navigate to the Companion workspace:
```bash
cd packages/@uppy/companion
```
**Now cherry pick your desired commits**.
Next edit `CHANGELOG.md` and then commit it:
```bash
git add CHANGELOG.md
git commit -m 'Update changelog'
```
Now lets create the version & tag:
```bash
mkdir -p .git && npm version --workspaces-update=false --tag-version-prefix='@uppy/companion@' patch
```
**Important:** Build Companion lib folder
```bash
yarn run build
```
Run a “dry-run” first:
```bash
corepack yarn pack
```
If the earlier command succeeded, lets publish!
```bash
corepack yarn npm publish --access public --tag=none
```
Now we can push our branch and tags.
```bash
git push && git push --tags
```
#### Hotfix other packages
For other Uppy packages, the process should be like Companion, but hasnt been
documented yet. Make sure to remember to run `yarn` as well as building the
package first, then you can release it. If you do release any other packages,
please update this doc.
## CSS guidelines
The CSS standards followed in this project closely resemble those from
[Mediums CSS Guidelines](https://gist.github.com/fat/a47b882eb5f84293c4ed). If
something is not mentioned here, follow their guidelines.
### Naming conventions
This project uses naming conventions adopted from the SUIT CSS framework.
[Read about them here](https://github.com/suitcss/suit/blob/master/doc/naming-conventions.md).
To quickly summarize:
#### Utilities
Syntax: `u-[sm-|md-|lg-]<utilityName>`
```css
.u-utilityName
.u-floatLeft
.u-lg-col6
```
#### Components
Syntax: `[<namespace>-]<ComponentName>[-descendentName][--modifierName]`
```css
.twt-Button /* Namespaced component */
.MyComponent /* Components pascal cased */
.Button--default /* Modified button style */
.Button--large
.Tweet
.Tweet-header /* Descendents */
.Tweet-bodyText
.Accordion.is-collapsed /* State of component */
.Accordion.is-expanded
```
### SASS
This project uses SASS, with some limitations on nesting. One-level-deep nesting
is allowed, but nesting may not extend a selector by using the `&` operator. For
example:
```sass
/* BAD */
.Button {
&--disabled {
...
}
}
/* GOOD */
.Button {
...
}
.Button--disabled {
...
}
```
### Mobile-first responsive approach
Style to the mobile breakpoint with your selectors, then use `min-width` media
queries to add any styles to the tablet or desktop breakpoints.
### Selector, rule ordering
- All selectors are sorted alphabetically and by type.
- HTML elements go above classes and IDs in a file.
- Rules are sorted alphabetically.
```scss
/* BAD */
.wrapper {
width: 940px;
margin: auto;
}
h1 {
color: red;
}
.article {
width: 100%;
padding: 32px;
}
/* GOOD */
h1 {
color: red;
}
.article {
padding: 32px;
width: 100%;
}
.wrapper {
margin: auto;
width: 940px;
}
```
## Adding a new integration
Before opening a pull request for the new integration, open an issue to discuss
said integration with the Uppy team. After discussing the integration, you can
get started on it. First off, you need to construct the basic components for
your integration. The following components are the current standard:
- `Dashboard`: Inline Dashboard (`inline: true`)
- `DashboardModal`: Dashboard as a modal
- `DragDrop`
- `ProgressBar`
- `StatusBar`
All these components should function as references to the normal component.
Depending on how the framework youre using handles references to the DOM, your
approach to creating these may be different. For example, in React, you can
assign a property of the component to the reference of a component
([see here](https://github.com/transloadit/uppy/blob/425f9ecfbc8bc48ce6b734e4fc14fa60d25daa97/packages/%40uppy/react/src/Dashboard.js#L47-L54)).
This may differ in your framework, but from what weve found, the concepts are
generally pretty similar.
If youre familiar with React, Vue or soon Svelte, it might be useful to read
through the code of those integrations, as they lay out a pretty good structure.
After the basic components have been built, here are a few more important tasks
to get done:
- Add TypeScript support in some capacity (if possible)
- Write documentation
- Add an example
- Configuring the build system
### Common issues
Before going into these tasks, here are a few common gotchas that you should be
aware of.
#### Dependencies
Your `package.json` should resemble something like this:
```json
{
"name": "@uppy/framework",
"dependencies": {
"@uppy/dashboard": "workspace:^",
"@uppy/drag-drop": "workspace:^",
"@uppy/progress-bar": "workspace:^",
"@uppy/status-bar": "workspace:^",
"@uppy/utils": "workspace:^",
"prop-types": "^15.6.1"
},
"peerDependencies": {
"@uppy/core": "workspace:^"
},
"publishConfig": {
"access": "public"
}
}
```
The most important part about this is that `@uppy/core` is a peer dependency. If
your framework complains about `@uppy/core` not being resolved, you can also add
it as a dev dependency
### Adding TypeScript Support
This section wont be too in-depth, because TypeScript depends on your
framework. As general advice, prefer using `d.ts` files and vanilla JavaScript
over TypeScript files. This is circumstantial, but it makes handling the build
system a lot easier when TypeScript doesnt have to transpiled. The version of
typescript in the monorepo is `4.1`.
### Writing docs
Generally, documentation for integrations can be broken down into a few pieces
that apply to every component, and then documentation for each component. The
structure should look something like this:
- Installation
- Initializing Uppy (may vary depending on how the framework handles reactivity)
- Usage
- _For each component_
- Loading CSS
- Props
It may be easier to copy the documentation of earlier integrations and change
the parts that need to be changed rather than writing this from scratch.
Preferably, keep the documentation to one page. For the front-matter, write
something like:
```markdown
title: Framework Name type: docs module: "@uppy/framework" order: 0 category:
"Other Integrations"
```
This data is used to generate Uppys website.
Any change of the documentation that involves a security best practice must
substantiated with an external reference. See
[#3565](https://github.com/transloadit/uppy/issues/3565).
### Adding an example
You can likely use whatever code generation tool for your framework (ex.
`create-react-app`) to create this example. Make sure you add the same version
of `@uppy/core` to this as your peer dependency required, or you may run into
strange issues. Try to include all the components are some of their
functionality.
[The React example](https://github.com/transloadit/uppy/blob/main/examples/react-example/App.js)
is a great... well example of how to do this well.
### Integrating the build system
The biggest part of this is understanding Uppys build system. The high level
description is that `babel` goes through almost all the packages and transpiles
all the Javascript files in the `src` directory to more compatible JavaScript in
the `lib` folder. If youre using vanilla JavaScript for your integration (like
React and Vue do), then you can use this build system and use the files
generated as your entry points.
If youre using some kind of more abstract file format (like Svelte), then you
probably want do to a few things: add the directory name to
[this `IGNORE` regex](https://github.com/transloadit/uppy/blob/425f9ecfbc8bc48ce6b734e4fc14fa60d25daa97/bin/build-lib.js#L15);
add all your build dependencies to the root `package.json` (try to keep this
small); add a new `build:framework` script to the root `package.json`. This
script usually looks something like this:
```json
{
"scripts": {
"build:framework": "cd framework && yarn run build"
}
}
```
Then, add this script to the `build:js` script. Try running the `build:js`
script and make sure it does not error. It may also be of use to make sure that
global dependencies arent being used (ex. not having rollup locally and relying
on a global install), as these dependencies wont be present on the machines
handling building.
## I18n and locales
For more information about how to contribute to translations, see

17
.github/stale.yml vendored
View file

@ -1,17 +0,0 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 365
# Number of days of inactivity before a stale issue is closed
daysUntilClose: false
# Issues with these labels will never be considered stale
exemptLabels:
- Keep open
- 🔐 Security
# Label to use when marking an issue as stale
staleLabel: Stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. If the issue is still relevant, please upvote or leave a
comment. Thank you for your contribution!
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false

View file

@ -1,4 +1,4 @@
name: Test different bundlers with Uppy
name: Bundlers
on:
push:
@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
@ -42,14 +42,12 @@ jobs:
node-version: lts/*
- name: Install dependencies
run:
corepack yarn workspaces focus $(corepack yarn workspaces list --json
| jq -r .name | awk '/^@uppy-example/{ next } { if ($0!="uppy.io")
print $0 }')
corepack yarn install
env:
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
CYPRESS_INSTALL_BINARY: 0
- name: Build lib
run: corepack yarn run build:lib
- name: Build
run: corepack yarn run build
- name: Make Uppy bundle use local version
run: |
node <<'EOF'
@ -97,12 +95,13 @@ jobs:
- name: Add Rollup as a dev dependency
run: >-
npm i --save-dev @rollup/plugin-commonjs @rollup/plugin-node-resolve
rollup@${{matrix.bundler-version}}
@rollup/plugin-json rollup@${{matrix.bundler-version}}
- run: npx rollup --version
- name: Create Rollup config file
run: >-
echo ' import cjs from "@rollup/plugin-commonjs"; import { nodeResolve
} from "@rollup/plugin-node-resolve";
} from "@rollup/plugin-node-resolve"; import json from
"@rollup/plugin-json";
export default {
input: "./lib/index.js",
@ -110,6 +109,7 @@ jobs:
file: "/dev/null",
},
plugins: [
json(),
cjs(),
nodeResolve({ browser: true, exportConditions: ["browser"] }),
],
@ -136,10 +136,26 @@ jobs:
run: npm i --save-dev webpack-cli webpack@${{matrix.bundler-version}}
- run: npx webpack --version
- name: Create Webpack config file
run:
echo 'export default
{mode:"production",target:"web",entry:"./lib/index.js"}' >
webpack.config.js
run: |
echo 'export default {
mode: "production",
target: "web",
entry: "./lib/index.js",
resolve: {
fallback: {
fs: false,
path: false,
stream: false,
util: false,
assert: false,
constants: false,
crypto: false,
http: false,
https: false,
url: false
}
}
}' > webpack.config.js
- name: Bundle
run: npx webpack

View file

@ -6,10 +6,6 @@ on:
paths-ignore:
- '**.md'
- 'assets/**'
- 'e2e/**'
- 'examples/**'
- 'private/**'
- 'website/**'
- '.github/**'
- '!.github/workflows/ci.yml'
pull_request:
@ -17,11 +13,6 @@ on:
types: [opened, synchronize, reopened]
paths-ignore:
- '**.md'
- 'assets/**'
- 'e2e/**'
- 'examples/**'
- 'private/**'
- 'website/**'
- '.github/**'
- '!.github/workflows/ci.yml'
@ -30,14 +21,14 @@ env:
jobs:
unit_tests:
name: Unit tests
name: Tests
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
node-version: [lts/*]
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
@ -56,21 +47,25 @@ jobs:
node-version: ${{matrix.node-version}}
- name: Install dependencies
run:
corepack yarn workspaces focus $(corepack yarn workspaces list --json
| jq -r .name | awk '/^@uppy-example/{ next } { if ($0!="uppy.io")
print $0 }')
env:
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
CYPRESS_INSTALL_BINARY: 0
corepack yarn install
- name: Install Playwright Browsers
run: corepack yarn workspace @uppy/dashboard playwright install --with-deps
- name: Build
run: corepack yarn run build
- name: Run tests
run: corepack yarn run test:unit
run: corepack yarn run test
env:
COMPANION_DATADIR: ./output
COMPANION_DOMAIN: localhost:3020
COMPANION_PROTOCOL: http
COMPANION_REDIS_URL: redis://localhost:6379
types:
name: Type tests
name: Types
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
@ -89,11 +84,24 @@ jobs:
node-version: lts/*
- name: Install dependencies
run:
corepack yarn workspaces focus $(corepack yarn workspaces list --json
| jq -r .name | awk '/^@uppy-example/{ next } { if ($0!="uppy.io")
print $0 }')
env:
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
CYPRESS_INSTALL_BINARY: 0
- name: Attempt building TS packages
run: corepack yarn run build:ts
corepack yarn install
- run: corepack yarn run typecheck
lint_js:
name: Lint
runs-on: ubuntu-latest
env:
SKIP_YARN_COREPACK_CHECK: true
steps:
- name: Checkout sources
uses: actions/checkout@v5
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run:
corepack yarn workspaces focus @uppy-dev/build
- name: Run linter
run: corepack yarn run check:ci

View file

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Set SHA commit in version
run:
(cd packages/@uppy/companion && node -e 'const
@ -46,24 +46,24 @@ jobs:
COMPOSE_DOCKER_CLI_BUILD: 0
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Docker meta
id: docker_meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: transloadit/companion
tags: |
type=edge
type=raw,value=latest,enable=false
- uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- uses: docker/setup-buildx-action@v3
- name: Log in to DockerHub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
username: ${{secrets.DOCKER_USERNAME}}
password: ${{secrets.DOCKER_PASSWORD}}
- name: Build and push
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
with:
push: true
context: .
@ -77,12 +77,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Alter dockerfile
run: |
sed -i 's/^EXPOSE 3020$/EXPOSE $PORT/g' Dockerfile
# https://github.com/AkhileshNS/heroku-deploy/issues/188
- name: Install Heroku CLI
run: |
curl https://cli-assets.heroku.com/install.sh | sh
- name: Deploy to heroku
uses: akhileshns/heroku-deploy@581dd286c962b6972d427fcf8980f60755c15520 # v3.13.15
uses: akhileshns/heroku-deploy@e3eb99d45a8e2ec5dca08735e089607befa4bf28 # v3.14.15
with:
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
heroku_app_name: companion-demo

View file

@ -1,51 +0,0 @@
name: Companion
on:
push:
branches: [main]
paths:
- yarn.lock
- 'packages/@uppy/companion/**'
- '.github/workflows/companion.yml'
pull_request:
# We want all branches so we configure types to be the GH default again
types: [opened, synchronize, reopened]
paths:
- yarn.lock
- 'packages/@uppy/companion/**'
- '.github/workflows/companion.yml'
env:
YARN_ENABLE_GLOBAL_CACHE: false
jobs:
test:
name: Unit tests
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x, latest]
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: ${{matrix.node-version}}
- name: Install dependencies
run: corepack yarn workspaces focus @uppy/companion
- name: Run tests
run: corepack yarn run test:companion
- name: Run type checks in focused workspace
run: corepack yarn run build:companion

View file

@ -1,4 +1,5 @@
name: End-to-end tests
name: Output
on:
push:
branches: [main]
@ -10,8 +11,8 @@ on:
- 'website/**'
- '.github/**'
- '!.github/workflows/e2e.yml'
pull_request_target:
types: [opened, synchronize, reopened, labeled]
pull_request:
types: [opened, synchronize, reopened]
paths-ignore:
- '**.md'
- '**.d.ts'
@ -19,19 +20,6 @@ on:
- 'private/**'
- 'website/**'
- '.github/**'
pull_request:
types: [opened, synchronize, reopened]
paths:
- .github/workflows/e2e.yml
concurrency:
group:
${{ github.workflow }}--${{ github.event.pull_request.head.repo.full_name ||
github.repository }} -- ${{ github.head_ref || github.ref }}
cancel-in-progress:
# For PRs coming from forks, we need the previous job to run until the end
# to be sure it can remove the `safe to test` label before it affects the next run.
${{ github.event.pull_request.head.repo.full_name == github.repository }}
permissions:
pull-requests: write
@ -40,30 +28,18 @@ env:
jobs:
compare_diff:
name: Diff lib folders
runs-on: ubuntu-latest
env:
DIFF_BUILDER: true
outputs:
diff: ${{ steps.diff.outputs.OUTPUT_DIFF }}
is_accurate_diff: ${{ steps.diff.outputs.IS_ACCURATE_DIFF }}
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v5
with:
fetch-depth: 2
ref:
${{ github.event.pull_request && format('refs/pull/{0}/merge',
github.event.pull_request.number) || github.sha }}
- name: Check if there are "unsafe" changes
id: build_chain_changes
# If there are changes in JS script that generates the output, we cannot
# test them here without human review to make sure they don't contain
# someting "nasty".
run: |
EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
echo "MIGHT_CONTAIN_OTHER_CHANGES<<$EOF" >> "$GITHUB_OUTPUT"
git --no-pager diff HEAD^ --name-only bin package.json yarn.lock babel.config.js >> "$GITHUB_OUTPUT"
echo "$EOF" >> "$GITHUB_OUTPUT"
ref: ${{ github.event.pull_request && format('refs/pull/{0}/merge', github.event.pull_request.number) || github.sha }}
- run: git reset HEAD^ --hard
- name: Get yarn cache directory path
id: yarn-cache-dir-path
@ -81,50 +57,21 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run:
corepack yarn workspaces focus $(corepack yarn workspaces list --json
| jq -r .name | awk '/^@uppy-example/{ next } { if ($0!="uppy.io")
print $0 }')
- run: corepack yarn install
env:
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
CYPRESS_INSTALL_BINARY: 0
- run: corepack yarn build:js:typeless
- run: corepack yarn build
- name: Store output file
run: tar cf /tmp/previousVersion.tar packages/@uppy/*/lib
- name: Fetch source from the PR
if: steps.build_chain_changes.outputs.MIGHT_CONTAIN_OTHER_CHANGES == ''
run: |
git checkout FETCH_HEAD -- packages
echo 'IS_ACCURATE_DIFF=true' >> "$GITHUB_ENV"
- name: Fetch source from the PR
if:
steps.build_chain_changes.outputs.MIGHT_CONTAIN_OTHER_CHANGES != '' &&
(!github.event.pull_request || (github.event.action == 'labeled' &&
github.event.label.name == 'safe to test' &&
github.event.pull_request.state == 'open') ||
(github.event.pull_request.head.repo.full_name == github.repository &&
github.event.event_name != 'labeled'))
run: |
git reset FETCH_HEAD --hard
corepack yarn workspaces focus $(\
corepack yarn workspaces list --json | \
jq -r .name | \
awk '/^@uppy-example/{ next } { if ($0!="uppy.io") print $0 }'\
)
echo 'IS_ACCURATE_DIFF=true' >> "$GITHUB_ENV"
- run: corepack yarn install --no-immutable
env:
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
CYPRESS_INSTALL_BINARY: 0
- name: Fetch source from the PR
if:
steps.build_chain_changes.outputs.MIGHT_CONTAIN_OTHER_CHANGES != '' &&
github.event.pull_request.head.repo.full_name != github.repository &&
(github.event.action != 'labeled' || github.event.label.name != 'safe
to test')
run: |
git checkout FETCH_HEAD -- packages
- run: corepack yarn build:js:typeless
- run: corepack yarn build
- name: Store output file
run: tar cf /tmp/newVersion.tar packages/@uppy/*/lib
- name: Setup git
@ -154,7 +101,6 @@ jobs:
echo "OUTPUT_DIFF<<$EOF" >> "$GITHUB_OUTPUT"
cd /tmp/uppy && git --no-pager diff >> "$GITHUB_OUTPUT"
echo "$EOF" >> "$GITHUB_OUTPUT"
echo "IS_ACCURATE_DIFF=$IS_ACCURATE_DIFF" >> "$GITHUB_OUTPUT"
- name: Add/update comment
if: github.event.pull_request
uses: marocchino/sticky-pull-request-comment@v2
@ -166,145 +112,4 @@ jobs:
${{ steps.diff.outputs.OUTPUT_DIFF || 'No diff' }}
```
${{ env.IS_ACCURATE_DIFF != 'true' && format(fromJson('"The following build files have been modified and might affect the actual diff:\n\n```\n{0}\n```"'), steps.build_chain_changes.outputs.MIGHT_CONTAIN_OTHER_CHANGES) || '' }}
</details>
- name: Remove 'safe to test' label if cancelled
if:
cancelled() && github.event.pull_request &&
github.event.pull_request.head.repo.full_name != github.repository
run: gh pr edit "$NUMBER" --remove-label 'safe to test'
env:
NUMBER: ${{ github.event.pull_request.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
toggle-pending-e2e-label:
# Add the 'pending end-to-end tests' label for PRs that come from forks.
# For those PRs, we want to review the code before running e2e tests.
# See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/.
needs: [compare_diff]
if:
github.event.pull_request.state == 'open' &&
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) && (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 }}
run:
gh pr edit "$PR_URL" --repo ${{ github.repository }} --add-label
'pending end-to-end tests'
- name: Remove label
if:
needs.compare_diff.outputs.diff == '' &&
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 }}
run:
gh pr edit "$PR_URL" --remove-label 'safe to test' --remove-label
'pending end-to-end tests'
e2e:
needs: [compare_diff]
if:
${{ needs.compare_diff.outputs.diff != '' && (!github.event.pull_request
|| (github.event.action == 'labeled' && github.event.label.name == 'safe
to test' && github.event.pull_request.state == 'open') ||
(github.event.pull_request.head.repo.full_name == github.repository &&
github.event.event_name != 'labeled')) }}
name: Browser tests
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Create cache folder for Cypress
id: cypress-cache-dir-path
run: echo "dir=$(mktemp -d)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
with:
path: ${{ steps.cypress-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-cypress
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Start Redis
uses: supercharge/redis-github-action@ea9b21c6ecece47bd99595c532e481390ea0f044 # 1.8.0
with:
redis-version: 7
- name: Install dependencies
run: corepack yarn install --immutable
env:
# https://docs.cypress.io/guides/references/advanced-installation#Binary-cache
CYPRESS_CACHE_FOLDER: ${{ steps.cypress-cache-dir-path.outputs.dir }}
- name: Build Uppy packages
run: corepack yarn build
- name: Run end-to-end browser tests
run: corepack yarn run e2e:ci
env:
COMPANION_DATADIR: ./output
COMPANION_DOMAIN: localhost:3020
COMPANION_PROTOCOL: http
COMPANION_REDIS_URL: redis://localhost:6379
COMPANION_UNSPLASH_KEY: ${{secrets.COMPANION_UNSPLASH_KEY}}
COMPANION_UNSPLASH_SECRET: ${{secrets.COMPANION_UNSPLASH_SECRET}}
COMPANION_AWS_KEY: ${{secrets.COMPANION_AWS_KEY}}
COMPANION_AWS_SECRET: ${{secrets.COMPANION_AWS_SECRET}}
COMPANION_AWS_BUCKET: ${{secrets.COMPANION_AWS_BUCKET}}
COMPANION_AWS_REGION: ${{secrets.COMPANION_AWS_REGION}}
VITE_COMPANION_URL: http://localhost:3020
VITE_TRANSLOADIT_KEY: ${{secrets.TRANSLOADIT_KEY}}
VITE_TRANSLOADIT_SECRET: ${{secrets.TRANSLOADIT_SECRET}}
VITE_TRANSLOADIT_TEMPLATE: ${{secrets.TRANSLOADIT_TEMPLATE}}
VITE_TRANSLOADIT_SERVICE_URL: ${{secrets.TRANSLOADIT_SERVICE_URL}}
# https://docs.cypress.io/guides/references/advanced-installation#Binary-cache
CYPRESS_CACHE_FOLDER: ${{ steps.cypress-cache-dir-path.outputs.dir }}
- name: Upload videos in case of failure
uses: actions/upload-artifact@v4
if: failure()
with:
name: videos-and-screenshots
path: |
e2e/cypress/videos/
e2e/cypress/screenshots/
- name: Remove labels
# Remove the 'pending end-to-end tests' label if tests ran successfully
if:
github.event.pull_request &&
contains(github.event.pull_request.labels.*.name, 'pending end-to-end
tests')
run: gh pr edit "$NUMBER" --remove-label 'pending end-to-end tests'
env:
NUMBER: ${{ github.event.pull_request.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Remove 'safe to test' label
if:
always() && github.event.pull_request &&
contains(github.event.pull_request.labels.*.name, 'safe to test')
run: gh pr edit "$NUMBER" --remove-label 'safe to test'
env:
NUMBER: ${{ github.event.pull_request.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -1,100 +0,0 @@
name: Linters
on:
push:
branches: [main]
paths-ignore:
- '.github/**'
- '!.github/workflows/linters.yml'
- '!.github/CONTRIBUTING.md'
pull_request:
# We want all branches so we configure types to be the GH default again
types: [opened, synchronize, reopened]
paths-ignore:
- '.github/**'
- '!.github/workflows/linters.yml'
- '!.github/CONTRIBUTING.md'
env:
YARN_ENABLE_GLOBAL_CACHE: false
jobs:
lint_js:
name: Lint JavaScript/TypeScript
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
# List all projects that use a custom ESLint config:
run:
corepack yarn workspaces focus @uppy/angular @uppy-example/angular
@uppy-example/react-native-expo @uppy/react-native @uppy-dev/build
- name: Run linter
run: corepack yarn run lint
- name: Run Prettier
run: corepack yarn run format:check-diff
lint_md:
name: Lint Markdown
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: corepack yarn workspaces focus @uppy-dev/build
- name: Run linter
run: corepack yarn run lint:markdown
lint_docs:
name: Lint Docs
runs-on: ubuntu-latest
steps:
- name: Checkout Uppy.io sources
uses: actions/checkout@v4
with:
repository: transloadit/uppy.io
- run: rm -rf docs # the other PR has not landed
- name: Checkout docs
uses: actions/checkout@v4
with:
path: uppy
- run: mv uppy /tmp/uppy && ln -s /tmp/uppy/docs docs
- name: Install dependencies
run: corepack yarn --immutable
- name: Lint files
run: corepack yarn lint
- name: Test build website
run: corepack yarn build

View file

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:

View file

@ -25,7 +25,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
@ -44,7 +44,7 @@ jobs:
node-version: lts/*
- name: Install upload-to-cdn dependencies
if: ${{ inputs.version }}
run: corepack yarn workspaces focus @uppy-dev/upload-to-cdn
run: corepack yarn workspaces focus uppy
env:
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
CYPRESS_INSTALL_BINARY: 0
@ -59,7 +59,7 @@ jobs:
run: corepack yarn run build
- name: Upload "${{ inputs.package }}" to CDN
if: ${{ !inputs.force }}
run: corepack yarn run uploadcdn "$PACKAGE" "$VERSION"
run: corepack yarn workspace uppy node upload-to-cdn.js "$PACKAGE" "$VERSION"
env:
PACKAGE: ${{inputs.package}}
VERSION: ${{inputs.version}}
@ -67,7 +67,7 @@ jobs:
EDGLY_SECRET: ${{secrets.EDGLY_SECRET}}
- name: Upload "${{ inputs.package }}" to CDN
if: ${{ inputs.force }}
run: corepack yarn run uploadcdn "$PACKAGE" "$VERSION" -- --force
run: corepack yarn workspace uppy node upload-to-cdn.js "$PACKAGE" "$VERSION" --force
env:
PACKAGE: ${{inputs.package}}
VERSION: ${{inputs.version}}

View file

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

View file

@ -1,149 +1,99 @@
name: Release
on:
pull_request_review:
types: [submitted]
name: release
env:
YARN_ENABLE_GLOBAL_CACHE: false
on:
push:
branches:
- main
concurrency: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
name: Publish releases
if:
${{ github.event.review.state == 'approved' && github.event.sender.login
== github.event.pull_request.assignee.login &&
github.event.pull_request.head.ref == 'release-candidate' }}
outputs:
companionWasReleased:
${{ steps.checkIfCompanionWasReleased.outputs.version }}
name: Release
runs-on: ubuntu-latest
outputs:
companionWasReleased: ${{ steps.checkIfCompanionWasReleased.outputs.version }}
published: ${{ steps.changesets.outputs.published }}
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Checkout Repo
uses: actions/checkout@v5
with:
fetch-depth: 2
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run:
echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- uses: actions/cache@v4
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install Node.js
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
# Allow yarn to make changes during release
- run: corepack yarn config set enableHardenedMode false
- run: corepack yarn --mode=update-lockfile
- name: Install dependencies
run: corepack yarn install --immutable
env:
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
CYPRESS_INSTALL_BINARY: 0
- name: Get CHANGELOG diff
run:
git --no-pager diff HEAD^ -- CHANGELOG.md | awk '{ if( substr($0,0,1)
== "+" && $1 != "+##" && $1 != "+Released:" && $1 != "+++" ) { print
substr($0,2) } }' > CHANGELOG.diff.md
- name: Copy README for `uppy` package
run: cp README.md packages/uppy/.
- name: Build before publishing
run: corepack yarn run build
- name: Hack to allow the publish of the Angular package
- name: Build
run: corepack yarn build
- name: '@uppy/angular prepublish'
run: corepack yarn workspace @uppy/angular prepublishOnly
- name: Publish to the npm registry
run:
corepack yarn workspaces foreach --all --no-private npm publish
--access public --tolerate-republish
- run: |
echo '' >> .yarnrc.yml
echo 'npmAuthToken: "${NPM_TOKEN}"' >> .yarnrc.yml
- name: Create Release Pull Request or Publish
id: changesets
uses: changesets/action@v1
with:
version: corepack yarn run version
publish: corepack yarn run release
commit: '[ci] release'
title: '[ci] release'
env:
YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Merge PR
id: merge
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Remove npmAuthToken from yarnrc
run: |
gh api -X PUT repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/merge \
-F merge_method="squash" \
-F commit_message="$(cat CHANGELOG.diff.md)" \
--jq 'if .merged then "sha="+.sha else error("not merged") end' >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create tags
run: |
git --no-pager diff --name-only HEAD^ | awk '$0 ~ /^packages\/.+\/package\.json$/ { print "jq -r '"'"'\"gh api /repos/{owner}/{repo}/git/refs -f ref=\\\"refs/tags/\"+.name+\"@\"+.version+\"\\\" -f sha=${{ steps.merge.outputs.sha }}\"'"'"' < " $0 }' > createTags.sh
cat createTags.sh
sh createTags.sh | sh
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Get Uppy version number
id: uppyVersion
run:
jq -r '"version="+.version' < packages/uppy/package.json >>
$GITHUB_OUTPUT
- name: Create GitHub release
run:
gh release create uppy@${{ steps.uppyVersion.outputs.version }} -t
"Uppy ${{ steps.uppyVersion.outputs.version }}" -F CHANGELOG.diff.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload `uppy` to CDN
run: corepack yarn run uploadcdn uppy
env:
EDGLY_KEY: ${{secrets.EDGLY_KEY}}
EDGLY_SECRET: ${{secrets.EDGLY_SECRET}}
- name: Upload `@uppy/locales` to CDN if it was released
run:
git diff --exit-code --quiet HEAD^ --
packages/@uppy/locales/package.json ||corepack yarn run uploadcdn
@uppy/locales
env:
EDGLY_KEY: ${{secrets.EDGLY_KEY}}
EDGLY_SECRET: ${{secrets.EDGLY_SECRET}}
sed -i '/npmAuthToken:/d' .yarnrc.yml
- name: Check if Companion was released
id: checkIfCompanionWasReleased
run:
git diff --exit-code --quiet HEAD^ --
packages/@uppy/companion/package.json || echo "version=$(jq -r
.version < packages/@uppy/companion/package.json)" >> $GITHUB_OUTPUT
- name: Remove release-candidate branch
run:
gh api -X DELETE repos/${{ github.repository
}}/git/refs/heads/release-candidate || echo "Already deleted"
if: steps.changesets.outputs.published == 'true'
run: |
git diff --exit-code --quiet HEAD^ -- packages/@uppy/companion/package.json || echo "version=$(jq -r .version < packages/@uppy/companion/package.json)" >> $GITHUB_OUTPUT
- name: Upload `uppy` to CDN
if: steps.changesets.outputs.published == 'true'
run: node packages/uppy/upload-to-cdn.js uppy
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Remove release branch
run:
gh api -X DELETE repos/${{ github.repository }}/git/refs/heads/release
EDGLY_KEY: ${{secrets.EDGLY_KEY}}
EDGLY_SECRET: ${{secrets.EDGLY_SECRET}}
- name: Upload `@uppy/locales` to CDN if it was released
if: steps.changesets.outputs.published == 'true'
run: node packages/uppy/upload-to-cdn.js @uppy/locales
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Disable Release workflow
run: gh workflow disable Release --repo ${{ github.repository }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: In case of failure
if: ${{ failure() }}
run:
gh pr comment ${{ github.event.pull_request.number }} --body "Release
job failed, please take action."
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
EDGLY_KEY: ${{secrets.EDGLY_KEY}}
EDGLY_SECRET: ${{secrets.EDGLY_SECRET}}
# See also companion-deploy.yml
docker:
name: DockerHub
needs: release
if: ${{ needs.release.outputs.companionWasReleased }}
if: ${{ needs.release.outputs.published == 'true' && needs.release.outputs.companionWasReleased }}
runs-on: ubuntu-latest
env:
DOCKER_BUILDKIT: 0
COMPOSE_DOCKER_CLI_BUILD: 0
steps:
- name: Checkout sources
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Docker meta
id: docker_meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with:
images: transloadit/companion
tags: |
@ -151,15 +101,15 @@ jobs:
type=semver,pattern={{version}},value=${{ needs.release.outputs.companionWasReleased }}
# set latest tag for default branch
type=raw,value=latest,enable=true
- uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
- uses: docker/setup-qemu-action@29109295f81e9208d7d86ff1c6c12d2833863392 # v3.6.0
- uses: docker/setup-buildx-action@v3
- name: Log in to DockerHub
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
username: ${{secrets.DOCKER_USERNAME}}
password: ${{secrets.DOCKER_PASSWORD}}
- name: Build and push
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0
uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0
with:
push: true
context: .

5
.gitignore vendored
View file

@ -16,9 +16,14 @@ yarn-error.log
.env
tsconfig.tsbuildinfo
tsconfig.build.tsbuildinfo
.svelte-kit
.turbo
__screenshots__
dist/
lib/
# @uppy/svelte needs lib inside src
!src/lib
coverage/
examples/dev/bundle.js
examples/aws-php/vendor/*

1
.npmrc Normal file
View file

@ -0,0 +1 @@
//registry.npmjs.org/:_authToken=${NPM_TOKEN}

View file

@ -1,9 +0,0 @@
node_modules/
*.js
*.jsx
*.cjs
*.mjs
!private/js2ts/*
!examples/svelte-example/*
*.lock
CHANGELOG.md

View file

@ -1,29 +0,0 @@
/** @type {import("prettier").Config} */
module.exports = {
proseWrap: 'always',
singleQuote: true,
trailingComma: 'all',
semi: false,
experimentalTernaries: true,
overrides: [
{
files: 'packages/@uppy/angular/**',
options: {
semi: true,
},
},
{
files: 'docs/**',
options: {
semi: true,
useTabs: true,
},
},
{
files: ['tsconfig.json'],
options: {
parser: 'jsonc',
},
},
],
}

View file

@ -1,8 +0,0 @@
website/src/_posts/201*
website/src/_posts/2020-*
website/src/_posts/2021-0*
examples/
CHANGELOG.md
CHANGELOG.next.md
BACKLOG.md
node_modules/

View file

@ -1,12 +0,0 @@
{
"extends": [
"stylelint-config-standard",
"stylelint-config-standard-scss",
"stylelint-config-rational-order"
],
"rules": {
"at-rule-no-unknown": null,
"scss/at-rule-no-unknown": true
},
"defaultSeverity": "warning"
}

3
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"recommendations": ["biomejs.biome"]
}

View file

@ -1,29 +1,23 @@
{
"folders": [
{
"path": "..",
},
"path": ".."
}
],
"settings": {
"editor.defaultFormatter": "biomejs.biome",
"workbench.colorCustomizations": {
"titleBar.activeForeground": "#ffffff",
"titleBar.activeBackground": "#ff009d",
"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,
},
},
"yarn.lock": true
}
}
}

View file

@ -1,29 +0,0 @@
{
"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
}
}
}

View file

@ -0,0 +1,52 @@
diff --git a/dist/changesets-cli.cjs.js b/dist/changesets-cli.cjs.js
index 82ed39a6b2a38fe0aaabb71c2ec745c502dbe06a..c779498212942c5ee822501a5630c3755e335db8 100644
--- a/dist/changesets-cli.cjs.js
+++ b/dist/changesets-cli.cjs.js
@@ -634,9 +634,8 @@ async function getPublishTool(cwd) {
const pm = await packageManagerDetector.detect({
cwd
});
- if (!pm || pm.name !== "pnpm") return {
- name: "npm"
- };
+ if (!pm) return { name: "npm" };
+ if (pm.name === "yarn") return { name: "yarn" };
try {
let result = await spawn__default["default"]("pnpm", ["--version"], {
cwd
@@ -771,6 +770,9 @@ async function internalPublish(packageJson, opts, twoFactorState) {
} = publishTool.name === "pnpm" ? await spawn__default["default"]("pnpm", ["publish", "--json", ...publishFlags], {
env: Object.assign({}, process.env, envOverride),
cwd: opts.cwd
+ }) : publishTool.name === "yarn" ? await spawn__default["default"]("yarn", ["npm", "publish", ...publishFlags], {
+ env: Object.assign({}, process.env, envOverride),
+ cwd: opts.cwd,
}) : await spawn__default["default"](publishTool.name, ["publish", opts.publishDir, "--json", ...publishFlags], {
env: Object.assign({}, process.env, envOverride)
});
diff --git a/dist/changesets-cli.esm.js b/dist/changesets-cli.esm.js
index 1e945455b39b9c6424b26960b3d26a095ad980e4..3a7339db5d9d1ffac16c0ef5a7d56a9d506483a4 100644
--- a/dist/changesets-cli.esm.js
+++ b/dist/changesets-cli.esm.js
@@ -596,9 +596,8 @@ async function getPublishTool(cwd) {
const pm = await detect({
cwd
});
- if (!pm || pm.name !== "pnpm") return {
- name: "npm"
- };
+ if (!pm) return { name: "npm" };
+ if (pm.name === "yarn") return { name: "yarn" };
try {
let result = await spawn$1("pnpm", ["--version"], {
cwd
@@ -733,6 +732,9 @@ async function internalPublish(packageJson, opts, twoFactorState) {
} = publishTool.name === "pnpm" ? await spawn$1("pnpm", ["publish", "--json", ...publishFlags], {
env: Object.assign({}, process.env, envOverride),
cwd: opts.cwd
+ }) : publishTool.name === "yarn" ? await spawn$1("yarn", ["npm", "publish", ...publishFlags], {
+ env: Object.assign({}, process.env, envOverride),
+ cwd: opts.cwd,
}) : await spawn$1(publishTool.name, ["publish", opts.publishDir, "--json", ...publishFlags], {
env: Object.assign({}, process.env, envOverride)
});

View file

@ -1,16 +0,0 @@
diff --git a/dist/error.d.ts b/dist/error.d.ts
index bd657d5311ff3d255dc57a2f7224301d532a3177..8924e0982c64c566f4d9808cde62292d7ed334ac 100644
--- a/dist/error.d.ts
+++ b/dist/error.d.ts
@@ -1,9 +1,9 @@
import { D as DiffOptions } from './types-widbdqe5.js';
import 'pretty-format';
-declare function serializeError(val: any, seen?: WeakMap<WeakKey, any>): any;
+declare function serializeError(val: any, seen?: WeakMap<object, any>): any;
declare function processError(err: any, diffOptions?: DiffOptions): any;
-declare function replaceAsymmetricMatcher(actual: any, expected: any, actualReplaced?: WeakSet<WeakKey>, expectedReplaced?: WeakSet<WeakKey>): {
+declare function replaceAsymmetricMatcher(actual: any, expected: any, actualReplaced?: WeakSet<object>, expectedReplaced?: WeakSet<object>): {
replacedActual: any;
replacedExpected: any;
};

View file

@ -1,12 +0,0 @@
diff --git a/package.json b/package.json
index d4133284745349488cf0e623b983c78fd7cc7fb7..52433fa0ecea99ebbfe5b94a8023f73350896f7d 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"repository": "sindresorhus/p-queue",
"funding": "https://github.com/sponsors/sindresorhus",
"type": "module",
+ "main": "./dist/index.js",
"exports": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"

View file

@ -1,34 +0,0 @@
diff --git a/index.js b/index.js
index a20646d922945004cb737918ef6b6d063bb3c2a4..a44863e9555abdaa569f309b1197fddc8dd244a5 100644
--- a/index.js
+++ b/index.js
@@ -147,7 +147,7 @@ Hook.prototype.log = function log(lines, exit) {
* @api private
*/
Hook.prototype.initialize = function initialize() {
- ['git', 'npm'].forEach(function each(binary) {
+ ['git', 'corepack'].forEach(function each(binary) {
try { this[binary] = which.sync(binary); }
catch (e) {}
}, this);
@@ -159,9 +159,9 @@ Hook.prototype.initialize = function initialize() {
if (!this.npm) {
try {
process.env.PATH += path.delimiter + path.dirname(process.env._);
- this.npm = which.sync('npm');
+ this.npm = which.sync('corepack');
} catch (e) {
- return this.log(this.format(Hook.log.binary, 'npm'), 0);
+ return this.log(this.format(Hook.log.binary, 'corepack'), 0);
}
}
@@ -225,7 +225,7 @@ Hook.prototype.run = function runner() {
// this doesn't have the required `isAtty` information that libraries use to
// output colors resulting in script output that doesn't have any color.
//
- spawn(hooked.npm, ['run', script, '--silent'], {
+ spawn(hooked.npm, ['yarn', script], {
env: process.env,
cwd: hooked.root,
stdio: [0, 1, 2]

View file

@ -1,60 +0,0 @@
diff --git a/debug/package.json b/debug/package.json
index 054944f5478a0a5cf7b6b8791950c595f956157b..06a4fe2719605eb42c5ee795101c21cfd10b59ce 100644
--- a/debug/package.json
+++ b/debug/package.json
@@ -9,6 +9,7 @@
"umd:main": "dist/debug.umd.js",
"source": "src/index.js",
"license": "MIT",
+ "type": "module",
"mangle": {
"regex": "^(?!_renderer)^_"
},
diff --git a/devtools/package.json b/devtools/package.json
index 09b04a77690bdfba01083939ff9eaf987dd50bcb..92c159fbb3cf312c6674202085fb237d6fb921ad 100644
--- a/devtools/package.json
+++ b/devtools/package.json
@@ -10,6 +10,7 @@
"source": "src/index.js",
"license": "MIT",
"types": "src/index.d.ts",
+ "type": "module",
"peerDependencies": {
"preact": "^10.0.0"
},
diff --git a/hooks/package.json b/hooks/package.json
index 74807025bf3de273ebada2cd355428a2c972503d..98501726ffbfe55ffa09928e56a9dcafb9a348ff 100644
--- a/hooks/package.json
+++ b/hooks/package.json
@@ -10,6 +10,7 @@
"source": "src/index.js",
"license": "MIT",
"types": "src/index.d.ts",
+ "type": "module",
"scripts": {
"build": "microbundle build --raw",
"dev": "microbundle watch --raw --format cjs",
diff --git a/jsx-runtime/package.json b/jsx-runtime/package.json
index 7a4027831223f16519a74e3028c34f2f8f5f011a..6b58d17dbacce81894467ef43c0a8e2435e388c4 100644
--- a/jsx-runtime/package.json
+++ b/jsx-runtime/package.json
@@ -10,6 +10,7 @@
"source": "src/index.js",
"types": "src/index.d.ts",
"license": "MIT",
+ "type": "module",
"peerDependencies": {
"preact": "^10.0.0"
},
diff --git a/package.json b/package.json
index 60279c24a08b808ffbf7dc64a038272bddb6785d..088f35fb2c92f2e9b7248557857af2839988d1aa 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"umd:main": "dist/preact.umd.js",
"unpkg": "dist/preact.min.js",
"source": "src/index.js",
+ "type": "module",
"exports": {
".": {
"types": "./src/index.d.ts",

View file

@ -1,19 +0,0 @@
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

View file

@ -1,13 +0,0 @@
diff --git a/src/utils.js b/src/utils.js
index 1f636c6617a71a68318dc587a1c9e6081020f9aa..b28e840ed08f26a4eadd242a6f541fbaefea0eda 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -112,7 +112,7 @@ const getArguments = cliArgs => {
}
function normalizeCommand (command) {
- return UTILS.isPackageScriptName(command) ? `npm run ${command}` : command
+ return UTILS.isPackageScriptName(command) ? `corepack yarn ${command}` : command
}
/**

View file

@ -1,12 +0,0 @@
diff --git a/package.json b/package.json
index f0ab3711ee4f490cbf961ebe6283ce2a28b6824b..644235a3ef52c974e946403a3fcdd137d01fad0c 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"require": "./dist/index.js",
"import": "./wrapper.mjs"
},
+ "jest": "./dist/index.js",
"default": "./dist/esm-browser/index.js"
},
"./package.json": "./package.json"

View file

@ -8,3 +8,6 @@ compressionLevel: mixed
initScope: uppy
nodeLinker: node-modules
npmPublishAccess: public

View file

@ -1,143 +0,0 @@
# Backlog
<!--lint disable no-literal-urls no-undefined-references-->
These are ideas that are planned for specific versions or act as a backlog
without a clear date. PRs are welcome! Please do open an issue to discuss first
if it's a big feature, priorities may have changed after something was added
here.
## `3.0.0`
- [x] Switch to ES Modules (ESM)
- [x] @uppy/image-editor: Remove silly hack to work around non-ESM.
- [ ] Some not too breaking breaking changes. Go through TODOs (@arturi,
@aduh95, @Murderlon)
- [ ] Companion breaking changes, like S3 keys (@mifi)
- [x] New remote-sources preset
- [x] Deprecate Robodog
- [x] Remove from 3.x branch (@aduh95)
- [x] Update docs that refer to Robodog (@arturi)
- [ ] Update Transloadit.com examples and docs to use @uppy/transloadit +
@uppy/remote-sources plugins instead of @uppy/robodog (@arturi)
## `4.0.0`
- [ ] core: change the preprocessing --> uploading flow to allow for files to
start uploading right away after their preprocessing step has finished.
See #1738 (@goto-but-stop)
- [ ] companion: add more reliable tests to catch edge cases in companion. For
example testing that oauth works for multiple companion instances that use
a master Oauth domain.
- [ ] Consider updating the name of @uppy/aws-s3 and @uppy/aws-s3-multipart to
reflect it also supports Google Cloud Storage, Wasabi, and other cloud
providers.
- [ ] Consider fixing all locale files to follow the bcp-47 standard (nl_NL -->
nl-NL)
## Unplanned
### Core
- [ ] Make sure Uppy works well in VR
- [ ] normalize file names when uploading from iOS? Can we do it with meta data?
date? `image-${index}`? #678
- [ ] Can Uppy upload a lot of files at once? Seems to fail now:
https://github.com/transloadit/uppy/issues/3313 (@aduh95, @Murderlon)
- [ ] Consider how we can make Uppy smaller. Replace some packages with smaller
alternatives. Talk about Socket.io again (@aduh95)
- [ ] Better events — more data, consistency, naming (@Murderlon)
### Dashboard
- [ ] Dashboard UI should support 20 providers (@arturi)
- [ ] Allow minimizing the Dashboard during upload (Uppy then becomes just a
tiny progress indicator) (@arturi)
- [ ] Display data like image resolution on file cards. should be done by
thumbnail generator maybe #783
- [ ] Possibility to edit/delete more than one file at once. example: add
copyrigh info to 1000 files #118, #97
- [ ] Possibility to work on already uploaded / in progress files. We'll just
provide the `fileId` to the `file-edit-complete` event so that folks can
more easily roll out custom code for this themselves #112, #113, #2063
- [ ] Focus jumps weirdly if you remove a file
https://github.com/transloadit/uppy/pull/2161#issuecomment-613565486
- [ ] A mini UI that features drop & progress (may involve a `mini: true`
options for dashboard, may involve drop+progress or new plugin) (@arturi)
- [ ] Add a Load More button so you don't have to TAB endlessly to get to the
upload button (https://github.com/transloadit/uppy/issues/1419)
### New plugins
- [ ] WordPress Back-end plugin. Should be another Transloadit Integration based
on Robodog Dashboard(?) we should add a provider, and possibly offer
already-uploaded content
- [ ] WordPress Front-end Gravity Forms Uppy plugin so one form field could be
an Uppy-powered file input
- [ ] A WakeLock based plugin that keeps your phone from going to sleep while an
upload is ongoing https://github.com/transloadit/uppy/issues/1725
- [ ] Improve image editor: filters for images, no crashes (@aduh95)
### New providers
- [ ] Google Photos (#2163)
- [ ] MediaLibrary provider which shows you files that have already been
uploaded #450, #1121, #1112 #362
- [ ] Giphy image search (on top of Unsplash plugin) ()
- [ ] Image search (via Google or Bing or DuckDuckGo): use duckduckgo-images-api
or Google Search API (@arturi)
- [ ] Vimeo #2872
### Miscellaneous
- [ ] goldenretriever: make it work with aws multipart
https://community.transloadit.com/t/resumable-aws-s3-multipart-integration/14888
(@goto-bus-stop)
- [ ] provider: add sorting (by date) #254
- [ ] qa: add one integration test (or add to existing test) that uses more
exotic (tus) options such as `useFastRemoteRetry` or
`removeFingerprintOnSuccess`
https://github.com/transloadit/uppy/issues/1327 (@arturi,
@ifedapoolarewaju)
- [x] react: Add a React Hook to manage an Uppy instance
https://github.com/transloadit/uppy/pull/1247#issuecomment-458063951
(@goto-bus-stop)
- [ ] rn: Uppy React Native works with Expo, now let's make it work without
- [ ] rn: Uppy React Native works with Url Plugin, now let's make it work with
Instagram
- [ ] security: consider iframe / more security for Transloadit/Uppy integration
widget and Uppy itself. Page cant get files from Google Drive if its an
iframe
- [ ] statusbar: Add a confirmation of the cancel action
(https://github.com/transloadit/uppy/issues/1418) as well as ask the user
if they really want to navigate away while an upload is in progress via
`onbeforeunload` (@arturi)
- [ ] uploaders: consider not showing progress updates from the server after an
uploads been paused. Perhaps the button can be disabled and say
`Pausing..` until Companion has actually stopped transmitting updates
(@arturi, @ifedapoolarewaju)
- [ ] xhr: allow sending custom headers per file (as proposed in #785)
- [ ] website: It would be nice in the long run to have a dynamic package
builder here right on the website where you can select the plugins you
need/want and it builds and downloads a minified version of them? Sort of
like jQuery UI: https://jqueryui.com/download/
- [ ] webcam: Specify the resolution of the webcam images/video. We should add a
way to specify any custom 'constraints' (aspect ratio, resolution,
mimetype (`/video/mp4;codec=h264`), bits per second, etc) to the Webcam
plugin #876
- [ ] Constructor to build Uppy with what you need, “Dashboard example meets
Transloadit Wizard”. Select language, modes, providers — get code ready to
use. Maybe integrate Transloadit Wizard in there as well (@arturi,
@Murderlon)
### Needs research
- [ ] Add a prepublish test that checks if `npm pack` is not massive
(@goto-bus-stop)
- [ ] Add https://github.com/pa11y/pa11y for automated accessibility testing?
- [ ] Add lighthouse for automated performance testing?
- [ ] Switch one existing e2e test to use Parcel (create-react-app already using
webpack) (@arturi)
- [ ] Add typescript with JSDoc for @uppy/core
https://github.com/Microsoft/TypeScript/wiki/Type-Checking-JavaScript-Files
(@arturi)

View file

@ -2,7 +2,7 @@
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/v4.3.0/uppy.min.js"></script>`)
(`<script src="https://releases.transloadit.com/uppy/v5.0.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

View file

@ -12,6 +12,542 @@ Please add your entries in this format:
In the current stage we aim to release a new version at least every month.
## 4.18.0
Released: 2025-06-30
| Package | Version | Package | Version |
| -------------------------- | ------- | -------------------------- | ------- |
| @uppy/components | 0.2.0 | @uppy/remote-sources | 2.3.4 |
| @uppy/core | 4.4.7 | @uppy/screen-capture | 4.3.1 |
| @uppy/google-drive-picker | 0.3.6 | @uppy/svelte | 4.5.0 |
| @uppy/google-photos-picker | 0.3.6 | @uppy/vue | 2.3.0 |
| @uppy/locales | 4.6.0 | @uppy/webcam | 4.2.1 |
| @uppy/provider-views | 4.4.5 | uppy | 4.18.0 |
| @uppy/react | 4.4.0 | | |
- meta: Remove remark reference from CI (Murderlon)
- @uppy/components,@uppy/screen-capture: useScreenCapture fixes (Prakash / #5793)
- examples: Add useRemoteSource (Merlijn Vos / #5778)
- @uppy/components: Use webcam fixes2 (Mikael Finstad / #5791)
- meta: Remove remark (Merlijn Vos / #5790)
- meta: Delete old, unused files (Merlijn Vos / #5788)
- meta: Sort package.json (Murderlon)
- examples: Headless Hooks: Add useScreenCapture (Prakash / #5784)
- @uppy/locales: Update pt_BR localization (Gabriel Pereira / #5780)
- e2e: Skip for now then (Murderlon)
- e2e: fixup! Fix CI for now (Murderlon)
- e2e: Fix CI for now (Murderlon)
- examples: Add useWebcam (Merlijn Vos / #5741)
- @uppy/react,@uppy/svelte,@uppy/vue: Add useDropzone & useFileInput (Merlijn Vos / #5735)
- meta: build(deps): bump base-x from 3.0.9 to 3.0.11 (dependabot[bot] / #5772)
- @uppy/provider-views: improve metadata handling in Google Photos Picker (ben rosenbaum / #5769)
## 4.17.0
Released: 2025-06-02
| Package | Version | Package | Version |
| -------------------- | ------- | -------------------- | ------- |
| @uppy/companion | 5.8.0 | @uppy/screen-capture | 4.3.0 |
| @uppy/components | 0.1.0 | @uppy/svelte | 4.4.0 |
| @uppy/core | 4.4.6 | @uppy/utils | 6.1.5 |
| @uppy/locales | 4.5.3 | @uppy/vue | 2.2.0 |
| @uppy/provider-views | 4.4.4 | @uppy/webcam | 4.2.0 |
| @uppy/react | 4.3.0 | uppy | 4.17.0 |
- @uppy/provider-views: fix: handle pagination for Google Photos picker (fixes #5765) (ben rosenbaum / #5768)
- @uppy/companion: add max filename length env var (Freeman / #5763)
- @uppy/core: fix missing required meta field error not updating (Prakash / #5766)
- @uppy/screen-capture: add screenshot button (Prakash / #5737)
- @uppy/locales: Update cs_CZ.ts (Martin Štorek / #5749)
- examples: Headless components (Merlijn Vos / #5727)
- @uppy/companion: fix cookie maxAge to milliseconds (zolotarov@brights.io / #5746)
- meta: document how to fix a broken release (Mikael Finstad / #5755)
- @uppy/companion: improve Zoom folder structure (Merlijn Vos / #5739)
## 4.16.0
Released: 2025-05-18
| Package | Version | Package | Version |
| -------------------------- | ------- | -------------------------- | ------- |
| @uppy/audio | 2.1.3 | @uppy/image-editor | 3.3.3 |
| @uppy/box | 3.2.3 | @uppy/instagram | 4.2.3 |
| @uppy/companion | 5.7.0 | @uppy/onedrive | 4.2.4 |
| @uppy/companion-client | 4.4.2 | @uppy/remote-sources | 2.3.3 |
| @uppy/core | 4.4.5 | @uppy/screen-capture | 4.2.3 |
| @uppy/dashboard | 4.3.4 | @uppy/unsplash | 4.3.4 |
| @uppy/drag-drop | 4.1.3 | @uppy/url | 4.2.4 |
| @uppy/dropbox | 4.2.3 | @uppy/utils | 6.1.4 |
| @uppy/facebook | 4.2.3 | @uppy/webcam | 4.1.3 |
| @uppy/file-input | 4.1.3 | @uppy/webdav | 0.3.3 |
| @uppy/google-drive | 4.3.3 | @uppy/zoom | 3.2.3 |
| @uppy/google-drive-picker | 0.3.5 | uppy | 4.16.0 |
| @uppy/google-photos-picker | 0.3.5 | | |
- meta: Revert "Release: uppy@4.16.0 (#5750)" (Mikael Finstad)
- meta: force cdn upload (Mikael Finstad)
- meta: fix invalid brach option (now ref) (Mikael Finstad)
- meta: improve release script output (Mikael Finstad)
- meta: fix error (Mikael Finstad)
- meta: Release: uppy@4.16.0 (github-actions[bot] / #5750)
- meta: Fix node versions (Mikael Finstad / #5740)
- @uppy/companion-client: don't reject on incorrect origin (Mikael Finstad / #5736)
- @uppy/companion: implement credentials param `transloadit_gateway` (Mikael Finstad / #5725)
- @uppy/companion: Fix AES key wear-out (Florian Maury / #5724)
- @uppy/core: fix undefined reference when cancelling an upload (Prakash / #5730)
- @uppy/audio,@uppy/box,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/google-drive-picker,@uppy/google-drive,@uppy/google-photos-picker,@uppy/image-editor,@uppy/instagram,@uppy/onedrive,@uppy/remote-sources,@uppy/screen-capture,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/webcam,@uppy/webdav,@uppy/zoom: ts: make locale strings optional (Merlijn Vos / #5728)
## 4.15.0
Released: 2025-04-14
| Package | Version | Package | Version |
| -------------------- | ------- | -------------------- | ------- |
| @uppy/angular | 0.8.0 | uppy | 4.15.0 |
| @uppy/provider-views | 4.4.3 | | |
- @uppy/provider-views: Fix google photos picker (Mikael Finstad / #5717)
- meta: build(deps): bump nanoid from 3.3.7 to 5.1.2 (dependabot[bot] / #5664)
- examples: build(deps-dev): bump vite from 5.4.16 to 5.4.17 (dependabot[bot] / #5707)
- @uppy/utils: Fix type check ci (Mikael Finstad / #5714)
- @uppy/angular: Support Angular 19 (#5709) (Arnaud Flaesch / #5715)
- meta: simplify e2e (Mikael Finstad / #5711)
- meta: fix ready to commit (Mikael Finstad / #5713)
## 4.14.0
Released: 2025-04-08
| Package | Version | Package | Version |
| -------------------------- | ------- | -------------------------- | ------- |
| @uppy/audio | 2.1.2 | @uppy/locales | 4.5.2 |
| @uppy/box | 3.2.2 | @uppy/onedrive | 4.2.3 |
| @uppy/companion | 5.6.0 | @uppy/react | 4.2.3 |
| @uppy/core | 4.4.4 | @uppy/remote-sources | 2.3.2 |
| @uppy/dashboard | 4.3.3 | @uppy/screen-capture | 4.2.2 |
| @uppy/drag-drop | 4.1.2 | @uppy/status-bar | 4.1.3 |
| @uppy/dropbox | 4.2.2 | @uppy/transloadit | 4.2.2 |
| @uppy/facebook | 4.2.2 | @uppy/unsplash | 4.3.3 |
| @uppy/file-input | 4.1.2 | @uppy/url | 4.2.3 |
| @uppy/google-drive | 4.3.2 | @uppy/utils | 6.1.3 |
| @uppy/google-drive-picker | 0.3.4 | @uppy/webcam | 4.1.2 |
| @uppy/google-photos-picker | 0.3.4 | @uppy/webdav | 0.3.2 |
| @uppy/image-editor | 3.3.2 | @uppy/zoom | 3.2.2 |
| @uppy/instagram | 4.2.2 | uppy | 4.14.0 |
- @uppy/core: dry retryAll() and upload() (Mikael Finstad / #5691)
- @uppy/angular: Revert "Support Angular 19" (Mikael Finstad / #5710)
- @uppy/angular: Support Angular 19 (Arnaud Flaesch / #5709)
- @uppy/companion: implement dropbox business teams (Mikael Finstad / #5708)
- @uppy/utils: add msg mimetype (Merlijn Vos / #5699)
- @uppy/core: fix locale type for plugins (Merlijn Vos / #5700)
- examples: build(deps-dev): bump vite from 5.4.14 to 5.4.15 (dependabot[bot] / #5703)
- @uppy/locales: Update nb_NO.ts (Tore Sinding Bekkedal / #5678)
- meta: build(deps): bump docker/login-action from 3.3.0 to 3.4.0 (dependabot[bot] / #5689)
- examples: @uppy-example/aws-nodejs: fix fileType not present in S3 objects (Prakash / #5680)
- @uppy/core: fix events when retrying with upload() (Prakash / #5696)
- meta: Fix locales building (Mikael Finstad / #5693)
- @uppy/google-photos: remove google photos 😢 (Mikael Finstad / #5690)
- @uppy/locales: Update cs_CZ.ts (David Petrásek / #5658)
## 4.13.4
Released: 2025-03-13
| Package | Version | Package | Version |
| ----------- | ------- | ----------- | ------- |
| @uppy/core | 4.4.3 | uppy | 4.13.4 |
| @uppy/react | 4.2.2 | | |
- @uppy/core: make upload() idempotent (Merlijn Vos / #5677)
- @uppy/react: pass getServerSnapshot to useSyncExternalStoreWithSelector (Merlijn Vos / #5685)
- meta: Fix BasePlugin export for CDN bundle (Merlijn Vos / #5684)
- meta: build(deps): bump docker/build-push-action from 6.14.0 to 6.15.0 (dependabot[bot] / #5673)
- meta: build(deps): bump docker/setup-qemu-action from 3.4.0 to 3.6.0 (dependabot[bot] / #5675)
- meta: build(deps): bump docker/metadata-action from 5.6.1 to 5.7.0 (dependabot[bot] / #5674)
## 4.13.3
Released: 2025-02-25
| Package | Version | Package | Version |
| ---------------- | ------- | ---------------- | ------- |
| @uppy/companion | 5.5.2 | @uppy/xhr-upload | 4.3.3 |
| @uppy/dashboard | 4.3.2 | uppy | 4.13.3 |
| @uppy/status-bar | 4.1.2 | | |
- @uppy/dashboard: do not allow drag&drop of file preview (Merlijn Vos / #5650)
- @uppy/xhr-upload: fix when responseType is set to JSON (Merlijn Vos / #5651)
- meta: build(deps): bump akhileshns/heroku-deploy from 3.13.15 to 3.14.15 (dependabot[bot] / #5659)
- meta: build(deps): bump docker/build-push-action from 6.13.0 to 6.14.0 (dependabot[bot] / #5660)
- @uppy/status-bar: fix aria-hidden warning (Merlijn Vos / #5663)
- @uppy/companion: log when tus uploaded size differs (Mikael Finstad / #5647)
- @uppy/companion: remove redundant HEAD request for file size (Mikael Finstad / #5648)
- meta: build(deps): bump elliptic from 6.6.0 to 6.6.1 (dependabot[bot] / #5649)
- examples: build(deps-dev): bump esbuild from 0.21.5 to 0.25.0 (dependabot[bot] / #5643)
- meta: build(deps-dev): bump vitest from 1.6.0 to 1.6.1 (dependabot[bot] / #5641)
- meta: build(deps): bump docker/setup-qemu-action from 3.3.0 to 3.4.0 (dependabot[bot] / #5640)
## 4.13.2
Released: 2025-02-03
| Package | Version | Package | Version |
| -------------------------- | ------- | -------------------------- | ------- |
| @uppy/core | 4.4.2 | @uppy/provider-views | 4.4.2 |
| @uppy/google-drive-picker | 0.3.3 | @uppy/url | 4.2.2 |
| @uppy/google-photos-picker | 0.3.3 | @uppy/utils | 6.1.2 |
| @uppy/locales | 4.5.1 | uppy | 4.13.2 |
- @uppy/utils: do not strip www in getSocketHost (Merlijn Vos / #5621)
- @uppy/url: skip drag/dropped local files (Merlijn Vos / #5626)
- @uppy/provider-views: fix google photos picker videos (Mikael Finstad / #5635)
- @uppy/core,@uppy/google-drive-picker,@uppy/google-photos-picker,@uppy/provider-views:
fix google picker i18n (Mikael Finstad / #5632)
## 4.13.1
Released: 2025-01-22
| Package | Version | Package | Version |
| --------------- | ------- | --------------- | ------- |
| @uppy/aws-s3 | 4.2.3 | @uppy/tus | 4.2.2 |
| @uppy/companion | 5.5.1 | uppy | 4.13.1 |
- @uppy/tus: fix resumeFromPreviousUpload race condition (Merlijn Vos / #5616)
- @uppy/aws-s3: Fixed default shouldUseMultipart (Mika Laitinen / #5613)
- meta: build(deps): bump docker/build-push-action from 6.11.0 to 6.12.0 (dependabot[bot] / #5611)
- @uppy/aws-s3: remove console.error (Mikael Finstad / #5607)
- @uppy/companion: unify http error responses (Mikael Finstad / #5595)
## 4.13.0
Released: 2025-01-15
| Package | Version | Package | Version |
| --------------- | ------- | --------------- | ------- |
| @uppy/aws-s3 | 4.2.2 | @uppy/unsplash | 4.3.2 |
| @uppy/companion | 5.5.0 | uppy | 4.13.0 |
- @uppy/aws-s3: always set S3 meta to UppyFile & include key (Merlijn Vos / #5602)
- @uppy/companion: fix forcePathStyle boolean conversion (Mikael Finstad / #5308)
- meta: Fix Webpack CI (Merlijn Vos / #5604)
- @uppy/aws-s3: allow uploads to fail/succeed independently (Merlijn Vos / #5603)
- meta: Add types for css files (Merlijn Vos / #5591)
- @uppy/unsplash: make utmSource optional (Merlijn Vos / #5601)
- meta: build(deps): bump docker/setup-qemu-action from 3.2.0 to 3.3.0 (dependabot[bot] / #5599)
- meta: build(deps): bump docker/build-push-action from 6.10.0 to 6.11.0 (dependabot[bot] / #5600)
- @uppy/companion: add COMPANION_TUS_DEFERRED_UPLOAD_LENGTH (Dominik Schmidt / #5561)
## 4.12.2
Released: 2025-01-09
| Package | Version | Package | Version |
| -------------------------- | ------- | -------------------------- | ------- |
| @uppy/audio | 2.1.1 | @uppy/instagram | 4.2.1 |
| @uppy/aws-s3 | 4.2.1 | @uppy/onedrive | 4.2.2 |
| @uppy/box | 3.2.1 | @uppy/progress-bar | 4.2.1 |
| @uppy/companion-client | 4.4.1 | @uppy/provider-views | 4.4.1 |
| @uppy/compressor | 2.2.1 | @uppy/react | 4.2.1 |
| @uppy/core | 4.4.1 | @uppy/remote-sources | 2.3.1 |
| @uppy/dashboard | 4.3.1 | @uppy/screen-capture | 4.2.1 |
| @uppy/drag-drop | 4.1.1 | @uppy/status-bar | 4.1.1 |
| @uppy/drop-target | 3.1.1 | @uppy/thumbnail-generator | 4.1.1 |
| @uppy/dropbox | 4.2.1 | @uppy/transloadit | 4.2.1 |
| @uppy/facebook | 4.2.1 | @uppy/tus | 4.2.1 |
| @uppy/file-input | 4.1.1 | @uppy/unsplash | 4.3.1 |
| @uppy/form | 4.1.1 | @uppy/url | 4.2.1 |
| @uppy/golden-retriever | 4.1.1 | @uppy/vue | 2.1.1 |
| @uppy/google-drive | 4.3.1 | @uppy/webcam | 4.1.1 |
| @uppy/google-drive-picker | 0.3.2 | @uppy/webdav | 0.3.1 |
| @uppy/google-photos | 0.5.1 | @uppy/xhr-upload | 4.3.2 |
| @uppy/google-photos-picker | 0.3.2 | @uppy/zoom | 3.2.1 |
| @uppy/image-editor | 3.3.1 | uppy | 4.12.2 |
| @uppy/informer | 4.2.1 | | |
- @uppy/provider-views: Import types consistently from @uppy/core (Merlijn Vos / #5589)
- @uppy/status-bar: fix double upload progress (Mikael Finstad / #5587)
- @uppy/provider-views: fix incorrect import (Merlijn Vos / #5588)
## 4.12.1
Released: 2025-01-08
| Package | Version | Package | Version |
| --------------- | ------- | --------------- | ------- |
| @uppy/companion | 5.4.1 | uppy | 4.12.1 |
- @uppy/companion: upgrade express & express-session (Merlijn Vos / #5582)
## 4.12.0
Released: 2025-01-08
| Package | Version | Package | Version |
| -------------------------- | ------- | -------------------------- | ------- |
| @uppy/google-drive-picker | 0.3.1 | @uppy/unsplash | 4.3.0 |
| @uppy/google-photos-picker | 0.3.1 | @uppy/utils | 6.1.1 |
| @uppy/onedrive | 4.2.1 | @uppy/xhr-upload | 4.3.1 |
| @uppy/provider-views | 4.4.0 | uppy | 4.12.0 |
| @uppy/svelte | 4.3.0 | | |
- @uppy/unsplash,@uppy/provider-views: add utmSource option (Merlijn Vos / #5580)
- @uppy/xhr-upload: allow custom error message in onAfterResponse (Merlijn Vos / #5578)
- @uppy/onedrive: fix AsyncStore import (Merlijn Vos / #5579)
- @uppy/google-drive-picker,@uppy/google-photos-picker: Fix Google Picker plugins locale (Merlijn Vos / #5575)
## 4.11.0
Released: 2025-01-06
| Package | Version | Package | Version |
| -------------------------- | ------- | -------------------------- | ------- |
| @uppy/audio | 2.1.0 | @uppy/onedrive | 4.2.0 |
| @uppy/aws-s3 | 4.2.0 | @uppy/progress-bar | 4.2.0 |
| @uppy/box | 3.2.0 | @uppy/provider-views | 4.3.0 |
| @uppy/companion-client | 4.4.0 | @uppy/react | 4.2.0 |
| @uppy/compressor | 2.2.0 | @uppy/remote-sources | 2.3.0 |
| @uppy/core | 4.4.0 | @uppy/screen-capture | 4.2.0 |
| @uppy/dashboard | 4.3.0 | @uppy/status-bar | 4.1.0 |
| @uppy/drag-drop | 4.1.0 | @uppy/store-default | 4.2.0 |
| @uppy/drop-target | 3.1.0 | @uppy/svelte | 4.2.0 |
| @uppy/dropbox | 4.2.0 | @uppy/thumbnail-generator | 4.1.0 |
| @uppy/facebook | 4.2.0 | @uppy/transloadit | 4.2.0 |
| @uppy/file-input | 4.1.0 | @uppy/tus | 4.2.0 |
| @uppy/form | 4.1.0 | @uppy/unsplash | 4.2.0 |
| @uppy/golden-retriever | 4.1.0 | @uppy/url | 4.2.0 |
| @uppy/google-drive | 4.3.0 | @uppy/utils | 6.1.0 |
| @uppy/google-drive-picker | 0.3.0 | @uppy/vue | 2.1.0 |
| @uppy/google-photos | 0.5.0 | @uppy/webcam | 4.1.0 |
| @uppy/google-photos-picker | 0.3.0 | @uppy/webdav | 0.3.0 |
| @uppy/image-editor | 3.3.0 | @uppy/xhr-upload | 4.3.0 |
| @uppy/informer | 4.2.0 | @uppy/zoom | 3.2.0 |
| @uppy/instagram | 4.2.0 | uppy | 4.11.0 |
| @uppy/locales | 4.5.0 | | |
- meta: build(deps): bump docker/metadata-action from 5.5.1 to 5.6.1 (dependabot[bot] / #5525)
- examples,@uppy/svelte: build(deps-dev): bump @sveltejs/kit from 2.5.17 to 2.8.3 (dependabot[bot] / #5526)
- meta: build(deps): bump docker/build-push-action from 6.9.0 to 6.10.0 (dependabot[bot] / #5531)
- meta: build(deps): bump elliptic from 6.5.7 to 6.6.0 (dependabot[bot] / #5498)
- @uppy/utils: Use .js(x) for all imports instead .ts(x) (Merlijn Vos / #5573)
- @uppy/angular,@uppy/audio,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive-picker,@uppy/google-drive,@uppy/google-photos-picker,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/vue,@uppy/webcam,@uppy/webdav,@uppy/xhr-upload,@uppy/zoom: Remove "paths" from all tsconfig's (Merlijn Vos / #5572)
- @uppy/tus: fix onBeforeRequest type (Dominik Schmidt / #5566)
## 4.10.0
Released: 2025-01-06
| Package | Version | Package | Version |
| -------------------- | ------- | -------------------- | ------- |
| @uppy/companion | 5.4.0 | @uppy/store-redux | 4.0.2 |
| @uppy/core | 4.3.2 | @uppy/url | 4.1.3 |
| @uppy/dashboard | 4.2.0 | @uppy/webdav | 0.2.0 |
| @uppy/provider-views | 4.2.1 | uppy | 4.10.0 |
| @uppy/react | 4.1.0 | | |
- @uppy/react: allow React 19 as peer dependency (Shubs / #5556)
- @uppy/webdav: add plugin icon (Merlijn Vos / #5555)
- @uppy/companion: pass fetched origins to window.postMessage() (Merlijn Vos / #5529)
- @uppy/core,@uppy/dashboard,@uppy/provider-views,@uppy/store-redux,@uppy/url: build(deps): bump nanoid from 5.0.7 to 5.0.9 (dependabot[bot] / #5544)
## 4.9.0
Released: 2024-12-17
| Package | Version | Package | Version |
| -------------------------- | ------- | -------------------------- | ------- |
| @uppy/companion | 5.3.0 | @uppy/progress-bar | 4.1.0 |
| @uppy/companion-client | 4.3.0 | @uppy/provider-views | 4.2.0 |
| @uppy/core | 4.3.1 | @uppy/status-bar | 4.0.6 |
| @uppy/google-drive-picker | 0.2.1 | @uppy/utils | 6.0.6 |
| @uppy/google-photos-picker | 0.2.1 | @uppy/webdav | 0.1.0 |
| @uppy/locales | 4.4.0 | uppy | 4.9.0 |
- @uppy/webdav: Add @uppy/webdav (Merlijn Vos / #5551)
- @uppy/google-drive-picker,@uppy/google-photos-picker,@uppy/locales: Add missing Google Picker locale entries (Merlijn Vos / #5552)
- @uppy/core: bring back validateRestrictions (Merlijn Vos / #5538)
- @uppy/google-drive-picker,@uppy/google-photos-picker: Fix TS generics on new Google Picker plugins (Merlijn Vos / #5550)
- @uppy/locales: Add missing French locale entries (Steven SAN / #5549)
- e2e,@uppy/status-bar,@uppy/utils: Companion stream upload unknown size files (Mikael Finstad / #5489)
## 4.8.0
Released: 2024-12-05
| Package | Version | Package | Version |
| -------------------------- | ------- | -------------------------- | ------- |
| @uppy/audio | 2.0.2 | @uppy/instagram | 4.1.2 |
| @uppy/aws-s3 | 4.1.3 | @uppy/locales | 4.3.1 |
| @uppy/box | 3.1.2 | @uppy/onedrive | 4.1.2 |
| @uppy/companion | 5.2.0 | @uppy/progress-bar | 4.0.2 |
| @uppy/companion-client | 4.2.0 | @uppy/provider-views | 4.1.0 |
| @uppy/compressor | 2.1.1 | @uppy/react | 4.0.4 |
| @uppy/core | 4.3.0 | @uppy/remote-sources | 2.2.1 |
| @uppy/dashboard | 4.1.3 | @uppy/screen-capture | 4.1.2 |
| @uppy/drag-drop | 4.0.5 | @uppy/status-bar | 4.0.5 |
| @uppy/drop-target | 3.0.2 | @uppy/store-default | 4.1.2 |
| @uppy/dropbox | 4.1.2 | @uppy/thumbnail-generator | 4.0.2 |
| @uppy/facebook | 4.1.2 | @uppy/transloadit | 4.1.4 |
| @uppy/file-input | 4.0.4 | @uppy/tus | 4.1.5 |
| @uppy/form | 4.0.2 | @uppy/unsplash | 4.1.2 |
| @uppy/golden-retriever | 4.0.2 | @uppy/url | 4.1.2 |
| @uppy/google-drive | 4.2.0 | @uppy/utils | 6.0.5 |
| @uppy/google-drive-picker | 0.2.0 | @uppy/vue | 2.0.3 |
| @uppy/google-photos | 0.4.0 | @uppy/webcam | 4.0.3 |
| @uppy/google-photos-picker | 0.2.0 | @uppy/xhr-upload | 4.2.3 |
| @uppy/image-editor | 3.2.1 | @uppy/zoom | 3.1.2 |
| @uppy/informer | 4.1.2 | uppy | 4.8.0 |
- @uppy/companion-client: Fix allowed origins (Mikael Finstad / #5536)
- meta: Build lib refactor to esm (Mikael Finstad / #5537)
- @uppy/provider-views: Google picker scope (Mikael Finstad / #5535)
- @uppy/core,@uppy/provider-views: move useStore out of core (Mikael Finstad / #5533)
- @uppy/companion,@uppy/google-drive-picker,@uppy/google-photos-picker: Google Picker (Mikael Finstad / #5443)
- @uppy/aws-s3: console.error instead of throw for missing etag (Merlijn Vos / #5521)
- docs: Put docs back in uppy.io repository where they belong (Merlijn Vos / #5527)
- docs: typo (Azhar Rizqullah / #5523)
- @uppy/audio,@uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/compressor,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/drop-target,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react,@uppy/remote-sources,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: cleanup tsconfig (Mikael Finstad / #5520)
- meta: fix missing lint (Mikael Finstad / #5519)
- docs: Add Next.js docs (Merlijn Vos / #5502)
- e2e: try to fix flaky test (Mikael Finstad / #5512)
- meta: Fix broken lint on CI (Mikael Finstad / #5507)
## 4.7.0
Released: 2024-11-11
| Package | Version | Package | Version |
| --------------- | ------- | --------------- | ------- |
| @uppy/aws-s3 | 4.1.2 | @uppy/tus | 4.1.4 |
| @uppy/companion | 5.1.4 | uppy | 4.7.0 |
| @uppy/locales | 4.3.0 | | |
- @uppy/aws-s3: clarify and warn when incorrect buckets settings are used (Mikael Finstad / #5505)
- @uppy/tus: fix event upload-success response.body.xhr (ItsOnlyBinary / #5503)
- @uppy/companion: Enable CSRF protection in grant (OAuth2) (Mikael Finstad / #5504)
- @uppy/locales: Add ms_MY (Malay) locale (Salimi / #5488)
## 4.6.0
Released: 2024-10-31
| Package | Version | Package | Version |
| ------------------------- | ------- | ------------------------- | ------- |
| @uppy/aws-s3 | 4.1.1 | @uppy/provider-views | 4.0.2 |
| @uppy/box | 3.1.1 | @uppy/react | 4.0.3 |
| @uppy/companion | 5.1.3 | @uppy/react-native | 0.6.1 |
| @uppy/companion-client | 4.1.1 | @uppy/redux-dev-tools | 4.0.1 |
| @uppy/core | 4.2.3 | @uppy/screen-capture | 4.1.1 |
| @uppy/dashboard | 4.1.2 | @uppy/status-bar | 4.0.4 |
| @uppy/drag-drop | 4.0.4 | @uppy/store-default | 4.1.1 |
| @uppy/dropbox | 4.1.1 | @uppy/store-redux | 4.0.1 |
| @uppy/facebook | 4.1.1 | @uppy/svelte | 4.1.1 |
| @uppy/file-input | 4.0.3 | @uppy/thumbnail-generator | 4.0.1 |
| @uppy/form | 4.0.1 | @uppy/transloadit | 4.1.3 |
| @uppy/golden-retriever | 4.0.1 | @uppy/tus | 4.1.3 |
| @uppy/google-drive | 4.1.1 | @uppy/unsplash | 4.1.1 |
| @uppy/google-photos | 0.3.1 | @uppy/url | 4.1.1 |
| @uppy/image-editor | 3.2.0 | @uppy/utils | 6.0.4 |
| @uppy/informer | 4.1.1 | @uppy/vue | 2.0.2 |
| @uppy/instagram | 4.1.1 | @uppy/webcam | 4.0.2 |
| @uppy/locales | 4.2.1 | @uppy/xhr-upload | 4.2.2 |
| @uppy/onedrive | 4.1.1 | @uppy/zoom | 3.1.1 |
| @uppy/progress-bar | 4.0.1 | uppy | 4.6.0 |
- @uppy/xhr-upload: fix stale file references in events (Merlijn Vos / #5499)
- @uppy/image-editor: upgrade cropperjs (Merlijn Vos / #5497)
- @uppy/aws-s3,@uppy/box,@uppy/companion-client,@uppy/core,@uppy/dashboard,@uppy/drag-drop,@uppy/dropbox,@uppy/facebook,@uppy/file-input,@uppy/form,@uppy/golden-retriever,@uppy/google-drive,@uppy/google-photos,@uppy/image-editor,@uppy/informer,@uppy/instagram,@uppy/locales,@uppy/onedrive,@uppy/progress-bar,@uppy/provider-views,@uppy/react-native,@uppy/react,@uppy/redux-dev-tools,@uppy/screen-capture,@uppy/status-bar,@uppy/store-default,@uppy/store-redux,@uppy/svelte,@uppy/thumbnail-generator,@uppy/transloadit,@uppy/tus,@uppy/unsplash,@uppy/url,@uppy/utils,@uppy/vue,@uppy/webcam,@uppy/xhr-upload,@uppy/zoom: Fix links (Anthony Veaudry / #5492)
- docs,@uppy/companion: disallow corsOrigins "*" (Mikael Finstad / #5496)
## 4.5.0
Released: 2024-10-15
| Package | Version | Package | Version |
| ---------------- | ------- | ---------------- | ------- |
| @uppy/companion | 5.1.2 | @uppy/svelte | 4.1.0 |
| @uppy/core | 4.2.2 | @uppy/tus | 4.1.2 |
| @uppy/dashboard | 4.1.1 | @uppy/utils | 6.0.3 |
| @uppy/drag-drop | 4.0.3 | @uppy/xhr-upload | 4.2.1 |
| @uppy/file-input | 4.0.2 | uppy | 4.5.0 |
| @uppy/locales | 4.2.0 | | |
- @uppy/dashboard: Dashboard - convert some files to typescript (Evgenia Karunus / #5367)
- @uppy/dashboard,@uppy/drag-drop,@uppy/file-input: `.handleInputChange()` - use `.currentTarget`; clear the input using `''` (Evgenia Karunus / #5381)
- meta: build(deps): bump @blakeembrey/template from 1.1.0 to 1.2.0 (dependabot[bot] / #5448)
- @uppy/locales: Update packages/@uppy/locales/src/fr_FR.ts (Zéfyx / #5472)
- @uppy/svelte: use SvelteKit as the build tool (Merlijn Vos / #5484)
- @uppy/xhr-upload: add response to upload-error callback (Caleb Hardin / #5486)
- @uppy/tus: tus: Avoid duplicate `upload-error` event (Marius / #5485)
- @uppy/companion: Fix redis emitter (Mikael Finstad / #5474)
- meta: build(deps): bump docker/build-push-action from 6.8.0 to 6.9.0 (dependabot[bot] / #5483)
## 4.4.1
Released: 2024-09-30
| Package | Version | Package | Version |
| ----------------- | ------- | ----------------- | ------- |
| @uppy/core | 4.2.1 | uppy | 4.4.1 |
| @uppy/transloadit | 4.1.2 | | |
- @uppy/transloadit: fix multiple upload batches & run again (Merlijn Vos / #5478)
- meta: build(deps): bump docker/build-push-action from 6.7.0 to 6.8.0 (dependabot[bot] / #5477)
- meta: build(deps): bump vite from 5.2.11 to 5.4.8 (dependabot[bot] / #5471)
- @uppy/svelte: build(deps-dev): bump rollup from 4.18.0 to 4.22.4 (dependabot[bot] / #5470)
- meta: build(deps): bump vite from 5.2.11 to 5.4.6 (dependabot[bot] / #5466)
## 4.4.0
Released: 2024-09-20
| Package | Version | Package | Version |
| ----------------- | ------- | ----------------- | ------- |
| @uppy/companion | 5.1.1 | @uppy/tus | 4.1.1 |
| @uppy/svelte | 4.0.2 | @uppy/xhr-upload | 4.2.0 |
| @uppy/transloadit | 4.1.1 | uppy | 4.4.0 |
- @uppy/tus: fix retry check for status code 400 (Merlijn Vos / #5461)
- meta: Merge branch 'main' of https://github.com/transloadit/uppy (Murderlon)
- meta: fix AwsS3 endpoint option in private/dev (Murderlon)
- examples: build(deps): bump body-parser from 1.20.2 to 1.20.3 (dependabot[bot] / #5462)
- examples: build(deps-dev): bump vite from 5.3.1 to 5.3.6 (dependabot[bot] / #5459)
- @uppy/tus: set response from tus-js-client (Merlijn Vos / #5456)
- docs: fix assemblyOptions example for React (Merlijn Vos / #5450)
- docs: rename Edgly to Smart CDN (Merlijn Vos / #5449)
- @uppy/tus: correctly type tus on UppyFile (Merlijn Vos / #5454)
- docs: remove old legacy CDN reference (Murderlon)
- @uppy/xhr-upload: pass files to onBeforeRequest (Merlijn Vos / #5447)
- @uppy/svelte: fix generated module to not bundle Svelte (Antoine du Hamel / #5446)
- examples,@uppy/svelte: Bump svelte from 4.2.18 to 4.2.19 (dependabot[bot] / #5440)
- meta: bump Yarn to 4.4.1 (Antoine du Hamel / #5445)
- docs: fix broken links in locale docs (Serghei Cebotari / #5441)
## 4.3.0
Released: 2024-08-29
@ -6154,7 +6690,7 @@ Released: 2018-02-11.
- dashboard: Use more accessible tip lib microtip (#536 / @arturi)
- docs: Add PHP snippets to XHRUpload docs (#567 / @goto-bus-stop)
- meta: Added instruction to fork the repo first (#512 / muhammadInam)
- meta: Automatically host releases on edgly and use that as our main CDN (#558 / @kvz)
- meta: Automatically host releases on Smart CDN and use that as our main CDN (#558 / @kvz)
- meta: Dependency version updates (#523 / @goto-bus-stop)
- meta: Remove unused files from published package (#586 / @goto-bus-stop)
- s3: Respect `limit` option for upload parameter requests too; fix isXml() check when no content-type is available (#545, #544, #528 / @goto-bus-stop)

130
CLAUDE.md Normal file
View file

@ -0,0 +1,130 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Overview
Uppy is a modular JavaScript file uploader that integrates seamlessly with any application. It's built using a monorepo structure with Yarn workspaces, Turbo for build orchestration, and Biome for linting/formatting.
## Development Commands
### Core Commands
- `yarn build` - Build all packages (uses Turbo)
- `yarn build:watch` - Watch mode for building packages
- `yarn test` - Run tests for all packages
- `yarn test:watch` - Run tests in watch mode
- `yarn typecheck` - Type checking across all packages
- `yarn check` - Run Biome linting and formatting
- `yarn check:ci` - Run Biome in CI mode (no writes)
### Development Server
- `yarn dev` - Start development server (from private/dev workspace)
- `yarn dev:with-companion` - Start dev server with Companion backend
- `yarn start:companion` - Start only the Companion server
### Single Test Execution
To run tests for a specific package:
```bash
yarn workspace @uppy/[package-name] test
# Example: yarn workspace @uppy/core test
```
### Individual Package Development
```bash
yarn workspace @uppy/[package-name] build
```
## Architecture
### Monorepo Structure
- `packages/@uppy/` - Core Uppy packages (plugins, utilities)
- `packages/uppy/` - Main bundle package
- `examples/` - Example implementations for various frameworks
- `private/` - Internal development tools
### Core Architecture
- **Uppy Core** (`@uppy/core`) - Main class that manages plugins, state, and events
- **Plugins** - Modular components for different functionalities:
- **UI Plugins**: Dashboard, Drag & Drop, File Input, Webcam, etc.
- **Provider Plugins**: Google Drive, Dropbox, Instagram, etc. (require Companion)
- **Uploader Plugins**: Tus (resumable), XHR Upload, AWS S3, etc.
- **Utility Plugins**: Golden Retriever (recovery), Thumbnail Generator, etc.
### Plugin Types
1. **Acquirer** - Gets files (File Input, Webcam, Google Drive)
2. **Modifier** - Modifies files (Image Editor, Compressor)
3. **Uploader** - Uploads files (Tus, XHR, AWS S3)
4. **Presenter** - Shows UI (Dashboard, Progress Bar, Status Bar)
5. **Orchestrator** - Manages workflow
6. **Logger** - Handles logging
### Key Packages
- `@uppy/core` - Main Uppy class and plugin system
- `@uppy/dashboard` - Complete UI with file management
- `@uppy/companion` - Server-side component for remote providers
- `@uppy/companion-client` - Client for communicating with Companion
- `@uppy/utils` - Shared utilities
- `@uppy/components` - Headless Preact components (with framework wrappers)
## Build System
### Turbo Configuration
The build system uses Turbo (turbo.json) for task orchestration:
- Builds have dependency ordering (`dependsOn: ["^build"]`)
- TypeScript builds output to `lib/` and `dist/` directories
- CSS builds process SCSS files to CSS
- Special handling for the main `uppy` package bundle
### TypeScript Configuration
- Shared config in `tsconfig.shared.json`
- Individual packages have `tsconfig.json` and `tsconfig.build.json`
- Build outputs include declaration files (.d.ts) and source maps
## Code Standards
### Linting and Formatting
- **Biome** is used for linting and formatting (replaces ESLint/Prettier)
- Configuration in `biome.json`
- Use single quotes, semicolons as needed
- 2-space indentation
### Framework Integration
- **Preact** for UI components with framework wrappers generated for React, Vue, Svelte
- **TypeScript** throughout the codebase
- Components in `@uppy/components` use Tailwind CSS with `uppy:` prefix
### Plugin Development
- All plugins extend `BasePlugin` or `UIPlugin`
- Plugins must have unique IDs and specify their type
- State management through Uppy's store system
- Event-driven architecture using namespace-emitter
## Testing
### Test Framework
- **Vitest** for unit tests (migrated from Jest)
- **Vitest Browser Mode** for browser testing (migrated from Cypress)
- Unit tests use `.test.{ts,tsx}` extensions and browser tests `.browser.test.{ts,tsx}`
### Running Tests
- Tests run in parallel across packages via Turbo
- Examples also include tests (React, Vue, SvelteKit examples)
- Use `yarn test` for development
## Companion Server
The Companion server enables integration with remote file sources:
- Handles OAuth flows for providers like Google Drive, Dropbox
- Proxies file downloads to avoid CORS issues
- Provides search functionality for providers
- Located in `packages/@uppy/companion/`
## Important Notes
- Never commit sensitive information (API keys, tokens)
- Follow existing code conventions and patterns within each package
- Use the `@uppy/utils` package for shared functionality
- UI components should be accessible and support internationalization
- Always run `yarn check` and `yarn typecheck` before committing
- When adding a new component to `@uppy/components`, you have to run `yarn migrate:components` from root.
This is not needed for changing existing components.

View file

@ -1,4 +1,4 @@
FROM node:18.17.1-alpine as build
FROM node:22.18.0-alpine AS build
# Create link to node on amd64 so that corepack can find it
RUN if [ "$(uname -m)" == "aarch64" ]; then mkdir -p /usr/local/sbin/ && ln -s /usr/local/bin/node /usr/local/sbin/node; fi
@ -17,7 +17,7 @@ RUN cd /app && corepack yarn workspace @uppy/companion build
# Now remove all non-prod dependencies for a leaner image
RUN cd /app && corepack yarn workspaces focus @uppy/companion --production
FROM node:18.17.1-alpine
FROM node:22.18.0-alpine
WORKDIR /app

View file

@ -1,15 +0,0 @@
FROM node:18.17.1-alpine
COPY package.json /app/package.json
WORKDIR /app
RUN apk --update add --virtual native-dep \
make gcc g++ python3 libgcc libstdc++ git && \
corepack yarn install && \
apk del native-dep
RUN apk add bash
COPY . /app
RUN npm install -g nodemon
CMD ["npm","test"]

View file

@ -1,43 +0,0 @@
# Licensed under MIT.
# Copyright (2016) by Kevin van Zonneveld https://twitter.com/kvz
#
# https://www.npmjs.com/package/fakefile
#
# Please do not edit this file directly, but propose changed upstream instead:
# https://github.com/kvz/fakefile/blob/master/Makefile
#
# This Makefile offers convience shortcuts into any Node.js project that utilizes npm scripts.
# It functions as a wrapper around the actual listed in `package.json`
# So instead of typing:
#
# $ npm script build:assets
#
# you could also type:
#
# $ make build-assets
#
# Notice that colons (:) are replaced by dashes for Makefile compatibility.
#
# The benefits of this wrapper are:
#
# - You get to keep the the scripts package.json, which is more portable
# (Makefiles & Windows are harder to mix)
# - Offer a polite way into the project for developers coming from different
# languages (npm scripts is obviously very Node centric)
# - Profit from better autocomplete (make <TAB><TAB>) than npm currently offers.
# OSX users will have to install bash-completion
# (http://davidalger.com/development/bash-completion-on-os-x-with-brew/)
define npm_script_targets
TARGETS := $(shell node -e 'for (var k in require("./package.json").scripts) {console.log(k.replace(/:/g, "-"));}')
$$(TARGETS):
npm run $(subst -,:,$(MAKECMDGOALS))
.PHONY: $$(TARGETS)
endef
$(eval $(call npm_script_targets))
# These npm run scripts are available, without needing to be mentioned in `package.json`
install:
npm install

121
README.md
View file

@ -30,7 +30,7 @@ Uppy is being developed by the folks at [Transloadit](https://transloadit.com),
a versatile API to handle any file in your app.
<table>
<tr><th>Tests</th><td><img src="https://github.com/transloadit/uppy/workflows/CI/badge.svg" alt="CI status for Uppy tests"></td><td><img src="https://github.com/transloadit/uppy/workflows/Companion/badge.svg" alt="CI status for Companion tests"></td><td><img src="https://github.com/transloadit/uppy/workflows/End-to-end%20tests/badge.svg" alt="CI status for browser tests"></td></tr>
<tr><th>Tests</th><td><img src="https://github.com/transloadit/uppy/workflows/CI/badge.svg" alt="CI status for Uppy tests"></td><td><img src="https://github.com/transloadit/uppy/workflows/Companion/badge.svg" alt="CI status for Companion tests"></td></tr>
<tr><th>Deploys</th><td><img src="https://github.com/transloadit/uppy/workflows/Release/badge.svg" alt="CI status for CDN deployment"></td><td><img src="https://github.com/transloadit/uppy/workflows/Companion%20Edge%20Deploy/badge.svg" alt="CI status for Companion deployment"></td><td><img src="https://github.com/transloadit/uppy.io/workflows/Deploy%20to%20GitHub%20Pages/badge.svg" alt="CI status for website deployment"></td></tr>
</table>
@ -61,6 +61,22 @@ const uppy = new Uppy()
**[read the docs](https://uppy.io/docs)** for more details on how to use Uppy
and its plugins.
## Integrations
Uppy has first-class support for plain JS/HTML,
[React](https://uppy.io/docs/react/), [Svelte](https://uppy.io/docs/svelte/),
[Vue](https://uppy.io/docs/vue/), and [Angular](https://uppy.io/docs/angular/).
For the supported frameworks (except Angular) Uppy offers three ways to build user interfaces:
1. **Pre-composed, plug-and-play components.** Mainly `<Dashboard />`.
The downside is that you cant customize the UI.
2. **Headless components.** Smaller components, easier to override the styles
or compose them together with your own components.
3. **Hooks.** Attach our logic to your own components, no restrictions, create a
tailor-made UI.
## Features
- Lightweight, modular plugin-based architecture, light on dependencies :zap:
@ -87,13 +103,12 @@ npm install @uppy/core @uppy/dashboard @uppy/tus
```
Add CSS
[uppy.min.css](https://releases.transloadit.com/uppy/v4.3.0/uppy.min.css),
[uppy.min.css](https://releases.transloadit.com/uppy/v5.0.0/uppy.min.css),
either to your HTML pages `<head>` or include in JS, if your bundler of choice
supports it.
Alternatively, you can also use a pre-built bundle from Transloadits CDN:
Edgly. In that case `Uppy` will attach itself to the global `window.Uppy`
object.
Alternatively, you can also use a pre-built bundle from Transloadits CDN: Smart
CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
> ⚠️ The bundle consists of most Uppy plugins, so this method is not recommended
> for production, as your users will have to download all plugins when you are
@ -102,7 +117,7 @@ object.
```html
<!-- 1. Add CSS to `<head>` -->
<link
href="https://releases.transloadit.com/uppy/v4.3.0/uppy.min.css"
href="https://releases.transloadit.com/uppy/v5.0.0/uppy.min.css"
rel="stylesheet"
/>
@ -113,7 +128,7 @@ object.
Uppy,
Dashboard,
Tus,
} from 'https://releases.transloadit.com/uppy/v4.3.0/uppy.min.mjs'
} from 'https://releases.transloadit.com/uppy/v5.0.0/uppy.min.mjs'
const uppy = new Uppy()
uppy.use(Dashboard, { target: '#files-drag-drop' })
@ -139,14 +154,7 @@ object.
- [`Dashboard`](https://uppy.io/docs/dashboard/) — universal UI with previews,
progress bars, metadata editor and all the cool stuff. Required for most UI
plugins like Webcam and Instagram
- [`Progress Bar`](https://uppy.io/docs/progress-bar/) — minimal progress bar
that fills itself when upload progresses
- [`Status Bar`](https://uppy.io/docs/status-bar/) — more detailed progress,
pause/resume/cancel buttons, percentage, speed, uploaded/total sizes (included
by default with `Dashboard`)
- [`Informer`](https://uppy.io/docs/informer/) — send notifications like “smile”
before taking a selfie or “upload failed” when all is lost (also included by
default with `Dashboard`)
- Headless components ([react](https://uppy.io/docs/react/), [svelte](https://uppy.io/docs/svelte/), [vue](https://uppy.io/docs/vue/))
### Sources
@ -178,8 +186,6 @@ server-side component, is needed for a plugin to work.
backend out there (like Apache, Nginx)
- [`AWS S3`](https://uppy.io/docs/aws-s3/) — plain upload to AWS S3 or
compatible services
- [`AWS S3 Multipart`](https://uppy.io/docs/aws-s3-multipart/) — S3-style
“Multipart” upload to AWS or compatible services
### File Processing
@ -195,15 +201,6 @@ server-side component, is needed for a plugin to work.
image previews (included by default with `Dashboard`)
- [`Form`](https://uppy.io/docs/form/) — collects metadata from `<form>` right
before an Uppy upload, then optionally appends results back to the form
- [`Redux`](https://uppy.io/docs/redux/) — for your emerging
[time traveling](https://github.com/gaearon/redux-devtools) needs
## React
- [React](https://uppy.io/docs/react/) — components to integrate Uppy UI plugins
with React apps
- [React Native](./examples/react-native-expo/) — basic Uppy component for React
Native with Expo
## Browser Support
@ -296,77 +293,7 @@ Use Uppy in your project?
## Contributors
<table id="contributors_table">
<tr><td><a href=https://github.com/arturi><img width="117" alt="arturi" src="https://avatars.githubusercontent.com/u/1199054?v=4&s=117"></a></td><td><a href=https://github.com/goto-bus-stop><img width="117" alt="goto-bus-stop" src="https://avatars.githubusercontent.com/u/1006268?v=4&s=117"></a></td><td><a href=https://github.com/kvz><img width="117" alt="kvz" src="https://avatars.githubusercontent.com/u/26752?v=4&s=117"></a></td><td><a href=https://github.com/aduh95><img width="117" alt="aduh95" src="https://avatars.githubusercontent.com/u/14309773?v=4&s=117"></a></td><td><a href=https://github.com/ifedapoolarewaju><img width="117" alt="ifedapoolarewaju" src="https://avatars.githubusercontent.com/u/8383781?v=4&s=117"></a></td><td><a href=https://github.com/Murderlon><img width="117" alt="Murderlon" src="https://avatars.githubusercontent.com/u/9060226?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/hedgerh><img width="117" alt="hedgerh" src="https://avatars.githubusercontent.com/u/2524280?v=4&s=117"></a></td><td><a href=https://github.com/mifi><img width="117" alt="mifi" src="https://avatars.githubusercontent.com/u/402547?v=4&s=117"></a></td><td><a href=https://github.com/nqst><img width="117" alt="nqst" src="https://avatars.githubusercontent.com/u/375537?v=4&s=117"></a></td><td><a href=https://github.com/AJvanLoon><img width="117" alt="AJvanLoon" src="https://avatars.githubusercontent.com/u/15716628?v=4&s=117"></a></td><td><a href=https://github.com/apps/github-actions><img width="117" alt="github-actions[bot]" src="https://avatars.githubusercontent.com/in/15368?v=4&s=117"></a></td><td><a href=https://github.com/lakesare><img width="117" alt="lakesare" src="https://avatars.githubusercontent.com/u/7578559?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/apps/dependabot><img width="117" alt="dependabot[bot]" src="https://avatars.githubusercontent.com/in/29110?v=4&s=117"></a></td><td><a href=https://github.com/kiloreux><img width="117" alt="kiloreux" src="https://avatars.githubusercontent.com/u/6282557?v=4&s=117"></a></td><td><a href=https://github.com/samuelayo><img width="117" alt="samuelayo" src="https://avatars.githubusercontent.com/u/14964486?v=4&s=117"></a></td><td><a href=https://github.com/sadovnychyi><img width="117" alt="sadovnychyi" src="https://avatars.githubusercontent.com/u/193864?v=4&s=117"></a></td><td><a href=https://github.com/richardwillars><img width="117" alt="richardwillars" src="https://avatars.githubusercontent.com/u/291004?v=4&s=117"></a></td><td><a href=https://github.com/ajkachnic><img width="117" alt="ajkachnic" src="https://avatars.githubusercontent.com/u/44317699?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/zcallan><img width="117" alt="zcallan" src="https://avatars.githubusercontent.com/u/13760738?v=4&s=117"></a></td><td><a href=https://github.com/YukeshShr><img width="117" alt="YukeshShr" src="https://avatars.githubusercontent.com/u/71844521?v=4&s=117"></a></td><td><a href=https://github.com/janko><img width="117" alt="janko" src="https://avatars.githubusercontent.com/u/795488?v=4&s=117"></a></td><td><a href=https://github.com/oliverpool><img width="117" alt="oliverpool" src="https://avatars.githubusercontent.com/u/3864879?v=4&s=117"></a></td><td><a href=https://github.com/Botz><img width="117" alt="Botz" src="https://avatars.githubusercontent.com/u/2706678?v=4&s=117"></a></td><td><a href=https://github.com/mcallistertyler><img width="117" alt="mcallistertyler" src="https://avatars.githubusercontent.com/u/14939210?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/mokutsu-coursera><img width="117" alt="mokutsu-coursera" src="https://avatars.githubusercontent.com/u/65177495?v=4&s=117"></a></td><td><a href=https://github.com/dschmidt><img width="117" alt="dschmidt" src="https://avatars.githubusercontent.com/u/448487?v=4&s=117"></a></td><td><a href=https://github.com/DJWassink><img width="117" alt="DJWassink" src="https://avatars.githubusercontent.com/u/1822404?v=4&s=117"></a></td><td><a href=https://github.com/mrbatista><img width="117" alt="mrbatista" src="https://avatars.githubusercontent.com/u/6544817?v=4&s=117"></a></td><td><a href=https://github.com/taoqf><img width="117" alt="taoqf" src="https://avatars.githubusercontent.com/u/15901911?v=4&s=117"></a></td><td><a href=https://github.com/timodwhit><img width="117" alt="timodwhit" src="https://avatars.githubusercontent.com/u/2761203?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/tuoxiansp><img width="117" alt="tuoxiansp" src="https://avatars.githubusercontent.com/u/3960056?v=4&s=117"></a></td><td><a href=https://github.com/eltociear><img width="117" alt="eltociear" src="https://avatars.githubusercontent.com/u/22633385?v=4&s=117"></a></td><td><a href=https://github.com/tim-kos><img width="117" alt="tim-kos" src="https://avatars.githubusercontent.com/u/15005?v=4&s=117"></a></td><td><a href=https://github.com/pauln><img width="117" alt="pauln" src="https://avatars.githubusercontent.com/u/574359?v=4&s=117"></a></td><td><a href=https://github.com/MikeKovarik><img width="117" alt="MikeKovarik" src="https://avatars.githubusercontent.com/u/3995401?v=4&s=117"></a></td><td><a href=https://github.com/toadkicker><img width="117" alt="toadkicker" src="https://avatars.githubusercontent.com/u/523330?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/dominiceden><img width="117" alt="dominiceden" src="https://avatars.githubusercontent.com/u/6367692?v=4&s=117"></a></td><td><a href=https://github.com/ap--><img width="117" alt="ap--" src="https://avatars.githubusercontent.com/u/1463443?v=4&s=117"></a></td><td><a href=https://github.com/tranvansang><img width="117" alt="tranvansang" src="https://avatars.githubusercontent.com/u/13043196?v=4&s=117"></a></td><td><a href=https://github.com/LiviaMedeiros><img width="117" alt="LiviaMedeiros" src="https://avatars.githubusercontent.com/u/74449973?v=4&s=117"></a></td><td><a href=https://github.com/bertho-zero><img width="117" alt="bertho-zero" src="https://avatars.githubusercontent.com/u/8525267?v=4&s=117"></a></td><td><a href=https://github.com/juliangruber><img width="117" alt="juliangruber" src="https://avatars.githubusercontent.com/u/10247?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/Hawxy><img width="117" alt="Hawxy" src="https://avatars.githubusercontent.com/u/975824?v=4&s=117"></a></td><td><a href=https://github.com/elenalape><img width="117" alt="elenalape" src="https://avatars.githubusercontent.com/u/22844059?v=4&s=117"></a></td><td><a href=https://github.com/gavboulton><img width="117" alt="gavboulton" src="https://avatars.githubusercontent.com/u/3900826?v=4&s=117"></a></td><td><a href=https://github.com/mejiaej><img width="117" alt="mejiaej" src="https://avatars.githubusercontent.com/u/4699893?v=4&s=117"></a></td><td><a href=https://github.com/Acconut><img width="117" alt="Acconut" src="https://avatars.githubusercontent.com/u/1375043?v=4&s=117"></a></td><td><a href=https://github.com/jhen0409><img width="117" alt="jhen0409" src="https://avatars.githubusercontent.com/u/3001525?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/mkabatek><img width="117" alt="mkabatek" src="https://avatars.githubusercontent.com/u/1764486?v=4&s=117"></a></td><td><a href=https://github.com/stephentuso><img width="117" alt="stephentuso" src="https://avatars.githubusercontent.com/u/11889560?v=4&s=117"></a></td><td><a href=https://github.com/bencergazda><img width="117" alt="bencergazda" src="https://avatars.githubusercontent.com/u/5767697?v=4&s=117"></a></td><td><a href=https://github.com/a-kriya><img width="117" alt="a-kriya" src="https://avatars.githubusercontent.com/u/26761352?v=4&s=117"></a></td><td><a href=https://github.com/yonahforst><img width="117" alt="yonahforst" src="https://avatars.githubusercontent.com/u/1440796?v=4&s=117"></a></td><td><a href=https://github.com/stanislav-cervenak><img width="117" alt="stanislav-cervenak" src="https://avatars.githubusercontent.com/u/6931349?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/sksavant><img width="117" alt="sksavant" src="https://avatars.githubusercontent.com/u/1040701?v=4&s=117"></a></td><td><a href=https://github.com/ogtfaber><img width="117" alt="ogtfaber" src="https://avatars.githubusercontent.com/u/320955?v=4&s=117"></a></td><td><a href=https://github.com/nndevstudio><img width="117" alt="nndevstudio" src="https://avatars.githubusercontent.com/u/22050968?v=4&s=117"></a></td><td><a href=https://github.com/MatthiasKunnen><img width="117" alt="MatthiasKunnen" src="https://avatars.githubusercontent.com/u/16807587?v=4&s=117"></a></td><td><a href=https://github.com/dargmuesli><img width="117" alt="dargmuesli" src="https://avatars.githubusercontent.com/u/4778485?v=4&s=117"></a></td><td><a href=https://github.com/manuelkiessling><img width="117" alt="manuelkiessling" src="https://avatars.githubusercontent.com/u/206592?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/johnnyperkins><img width="117" alt="johnnyperkins" src="https://avatars.githubusercontent.com/u/16482282?v=4&s=117"></a></td><td><a href=https://github.com/ofhope><img width="117" alt="ofhope" src="https://avatars.githubusercontent.com/u/1826459?v=4&s=117"></a></td><td><a href=https://github.com/yaegor><img width="117" alt="yaegor" src="https://avatars.githubusercontent.com/u/3315?v=4&s=117"></a></td><td><a href=https://github.com/zhuangya><img width="117" alt="zhuangya" src="https://avatars.githubusercontent.com/u/499038?v=4&s=117"></a></td><td><a href=https://github.com/sparanoid><img width="117" alt="sparanoid" src="https://avatars.githubusercontent.com/u/96356?v=4&s=117"></a></td><td><a href=https://github.com/ThomasG77><img width="117" alt="ThomasG77" src="https://avatars.githubusercontent.com/u/642120?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/subha1206><img width="117" alt="subha1206" src="https://avatars.githubusercontent.com/u/36275153?v=4&s=117"></a></td><td><a href=https://github.com/schonert><img width="117" alt="schonert" src="https://avatars.githubusercontent.com/u/2185697?v=4&s=117"></a></td><td><a href=https://github.com/SlavikTraktor><img width="117" alt="SlavikTraktor" src="https://avatars.githubusercontent.com/u/11923751?v=4&s=117"></a></td><td><a href=https://github.com/scottbessler><img width="117" alt="scottbessler" src="https://avatars.githubusercontent.com/u/293802?v=4&s=117"></a></td><td><a href=https://github.com/jrschumacher><img width="117" alt="jrschumacher" src="https://avatars.githubusercontent.com/u/46549?v=4&s=117"></a></td><td><a href=https://github.com/rosenfeld><img width="117" alt="rosenfeld" src="https://avatars.githubusercontent.com/u/32246?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/rdimartino><img width="117" alt="rdimartino" src="https://avatars.githubusercontent.com/u/11539300?v=4&s=117"></a></td><td><a href=https://github.com/ahmedkandel><img width="117" alt="ahmedkandel" src="https://avatars.githubusercontent.com/u/28398523?v=4&s=117"></a></td><td><a href=https://github.com/Youssef1313><img width="117" alt="Youssef1313" src="https://avatars.githubusercontent.com/u/31348972?v=4&s=117"></a></td><td><a href=https://github.com/allenfantasy><img width="117" alt="allenfantasy" src="https://avatars.githubusercontent.com/u/1009294?v=4&s=117"></a></td><td><a href=https://github.com/Zyclotrop-j><img width="117" alt="Zyclotrop-j" src="https://avatars.githubusercontent.com/u/4939546?v=4&s=117"></a></td><td><a href=https://github.com/anark><img width="117" alt="anark" src="https://avatars.githubusercontent.com/u/101184?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/bdirito><img width="117" alt="bdirito" src="https://avatars.githubusercontent.com/u/8117238?v=4&s=117"></a></td><td><a href=https://github.com/darthf1><img width="117" alt="darthf1" src="https://avatars.githubusercontent.com/u/17253332?v=4&s=117"></a></td><td><a href=https://github.com/fortrieb><img width="117" alt="fortrieb" src="https://avatars.githubusercontent.com/u/4126707?v=4&s=117"></a></td><td><a href=https://github.com/frederikhors><img width="117" alt="frederikhors" src="https://avatars.githubusercontent.com/u/41120635?v=4&s=117"></a></td><td><a href=https://github.com/heocoi><img width="117" alt="heocoi" src="https://avatars.githubusercontent.com/u/13751011?v=4&s=117"></a></td><td><a href=https://github.com/jarey><img width="117" alt="jarey" src="https://avatars.githubusercontent.com/u/5025224?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/muhammadInam><img width="117" alt="muhammadInam" src="https://avatars.githubusercontent.com/u/7801708?v=4&s=117"></a></td><td><a href=https://github.com/rettgerst><img width="117" alt="rettgerst" src="https://avatars.githubusercontent.com/u/11684948?v=4&s=117"></a></td><td><a href=https://github.com/jukakoski><img width="117" alt="jukakoski" src="https://avatars.githubusercontent.com/u/52720967?v=4&s=117"></a></td><td><a href=https://github.com/olemoign><img width="117" alt="olemoign" src="https://avatars.githubusercontent.com/u/11632871?v=4&s=117"></a></td><td><a href=https://github.com/5idereal><img width="117" alt="5idereal" src="https://avatars.githubusercontent.com/u/30827929?v=4&s=117"></a></td><td><a href=https://github.com/btrice><img width="117" alt="btrice" src="https://avatars.githubusercontent.com/u/4358225?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/AndrwM><img width="117" alt="AndrwM" src="https://avatars.githubusercontent.com/u/565743?v=4&s=117"></a></td><td><a href=https://github.com/behnammodi><img width="117" alt="behnammodi" src="https://avatars.githubusercontent.com/u/1549069?v=4&s=117"></a></td><td><a href=https://github.com/BePo65><img width="117" alt="BePo65" src="https://avatars.githubusercontent.com/u/6582465?v=4&s=117"></a></td><td><a href=https://github.com/bradedelman><img width="117" alt="bradedelman" src="https://avatars.githubusercontent.com/u/124367?v=4&s=117"></a></td><td><a href=https://github.com/camiloforero><img width="117" alt="camiloforero" src="https://avatars.githubusercontent.com/u/6606686?v=4&s=117"></a></td><td><a href=https://github.com/command-tab><img width="117" alt="command-tab" src="https://avatars.githubusercontent.com/u/3069?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/craig-jennings><img width="117" alt="craig-jennings" src="https://avatars.githubusercontent.com/u/1683368?v=4&s=117"></a></td><td><a href=https://github.com/davekiss><img width="117" alt="davekiss" src="https://avatars.githubusercontent.com/u/1256071?v=4&s=117"></a></td><td><a href=https://github.com/denysdesign><img width="117" alt="denysdesign" src="https://avatars.githubusercontent.com/u/1041797?v=4&s=117"></a></td><td><a href=https://github.com/ethanwillis><img width="117" alt="ethanwillis" src="https://avatars.githubusercontent.com/u/182492?v=4&s=117"></a></td><td><a href=https://github.com/frobinsonj><img width="117" alt="frobinsonj" src="https://avatars.githubusercontent.com/u/16726902?v=4&s=117"></a></td><td><a href=https://github.com/richmeij><img width="117" alt="richmeij" src="https://avatars.githubusercontent.com/u/9741858?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/richartkeil><img width="117" alt="richartkeil" src="https://avatars.githubusercontent.com/u/8680858?v=4&s=117"></a></td><td><a href=https://github.com/paescuj><img width="117" alt="paescuj" src="https://avatars.githubusercontent.com/u/5363448?v=4&s=117"></a></td><td><a href=https://github.com/msand><img width="117" alt="msand" src="https://avatars.githubusercontent.com/u/1131362?v=4&s=117"></a></td><td><a href=https://github.com/martiuslim><img width="117" alt="martiuslim" src="https://avatars.githubusercontent.com/u/17944339?v=4&s=117"></a></td><td><a href=https://github.com/Martin005><img width="117" alt="Martin005" src="https://avatars.githubusercontent.com/u/10096404?v=4&s=117"></a></td><td><a href=https://github.com/mskelton><img width="117" alt="mskelton" src="https://avatars.githubusercontent.com/u/25914066?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/mactavishz><img width="117" alt="mactavishz" src="https://avatars.githubusercontent.com/u/12948083?v=4&s=117"></a></td><td><a href=https://github.com/lafe><img width="117" alt="lafe" src="https://avatars.githubusercontent.com/u/4070008?v=4&s=117"></a></td><td><a href=https://github.com/dogrocker><img width="117" alt="dogrocker" src="https://avatars.githubusercontent.com/u/8379027?v=4&s=117"></a></td><td><a href=https://github.com/jedwood><img width="117" alt="jedwood" src="https://avatars.githubusercontent.com/u/369060?v=4&s=117"></a></td><td><a href=https://github.com/jasonbosco><img width="117" alt="jasonbosco" src="https://avatars.githubusercontent.com/u/458383?v=4&s=117"></a></td><td><a href=https://github.com/ghasrfakhri><img width="117" alt="ghasrfakhri" src="https://avatars.githubusercontent.com/u/4945963?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/geertclerx><img width="117" alt="geertclerx" src="https://avatars.githubusercontent.com/u/1381327?v=4&s=117"></a></td><td><a href=https://github.com/JimmyLv><img width="117" alt="JimmyLv" src="https://avatars.githubusercontent.com/u/4997466?v=4&s=117"></a></td><td><a href=https://github.com/rart><img width="117" alt="rart" src="https://avatars.githubusercontent.com/u/3928341?v=4&s=117"></a></td><td><a href=https://github.com/rossng><img width="117" alt="rossng" src="https://avatars.githubusercontent.com/u/565371?v=4&s=117"></a></td><td><a href=https://github.com/scherroman><img width="117" alt="scherroman" src="https://avatars.githubusercontent.com/u/7923938?v=4&s=117"></a></td><td><a href=https://github.com/robwilson1><img width="117" alt="robwilson1" src="https://avatars.githubusercontent.com/u/7114944?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/SxDx><img width="117" alt="SxDx" src="https://avatars.githubusercontent.com/u/2004247?v=4&s=117"></a></td><td><a href=https://github.com/refo><img width="117" alt="refo" src="https://avatars.githubusercontent.com/u/1114116?v=4&s=117"></a></td><td><a href=https://github.com/raulibanez><img width="117" alt="raulibanez" src="https://avatars.githubusercontent.com/u/1070825?v=4&s=117"></a></td><td><a href=https://github.com/luarmr><img width="117" alt="luarmr" src="https://avatars.githubusercontent.com/u/817416?v=4&s=117"></a></td><td><a href=https://github.com/eman8519><img width="117" alt="eman8519" src="https://avatars.githubusercontent.com/u/2380804?v=4&s=117"></a></td><td><a href=https://github.com/pedantic-git><img width="117" alt="pedantic-git" src="https://avatars.githubusercontent.com/u/962930?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/Pzoco><img width="117" alt="Pzoco" src="https://avatars.githubusercontent.com/u/3101348?v=4&s=117"></a></td><td><a href=https://github.com/ppadmavilasom><img width="117" alt="ppadmavilasom" src="https://avatars.githubusercontent.com/u/11167452?v=4&s=117"></a></td><td><a href=https://github.com/phillipalexander><img width="117" alt="phillipalexander" src="https://avatars.githubusercontent.com/u/1577682?v=4&s=117"></a></td><td><a href=https://github.com/pmusaraj><img width="117" alt="pmusaraj" src="https://avatars.githubusercontent.com/u/368961?v=4&s=117"></a></td><td><a href=https://github.com/pedrofs><img width="117" alt="pedrofs" src="https://avatars.githubusercontent.com/u/56484?v=4&s=117"></a></td><td><a href=https://github.com/plneto><img width="117" alt="plneto" src="https://avatars.githubusercontent.com/u/5697434?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/patricklindsay><img width="117" alt="patricklindsay" src="https://avatars.githubusercontent.com/u/7923681?v=4&s=117"></a></td><td><a href=https://github.com/tcgj><img width="117" alt="tcgj" src="https://avatars.githubusercontent.com/u/7994529?v=4&s=117"></a></td><td><a href=https://github.com/Tashows><img width="117" alt="Tashows" src="https://avatars.githubusercontent.com/u/16656928?v=4&s=117"></a></td><td><a href=https://github.com/taj><img width="117" alt="taj" src="https://avatars.githubusercontent.com/u/16062635?v=4&s=117"></a></td><td><a href=https://github.com/strayer><img width="117" alt="strayer" src="https://avatars.githubusercontent.com/u/310624?v=4&s=117"></a></td><td><a href=https://github.com/sjauld><img width="117" alt="sjauld" src="https://avatars.githubusercontent.com/u/8232503?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/steverob><img width="117" alt="steverob" src="https://avatars.githubusercontent.com/u/1220480?v=4&s=117"></a></td><td><a href=https://github.com/amaitu><img width="117" alt="amaitu" src="https://avatars.githubusercontent.com/u/15688439?v=4&s=117"></a></td><td><a href=https://github.com/quigebo><img width="117" alt="quigebo" src="https://avatars.githubusercontent.com/u/741?v=4&s=117"></a></td><td><a href=https://github.com/waptik><img width="117" alt="waptik" src="https://avatars.githubusercontent.com/u/1687551?v=4&s=117"></a></td><td><a href=https://github.com/SpazzMarticus><img width="117" alt="SpazzMarticus" src="https://avatars.githubusercontent.com/u/5716457?v=4&s=117"></a></td><td><a href=https://github.com/szh><img width="117" alt="szh" src="https://avatars.githubusercontent.com/u/546965?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/sergei-zelinsky><img width="117" alt="sergei-zelinsky" src="https://avatars.githubusercontent.com/u/19428086?v=4&s=117"></a></td><td><a href=https://github.com/sebasegovia01><img width="117" alt="sebasegovia01" src="https://avatars.githubusercontent.com/u/35777287?v=4&s=117"></a></td><td><a href=https://github.com/sdebacker><img width="117" alt="sdebacker" src="https://avatars.githubusercontent.com/u/134503?v=4&s=117"></a></td><td><a href=https://github.com/Rattone><img width="117" alt="Rattone" src="https://avatars.githubusercontent.com/u/7362607?v=4&s=117"></a></td><td><a href=https://github.com/samuelcolburn><img width="117" alt="samuelcolburn" src="https://avatars.githubusercontent.com/u/9741902?v=4&s=117"></a></td><td><a href=https://github.com/fortunto2><img width="117" alt="fortunto2" src="https://avatars.githubusercontent.com/u/1236751?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/GNURub><img width="117" alt="GNURub" src="https://avatars.githubusercontent.com/u/1318648?v=4&s=117"></a></td><td><a href=https://github.com/Mitchell8210><img width="117" alt="Mitchell8210" src="https://avatars.githubusercontent.com/u/23045264?v=4&s=117"></a></td><td><a href=https://github.com/achmiral><img width="117" alt="achmiral" src="https://avatars.githubusercontent.com/u/10906059?v=4&s=117"></a></td><td><a href=https://github.com/ken-kuro><img width="117" alt="ken-kuro" src="https://avatars.githubusercontent.com/u/47441476?v=4&s=117"></a></td><td><a href=https://github.com/milannakum><img width="117" alt="milannakum" src="https://avatars.githubusercontent.com/u/11374368?v=4&s=117"></a></td><td><a href=https://github.com/mkopinsky><img width="117" alt="mkopinsky" src="https://avatars.githubusercontent.com/u/591435?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/mhulet><img width="117" alt="mhulet" src="https://avatars.githubusercontent.com/u/293355?v=4&s=117"></a></td><td><a href=https://github.com/hrsh><img width="117" alt="hrsh" src="https://avatars.githubusercontent.com/u/1929359?v=4&s=117"></a></td><td><a href=https://github.com/mauricioribeiro><img width="117" alt="mauricioribeiro" src="https://avatars.githubusercontent.com/u/2589856?v=4&s=117"></a></td><td><a href=https://github.com/matthewhartstonge><img width="117" alt="matthewhartstonge" src="https://avatars.githubusercontent.com/u/6119549?v=4&s=117"></a></td><td><a href=https://github.com/mjesuele><img width="117" alt="mjesuele" src="https://avatars.githubusercontent.com/u/871117?v=4&s=117"></a></td><td><a href=https://github.com/mattfik><img width="117" alt="mattfik" src="https://avatars.githubusercontent.com/u/1638028?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/mateuscruz><img width="117" alt="mateuscruz" src="https://avatars.githubusercontent.com/u/8962842?v=4&s=117"></a></td><td><a href=https://github.com/masum-ulu><img width="117" alt="masum-ulu" src="https://avatars.githubusercontent.com/u/49063256?v=4&s=117"></a></td><td><a href=https://github.com/masaok><img width="117" alt="masaok" src="https://avatars.githubusercontent.com/u/1320083?v=4&s=117"></a></td><td><a href=https://github.com/martin-brennan><img width="117" alt="martin-brennan" src="https://avatars.githubusercontent.com/u/920448?v=4&s=117"></a></td><td><a href=https://github.com/marcusforsberg><img width="117" alt="marcusforsberg" src="https://avatars.githubusercontent.com/u/1009069?v=4&s=117"></a></td><td><a href=https://github.com/marcosthejew><img width="117" alt="marcosthejew" src="https://avatars.githubusercontent.com/u/1500967?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/mperrando><img width="117" alt="mperrando" src="https://avatars.githubusercontent.com/u/525572?v=4&s=117"></a></td><td><a href=https://github.com/pascalwengerter><img width="117" alt="pascalwengerter" src="https://avatars.githubusercontent.com/u/16822008?v=4&s=117"></a></td><td><a href=https://github.com/ParsaArvanehPA><img width="117" alt="ParsaArvanehPA" src="https://avatars.githubusercontent.com/u/62149413?v=4&s=117"></a></td><td><a href=https://github.com/cryptic022><img width="117" alt="cryptic022" src="https://avatars.githubusercontent.com/u/18145703?v=4&s=117"></a></td><td><a href=https://github.com/Ozodbek1405><img width="117" alt="Ozodbek1405" src="https://avatars.githubusercontent.com/u/86141593?v=4&s=117"></a></td><td><a href=https://github.com/leftdevel><img width="117" alt="leftdevel" src="https://avatars.githubusercontent.com/u/843337?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/nil1511><img width="117" alt="nil1511" src="https://avatars.githubusercontent.com/u/2058170?v=4&s=117"></a></td><td><a href=https://github.com/coreprocess><img width="117" alt="coreprocess" src="https://avatars.githubusercontent.com/u/1226918?v=4&s=117"></a></td><td><a href=https://github.com/nicojones><img width="117" alt="nicojones" src="https://avatars.githubusercontent.com/u/6078915?v=4&s=117"></a></td><td><a href=https://github.com/trungcva10a6tn><img width="117" alt="trungcva10a6tn" src="https://avatars.githubusercontent.com/u/18293783?v=4&s=117"></a></td><td><a href=https://github.com/naveed-ahmad><img width="117" alt="naveed-ahmad" src="https://avatars.githubusercontent.com/u/701567?v=4&s=117"></a></td><td><a href=https://github.com/nadeemc><img width="117" alt="nadeemc" src="https://avatars.githubusercontent.com/u/3429228?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/pleasespammelater><img width="117" alt="pleasespammelater" src="https://avatars.githubusercontent.com/u/11870394?v=4&s=117"></a></td><td><a href=https://github.com/marton-laszlo-attila><img width="117" alt="marton-laszlo-attila" src="https://avatars.githubusercontent.com/u/73295321?v=4&s=117"></a></td><td><a href=https://github.com/navruzm><img width="117" alt="navruzm" src="https://avatars.githubusercontent.com/u/168341?v=4&s=117"></a></td><td><a href=https://github.com/mogzol><img width="117" alt="mogzol" src="https://avatars.githubusercontent.com/u/11789801?v=4&s=117"></a></td><td><a href=https://github.com/shahimclt><img width="117" alt="shahimclt" src="https://avatars.githubusercontent.com/u/8318002?v=4&s=117"></a></td><td><a href=https://github.com/mnafees><img width="117" alt="mnafees" src="https://avatars.githubusercontent.com/u/1763885?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/boudra><img width="117" alt="boudra" src="https://avatars.githubusercontent.com/u/711886?v=4&s=117"></a></td><td><a href=https://github.com/netdown><img width="117" alt="netdown" src="https://avatars.githubusercontent.com/u/4265403?v=4&s=117"></a></td><td><a href=https://github.com/mosi-kha><img width="117" alt="mosi-kha" src="https://avatars.githubusercontent.com/u/35611016?v=4&s=117"></a></td><td><a href=https://github.com/maddy-jo><img width="117" alt="maddy-jo" src="https://avatars.githubusercontent.com/u/3241493?v=4&s=117"></a></td><td><a href=https://github.com/mdxiaohu><img width="117" alt="mdxiaohu" src="https://avatars.githubusercontent.com/u/42248614?v=4&s=117"></a></td><td><a href=https://github.com/magumbo><img width="117" alt="magumbo" src="https://avatars.githubusercontent.com/u/6683765?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/jx-zyf><img width="117" alt="jx-zyf" src="https://avatars.githubusercontent.com/u/26456842?v=4&s=117"></a></td><td><a href=https://github.com/kode-ninja><img width="117" alt="kode-ninja" src="https://avatars.githubusercontent.com/u/7857611?v=4&s=117"></a></td><td><a href=https://github.com/sontixyou><img width="117" alt="sontixyou" src="https://avatars.githubusercontent.com/u/19817196?v=4&s=117"></a></td><td><a href=https://github.com/jur-ng><img width="117" alt="jur-ng" src="https://avatars.githubusercontent.com/u/111122756?v=4&s=117"></a></td><td><a href=https://github.com/johnmanjiro13><img width="117" alt="johnmanjiro13" src="https://avatars.githubusercontent.com/u/28798279?v=4&s=117"></a></td><td><a href=https://github.com/jyoungblood><img width="117" alt="jyoungblood" src="https://avatars.githubusercontent.com/u/56104?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/green-mike><img width="117" alt="green-mike" src="https://avatars.githubusercontent.com/u/5584225?v=4&s=117"></a></td><td><a href=https://github.com/gaelicwinter><img width="117" alt="gaelicwinter" src="https://avatars.githubusercontent.com/u/6510266?v=4&s=117"></a></td><td><a href=https://github.com/franckl><img width="117" alt="franckl" src="https://avatars.githubusercontent.com/u/3875803?v=4&s=117"></a></td><td><a href=https://github.com/fingul><img width="117" alt="fingul" src="https://avatars.githubusercontent.com/u/894739?v=4&s=117"></a></td><td><a href=https://github.com/elliotsayes><img width="117" alt="elliotsayes" src="https://avatars.githubusercontent.com/u/7699058?v=4&s=117"></a></td><td><a href=https://github.com/dzcpy><img width="117" alt="dzcpy" src="https://avatars.githubusercontent.com/u/203980?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/dkisic><img width="117" alt="dkisic" src="https://avatars.githubusercontent.com/u/32257921?v=4&s=117"></a></td><td><a href=https://github.com/zanzlender><img width="117" alt="zanzlender" src="https://avatars.githubusercontent.com/u/44570474?v=4&s=117"></a></td><td><a href=https://github.com/olitomas><img width="117" alt="olitomas" src="https://avatars.githubusercontent.com/u/6918659?v=4&s=117"></a></td><td><a href=https://github.com/yoann-hellopret><img width="117" alt="yoann-hellopret" src="https://avatars.githubusercontent.com/u/46525558?v=4&s=117"></a></td><td><a href=https://github.com/vedran555><img width="117" alt="vedran555" src="https://avatars.githubusercontent.com/u/38395951?v=4&s=117"></a></td><td><a href=https://github.com/tusharjkhunt><img width="117" alt="tusharjkhunt" src="https://avatars.githubusercontent.com/u/31904234?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/thanhthot><img width="117" alt="thanhthot" src="https://avatars.githubusercontent.com/u/50633205?v=4&s=117"></a></td><td><a href=https://github.com/stduhpf><img width="117" alt="stduhpf" src="https://avatars.githubusercontent.com/u/28208228?v=4&s=117"></a></td><td><a href=https://github.com/slawexxx44><img width="117" alt="slawexxx44" src="https://avatars.githubusercontent.com/u/11180644?v=4&s=117"></a></td><td><a href=https://github.com/rtaieb><img width="117" alt="rtaieb" src="https://avatars.githubusercontent.com/u/35224301?v=4&s=117"></a></td><td><a href=https://github.com/rmoura-92><img width="117" alt="rmoura-92" src="https://avatars.githubusercontent.com/u/419044?v=4&s=117"></a></td><td><a href=https://github.com/rlebosse><img width="117" alt="rlebosse" src="https://avatars.githubusercontent.com/u/2794137?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/rhymes><img width="117" alt="rhymes" src="https://avatars.githubusercontent.com/u/146201?v=4&s=117"></a></td><td><a href=https://github.com/luntta><img width="117" alt="luntta" src="https://avatars.githubusercontent.com/u/14221637?v=4&s=117"></a></td><td><a href=https://github.com/phil714><img width="117" alt="phil714" src="https://avatars.githubusercontent.com/u/7584581?v=4&s=117"></a></td><td><a href=https://github.com/ordago><img width="117" alt="ordago" src="https://avatars.githubusercontent.com/u/6376814?v=4&s=117"></a></td><td><a href=https://github.com/odselsevier><img width="117" alt="odselsevier" src="https://avatars.githubusercontent.com/u/95745934?v=4&s=117"></a></td><td><a href=https://github.com/ninesalt><img width="117" alt="ninesalt" src="https://avatars.githubusercontent.com/u/7952255?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/neuronet77><img width="117" alt="neuronet77" src="https://avatars.githubusercontent.com/u/4220037?v=4&s=117"></a></td><td><a href=https://github.com/craigcbrunner><img width="117" alt="craigcbrunner" src="https://avatars.githubusercontent.com/u/2780521?v=4&s=117"></a></td><td><a href=https://github.com/weston-sankey-mark43><img width="117" alt="weston-sankey-mark43" src="https://avatars.githubusercontent.com/u/97678695?v=4&s=117"></a></td><td><a href=https://github.com/dwnste><img width="117" alt="dwnste" src="https://avatars.githubusercontent.com/u/17119722?v=4&s=117"></a></td><td><a href=https://github.com/nagyv><img width="117" alt="nagyv" src="https://avatars.githubusercontent.com/u/126671?v=4&s=117"></a></td><td><a href=https://github.com/stiig><img width="117" alt="stiig" src="https://avatars.githubusercontent.com/u/8639922?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/valentinoli><img width="117" alt="valentinoli" src="https://avatars.githubusercontent.com/u/23453691?v=4&s=117"></a></td><td><a href=https://github.com/vially><img width="117" alt="vially" src="https://avatars.githubusercontent.com/u/433598?v=4&s=117"></a></td><td><a href=https://github.com/bodryi><img width="117" alt="bodryi" src="https://avatars.githubusercontent.com/u/7326310?v=4&s=117"></a></td><td><a href=https://github.com/tyler-dot-earth><img width="117" alt="tyler-dot-earth" src="https://avatars.githubusercontent.com/u/595711?v=4&s=117"></a></td><td><a href=https://github.com/trivikr><img width="117" alt="trivikr" src="https://avatars.githubusercontent.com/u/16024985?v=4&s=117"></a></td><td><a href=https://github.com/tanadeau><img width="117" alt="tanadeau" src="https://avatars.githubusercontent.com/u/3734457?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/top-master><img width="117" alt="top-master" src="https://avatars.githubusercontent.com/u/31405473?v=4&s=117"></a></td><td><a href=https://github.com/tvaliasek><img width="117" alt="tvaliasek" src="https://avatars.githubusercontent.com/u/8644946?v=4&s=117"></a></td><td><a href=https://github.com/tomekp><img width="117" alt="tomekp" src="https://avatars.githubusercontent.com/u/1856393?v=4&s=117"></a></td><td><a href=https://github.com/tomsaleeba><img width="117" alt="tomsaleeba" src="https://avatars.githubusercontent.com/u/1773838?v=4&s=117"></a></td><td><a href=https://github.com/WIStudent><img width="117" alt="WIStudent" src="https://avatars.githubusercontent.com/u/2707930?v=4&s=117"></a></td><td><a href=https://github.com/tmaier><img width="117" alt="tmaier" src="https://avatars.githubusercontent.com/u/350038?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/Tiarhai><img width="117" alt="Tiarhai" src="https://avatars.githubusercontent.com/u/12871513?v=4&s=117"></a></td><td><a href=https://github.com/twarlop><img width="117" alt="twarlop" src="https://avatars.githubusercontent.com/u/2856082?v=4&s=117"></a></td><td><a href=https://github.com/codehero7386><img width="117" alt="codehero7386" src="https://avatars.githubusercontent.com/u/56253286?v=4&s=117"></a></td><td><a href=https://github.com/christianwengert><img width="117" alt="christianwengert" src="https://avatars.githubusercontent.com/u/12936057?v=4&s=117"></a></td><td><a href=https://github.com/cgoinglove><img width="117" alt="cgoinglove" src="https://avatars.githubusercontent.com/u/86150470?v=4&s=117"></a></td><td><a href=https://github.com/canvasbh><img width="117" alt="canvasbh" src="https://avatars.githubusercontent.com/u/44477734?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/c0b41><img width="117" alt="c0b41" src="https://avatars.githubusercontent.com/u/2834954?v=4&s=117"></a></td><td><a href=https://github.com/avalla><img width="117" alt="avalla" src="https://avatars.githubusercontent.com/u/986614?v=4&s=117"></a></td><td><a href=https://github.com/arggh><img width="117" alt="arggh" src="https://avatars.githubusercontent.com/u/17210302?v=4&s=117"></a></td><td><a href=https://github.com/alfatv><img width="117" alt="alfatv" src="https://avatars.githubusercontent.com/u/62238673?v=4&s=117"></a></td><td><a href=https://github.com/agreene-coursera><img width="117" alt="agreene-coursera" src="https://avatars.githubusercontent.com/u/30501355?v=4&s=117"></a></td><td><a href=https://github.com/aduh95-test-account><img width="117" alt="aduh95-test-account" src="https://avatars.githubusercontent.com/u/93441190?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/sartoshi-foot-dao><img width="117" alt="sartoshi-foot-dao" src="https://avatars.githubusercontent.com/u/99770068?v=4&s=117"></a></td><td><a href=https://github.com/zackbloom><img width="117" alt="zackbloom" src="https://avatars.githubusercontent.com/u/55347?v=4&s=117"></a></td><td><a href=https://github.com/zlawson-ut><img width="117" alt="zlawson-ut" src="https://avatars.githubusercontent.com/u/7375444?v=4&s=117"></a></td><td><a href=https://github.com/zachconner><img width="117" alt="zachconner" src="https://avatars.githubusercontent.com/u/11339326?v=4&s=117"></a></td><td><a href=https://github.com/yafkari><img width="117" alt="yafkari" src="https://avatars.githubusercontent.com/u/41365655?v=4&s=117"></a></td><td><a href=https://github.com/YehudaKremer><img width="117" alt="YehudaKremer" src="https://avatars.githubusercontent.com/u/946652?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/xhocquet><img width="117" alt="xhocquet" src="https://avatars.githubusercontent.com/u/8116516?v=4&s=117"></a></td><td><a href=https://github.com/willycamargo><img width="117" alt="willycamargo" src="https://avatars.githubusercontent.com/u/5041887?v=4&s=117"></a></td><td><a href=https://github.com/onhate><img width="117" alt="onhate" src="https://avatars.githubusercontent.com/u/980905?v=4&s=117"></a></td><td><a href=https://github.com/ardeois><img width="117" alt="ardeois" src="https://avatars.githubusercontent.com/u/1867939?v=4&s=117"></a></td><td><a href=https://github.com/CommanderRoot><img width="117" alt="CommanderRoot" src="https://avatars.githubusercontent.com/u/4395417?v=4&s=117"></a></td><td><a href=https://github.com/czj><img width="117" alt="czj" src="https://avatars.githubusercontent.com/u/14306?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/cbush06><img width="117" alt="cbush06" src="https://avatars.githubusercontent.com/u/15720146?v=4&s=117"></a></td><td><a href=https://github.com/Aarbel><img width="117" alt="Aarbel" src="https://avatars.githubusercontent.com/u/25119847?v=4&s=117"></a></td><td><a href=https://github.com/cfra><img width="117" alt="cfra" src="https://avatars.githubusercontent.com/u/1347051?v=4&s=117"></a></td><td><a href=https://github.com/csprance><img width="117" alt="csprance" src="https://avatars.githubusercontent.com/u/7902617?v=4&s=117"></a></td><td><a href=https://github.com/prattcmp><img width="117" alt="prattcmp" src="https://avatars.githubusercontent.com/u/1497950?v=4&s=117"></a></td><td><a href=https://github.com/subvertallchris><img width="117" alt="subvertallchris" src="https://avatars.githubusercontent.com/u/4097271?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/charlybillaud><img width="117" alt="charlybillaud" src="https://avatars.githubusercontent.com/u/31970410?v=4&s=117"></a></td><td><a href=https://github.com/Cretezy><img width="117" alt="Cretezy" src="https://avatars.githubusercontent.com/u/2672503?v=4&s=117"></a></td><td><a href=https://github.com/chao><img width="117" alt="chao" src="https://avatars.githubusercontent.com/u/55872?v=4&s=117"></a></td><td><a href=https://github.com/cellvinchung><img width="117" alt="cellvinchung" src="https://avatars.githubusercontent.com/u/5347394?v=4&s=117"></a></td><td><a href=https://github.com/cartfisk><img width="117" alt="cartfisk" src="https://avatars.githubusercontent.com/u/8764375?v=4&s=117"></a></td><td><a href=https://github.com/cyu><img width="117" alt="cyu" src="https://avatars.githubusercontent.com/u/2431?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/bryanjswift><img width="117" alt="bryanjswift" src="https://avatars.githubusercontent.com/u/9911?v=4&s=117"></a></td><td><a href=https://github.com/bedgerotto><img width="117" alt="bedgerotto" src="https://avatars.githubusercontent.com/u/4459657?v=4&s=117"></a></td><td><a href=https://github.com/wbaaron><img width="117" alt="wbaaron" src="https://avatars.githubusercontent.com/u/1048988?v=4&s=117"></a></td><td><a href=https://github.com/yoldar><img width="117" alt="yoldar" src="https://avatars.githubusercontent.com/u/1597578?v=4&s=117"></a></td><td><a href=https://github.com/efbautista><img width="117" alt="efbautista" src="https://avatars.githubusercontent.com/u/35430671?v=4&s=117"></a></td><td><a href=https://github.com/emuell><img width="117" alt="emuell" src="https://avatars.githubusercontent.com/u/11521600?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/EdgarSantiago93><img width="117" alt="EdgarSantiago93" src="https://avatars.githubusercontent.com/u/14806877?v=4&s=117"></a></td><td><a href=https://github.com/sweetro><img width="117" alt="sweetro" src="https://avatars.githubusercontent.com/u/6228717?v=4&s=117"></a></td><td><a href=https://github.com/jeetiss><img width="117" alt="jeetiss" src="https://avatars.githubusercontent.com/u/6726016?v=4&s=117"></a></td><td><a href=https://github.com/DennisKofflard><img width="117" alt="DennisKofflard" src="https://avatars.githubusercontent.com/u/8669129?v=4&s=117"></a></td><td><a href=https://github.com/hoangsvit><img width="117" alt="hoangsvit" src="https://avatars.githubusercontent.com/u/11882322?v=4&s=117"></a></td><td><a href=https://github.com/davilima6><img width="117" alt="davilima6" src="https://avatars.githubusercontent.com/u/422130?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/akizor><img width="117" alt="akizor" src="https://avatars.githubusercontent.com/u/1052439?v=4&s=117"></a></td><td><a href=https://github.com/KaminskiDaniell><img width="117" alt="KaminskiDaniell" src="https://avatars.githubusercontent.com/u/27357868?v=4&s=117"></a></td><td><a href=https://github.com/Cantabar><img width="117" alt="Cantabar" src="https://avatars.githubusercontent.com/u/6812207?v=4&s=117"></a></td><td><a href=https://github.com/mrboomer><img width="117" alt="mrboomer" src="https://avatars.githubusercontent.com/u/5942912?v=4&s=117"></a></td><td><a href=https://github.com/danilat><img width="117" alt="danilat" src="https://avatars.githubusercontent.com/u/22763?v=4&s=117"></a></td><td><a href=https://github.com/danschalow><img width="117" alt="danschalow" src="https://avatars.githubusercontent.com/u/3527437?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/danmichaelo><img width="117" alt="danmichaelo" src="https://avatars.githubusercontent.com/u/434495?v=4&s=117"></a></td><td><a href=https://github.com/Cruaier><img width="117" alt="Cruaier" src="https://avatars.githubusercontent.com/u/5204940?v=4&s=117"></a></td><td><a href=https://github.com/sercraig><img width="117" alt="sercraig" src="https://avatars.githubusercontent.com/u/24261518?v=4&s=117"></a></td><td><a href=https://github.com/Quorafind><img width="117" alt="Quorafind" src="https://avatars.githubusercontent.com/u/13215013?v=4&s=117"></a></td><td><a href=https://github.com/amitport><img width="117" alt="amitport" src="https://avatars.githubusercontent.com/u/1131991?v=4&s=117"></a></td><td><a href=https://github.com/tekacs><img width="117" alt="tekacs" src="https://avatars.githubusercontent.com/u/63247?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/Dogfalo><img width="117" alt="Dogfalo" src="https://avatars.githubusercontent.com/u/2775751?v=4&s=117"></a></td><td><a href=https://github.com/alirezahi><img width="117" alt="alirezahi" src="https://avatars.githubusercontent.com/u/16666064?v=4&s=117"></a></td><td><a href=https://github.com/aalepis><img width="117" alt="aalepis" src="https://avatars.githubusercontent.com/u/35684834?v=4&s=117"></a></td><td><a href=https://github.com/alexnj><img width="117" alt="alexnj" src="https://avatars.githubusercontent.com/u/683500?v=4&s=117"></a></td><td><a href=https://github.com/asmt3><img width="117" alt="asmt3" src="https://avatars.githubusercontent.com/u/1777709?v=4&s=117"></a></td><td><a href=https://github.com/ahmadissa><img width="117" alt="ahmadissa" src="https://avatars.githubusercontent.com/u/9936573?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/adritasharma><img width="117" alt="adritasharma" src="https://avatars.githubusercontent.com/u/29271635?v=4&s=117"></a></td><td><a href=https://github.com/Adrrei><img width="117" alt="Adrrei" src="https://avatars.githubusercontent.com/u/22191685?v=4&s=117"></a></td><td><a href=https://github.com/adityapatadia><img width="117" alt="adityapatadia" src="https://avatars.githubusercontent.com/u/1086617?v=4&s=117"></a></td><td><a href=https://github.com/adamvigneault><img width="117" alt="adamvigneault" src="https://avatars.githubusercontent.com/u/18236120?v=4&s=117"></a></td><td><a href=https://github.com/ajh-sr><img width="117" alt="ajh-sr" src="https://avatars.githubusercontent.com/u/71472057?v=4&s=117"></a></td><td><a href=https://github.com/adamdottv><img width="117" alt="adamdottv" src="https://avatars.githubusercontent.com/u/2363879?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/abannach><img width="117" alt="abannach" src="https://avatars.githubusercontent.com/u/43150303?v=4&s=117"></a></td><td><a href=https://github.com/aaron-russell><img width="117" alt="aaron-russell" src="https://avatars.githubusercontent.com/u/4678453?v=4&s=117"></a></td><td><a href=https://github.com/superhawk610><img width="117" alt="superhawk610" src="https://avatars.githubusercontent.com/u/18172185?v=4&s=117"></a></td><td><a href=https://github.com/ajschmidt8><img width="117" alt="ajschmidt8" src="https://avatars.githubusercontent.com/u/7400326?v=4&s=117"></a></td><td><a href=https://github.com/bducharme><img width="117" alt="bducharme" src="https://avatars.githubusercontent.com/u/4173569?v=4&s=117"></a></td><td><a href=https://github.com/azizk><img width="117" alt="azizk" src="https://avatars.githubusercontent.com/u/37282?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/azeemba><img width="117" alt="azeemba" src="https://avatars.githubusercontent.com/u/2160795?v=4&s=117"></a></td><td><a href=https://github.com/ayhankesicioglu><img width="117" alt="ayhankesicioglu" src="https://avatars.githubusercontent.com/u/36304312?v=4&s=117"></a></td><td><a href=https://github.com/avneetmalhotra><img width="117" alt="avneetmalhotra" src="https://avatars.githubusercontent.com/u/10562207?v=4&s=117"></a></td><td><a href=https://github.com/The-Flash><img width="117" alt="The-Flash" src="https://avatars.githubusercontent.com/u/24375820?v=4&s=117"></a></td><td><a href=https://github.com/atsawin><img width="117" alt="atsawin" src="https://avatars.githubusercontent.com/u/666663?v=4&s=117"></a></td><td><a href=https://github.com/ash-jc-allen><img width="117" alt="ash-jc-allen" src="https://avatars.githubusercontent.com/u/39652331?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/apuyou><img width="117" alt="apuyou" src="https://avatars.githubusercontent.com/u/520053?v=4&s=117"></a></td><td><a href=https://github.com/arthurdenner><img width="117" alt="arthurdenner" src="https://avatars.githubusercontent.com/u/13774309?v=4&s=117"></a></td><td><a href=https://github.com/Abourass><img width="117" alt="Abourass" src="https://avatars.githubusercontent.com/u/39917231?v=4&s=117"></a></td><td><a href=https://github.com/tyndria><img width="117" alt="tyndria" src="https://avatars.githubusercontent.com/u/17138916?v=4&s=117"></a></td><td><a href=https://github.com/anthony0030><img width="117" alt="anthony0030" src="https://avatars.githubusercontent.com/u/13033263?v=4&s=117"></a></td><td><a href=https://github.com/andychongyz><img width="117" alt="andychongyz" src="https://avatars.githubusercontent.com/u/12697240?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/andrii-bodnar><img width="117" alt="andrii-bodnar" src="https://avatars.githubusercontent.com/u/29282228?v=4&s=117"></a></td><td><a href=https://github.com/superandrew213><img width="117" alt="superandrew213" src="https://avatars.githubusercontent.com/u/13059204?v=4&s=117"></a></td><td><a href=https://github.com/radarhere><img width="117" alt="radarhere" src="https://avatars.githubusercontent.com/u/3112309?v=4&s=117"></a></td><td><a href=https://github.com/functino><img width="117" alt="functino" src="https://avatars.githubusercontent.com/u/415498?v=4&s=117"></a></td><td><a href=https://github.com/kevin-west-10x><img width="117" alt="kevin-west-10x" src="https://avatars.githubusercontent.com/u/65194914?v=4&s=117"></a></td><td><a href=https://github.com/kergekacsa><img width="117" alt="kergekacsa" src="https://avatars.githubusercontent.com/u/16637320?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/firesharkstudios><img width="117" alt="firesharkstudios" src="https://avatars.githubusercontent.com/u/17069637?v=4&s=117"></a></td><td><a href=https://github.com/kaspermeinema><img width="117" alt="kaspermeinema" src="https://avatars.githubusercontent.com/u/73821331?v=4&s=117"></a></td><td><a href=https://github.com/tykarol><img width="117" alt="tykarol" src="https://avatars.githubusercontent.com/u/9386320?v=4&s=117"></a></td><td><a href=https://github.com/jvelten><img width="117" alt="jvelten" src="https://avatars.githubusercontent.com/u/48118068?v=4&s=117"></a></td><td><a href=https://github.com/mellow-fellow><img width="117" alt="mellow-fellow" src="https://avatars.githubusercontent.com/u/19280122?v=4&s=117"></a></td><td><a href=https://github.com/jmontoyaa><img width="117" alt="jmontoyaa" src="https://avatars.githubusercontent.com/u/158935?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/jcalonso><img width="117" alt="jcalonso" src="https://avatars.githubusercontent.com/u/664474?v=4&s=117"></a></td><td><a href=https://github.com/jbelej><img width="117" alt="jbelej" src="https://avatars.githubusercontent.com/u/2229202?v=4&s=117"></a></td><td><a href=https://github.com/jszobody><img width="117" alt="jszobody" src="https://avatars.githubusercontent.com/u/203749?v=4&s=117"></a></td><td><a href=https://github.com/jorgeepc><img width="117" alt="jorgeepc" src="https://avatars.githubusercontent.com/u/3879892?v=4&s=117"></a></td><td><a href=https://github.com/jondewoo><img width="117" alt="jondewoo" src="https://avatars.githubusercontent.com/u/1108358?v=4&s=117"></a></td><td><a href=https://github.com/jonathanarbely><img width="117" alt="jonathanarbely" src="https://avatars.githubusercontent.com/u/18177203?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/jsanchez034><img width="117" alt="jsanchez034" src="https://avatars.githubusercontent.com/u/761087?v=4&s=117"></a></td><td><a href=https://github.com/Jokcy><img width="117" alt="Jokcy" src="https://avatars.githubusercontent.com/u/2088642?v=4&s=117"></a></td><td><a href=https://github.com/chromacoma><img width="117" alt="chromacoma" src="https://avatars.githubusercontent.com/u/1535623?v=4&s=117"></a></td><td><a href=https://github.com/profsmallpine><img width="117" alt="profsmallpine" src="https://avatars.githubusercontent.com/u/7328006?v=4&s=117"></a></td><td><a href=https://github.com/marc-mabe><img width="117" alt="marc-mabe" src="https://avatars.githubusercontent.com/u/302689?v=4&s=117"></a></td><td><a href=https://github.com/Lucklj521><img width="117" alt="Lucklj521" src="https://avatars.githubusercontent.com/u/93632042?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/lucax88x><img width="117" alt="lucax88x" src="https://avatars.githubusercontent.com/u/6294464?v=4&s=117"></a></td><td><a href=https://github.com/lucaperret><img width="117" alt="lucaperret" src="https://avatars.githubusercontent.com/u/1887122?v=4&s=117"></a></td><td><a href=https://github.com/ombr><img width="117" alt="ombr" src="https://avatars.githubusercontent.com/u/857339?v=4&s=117"></a></td><td><a href=https://github.com/louim><img width="117" alt="louim" src="https://avatars.githubusercontent.com/u/923718?v=4&s=117"></a></td><td><a href=https://github.com/dolphinigle><img width="117" alt="dolphinigle" src="https://avatars.githubusercontent.com/u/7020472?v=4&s=117"></a></td><td><a href=https://github.com/leomelzer><img width="117" alt="leomelzer" src="https://avatars.githubusercontent.com/u/23313?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/leods92><img width="117" alt="leods92" src="https://avatars.githubusercontent.com/u/879395?v=4&s=117"></a></td><td><a href=https://github.com/galli-leo><img width="117" alt="galli-leo" src="https://avatars.githubusercontent.com/u/5339762?v=4&s=117"></a></td><td><a href=https://github.com/dviry><img width="117" alt="dviry" src="https://avatars.githubusercontent.com/u/1230260?v=4&s=117"></a></td><td><a href=https://github.com/larowlan><img width="117" alt="larowlan" src="https://avatars.githubusercontent.com/u/555254?v=4&s=117"></a></td><td><a href=https://github.com/leaanthony><img width="117" alt="leaanthony" src="https://avatars.githubusercontent.com/u/1943904?v=4&s=117"></a></td><td><a href=https://github.com/hoangbits><img width="117" alt="hoangbits" src="https://avatars.githubusercontent.com/u/7990827?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/labohkip81><img width="117" alt="labohkip81" src="https://avatars.githubusercontent.com/u/36964869?v=4&s=117"></a></td><td><a href=https://github.com/kyleparisi><img width="117" alt="kyleparisi" src="https://avatars.githubusercontent.com/u/1286753?v=4&s=117"></a></td><td><a href=https://github.com/elkebab><img width="117" alt="elkebab" src="https://avatars.githubusercontent.com/u/6313468?v=4&s=117"></a></td><td><a href=https://github.com/kidonng><img width="117" alt="kidonng" src="https://avatars.githubusercontent.com/u/44045911?v=4&s=117"></a></td><td><a href=https://github.com/theJoeBiz><img width="117" alt="theJoeBiz" src="https://avatars.githubusercontent.com/u/189589?v=4&s=117"></a></td><td><a href=https://github.com/huydod><img width="117" alt="huydod" src="https://avatars.githubusercontent.com/u/37580530?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/HussainAlkhalifah><img width="117" alt="HussainAlkhalifah" src="https://avatars.githubusercontent.com/u/43642162?v=4&s=117"></a></td><td><a href=https://github.com/HughbertD><img width="117" alt="HughbertD" src="https://avatars.githubusercontent.com/u/1580021?v=4&s=117"></a></td><td><a href=https://github.com/hiromi2424><img width="117" alt="hiromi2424" src="https://avatars.githubusercontent.com/u/191297?v=4&s=117"></a></td><td><a href=https://github.com/giacomocerquone><img width="117" alt="giacomocerquone" src="https://avatars.githubusercontent.com/u/9303791?v=4&s=117"></a></td><td><a href=https://github.com/roenschg><img width="117" alt="roenschg" src="https://avatars.githubusercontent.com/u/9590236?v=4&s=117"></a></td><td><a href=https://github.com/gjungb><img width="117" alt="gjungb" src="https://avatars.githubusercontent.com/u/3391068?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/geoffappleford><img width="117" alt="geoffappleford" src="https://avatars.githubusercontent.com/u/731678?v=4&s=117"></a></td><td><a href=https://github.com/gabiganam><img width="117" alt="gabiganam" src="https://avatars.githubusercontent.com/u/28859646?v=4&s=117"></a></td><td><a href=https://github.com/fuadscodes><img width="117" alt="fuadscodes" src="https://avatars.githubusercontent.com/u/60370584?v=4&s=117"></a></td><td><a href=https://github.com/dtrucs><img width="117" alt="dtrucs" src="https://avatars.githubusercontent.com/u/1926041?v=4&s=117"></a></td><td><a href=https://github.com/ferdiusa><img width="117" alt="ferdiusa" src="https://avatars.githubusercontent.com/u/1997982?v=4&s=117"></a></td><td><a href=https://github.com/fgallinari><img width="117" alt="fgallinari" src="https://avatars.githubusercontent.com/u/6473638?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/Gkleinereva><img width="117" alt="Gkleinereva" src="https://avatars.githubusercontent.com/u/23621633?v=4&s=117"></a></td><td><a href=https://github.com/epexa><img width="117" alt="epexa" src="https://avatars.githubusercontent.com/u/2198826?v=4&s=117"></a></td><td><a href=https://github.com/EnricoSottile><img width="117" alt="EnricoSottile" src="https://avatars.githubusercontent.com/u/10349653?v=4&s=117"></a></td><td><a href=https://github.com/elliotdickison><img width="117" alt="elliotdickison" src="https://avatars.githubusercontent.com/u/2523678?v=4&s=117"></a></td><td><a href=https://github.com/eliOcs><img width="117" alt="eliOcs" src="https://avatars.githubusercontent.com/u/1283954?v=4&s=117"></a></td><td><a href=https://github.com/Jmales><img width="117" alt="Jmales" src="https://avatars.githubusercontent.com/u/22914881?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/jessica-coursera><img width="117" alt="jessica-coursera" src="https://avatars.githubusercontent.com/u/35155465?v=4&s=117"></a></td><td><a href=https://github.com/vith><img width="117" alt="vith" src="https://avatars.githubusercontent.com/u/3265539?v=4&s=117"></a></td><td><a href=https://github.com/janwilts><img width="117" alt="janwilts" src="https://avatars.githubusercontent.com/u/16721581?v=4&s=117"></a></td><td><a href=https://github.com/janklimo><img width="117" alt="janklimo" src="https://avatars.githubusercontent.com/u/7811733?v=4&s=117"></a></td><td><a href=https://github.com/jamestiotio><img width="117" alt="jamestiotio" src="https://avatars.githubusercontent.com/u/18364745?v=4&s=117"></a></td><td><a href=https://github.com/jcjmcclean><img width="117" alt="jcjmcclean" src="https://avatars.githubusercontent.com/u/1822574?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/Jbithell><img width="117" alt="Jbithell" src="https://avatars.githubusercontent.com/u/8408967?v=4&s=117"></a></td><td><a href=https://github.com/JakubHaladej><img width="117" alt="JakubHaladej" src="https://avatars.githubusercontent.com/u/77832677?v=4&s=117"></a></td><td><a href=https://github.com/jakemcallister><img width="117" alt="jakemcallister" src="https://avatars.githubusercontent.com/u/1185699?v=4&s=117"></a></td><td><a href=https://github.com/gaejabong><img width="117" alt="gaejabong" src="https://avatars.githubusercontent.com/u/978944?v=4&s=117"></a></td><td><a href=https://github.com/JacobMGEvans><img width="117" alt="JacobMGEvans" src="https://avatars.githubusercontent.com/u/27247160?v=4&s=117"></a></td><td><a href=https://github.com/mazoruss><img width="117" alt="mazoruss" src="https://avatars.githubusercontent.com/u/17625190?v=4&s=117"></a></td></tr>
<tr><td><a href=https://github.com/GreenJimmy><img width="117" alt="GreenJimmy" src="https://avatars.githubusercontent.com/u/39386?v=4&s=117"></a></td><td><a href=https://github.com/intenzive><img width="117" alt="intenzive" src="https://avatars.githubusercontent.com/u/11055931?v=4&s=117"></a></td><td><a href=https://github.com/NaxYo><img width="117" alt="NaxYo" src="https://avatars.githubusercontent.com/u/1963876?v=4&s=117"></a></td><td><a href=https://github.com/ishendyweb><img width="117" alt="ishendyweb" src="https://avatars.githubusercontent.com/u/10582418?v=4&s=117"></a></td><td><a href=https://github.com/IanVS><img width="117" alt="IanVS" src="https://avatars.githubusercontent.com/u/4616705?v=4&s=117"></a></td></tr>
<!--/contributors-->
</table>
![contributors table](https://contrib.rocks/image?repo=transloadit/uppy)
## License

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,004 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,017 B

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,006 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 MiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

View file

@ -1,26 +0,0 @@
module.exports = (api) => {
const targets = {}
if (api.env('test')) {
targets.node = 'current'
}
return {
presets: [
['@babel/preset-env', {
include: [
'@babel/plugin-proposal-nullish-coalescing-operator',
'@babel/plugin-proposal-optional-chaining',
'@babel/plugin-proposal-numeric-separator',
],
loose: true,
targets,
useBuiltIns: false, // Don't add polyfills automatically.
modules: false,
}],
],
plugins: [
['@babel/plugin-transform-react-jsx', { pragma: 'h', pragmaFrag: 'Fragment' }],
process.env.NODE_ENV !== 'dev' && 'babel-plugin-inline-package-json',
].filter(Boolean),
}
}

View file

@ -1,60 +0,0 @@
#!/usr/bin/env node
import fs from 'node:fs/promises'
import chalk from 'chalk'
import esbuild from 'esbuild'
const UPPY_ROOT = new URL('../', import.meta.url)
const PACKAGES_ROOT = new URL('./packages/', UPPY_ROOT)
function buildBundle (srcFile, bundleFile, { minify = true, standalone = '', plugins, target, format } = {}) {
return esbuild.build({
bundle: true,
sourcemap: true,
entryPoints: [srcFile],
outfile: bundleFile,
platform: 'browser',
minify,
keepNames: target !== 'es5',
plugins,
tsconfigRaw: '{}',
target,
format,
}).then(() => {
if (minify) {
console.info(chalk.green(`✓ Built Minified Bundle [${standalone}]:`), chalk.magenta(bundleFile))
} else {
console.info(chalk.green(`✓ Built Bundle [${standalone}]:`), chalk.magenta(bundleFile))
}
})
}
await fs.mkdir(new URL('./uppy/dist', PACKAGES_ROOT), { recursive: true })
const methods = [
buildBundle(
'./packages/uppy/src/bundle.ts',
'./packages/uppy/dist/uppy.min.mjs',
{ standalone: 'Uppy (ESM)', format: 'esm' },
),
buildBundle(
'./packages/uppy/bundle.mjs',
'./packages/uppy/dist/uppy.min.js',
{ standalone: 'Uppy', format: 'iife' },
),
]
// Add BUNDLE-README.MD
methods.push(
fs.copyFile(
new URL('./BUNDLE-README.md', UPPY_ROOT),
new URL('./uppy/dist/README.md', PACKAGES_ROOT),
),
)
await Promise.all(methods).then(() => {
console.info(chalk.yellow('✓ JS bundles 🎉'))
}, (err) => {
console.error(chalk.red('✗ Error:'), chalk.red(err.message))
})

View file

@ -1,97 +0,0 @@
const sass = require('sass')
const postcss = require('postcss')
const autoprefixer = require('autoprefixer')
const postcssLogical = require('postcss-logical')
const postcssDirPseudoClass = require('postcss-dir-pseudo-class')
const cssnano = require('cssnano')
const { promisify } = require('node:util')
const fs = require('node:fs')
const path = require('node:path')
const resolve = require('resolve')
const glob = promisify(require('glob'))
const renderScss = promisify(sass.render)
const { mkdir, writeFile } = fs.promises
const cwd = process.cwd()
let chalk
function handleErr (err) {
console.error(chalk.red('✗ Error:'), chalk.red(err.message))
}
async function compileCSS () {
({ default:chalk } = await import('chalk'))
const files = await glob('packages/{,@uppy/}*/src/style.scss')
for (const file of files) {
const importedFiles = new Set()
const scssResult = await renderScss({
file,
importer (url, from, done) {
resolve(url, {
basedir: path.dirname(from),
filename: from,
extensions: ['.scss'],
}, (err, resolved) => {
if (err) {
done(err)
return
}
const realpath = fs.realpathSync(resolved)
if (importedFiles.has(realpath)) {
done({ contents: '' })
return
}
importedFiles.add(realpath)
done({ file: realpath })
})
},
})
const plugins = [
autoprefixer,
postcssLogical(),
postcssDirPseudoClass(),
]
const postcssResult = await postcss(plugins)
.process(scssResult.css, { from: file })
postcssResult.warnings().forEach((warn) => {
console.warn(warn.toString())
})
const outdir = path.join(path.dirname(file), '../dist')
// Save the `uppy` package's CSS as `uppy.css`,
// the rest as `style.css`.
// const outfile = path.join(outdir, outdir.includes(path.normalize('packages/uppy/')) ? 'uppy.css' : 'style.css')
let outfile = path.join(outdir, 'style.css')
if (outdir.includes(path.normalize('packages/uppy/'))) {
outfile = path.join(outdir, 'uppy.css')
}
await mkdir(outdir, { recursive: true })
await writeFile(outfile, postcssResult.css)
console.info(
chalk.green('✓ Built Uppy CSS:'),
chalk.magenta(path.relative(cwd, outfile)),
)
const minifiedResult = await postcss([
cssnano({ safe: true }),
]).process(postcssResult.css, { from: outfile })
minifiedResult.warnings().forEach((warn) => {
console.warn(warn.toString())
})
await writeFile(outfile.replace(/\.css$/, '.min.css'), minifiedResult.css)
console.info(
chalk.green('✓ Minified Bundle CSS:'),
chalk.magenta(path.relative(cwd, outfile).replace(/\.css$/, '.min.css')),
)
}
}
compileCSS().then(() => {
console.info(chalk.yellow('✓ CSS Bundles 🎉'))
}, handleErr)

View file

@ -1,147 +0,0 @@
const babel = require('@babel/core')
const t = require('@babel/types')
const { promisify } = require('node:util')
const glob = promisify(require('glob'))
const fs = require('node:fs')
const path = require('node:path')
const { mkdir, stat, writeFile } = fs.promises
const PACKAGE_JSON_IMPORT = /^\..*\/package.json$/
const SOURCE = 'packages/{*,@uppy/*}/src/**/*.{js,ts}?(x)'
// Files not to build (such as tests)
const IGNORE = /\.test\.jsx?$|\.test\.tsx?$|__mocks__|svelte|angular|companion\//;
// Files that should trigger a rebuild of everything on change
const META_FILES = [
'babel.config.js',
'package.json',
'package-lock.json',
'yarn.lock',
'bin/build-lib.js',
]
function lastModified (file, createParentDir = false) {
return stat(file).then((s) => s.mtime, async (err) => {
if (err.code === 'ENOENT') {
if (createParentDir) {
await mkdir(path.dirname(file), { recursive: true })
}
return 0
}
throw err
})
}
const versionCache = new Map()
async function preparePackage (file) {
const packageFolder = file.slice(0, file.indexOf('/src/'))
if (versionCache.has(packageFolder)) return
// eslint-disable-next-line import/no-dynamic-require, global-require
const { version } = require(path.join(__dirname, '..', packageFolder, 'package.json'))
if (process.env.FRESH) {
// in case it hasn't been done before.
await mkdir(path.join(packageFolder, 'lib'), { recursive: true })
}
versionCache.set(packageFolder, version)
}
const nonJSImport = /^\.\.?\/.+\.([jt]sx|ts)$/
// eslint-disable-next-line no-shadow
function rewriteNonJSImportsToJS (path) {
const match = nonJSImport.exec(path.node.source.value)
if (match) {
// eslint-disable-next-line no-param-reassign
path.node.source.value = `${match[0].slice(0, -match[1].length)}js`
}
}
async function buildLib () {
const metaMtimes = await Promise.all(META_FILES.map((filename) => lastModified(path.join(__dirname, '..', filename))))
const metaMtime = Math.max(...metaMtimes)
const files = await glob(SOURCE)
/* eslint-disable no-continue */
for (const file of files) {
if (IGNORE.test(file)) {
continue
}
await preparePackage(file)
const libFile = file.replace('/src/', '/lib/').replace(/\.[jt]sx?$/, '.js')
// on a fresh build, rebuild everything.
if (!process.env.FRESH) {
const [srcMtime, libMtime] = await Promise.all([
lastModified(file),
lastModified(libFile, true),
])
// Skip files that haven't changed
if (srcMtime < libMtime && metaMtime < libMtime) {
continue
}
}
const plugins = [{
visitor: {
// eslint-disable-next-line no-shadow
ImportDeclaration (path) {
rewriteNonJSImportsToJS(path)
if (PACKAGE_JSON_IMPORT.test(path.node.source.value)
&& path.node.specifiers.length === 1
&& path.node.specifiers[0].type === 'ImportDefaultSpecifier') {
// Vendor-in version number from package.json files:
const version = versionCache.get(file.slice(0, file.indexOf('/src/')))
if (version != null) {
const [{ local }] = path.node.specifiers
path.replaceWith(
t.variableDeclaration('const', [t.variableDeclarator(local,
t.objectExpression([
t.objectProperty(t.stringLiteral('version'), t.stringLiteral(version)),
]))]),
)
}
}
},
ExportAllDeclaration: rewriteNonJSImportsToJS,
// eslint-disable-next-line no-shadow
ExportNamedDeclaration (path) {
if (path.node.source != null) {
rewriteNonJSImportsToJS(path)
}
},
},
}]
const isTSX = file.endsWith('.tsx')
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,
plugins,
// no comments because https://github.com/transloadit/uppy/pull/4868#issuecomment-1897717779
comments: !process.env.DIFF_BUILDER,
})
const [{ default: chalk }] = await Promise.all([
import('chalk'),
writeFile(libFile, code),
writeFile(`${libFile}.map`, JSON.stringify(map)),
])
console.log(chalk.green('Compiled lib:'), chalk.magenta(libFile))
}
/* eslint-enable no-continue */
}
console.log('Using Babel version:', require('@babel/core/package.json').version)
buildLib().catch((err) => {
console.error(err)
process.exit(1)
})

View file

@ -1,10 +0,0 @@
#!/bin/sh
# Convert a video file to a gif.
# `to-gif /path/to/input.mp4 /path/to/output.gif`
palette="/tmp/to-gif-palette.png"
filters="fps=15"
ffmpeg -v warning -i $1 -vf "$filters,palettegen" -y $palette
ffmpeg -v warning -i $1 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $2
# resize after
# gifsicle --resize-fit-width 1000 -i animation.gif > animation-1000px.gif

View file

@ -1,9 +0,0 @@
#!/bin/sh
# Convert a video file to a gif.
# `to-gif /path/to/input.mp4 /path/to/output.gif`
palette="/tmp/to-gif-palette.png"
filters="fps=15"
ffmpeg -v warning -i $1 -vf "$filters,palettegen" -y $palette
ffmpeg -v warning -i $1 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $2
# gifsicle --resize-fit-width 1000 -i animation.gif > animation-1000px.gif

View file

@ -1,32 +0,0 @@
#!/usr/bin/env bash
set -o pipefail
set -o errexit
set -o nounset
# set -o xtrace
# Set magic variables for current file & dir
__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
__base="$(basename ${__file} .sh)"
__root="$(cd "$(dirname "${__dir}")" && pwd)"
width=600
speed=0.7
input="${__root}/assets/uppy-demo-oct-2018.mov"
base="$(basename "${input}")"
output="${__root}/assets/${base}.gif"
ffmpeg \
-y \
-i "${input}" \
-vf fps=10,scale=${width}:-1:flags=lanczos,palettegen "${__root}/assets/${base}-palette.png"
ffmpeg \
-y \
-i "${input}" \
-i "${__root}/assets/${base}-palette.png" \
-filter_complex "setpts=${speed}*PTS,fps=10,scale=${width}:-1:flags=lanczos[x];[x][1:v]paletteuse" \
"${output}"
du -hs "${output}"
open -a 'Google Chrome' "${output}"

View file

@ -1,65 +0,0 @@
#!/usr/bin/env node
import fetchContrib from 'github-contributors-list/lib/contributors.js'
import layoutStrategy from 'github-contributors-list/lib/strategies/layout_strategies/json.js'
import sortStrategy from 'github-contributors-list/lib/strategies/sort_strategies/sort_desc.js'
import filterStrategy from 'github-contributors-list/lib/strategies/filter_strategies/login.js'
import { Buffer } from 'node:buffer'
import fs from 'node:fs/promises'
import process from 'node:process'
const README_FILE_NAME = new URL('../README.md', import.meta.url)
const readme = await fs.open(README_FILE_NAME, 'r+')
const readmeContent = await readme.readFile()
// Detect start of contributors section.
const START_TAG = Buffer.from('<table id="contributors_table">\n')
const START_TAG_POSITION =
readmeContent.indexOf(START_TAG) + START_TAG.byteLength
const args = {
owner: 'transloadit',
repository: 'uppy',
cols: 6,
format: 'json',
sortBy: 'contributions',
sortOrder: 'desc',
filter: [],
layoutStrategy,
sortStrategy,
filterStrategy,
}
const { contributors } = await fetchContrib(args).loadAll(
args.owner,
args.repository,
args.authToken || process.env.GITHUB_API_TOKEN,
args.fromDate,
)
let cursor = START_TAG_POSITION
for (const line of contributors) {
let row = '<tr>'
for (const { html_url, login, avatar_url } of line) {
row += `<td><a href=${html_url}><img width="117" alt=${JSON.stringify(login)} src=${JSON.stringify(avatar_url)}></a></td>`
}
row += '</tr>\n'
const { bytesWritten } = await readme.write(row, cursor, 'utf-8')
cursor += bytesWritten
}
if (cursor === START_TAG_POSITION) {
console.log('Empty response from githubcontrib. GitHubs rate limit?')
await readme.close()
process.exit(1)
}
await readme.truncate(cursor)
// Write the end of the file.
await readme.write(
readmeContent,
readmeContent.indexOf('<!--/contributors-->'),
undefined,
cursor,
)
await readme.close()

View file

@ -1,37 +0,0 @@
#!/usr/bin/env bash
# This script is meant to be run on a dev's machine to update the version on
# Yarn used by the monorepo. Its goal is to make sure that every mention of Yarn
# version is updated, and it re-installs the plugins to make sure those are
# up-to-date as well.
set -o pipefail
set -o errexit
set -o nounset
CURRENT_VERSION=$(corepack yarn --version)
LAST_VERSION=$(curl \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/yarnpkg/berry/releases?per_page=1 | \
awk '{ if ($1 == "\"tag_name\":") print $2 }' | \
sed 's#^"@yarnpkg/cli/##;s#",$##')
[ "$CURRENT_VERSION" = "$LAST_VERSION" ] && \
echo "Already using latest version." && \
exit 0
echo "Upgrading to Yarn $LAST_VERSION (from Yarn $CURRENT_VERSION)..."
PLUGINS=$(awk '{ if ($1 == "spec:") print $2 }' .yarnrc.yml)
echo "$PLUGINS" | xargs -n1 -t corepack yarn plugin remove
cp package.json .yarn/cache/tmp.package.json
sed "s#\"yarn\": \"$CURRENT_VERSION\"#\"yarn\": \"$LAST_VERSION\"#;s#\"yarn@$CURRENT_VERSION\"#\"yarn@$LAST_VERSION\"#" .yarn/cache/tmp.package.json > package.json
rm .yarn/cache/tmp.package.json
echo "$PLUGINS" | xargs -n1 -t corepack yarn plugin import
corepack yarn
git add package.json yarn.lock
git add .yarn/plugins

84
biome.json Normal file
View file

@ -0,0 +1,84 @@
{
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true,
"includes": [
"packages/**",
"examples/**",
"scripts/**",
"bin/**",
"e2e/**",
"private/**",
"!packages/**/{dist,lib}/**",
"!node_modules",
"!.svelte-kit",
"!packages/@uppy/components/src/input.css"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"formatWithErrors": true
},
"linter": {
"enabled": true,
"includes": [
"packages/**",
"examples/**",
"scripts/**",
"private/**",
"!packages/**/{dist,lib}/**",
"!node_modules",
"!.svelte-kit",
"!packages/@uppy/components/src/input.css",
"!**/*.vue",
"!**/*.svelte"
],
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "off"
},
"correctness": {
"useExhaustiveDependencies": "error",
"useHookAtTopLevel": "error",
"noUnusedFunctionParameters": "off",
"noUnusedVariables": {
"level": "error",
"options": {
"ignoreRestSiblings": true
}
}
},
"style": {
"noNonNullAssertion": "off"
},
"a11y": {
"noSvgWithoutTitle": "off"
},
"nursery": {
"noNestedComponentDefinitions": "error",
"noReactPropAssign": "error"
}
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "asNeeded"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}

View file

@ -1,19 +0,0 @@
version: '3.9'
services:
uppy:
image: transloadit/companion
build:
context: .
dockerfile: Dockerfile
environment:
- NODE_ENV=development
volumes:
- ./:/app
- /app/node_modules
- /mnt/uppy-server-data:/mnt/uppy-server-data
ports:
- '3020:3020'
command: '/app/src/standalone/start-server.js --config nodemon.json'
env_file:
- .env

View file

@ -1,8 +0,0 @@
version: '3.9'
services:
uppy:
image: companion
build:
context: .
dockerfile: Dockerfile.test

View file

@ -1,15 +0,0 @@
version: '3.9'
services:
uppy:
image: transloadit/companion
build:
context: .
dockerfile: Dockerfile
volumes:
- /app/node_modules
- /mnt/uppy-server-data:/mnt/uppy-server-data
ports:
- '3020:3020'
env_file:
- .env

View file

@ -1,4 +0,0 @@
# Uppy documentation
See instructions for linting this documentation and seeing this documentation in
the browser in <https://github.com/transloadit/uppy.io>.

View file

@ -1,989 +0,0 @@
---
sidebar_position: 4
---
# Companion
Companion is an open source server application which **takes away the complexity
of authentication and the cost of downloading files from remote sources**, such
as Instagram, Google Drive, and others. Companion is a server-to-server
orchestrator that streams files from a source to a destination, and files are
never stored in Companion. Companion can run either as a standalone
(self-hosted) application, [Transloadit-hosted](#hosted), or plugged in as an
Express middleware into an existing application. The Uppy client requests remote
files from Companion, which it will download and simultaneously upload to your
[Tus server](/docs/tus), [AWS bucket](/docs/aws-s3), or any server that supports
[PUT, POST or Multipart uploads](/docs/xhr-upload).
This means a user uploading a 5GB video from Google Drive from their phone isnt
eating into their data plans and you dont have to worry about implementing
OAuth.
## When should I use it?
If you want to let users download files from [Box][], [Dropbox][], [Facebook][],
[Google Drive][googledrive], [Google Photos][googlephotos], [Instagram][],
[OneDrive][], [Unsplash][], [Import from URL][url], or [Zoom][] — you need
Companion.
Companion supports the same [uploaders](/docs/guides/choosing-uploader) as Uppy:
[Tus](/docs/tus), [AWS S3](/docs/aws-s3), and [regular multipart](/docs/tus).
But instead of manually setting a plugin, Uppy sends along a header with the
uploader and Companion will use the same on the server. This means if you are
using [Tus](/docs/tus) for your local uploads, you can send your remote uploads
to the same Tus server (and likewise for your AWS S3 bucket).
:::note
Companion only deals with _remote_ files, _local_ files are still uploaded from
the client with your upload plugin.
:::
## Hosted
Using [Transloadit][] services comes with a hosted version of Companion so you
dont have to worry about hosting your own server. Whether you are on a free or
paid Transloadit [plan](https://transloadit.com/pricing/), you can use
Companion. Its not possible to rent a Companion server without a Transloadit
plan.
[**Sign-up for a (free) plan**](https://transloadit.com/pricing/).
:::tip
Choosing Transloadit for your file services also comes with credentials for all
remote providers. This means you dont have to waste time going through the
approval process of every app. You can still add your own credentials in the
Transloadit admin page if you want.
:::
:::info
Downloading and uploading files through Companion doesnt count towards your
[monthly quota](https://transloadit.com/docs/faq/1gb-worth/), its a way for
files to arrive at Transloadit servers, much like Uppy.
:::
To do so each provider plugin must be configured with Transloadits Companion
URLs:
```js
import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
import Dropbox from '@uppy/dropbox';
uppy.use(Dropbox, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
});
```
You may also hit rate limits, because the OAuth application is shared between
everyone using Transloadit.
To solve that, you can use your own OAuth keys with Transloadits hosted
Companion servers by using Transloadit Template Credentials. [Create a Template
Credential][template-credentials] on the Transloadit site. Select “Companion
OAuth” for the service, and enter the key and secret for the provider you want
to use. Then you can pass the name of the new credentials to that provider:
```js
import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
import Dropbox from '@uppy/dropbox';
uppy.use(Dropbox, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
companionKeysParams: {
key: 'YOUR_TRANSLOADIT_API_KEY',
credentialsName: 'my_companion_dropbox_creds',
},
});
```
## Installation & use
Companion is installed from npm. Depending on how you want to run Companion, the
install process is slightly different. Companion can be integrated as middleware
into your [Express](https://expressjs.com/) app or as a standalone server. Most
people probably want to run it as a standalone server, while the middleware
could be used to further customise Companion or integrate it into your own HTTP
server code.
:::note
Since v2, you need to be running `node.js >= v10.20.1` to use Companion. More
information in the
[migrating to 2.0](/docs/guides/migration-guides/#migrate-from-uppy-1x-to-2x)
guide.
Windows is not a supported platform right now. It may work, and were happy to
accept improvements in this area, but we cant provide support.
:::
### Standalone mode
You can use the standalone version if you want to run Companion as its own
Node.js process. Its a configured Express server with sessions, logging, and
security best practices. First youll typically want to install it globally:
```bash
npm install -g @uppy/companion
```
Standalone Companion will always serve HTTP (not HTTPS) and expects a reverse
proxy with SSL termination in front of it when running in production. See
[`COMPANION_PROTOCOL`](#server) for more information.
Companion ships with an executable file (`bin/companion`) which is the
standalone server. Unlike the middleware version, options are set via
environment variables.
:::info
Checkout [options](#options) for the available options in JS and environment
variable formats.
:::
You need at least these three to get started:
```bash
export COMPANION_SECRET="shh!Issa Secret!"
export COMPANION_DOMAIN="YOUR SERVER DOMAIN"
export COMPANION_DATADIR="PATH/TO/DOWNLOAD/DIRECTORY"
```
Then run:
```bash
companion
```
You can also pass in the path to your JSON config file, like so:
```bash
companion --config /path/to/companion.json
```
You may also want to run Companion in a process manager like
[PM2](https://pm2.keymetrics.io/) to make sure it gets restarted on upon
crashing as well as allowing scaling to many instances.
### Express middleware mode
First install it into your Node.js project with your favorite package manager:
```bash
npm install @uppy/companion
```
To plug Companion into an existing server, call its `.app` method, passing in an
[options](#options) object as a parameter. This returns a server instance that
you can mount on a route in your Express app.
```js
import express from 'express';
import bodyParser from 'body-parser';
import session from 'express-session';
import companion from '@uppy/companion';
const app = express();
// Companion requires body-parser and express-session middleware.
// You can add it like this if you use those throughout your app.
//
// If you are using something else in your app, you can add these
// middlewares in the same subpath as Companion instead.
app.use(bodyParser.json());
app.use(session({ secret: 'some secrety secret' }));
const companionOptions = {
providerOptions: {
drive: {
key: 'GOOGLE_DRIVE_KEY',
secret: 'GOOGLE_DRIVE_SECRET',
},
},
server: {
host: 'localhost:3020',
protocol: 'http',
// Default installations normally don't need a path.
// However if you specify a `path`, you MUST specify
// the same path in `app.use()` below,
// e.g. app.use('/companion', companionApp)
// path: '/companion',
},
filePath: '/path/to/folder/',
};
const { app: companionApp } = companion.app(companionOptions);
app.use(companionApp);
```
Companion uses WebSockets to communicate progress, errors, and successes to the
client. This is what Uppy listens to to update its internal state and UI.
Add the Companion WebSocket server using the `companion.socket` function:
```js
const server = app.listen(PORT);
companion.socket(server);
```
If WebSockets fail for some reason Uppy and Companion will fallback to HTTP
polling.
### Running many instances
We recommend running at least two instances in production, so that if the
Node.js event loop gets blocked by one or more requests (due to a bug or spike
in traffic), it doesnt also block or slow down all other requests as well (as
Node.js is single threaded).
As an example for scale, one enterprise customer of Transloadit, who self-hosts
Companion to power an education service that is used by many universities
globally, deploys 7 Companion instances. Their earlier solution ran on 35
instances. In our general experience Companion will saturate network interface
cards before other resources on commodity virtual servers (`c5d.2xlarge` for
instance).
Your mileage may vary, so we recommend to add observability. You can let
Prometheus crawl the `/metrics` endpoint and graph that with Grafana for
instance.
#### Using unique endpoints
One option is to run many instances with each instance having its own unique
endpoint. This could be on separate ports, (sub)domain names, or IPs. With this
setup, you can either:
1. Implement your own logic that will direct each upload to a specific Companion
endpoint by setting the `companionUrl` option
2. Setting the Companion option `COMPANION_SELF_ENDPOINT`. This option will
cause Companion to respond with a `i-am` HTTP header containing the value
from `COMPANION_SELF_ENDPOINT`. When Uppys sees this header, it will pin all
requests for the upload to this endpoint.
In either case, you would then also typically configure a single Companion
instance (one endpoint) to handle all OAuth authentication requests, so that you
only need to specify a single OAuth callback URL. See also `oauthDomain` and
`validHosts`.
#### Using a load balancer
The other option is to set up a load balancer in front of many Companion
instances. Then Uppy will only see a single endpoint and send all requests to
the associated load balancer, which will then distribute them between Companion
instances. The companion instances coordinate their messages and events over
Redis so that any instance can serve the clients requests. Note that sticky
sessions are **not** needed with this setup. Here are the requirements for this
setup:
- The instances need to be connected to the same Redis server.
- You need to set `COMPANION_SECRET` to the same value on both servers.
- if you use the `companionKeysParams` feature (Transloadit), you also need
`COMPANION_PREAUTH_SECRET` to be the same on each instance.
- All other configuration needs to be the same, except if youre running many
instances on the same machine, then `COMPANION_PORT` should be different for
each instance.
## API
### Options
:::tip
The headings display the JS and environment variable options (`option`
`ENV_OPTION`). When integrating Companion into your own server, you pass the
options to `companion.app()`. If you are using the standalone version, you
configure Companion using environment variables. Some options only exist as
environment variables or only as a JS option.
:::
<details>
<summary>Default configuration</summary>
```javascript
const options = {
server: {
protocol: 'http',
path: '',
},
providerOptions: {},
s3: {
endpoint: 'https://{service}.{region}.amazonaws.com',
conditions: [],
useAccelerateEndpoint: false,
getKey: ({ filename }) => `${crypto.randomUUID()}-${filename}`,
expires: 800, // seconds
},
allowLocalUrls: false,
logClientVersion: true,
periodicPingUrls: [],
streamingUpload: true,
clientSocketConnectTimeout: 60000,
metrics: true,
};
```
</details>
#### `filePath` `COMPANION_DATADIR`
Full path to the directory to which provider files will be downloaded
temporarily.
#### `secret` `COMPANION_SECRET` `COMPANION_SECRET_FILE`
A secret string which Companion uses to generate authorization tokens. You
should generate a long random string for this. For example:
```js
const crypto = require('node:crypto');
const secret = crypto.randomBytes(64).toString('hex');
```
:::caution
Omitting the `secret` in the standalone version will generate a secret for you,
using the above `crypto` string. But when integrating with Express you must
provide it yourself. This is an essential security measure.
:::
:::note
Using a secret file means passing an absolute path to a file with any extension,
which has only the secret, nothing else.
:::
#### `preAuthSecret` `COMPANION_PREAUTH_SECRET` `COMPANION_PREAUTH_SECRET_FILE`
If you are using the [Transloadit](/docs/transloadit) `companionKeysParams`
feature (Transloadit-hosted Companion using your own custom OAuth credentials),
set this variable to a strong randomly generated secret. See also
`COMPANION_SECRET` (but do not use the same secret!)
:::note
Using a secret file means passing an absolute path to a file with any extension,
which has only the secret, nothing else.
:::
#### `uploadUrls` `COMPANION_UPLOAD_URLS`
An allowlist (array) of strings (exact URLs) or regular expressions. Companion
will only accept uploads to these URLs. This ensures that your Companion
instance is only allowed to upload to your trusted servers and prevents
[SSRF](https://en.wikipedia.org/wiki/Server-side_request_forgery) attacks.
#### `COMPANION_PORT`
The port on which to start the standalone server, defaults to 3020. This is a
standalone-only option.
#### `COMPANION_COOKIE_DOMAIN`
Allows you to customize the domain of the cookies created for Express sessions.
This is a standalone-only option.
#### `COMPANION_HIDE_WELCOME`
Setting this to `true` disables the welcome message shown at `/`. This is a
standalone-only option.
#### `redisUrl` `COMPANION_REDIS_URL`
URL to running Redis server. This can be used to scale Companion horizontally
using many instances. See [How to scale Companion](#how-to-scale-companion).
#### `COMPANION_REDIS_EXPRESS_SESSION_PREFIX`
Set a custom prefix for redis keys created by
[connect-redis](https://github.com/tj/connect-redis). Defaults to
`companion-session:`. Sessions are used for storing authentication state and for
allowing thumbnails to be loaded by the browser via Companion and for OAuth2.
See also `COMPANION_REDIS_PUBSUB_SCOPE`.
#### `redisOptions` `COMPANION_REDIS_OPTIONS`
An object of
[options supported by the `ioredis` client](https://github.com/redis/ioredis).
See also
[`RedisOptions`](https://github.com/redis/ioredis/blob/af832752040e616daf51621681bcb40cab965a9b/lib/redis/RedisOptions.ts#L8).
#### `redisPubSubScope` `COMPANION_REDIS_PUBSUB_SCOPE`
Use a scope for the companion events at the Redis server. Setting this option
will prefix all events with the name provided and a colon. See also
`COMPANION_REDIS_EXPRESS_SESSION_PREFIX`.
#### `server`
Configuration options for the underlying server.
| Key / Environment variable | Value | Description |
| ---------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `protocol` `COMPANION_PROTOCOL` | `http` or `https` | Used to build a URL to reference the Companion instance itself, which is used for headers and cookies. Companion itself always runs as a HTTP server, so locally you should use `http`. You must to set this to `https` once you enabled SSL/HTTPS for your domain in production by running a reverse https-proxy in front of Companion, or with a built-in HTTPS feature of your hosting service. |
| `host` `COMPANION_DOMAIN` | `String` | Your servers publicly facing hostname (for example `example.com`). |
| `oauthDomain` `COMPANION_OAUTH_DOMAIN` | `String` | If you have several instances of Companion with different (and perhaps dynamic) subdomains, you can set a single fixed subdomain and server (such as `sub1.example.com`) to handle your OAuth authentication for you. This would then redirect back to the correct instance with the required credentials on completion. This way you only need to configure a single callback URL for OAuth providers. |
| `path` `COMPANION_PATH` | `String` | The server path to where the Companion app is sitting. For instance, if Companion is at `example.com/companion`, then the path would be `/companion`). |
| `implicitPath` `COMPANION_IMPLICIT_PATH` | `String` | If the URLs path in your reverse proxy is different from your Companion path in your express app, then you need to set this path as `implicitPath`. For instance, if your Companion URL is `example.com/mypath/companion`. Where the path `/mypath` is defined in your NGINX server, while `/companion` is set in your express app. Then you need to set the option `implicitPath` to `/mypath`, and set the `path` option to `/companion`. |
| `validHosts` `COMPANION_DOMAINS` | `Array` | If you are setting an `oauthDomain`, you need to set a list of valid hosts, so the oauth handler can validate the host of the Uppy instance requesting the authentication. This is essentially a list of valid domains running your Companion instances. The list may also contain regex patterns. e.g `['sub2.example.com', 'sub3.example.com', '(\\w+).example.com']` |
#### `sendSelfEndpoint` `COMPANION_SELF_ENDPOINT`
This is essentially the same as the `server.host + server.path` attributes. The
major reason for this attribute is that, when set, it adds the value as the
`i-am` header of every request response.
#### `providerOptions`
Object to enable providers with their keys and secrets. For example:
```json
{
"drive": {
"key": "***",
"secret": "***"
}
}
```
When using the standalone version you use the corresponding environment
variables or point to a secret file (such as `COMPANION_GOOGLE_SECRET_FILE`).
:::note
Secret files need an absolute path to a file with any extension which only has
the secret, nothing else.
:::
| Service | Key | Environment variables |
| ------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Box | `box` | `COMPANION_BOX_KEY`, `COMPANION_BOX_SECRET`, `COMPANION_BOX_SECRET_FILE` |
| Dropbox | `dropbox` | `COMPANION_DROPBOX_KEY`, `COMPANION_DROPBOX_SECRET`, `COMPANION_DROPBOX_SECRET_FILE` |
| Facebook | `facebook` | `COMPANION_FACEBOOK_KEY`, `COMPANION_FACEBOOK_SECRET`, `COMPANION_FACEBOOK_SECRET_FILE` |
| Google Drive | `drive` | `COMPANION_GOOGLE_KEY`, `COMPANION_GOOGLE_SECRET`, `COMPANION_GOOGLE_SECRET_FILE` |
| Google Photos | `googlephotos` | `COMPANION_GOOGLE_KEY`, `COMPANION_GOOGLE_SECRET`, `COMPANION_GOOGLE_SECRET_FILE` |
| Instagram | `instagram` | `COMPANION_INSTAGRAM_KEY`, `COMPANION_INSTAGRAM_SECRET`, `COMPANION_INSTAGRAM_SECRET_FILE` |
| OneDrive | `onedrive` | `COMPANION_ONEDRIVE_KEY`, `COMPANION_ONEDRIVE_SECRET`, `COMPANION_ONEDRIVE_SECRET_FILE`, `COMPANION_ONEDRIVE_DOMAIN_VALIDATION` (Settings this variable to `true` enables a route that can be used to validate your app with OneDrive) |
| Zoom | `zoom` | `COMPANION_ZOOM_KEY`, `COMPANION_ZOOM_SECRET`, `COMPANION_ZOOM_SECRET_FILE`, `COMPANION_ZOOM_VERIFICATION_TOKEN` |
#### `s3`
Companion comes with signature endpoints for AWS S3. These can be used by the
Uppy client to sign requests to upload files directly to S3, without exposing
secret S3 keys in the browser. Companion also supports uploading files from
providers like Dropbox and Instagram directly into S3.
##### `s3.key` `COMPANION_AWS_KEY`
The S3 access key ID.
##### `s3.secret` `COMPANION_AWS_SECRET` `COMPANION_AWS_SECRET_FILE`
The S3 secret access key.
:::note
Using a secret file means passing an absolute path to a file with any extension,
which has only the secret, nothing else.
:::
##### `s3.endpoint` `COMPANION_AWS_ENDPOINT`
Optional URL to a custom S3 (compatible) service. Otherwise uses the default
from the AWS SDK.
##### `s3.bucket` `COMPANION_AWS_BUCKET`
The name of the bucket to store uploaded files in.
A `string` or function that returns the name of the bucket as a `string` and
takes one argument which is an object with the following properties:
- `filename`, the original name of the uploaded file;
- `metadata` provided by the user for the file (will only be provided during the
initial calls for each uploaded files, otherwise it will be `undefined`).
- `req`, Express.js `Request` object. Do not use any Companion internals from
the req object, as these might change in any minor version of Companion.
#### `s3.forcePathStyle` `COMPANION_AWS_FORCE_PATH_STYLE`
This adds support for setting the S3 clients `forcePathStyle` option. That is
necessary to use Uppy/Companion alongside localstack in development
environments. **Default**: `false`.
##### `s3.region` `COMPANION_AWS_REGION`
The datacenter region where the target bucket is located.
##### `COMPANION_AWS_PREFIX`
An optional prefix for all uploaded keys. This is a standalone-only option. The
same can be achieved by the `getKey` option when using the express middleware.
##### `s3.awsClientOptions`
You can supply any
[S3 option supported by the AWS SDK](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property)
in the `providerOptions.s3.awsClientOptions` object, _except for_ the below:
- `accessKeyId`. Instead, use the `providerOptions.s3.key` property. This is to
make configuration names consistent between different Companion features.
- `secretAccessKey`. Instead, use the `providerOptions.s3.secret` property. This
is to make configuration names consistent between different Companion
features.
Be aware that some options may cause wrong behaviour if they conflict with
Companions assumptions. If you find that a particular option does not work as
expected, please
[open an issue on the Uppy repository](https://github.com/transloadit/uppy/issues/new)
so we can document it here.
##### `s3.getKey({ filename, metadata, req })`
Get the key name for a file. The key is the file path to which the file will be
uploaded in your bucket. This option should be a function receiving three
arguments:
- `filename`, the original name of the uploaded file;
- `metadata`, user-provided metadata for the file.
- `req`, Express.js `Request` object. Do not use any Companion internals from
the req object, as these might change in any minor version of Companion.
This function should return a string `key`. The `req` parameter can be used to
upload to a user-specific folder in your bucket, for example:
```js
app.use(authenticationMiddleware);
app.use(
uppy.app({
providerOptions: {
s3: {
getKey: ({ req, filename, metadata }) => `${req.user.id}/${filename}`,
/* auth options */
},
},
}),
);
```
The default implementation returns the `filename`, so all files will be uploaded
to the root of the bucket as their original file name.
```js
app.use(
uppy.app({
providerOptions: {
s3: {
getKey: ({ filename, metadata }) => filename,
},
},
}),
);
```
When signing on the client, this function will only be called for multipart
uploads.
#### `COMPANION_AWS_USE_ACCELERATE_ENDPOINT`
Enable S3
[Transfer Acceleration](https://docs.aws.amazon.com/AmazonS3/latest/userguide/transfer-acceleration.html).
This is a standalone-only option.
#### `COMPANION_AWS_EXPIRES`
Set `X-Amz-Expires` query parameter in the presigned urls (in seconds, default:
300\). This is a standalone-only option.
#### `COMPANION_AWS_ACL`
Set a
[Canned ACL](https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl)
for uploaded objects. This is a standalone-only option.
#### `customProviders`
This option enables you to add custom providers along with the already supported
providers. See [adding custom providers](#how-to-add-custom-providers) for more
information.
#### `logClientVersion`
A boolean flag to tell Companion whether to log its version upon startup.
#### `metrics` `COMPANION_HIDE_METRICS`
A boolean flag to tell Companion whether to provide an endpoint `/metrics` with
Prometheus metrics (by default metrics are enabled.)
#### `streamingUpload` `COMPANION_STREAMING_UPLOAD`
A boolean flag to tell Companion whether to enable streaming uploads. If
enabled, it will lead to _faster uploads_ because companion will start uploading
at the same time as downloading using `stream.pipe`. If `false`, files will be
fully downloaded first, then uploaded. Defaults to `true`.
#### `maxFileSize` `COMPANION_MAX_FILE_SIZE`
If this value is set, companion will limit the maximum file size to process. If
unset, it will process files without any size limit (this is the default).
#### `periodicPingUrls` `COMPANION_PERIODIC_PING_URLS`
If this value is set, companion will periodically send POST requests to the
specified URLs. Useful for keeping track of companion instances as a keep-alive.
#### `periodicPingInterval` `COMPANION_PERIODIC_PING_INTERVAL`
Interval for periodic ping requests (in ms).
#### `periodicPingStaticPayload` `COMPANION_PERIODIC_PING_STATIC_JSON_PAYLOAD`
A `JSON.stringify`-able JavaScript Object that will be sent as part of the JSON
body in the period ping requests.
#### `allowLocalUrls` `COMPANION_ALLOW_LOCAL_URLS`
A boolean flag to tell Companion whether to allow requesting local URLs
(non-internet IPs).
:::caution
Only enable this in development. **Enabling it in production is a security
risk.**
:::
#### `corsOrigins` (required)
Allowed CORS Origins. Passed as the `origin` option in
[cors](https://github.com/expressjs/cors#configuration-options).
Note this is used for both CORS `Access-Control-Allow-Origin` header, and for
the
[`targetOrigin`](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#targetorigin)
for `postMessage` calls in the context of OAuth.
Setting it to `true` treats any origin as a trusted one, making it easier to
impersonate your brand. Setting it to `false` disables cross-origin support, use
this if youre serving Companion and Uppy from the same domain name.
##### `COMPANION_CLIENT_ORIGINS`
Stand-alone alternative to the `corsOrigins` option. A comma-separated string of
origins, or `'true'` (which will be interpreted as the boolean value `true`), or
`'false'` (which will be interpreted as the boolean value `false`).
`COMPANION_CLIENT_ORIGINS_REGEX` will be ignored if this option is used.
##### `COMPANION_CLIENT_ORIGINS_REGEX`
:::note
In most cases, you should not be using a regex, and instead provide the list of
accepted origins to `COMPANION_CLIENT_ORIGINS`. If you have to use this option,
have in mind that this regex will be used to parse unfiltered user input, so
make sure youre validating the entirety of the string.
:::
Stand-alone alternative to the `corsOrigins` option. Like
`COMPANION_CLIENT_ORIGINS`, but allows a single regex instead.
#### `chunkSize` `COMPANION_CHUNK_SIZE`
Controls how big the uploaded chunks are for AWS S3 Multipart and Tus. Smaller
values lead to more overhead, but larger values lead to slower retries in case
of bad network connections. Passed to tus-js-client
[`chunkSize`](https://github.com/tus/tus-js-client/blob/master/docs/api.md#chunksize)
as well as
[AWS S3 Multipart](https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html)
`partSize`.
#### `enableUrlEndpoint` `COMPANION_ENABLE_URL_ENDPOINT`
Set this to `true` to enable the [URL functionalily](https://uppy.io/docs/url/).
Default: `false`.
### Events
The object returned by `companion.app()` also has a property `companionEmitter`
which is an `EventEmitter` that emits the following events:
- `upload-start` - When an upload starts, this event is emitted with an object
containing the property `token`, which is a unique ID for the upload.
- **token** - The event name is the token from `upload-start`. The event has an
object with the following properties:
- `action` - One of the following strings:
- `success` - When the upload succeeds.
- `error` - When the upload fails with an error.
- `payload` - the error or success payload.
Example code for using the `EventEmitter` to handle a finished file upload:
```js
const companionApp = companion.app(options);
const { companionEmitter: emitter } = companionApp;
emitter.on('upload-start', ({ token }) => {
console.log('Upload started', token);
function onUploadEvent({ action, payload }) {
if (action === 'success') {
emitter.off(token, onUploadEvent); // avoid listener leak
console.log('Upload finished', token, payload.url);
} else if (action === 'error') {
emitter.off(token, onUploadEvent); // avoid listener leak
console.error('Upload failed', payload);
}
}
emitter.on(token, onUploadEvent);
});
```
<!--retext-simplify ignore frequently-->
## Frequently asked questions
### Do you have a live example?
An example server is running at <https://companion.uppy.io>.
### How does the Authentication and Token mechanism work?
This section describes how Authentication works between Companion and Providers.
While this behaviour is the same for all Providers (Dropbox, Instagram, Google
Drive, etc.), we are going to be referring to Dropbox in place of any Provider
throughout this section.
The following steps describe the actions that take place when a user
Authenticates and Uploads from Dropbox through Companion:
- The visitor to a website with Uppy clicks `Connect to Dropbox`.
- Uppy sends a request to Companion, which in turn sends an OAuth request to
Dropbox (Requires that OAuth credentials from Dropbox have been added to
Companion).
- Dropbox asks the visitor to log in, and whether the Website should be allowed
to access your files
- If the visitor agrees, Companion will receive a token from Dropbox, with which
we can temporarily download files.
- Companion encrypts the token with a secret key and sends the encrypted token
to Uppy (client)
- Every time the visitor clicks on a folder in Uppy, it asks Companion for the
new list of files, with this question, the token (still encrypted by
Companion) is sent along.
- Companion decrypts the token, requests the list of files from Dropbox and
sends it to Uppy.
- When a file is selected for upload, Companion receives the token again
according to this procedure, decrypts it again, and thereby downloads the file
from Dropbox.
- As the bytes arrive, Companion uploads the bytes to the final destination
(depending on the configuration: Apache, a Tus server, S3 bucket, etc).
- Companion reports progress to Uppy, as if it were a local upload.
- Completed!
### How to use provider redirect URIs?
When generating your provider API keys on their corresponding developer
platforms (e.g
[Google Developer Console](https://console.developers.google.com/)), youd need
to provide a `redirect URI` for the OAuth authorization process. In general the
redirect URI for each provider takes the format:
`http(s)://$YOUR_COMPANION_HOST_NAME/$PROVIDER_NAME/redirect`
For example, if your Companion server is hosted on
`https://my.companion.server.com`, then the redirect URI you would supply for
your OneDrive provider would be:
`https://my.companion.server.com/onedrive/redirect`
Please see
[Supported Providers](https://uppy.io/docs/companion/#Supported-providers) for a
list of all Providers and their corresponding names.
### How to use Companion with Kubernetes?
We have a detailed
[guide](https://github.com/transloadit/uppy/blob/main/packages/%40uppy/companion/KUBERNETES.md)
on running Companion in Kubernetes.
### How to add custom providers?
As of now, Companion supports the
[providers listed here](https://uppy.io/docs/companion/#Supported-providers) out
of the box, but you may also choose to add your own custom providers. You can do
this by passing the `customProviders` option when calling the Uppy `app` method.
The custom provider is expected to support Oauth 1 or 2 for
authentication/authorization.
```javascript
import providerModule from './path/to/provider/module';
const options = {
customProviders: {
myprovidername: {
config: {
authorize_url: 'https://mywebsite.com/authorize',
access_url: 'https://mywebsite.com/token',
oauth: 2,
key: '***',
secret: '***',
scope: ['read', 'write'],
},
module: providerModule,
},
},
};
uppy.app(options);
```
The `customProviders` option should be an object containing each custom
provider. Each custom provider would, in turn, be an object with two keys,
`config` and `module`. The `config` option would contain Oauth API settings,
while the `module` would point to the provider module.
To work well with Companion, the **module** must be a class with the following
methods. Note that the methods must be `async`, return a `Promise` or reject
with an `Error`):
1. `async list ({ token, directory, query })` - Returns a object containing a
list of user files (such as a list of all the files in a particular
directory). See [example returned list data structure](#list-data). `token` -
authorization token (retrieved from oauth process) to send along with your
request
- `directory` - the id/name of the directory from which data is to be
retrieved. This may be ignored if it doesnt apply to your provider
- `query` - expressjs query params object received by the server (in case
some data you need in there).
2. `async download ({ token, id, query })` - Downloads a particular file from
the provider. Returns an object with a single property `{ stream }` - a
[`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream_readable),
which will be read from and uploaded to the destination. To prevent memory
leaks, make sure you release your stream if you reject this method with an
error.
- `token` - authorization token (retrieved from oauth process) to send along
with your request.
- `id` - ID of the file being downloaded.
- `query` - expressjs query params object received by the server (in case
some data you need in there).
3. `async size ({ token, id, query })` - Returns the byte size of the file that
needs to be downloaded as a `Number`. If the size of the object is not known,
`null` may be returned.
- `token` - authorization token (retrieved from oauth process) to send along
with your request.
- `id` - ID of the file being downloaded.
- `query` - expressjs query params object received by the server (in case
some data you need in there).
The class must also have:
- A unique `static authProvider` string property - a lowercased value which
indicates name of the [`grant`](https://github.com/simov/grant) OAuth2
provider to use (e.g `google` for Google). If your provider doesnt use
OAuth2, you can omit this property.
- A `static` property `static version = 2`, which is the current version of the
Companion Provider API.
See also
[example code with a custom provider](https://github.com/transloadit/uppy/blob/main/examples/custom-provider/server).
#### list data
```json
{
// username or email of the user whose provider account is being accessed
"username": "johndoe",
// list of files and folders in the directory. An item is considered a folder
// if it mainly exists as a collection to contain sub-items
"items": [
{
// boolean value of whether or NOT it's a folder
"isFolder": false,
// icon image URL
"icon": "https://random-api.url.com/fileicon.jpg",
// name of the item
"name": "myfile.jpg",
// the mime type of the item. Only relevant if the item is NOT a folder
"mimeType": "image/jpg",
// the id (in string) of the item
"id": "uniqueitemid",
// thumbnail image URL. Only relevant if the item is NOT a folder
"thumbnail": "https://random-api.url.com/filethumbnail.jpg",
// for folders this is typically the value that will be passed as "directory" in the list(...) method.
// For files, this is the value that will be passed as id in the download(...) method.
"requestPath": "file-or-folder-requestpath",
// datetime string (in ISO 8601 format) of when this item was last modified
"modifiedDate": "2020-06-29T19:59:58Z",
// the size in bytes of the item. Only relevant if the item is NOT a folder
"size": 278940,
"custom": {
// an object that may contain some more custom fields that you may need to send to the client. Only add this object if you have a need for it.
"customData1": "the value",
"customData2": "the value"
}
// more items here
}
],
// if the "items" list is paginated, this is the request path needed to fetch the next page.
"nextPagePath": "directory-name?cursor=cursor-to-next-page"
}
```
### How to run Companion locally?
1. To set up Companion for local development, please clone the Uppy repo and
install, like so:
```bash
git clone https://github.com/transloadit/uppy
cd uppy
yarn install
```
2. Configure your environment variables by copying the `env.example.sh` file to
`env.sh` and edit it to its correct values.
```bash
cp .env.example .env
$EDITOR .env
```
3. To start the server, run:
```bash
yarn run start:companion
```
This would get the Companion instance running on `http://localhost:3020`. It
uses [`node --watch`](https://nodejs.org/api/cli.html#--watch) so it will
automatically restart when files are changed.
[box]: /docs/box
[dropbox]: /docs/dropbox
[facebook]: /docs/facebook
[googledrive]: /docs/google-drive
[googlephotos]: /docs/google-photos
[instagram]: /docs/instagram
[onedrive]: /docs/onedrive
[unsplash]: /docs/unsplash
[url]: /docs/url
[zoom]: /docs/zoom
[transloadit]: https://transloadit.com
[template-credentials]:
https://transloadit.com/docs/#how-to-create-template-credentials

View file

@ -1,125 +0,0 @@
---
sidebar_position: 12
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import UppyCdnExample from '/src/components/UppyCdnExample';
# Compressor
The `@uppy/compressor` plugin optimizes images (JPEG, PNG, and any other format
supported by the clients browser) before upload, saving up to 60% in size
(roughly 18 MB for 10 images). It uses [Compressor.js][] library under the hood.
## When should I use it?
When your users are likely to upload images, potentially on mobile devices, and
saving data and faster uploads are important.
## Install
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/compressor
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/compressor
```
</TabItem>
<TabItem value="cdn" label="CDN">
<UppyCdnExample>
{`
import { Uppy, Compressor } from "{{UPPY_JS_URL}}"
const uppy = new Uppy()
uppy.use(Compressor, {
// Options
})
`}
</UppyCdnExample>
</TabItem>
</Tabs>
## Use
```js {7} showLineNumbers
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import Compressor from '@uppy/compressor';
new Uppy()
.use(Dashboard, {inline:true, target: '#dashboard')
.use(Compressor);
```
No action is needed from the user — Uppy will automatically optimize images,
show an [Informer](/docs/informer) message with saved bytes, and then begin the
upload as usual.
## API
### Options
:::tip
You can also pass any of the [Compressor.js options][] here as well.
:::
#### `id`
A unique identifier for this plugin (`string`, default: `'Compressor'`).
#### `quality`
The quality of the output image passed to [Compressor.js][] (`number`, default:
`0.6`).
It must be a number between `0` and `1`. Be careful to use `1` as it may make
the size of the output image become larger. In most cases, going with the
default value is best.
:::note
This option is only available for `image/jpeg` and `image/webp` images.
:::
#### `limit`
Number of images that will be compressed in parallel (`number`, default: `10`).
You likely dont need to change this, unless you are experiencing performance
issues.
#### `locale`
```js
export default {
strings: {
// Shown in the Status Bar
compressingImages: 'Compressing images...',
compressedX: 'Saved %{size} by compressing images',
},
};
```
## Events
#### `compressor:complete`
The event is emitted when all files are compressed. You can use it for side
effects or custom UI notifications.
[compressor.js]: https://github.com/fengyuanchen/compressorjs
[compressor.js options]: https://github.com/fengyuanchen/compressorjs#options

View file

@ -1,181 +0,0 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import UppyCdnExample from '/src/components/UppyCdnExample';
# Form
The `@uppy/form` plugin integrates with an existing HTML `<form>` element to
extract input data from it, and send along with the Uppy upload. It then appends
the upload result back to the form via a hidden input.
## When should I use this?
When you have an existing HTML `<form>` element and you would like to:
- **Attach the form input values to the files.** This is useful if you want to
associate meta data from the form (for example, name, location, id) with the
uploaded file, so you can process it on the backend. `@uppy/form` extracts the
input values before uploading/processing files and adds them to Uppy meta data
state (`uppy.state.meta`), as well as and each files meta, and appends to the
upload in an object with `[file input name attribute]` -> `[file input value]`
key/values.
- **Upload the files and put the response (such as the file URLs) into a hidden
field** (`<input type="hidden" name="uppyResult">`). Then you can POST and
handle the form yourself. The appended result is a stringified version of a
result returned from calling `uppy.upload()` or listening to `complete` event.
- **Automatically start the file upload on submit or submit the form after file
upload.** This is off by default. See [`submitOnSuccess`](#submitOnSuccess)
and [`triggerUploadOnSubmit`](#triggerUploadOnSubmit) options respectively for
details.
:::note
If you are using a UI framework or library like React, Vue or Svelte, youll
most likely handle form data there as well, and thus wont need this plugin.
Instead, pass meta data to Uppy via [`uppy.setMeta()`](/docs/uppy#setmetadata)
and listen to [`uppy.on('complete')`](/docs/uppy#complete) to get the upload
results back.
:::
## Install
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/form
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/form
```
</TabItem>
<TabItem value="cdn" label="CDN">
<UppyCdnExample>
{`
import { Uppy, Form } from "{{UPPY_JS_URL}}"
const uppy = new Uppy()
uppy.use(Form, {
// Options
})
`}
</UppyCdnExample>
</TabItem>
</Tabs>
## Use
```js title="app.js"
import Uppy from '@uppy/core';
import Form from '@uppy/form';
import DragDrop from '@uppy/drag-drop';
import '@uppy/core/dist/style.min.css';
import '@uppy/drag-drop/dist/style.min.css';
new Uppy().use(Form, {
target: '#my-form',
});
```
```html title="index.html"
<form id="my-form" action="/submit" method="get">
<label for="name">Enter your name: </label>
<input type="text" name="name" required />
<label for="dob">Date of birth: </label>
<input type="date" name="dob" />
<input type="submit" value="Save" />
</form>
```
By default the code above will:
1. Extract meta data from the form `#my-form`.
2. Send it with the Uppy upload.
3. Those fields will then be added to Uppy meta data state (`uppy.state.meta`)
and each files meta, and appended as (meta)data to the upload in an object
with `[file input name attribute]` -> `[file input value]` key/values.
4. When Uppy completes upload/processing, it will add an
`<input type="hidden" name="uppyResult">` with the stringified upload result
object back to the form.
:::note
You can disable both of these features, see options below.
:::
:::tip
`@uppy/form` can also start Uppy upload automatically once the form is
submitted, and even submit the form after the upload is complete. This is off by
default. See [`triggerUploadOnSubmit`](#triggerUploadOnSubmit) and
[`submitOnSuccess`](#submitOnSuccess) options below for details.
:::
## API
### Options
#### `id`
A unique identifier for this plugin (`string`, default: `'Form'`).
#### `target`
DOM element or CSS selector for the form element (`string` or `Element`,
default: `null`).
This is required for the plugin to work.
#### `resultName`
The `name` attribute for the `<input type="hidden">` where the result will be
added (`string`, default: `uppyResult`).
#### `getMetaFromForm`
Configures whether to extract metadata from the form (`boolean`, default:
`true`).
When set to `true`, the Form plugin will extract all fields from a `<form>`
element before upload begins. Those fields will then be added to Uppy meta data
state (`uppy.state.meta`) and each files meta, and appended as (meta)data to
the upload in an object with `[file input name attribute]` ->
`[file input value]` key/values.
#### `addResultToForm`
Configures whether to add upload/encoding results back to the form in an
`<input name="uppyResult" type="hidden">` element (`boolean`, default: `true`).
#### `triggerUploadOnSubmit`
Configures whether to start the upload when the form is submitted (`boolean`,
default: `false`).
When a user submits the form (via a submit button, the <kbd>Enter</kbd> key or
otherwise), this option will prevent form submission, and instead upload files
via Uppy. Then you could:
- Set `submitOnSuccess: true` if you need the form to _actually_ be submitted
once all files have been uploaded.
- Listen for `uppy.on('complete')` event to do something else if the file
uploads are all you need. For example, if the form is used for file metadata
only.
#### `submitOnSuccess`
Configures whether to submit the form after Uppy finishes uploading/encoding
(`boolean`, default: `false`).

View file

@ -1,4 +0,0 @@
{
"label": "Framework integrations",
"position": 8
}

View file

@ -1,117 +0,0 @@
---
slug: /angular
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Angular
[Angular][] components for Uppy UI plugins.
## When should I use it?
When you are using the Angular framework and you would like to use one of the UI
components.
## Install
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/angular
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/angular
```
</TabItem>
</Tabs>
:::note
You also need to install the UI plugin you want to use. For instance,
`@uppy/dashboard`.
:::
## Use
Instead of adding a UI plugin to an Uppy instance with `.use()`, the Uppy
instance can be passed into components as a `props` prop.
The following plugins are available as Angular component wrappers:
- Import `UppyAngularDashboardModule` used as `<uppy-dashboard>` renders
[`@uppy/dashboard`](/docs/dashboard)
- Import `UppyAngularDashboardModalModule` used as `<uppy-dashboard-modal>`
renders [`@uppy/dashboard`](/docs/dashboard) as a modal
- Import `UppyAngularProgressBarModule` used as `<uppy-progress-bar>` renders
[`@uppy/progress-bar`](/docs/progress-bar)
- Import `UppyAngularStatusBarModule` used as `<uppy-status-bar>` renders
[`@uppy/status-bar`](/docs/status-bar)
- Import `UppyAngularDragDropModule` used as `<uppy-drag-drop>` renders
[`@uppy/drag-drop`](/docs/drag-drop)
Each component takes a `props` prop that will be passed to the UI Plugin.
```typescript title="app.module.ts" showLineNumbers
import { NgModule } from '@angular/core';
import { UppyAngularDashboardModule } from '@uppy/angular';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, UppyAngularDashboardModule],
providers: [],
bootstrap: [AppComponent],
})
class {}
```
```html title="app.component.html" showLineNumbers
<uppy-dashboard [uppy]="uppy"> </uppy-dashboard>
```
You should initialize Uppy as a property of your component.
```typescript title="app.component.ts" showLineNumbers
import { Component } from '@angular/core';
import { Uppy } from '@uppy/core';
@Component({
selector: 'app-root',
})
export class AppComponent {
uppy: Uppy = new Uppy({ debug: true, autoProceed: true });
}
```
### CSS
All components have their own styling and should be added to your component
decorator. You can find the CSS import statements in the docs of the UI plugin
you want to use. For instance, for `@uppy/dashboard`:
```typescript
import { Component, ViewEncapsulation } from '@angular/core';
//...
@Component({
// ...
encapsulation: ViewEncapsulation.None,
styleUrls: [
'../node_modules/@uppy/core/dist/style.css',
'../node_modules/@uppy/dashboard/dist/style.css',
],
})
```
[angular]: https://angular.io

View file

@ -1,244 +0,0 @@
---
slug: /react
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# React
[React][] components for the Uppy UI plugins and hooks.
## Install
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/react
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/react
```
</TabItem>
</Tabs>
:::note
You also need to install the UI plugin you want to use. For instance,
`@uppy/dashboard`.
:::
## Use
`@uppy/react` exposes component wrappers for `Dashboard`, `DragDrop`, and all
other UI elements. The components can be used with either [React][] or
API-compatible alternatives such as [Preact][].
:::caution
If you find yourself writing many instances of `useState` and `useEffect` to
achieve something with Uppy in React, you are most likely breaking React best
practices. Consider reading
“[You Might Not Need an Effect](https://react.dev/learn/you-might-not-need-an-effect)”
and looking at our examples below.
:::
### Components
The following components are exported from `@uppy/react`:
- `<Dashboard />` renders [`@uppy/dashboard`](/docs/dashboard)
- `<DashboardModal />` renders [`@uppy/dashboard`](/docs/dashboard) as a modal
- `<DragDrop />` renders [`@uppy/drag-drop`](/docs/drag-drop)
- `<ProgressBar />` renders [`@uppy/progress-bar`](/docs/progress-bar)
- `<StatusBar />` renders [`@uppy/status-bar`](/docs/status-bar)
### Hooks
#### `useUppyState(uppy, selector)`
Use this hook when you need to access Uppys state reactively. Most of the
times, this is needed if you are building a custom UI for Uppy in React.
```js
// IMPORTANT: passing an initializer function to prevent Uppy from being reinstantiated on every render.
const [uppy] = useState(() => new Uppy());
const files = useUppyState(uppy, (state) => state.files);
const totalProgress = useUppyState(uppy, (state) => state.totalProgress);
// We can also get specific plugin state.
// Note that the value on `plugins` depends on the `id` of the plugin.
const metaFields = useUppyState(
uppy,
(state) => state.plugins?.Dashboard?.metaFields,
);
```
You can see all the values you can access on the
[`State`](https://github.com/transloadit/uppy/blob/c45407d099d87e25cecaf03c5d9ce59c582ca0dc/packages/%40uppy/core/src/Uppy.ts#L155-L181)
type. If you are accessing plugin state, you would have to look at the types of
the plugin.
#### `useUppyEvent(uppy, event, callback)`
Listen to Uppy events in a React component.
The first item in the array is an array of results from the event. Depending on
the event, that can be empty or have up to three values. The second item is a
function to clear the results. Values remain in state until the next event (if
that ever comes). Depending on your use case, you may want to keep the values in
state or clear the state after something else happenend.
```ts
// IMPORTANT: passing an initializer function to prevent Uppy from being reinstantiated on every render.
const [uppy] = useState(() => new Uppy());
const [results, clearResults] = useUppyEvent(uppy, 'transloadit:result');
const [stepName, result, assembly] = results; // strongly typed
useUppyEvent(uppy, 'cancel-all', clearResults);
```
## Examples
### Example: basic component
Here we have a basic component which ties Uppys state to the component. This
means you can render multiple instances. But be aware that as your component
unmounts, for instance because the user navigates to a different page, Uppys
state will be lost and uploads will stop.
:::note
If you render multiple instances of Uppy, make sure to give each instance a
unique `id`.
:::
```js
import React, { useEffect, useState } from 'react';
import Uppy from '@uppy/core';
import Webcam from '@uppy/webcam';
import { Dashboard } from '@uppy/react';
import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';
import '@uppy/webcam/dist/style.min.css';
function Component() {
// IMPORTANT: passing an initializer function to prevent Uppy from being reinstantiated on every render.
const [uppy] = useState(() => new Uppy().use(Webcam));
return <Dashboard uppy={uppy} />;
}
```
### Example: keep Uppy state and uploads while navigating between pages
When you want Uppys state to persist and keep uploads running between pages,
you can
[lift the state up](https://react.dev/learn/sharing-state-between-components#lifting-state-up-by-example).
```js
import React, { useState, useEffect } from 'react';
import Uppy from '@uppy/core';
import { Dashboard } from '@uppy/react';
function Page1() {
// ...
}
function Page2({ uppy }) {
return (
<>
<p>{totalProgress}</p>
<Dashboard id="dashboard" uppy={uppy} />
</>
);
}
export default function App() {
// keeping the uppy instance alive above the pages the user can switch during uploading
const [uppy] = useState(() => new Uppy());
return (
// Add your router here
<>
<Page1 />
<Page2 uppy={uppy} />
</>
);
}
```
### Example: updating Uppys options dynamically based on props
```js
// ...
function Component(props) {
// IMPORTANT: passing an initializer function to prevent the state from recreating.
const [uppy] = useState(() => new Uppy().use(Webcam));
useEffect(() => {
uppy.setOptions({ restrictions: props.restrictions });
}, [props.restrictions]);
useEffect(() => {
uppy.getPlugin('Webcam').setOptions({ modes: props.webcamModes });
}, [props.webcamModes]);
return <Dashboard uppy={uppy} />;
}
```
### Example: dynamic params and signature for Transloadit
When you go to production always make sure to set the `signature`. **Not using
[Signature Authentication](https://transloadit.com/docs/topics/signature-authentication/)
can be a security risk**. Signature Authentication is a security measure that
can prevent outsiders from tampering with your Assembly Instructions.
Generating a signature should be done on the server to avoid leaking secrets. In
React, this could get awkward with a `fetch` in a `useEffect` and setting it to
`useState`. Instead, its easier to use the
[`assemblyOptions`](/docs/transloadit#assemblyoptions) option to `fetch` the
params.
```js
// ...
function createUppy(userId) {
return new Uppy({ meta: { userId } }).use(Transloadit, {
async assemblyOptions(file) {
// You can send meta data along for use in your template.
// https://transloadit.com/docs/topics/assembly-instructions/#form-fields-in-instructions
const body = JSON.stringify({ userId: file.meta.userId });
const res = await fetch('/transloadit-params', { method: 'POST', body });
return response.json();
},
});
}
function Component({ userId }) {
// IMPORTANT: passing an initializer function to prevent Uppy from being reinstantiated on every render.
const [uppy] = useState(() => createUppy(userId));
useEffect(() => {
if (userId) {
// Adding to global `meta` will add it to every file.
uppy.setOptions({ meta: { userId } });
}
}, [uppy, userId]);
}
```
[react]: https://facebook.github.io/react
[preact]: https://preactjs.com/

View file

@ -1,71 +0,0 @@
---
slug: /svelte
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Svelte
[Svelte][] components for the Uppy UI plugins.
## Install
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/svelte
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/svelte
```
</TabItem>
</Tabs>
:::note
You also need to install the UI plugin you want to use. For instance,
`@uppy/dashboard`.
:::
## Use
The following plugins are available as Svelte component wrappers:
- `<Dashboard />` renders [`@uppy/dashboard`](/docs/dashboard)
- `<DashboardModal />` renders [`@uppy/dashboard`](/docs/dashboard) as a modal
- `<DragDrop />` renders [`@uppy/drag-drop`](/docs/drag-drop)
- `<ProgressBar />` renders [`@uppy/progress-bar`](/docs/progress-bar)
- `<StatusBar />` renders [`@uppy/status-bar`](/docs/status-bar)
Instead of adding a UI plugin to an Uppy instance with `.use()`, the Uppy
instance can be passed into components as an `uppy` prop. Due to the way Svelte
handles reactivity, you can initialize Uppy the same way you would with vanilla
JavaScript.
```svelte
<script>
import { Dashboard } from '@uppy/svelte';
import Uppy from '@uppy/core';
import Webcam from '@uppy/webcam';
// Don't forget the CSS: core and UI components + plugins you are using
import '@uppy/core/dist/style.css';
import '@uppy/dashboard/dist/style.css';
import '@uppy/webcam/dist/style.css';
const uppy = new Uppy().use(Webcam);
</script>
<main><Dashboard uppy={uppy} /></main>
```
[svelte]: https://svelte.dev

View file

@ -1,73 +0,0 @@
---
slug: /vue
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# Vue
[Vue][] components for the Uppy UI plugins.
## Install
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/vue
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/vue
```
</TabItem>
</Tabs>
:::note
You also need to install the UI plugin you want to use. For instance,
`@uppy/dashboard`.
:::
## Use
The following plugins are available as Vue component wrappers:
- `<Dashboard />` renders [`@uppy/dashboard`](/docs/dashboard) inline
- `<DashboardModal />` renders [`@uppy/dashboard`](/docs/dashboard) as a modal
- `<DragDrop />` renders [`@uppy/drag-drop`](/docs/drag-drop)
- `<ProgressBar />` renders [`@uppy/progress-bar`](/docs/progress-bar)
- `<StatusBar />` renders [`@uppy/status-bar`](/docs/status-bar)
Instead of adding a UI plugin to an Uppy instance with `.use()`, the Uppy
instance can be passed into components as an `uppy` prop. Due to the way Vue
handles reactivity, you can initialize Uppy the same way you would with vanilla
JavaScript.
```html
<script>
import { Dashboard } from '@uppy/vue';
import Uppy from '@uppy/core';
import Webcam from '@uppy/webcam';
// Don't forget the CSS: core and UI components + plugins you are using
import '@uppy/core/dist/style.css';
import '@uppy/dashboard/dist/style.css';
import '@uppy/webcam/dist/style.css';
const uppy = new Uppy().use(Webcam);
</script>
<template>
<Dashboard :uppy="uppy" />
</template>
```
[vue]: https://vuejs.org

View file

@ -1,150 +0,0 @@
---
sidebar_position: 11
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import UppyCdnExample from '/src/components/UppyCdnExample';
# Golden Retriever
The `@uppy/golden-retriever` plugin saves selected files in your browser cache,
so that if the browser crashes, or the user accidentally closes the tab, Uppy
can restore everything and continue uploading as if nothing happened. You can
read more about it
[on our blog](https://uppy.io/blog/2017/07/golden-retriever/).
The Golden Retriever uses three methods of browser data storage:
- `LocalStorage` to store file metadata and Uppy state only.
- `IndexedDB` for small files, usually under 5MiB.
- `Service Worker` (_optional_) for _all_ files because, unlike `IndexedDB`,
`Service Worker` can keep references to large files. Service Worker storage is
_quite_ temporary though, and doesnt persist across browser crashes or
restarts. It works well, however, for accidental refreshes or closed tabs.
Upon restore, the plugin first restores state from `LocalStorage` and then
checks both file storages — `IndexedDB` and `ServiceWorker` — merging the
results.
If restore is unsuccessful for certain files, they will be marked as “ghosts” in
the Dashboard UI, and a message + button offering to re-select those files will
be displayed.
Checkout the
[storage quotas](https://developer.mozilla.org/en-US/docs/Web/API/Storage_API/Storage_quotas_and_eviction_criteria#other_web_technologies)
on MDN to see how much data can be stored depending on the device and browser.
## When should I use this?
When you want extra insurance for the user-selected files. If you feel like
users might spend some time picking files, tweaking descriptions etc, and will
not appreciate having to do it over again if something crashes. Another use case
might be when you think user might want to pick a few files, navigate away to do
something else in your app or otherwise, and then come back to the same
selection.
## Install
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/golden-retriever
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/golden-retriever
```
</TabItem>
<TabItem value="cdn" label="CDN">
<UppyCdnExample>
{`
import { Uppy, GoldenRetriever } from "{{UPPY_JS_URL}}"
new Uppy().use(GoldenRetriever)
`}
</UppyCdnExample>
</TabItem>
</Tabs>
## Use
```js {7} showLineNumbers
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import GoldenRetriever from '@uppy/golden-retriever';
new Uppy()
.use(Dashboard, {inline:true, target: '#dashboard')
.use(GoldenRetriever);
```
By default, Golden Retriever will only use the `IndexedDB` storage, which is
good enough for files up to 5 MiB. `Service Worker` is optional and requires
setup.
### Enabling Service Worker
With the Service Worker storage, Golden Retriever will be able to temporary
store references to large files.
1. Bundle your own service worker `sw.js` file with Uppy GoldenRetrievers
service worker.
:::tip
For Webpack, check out
[serviceworker-webpack-plugin](https://github.com/oliviertassinari/serviceworker-webpack-plugin).
:::
```js title="sw.js"
import '@uppy/golden-retriever/lib/ServiceWorker';
```
2. Register it in your apps entry point:
```js
// you app.js entry point
uppy.use(GoldenRetriever, { serviceWorker: true });
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('/sw.js') // path to your bundled service worker with GoldenRetriever service worker
.then((registration) => {
console.log(
'ServiceWorker registration successful with scope: ',
registration.scope,
);
})
.catch((error) => {
console.log(`Registration failed with ${error}`);
});
}
```
Voilà, thats it. Happy retrieving!
## API
### Options
#### `id`
A unique identifier for this plugin (`string`, default: `'GoldenRetriever'`).
#### `expires`
How long to store metadata and files for. Used for `LocalStorage` and
`IndexedDB` (`number`, default: `24 * 60 * 60 * 1000` // 24 hours).
#### `serviceWorker`
Whether to enable `Service Worker` storage (`boolean`, default: `false`).

View file

@ -1,4 +0,0 @@
{
"label": "Guides",
"position": 2
}

View file

@ -1,62 +0,0 @@
---
sidebar_position: 7
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import UppyCdnExample from '/src/components/UppyCdnExample';
# Supporting IE11
We officially support recent versions of Chrome, Firefox, Safari and Edge.
Internet Explorer is not officially supported, as in we dont run tests for it,
but you can be mostly confident it works with the right polyfills. But it does
come with a risk of unexpected results in styling or functionality.
## Polyfills
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install core-js whatwg-fetch abortcontroller-polyfill md-gum-polyfill resize-observer-polyfill
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add core-js whatwg-fetch abortcontroller-polyfill md-gum-polyfill resize-observer-polyfill
```
</TabItem>
</Tabs>
```js
import 'core-js';
import 'whatwg-fetch';
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';
// Order matters: AbortController needs fetch which needs Promise (provided by core-js).
import 'md-gum-polyfill';
import ResizeObserver from 'resize-observer-polyfill';
window.ResizeObserver ??= ResizeObserver;
export { default } from '@uppy/core';
export * from '@uppy/core';
```
## Legacy CDN bundle
<UppyCdnExample uppyJsName="uppy.legacy.min.js">
{`
import { Uppy, DragDrop, Tus } from "{{UPPY_JS_URL}}"
const uppy = new Uppy()
uppy.use(DragDrop, { target: '#uppy' })
uppy.use(Tus, { endpoint: '//tusd.tusdemo.net/files/' })
`}
</UppyCdnExample>

View file

@ -1,382 +0,0 @@
---
sidebar_position: 3
---
# Building plugins
You can find already a few useful Uppy plugins out there, but there might come a
time when you will want to build your own. Plugins can hook into the upload
process or render a custom UI, typically to:
- Render some custom UI element, such as [StatusBar](/docs/status-bar) or
[Dashboard](/docs/dashboard).
- Do the actual uploading, such as [XHRUpload](/docs/xhr-upload) or
[Tus](/docs/tus).
- Do work before the upload, like compressing an image or calling external API.
- Interact with a third-party service to process uploads correctly, such as
[Transloadit](/docs/transloadit) or [AwsS3](/docs/aws-s3).
See a [full example of a plugin](#example-of-a-custom-plugin) below.
## Creating A Plugin
Uppy has two classes to create plugins with. `BasePlugin` for plugins that dont
need a user interface, and `UIPlugin` for ones that do. Each plugin has an `id`
and a `type`. `id`s are used to uniquely identify plugins. A `type` can be
anything—some plugins use `type`s to decide whether to do something to some
other plugin. For example, when targeting plugins at the built-in `Dashboard`
plugin, the Dashboard uses the `type` to figure out where to mount different UI
elements. `'acquirer'`-type plugins are mounted into the tab bar, while
`'progressindicator'`-type plugins are mounted into the progress bar area.
The plugin constructor receives the Uppy instance in the first parameter, and
any options passed to `uppy.use()` in the second parameter.
```js
import BasePlugin from '@uppy/core';
export default class MyPlugin extends BasePlugin {
constructor(uppy, opts) {
super(uppy, opts);
this.id = opts.id || 'MyPlugin';
this.type = 'example';
}
}
```
## Methods
Plugins can define methods to execute certain tasks. The most important method
is `install()`, which is called when a plugin is `.use`d.
All the below methods are optional! Only define the methods you need.
### `BasePlugin`
#### `install()`
Called when the plugin is `.use`d. Do any setup work here, like attaching events
or adding [upload hooks](#Upload-Hooks).
```js
export default class MyPlugin extends UIPlugin {
// ...
install() {
this.uppy.on('upload-progress', this.onProgress);
this.uppy.addPostProcessor(this.afterUpload);
}
}
```
#### `uninstall()`
Called when the plugin is removed, or the Uppy instance is closed. This should
undo all the work done in the `install()` method.
```js
export default class MyPlugin extends UIPlugin {
// ...
uninstall() {
this.uppy.off('upload-progress', this.onProgress);
this.uppy.removePostProcessor(this.afterUpload);
}
}
```
#### `afterUpdate()`
Called after every state update with a debounce, after everything has mounted.
#### `addTarget()`
Use this to add your plugin to another plugins target. This is what
`@uppy/dashboard` uses to add other plugins to its UI.
### `UIPlugin`
`UIPlugin` extends the `BasePlugin` class so it will also contain all the above
methods.
#### `mount(target)`
Mount this plugin to the `target` element. `target` can be a CSS query selector,
a DOM element, or another Plugin. If `target` is a Plugin, the source (current)
plugin will register with the target plugin, and the latter can decide how and
where to render the source plugin.
This method can be overridden to support for different render engines.
#### `render()`
Render this plugins UI. Uppy uses [Preact](https://preactjs.com) as its view
engine, so `render()` should return a Preact element. `render` is automatically
called by Uppy on each state change.
#### `onMount()`
Called after Preact has rendered the components of the plugin.
#### `update(state)`
Called on each state update. You will rarely need to use this, unless if you
want to build a UI plugin using something other than Preact.
#### `onUnmount()`
Called after the elements have been removed from the DOM. Can be used to do some
clean up or other side-effects.
## Upload Hooks
When creating an upload, Uppy runs files through an upload pipeline. The
pipeline consists of three parts, each of which can be hooked into:
Preprocessing, Uploading, and Postprocessing. Preprocessors can be used to
configure uploader plugins, encrypt files, resize images, etc., before uploading
them. Uploaders do the actual uploading work, such as creating an XMLHttpRequest
object and sending the file. Postprocessors do their work after files have been
uploaded completely. This could be anything from waiting for a file to propagate
across a CDN, to sending another request to relate some metadata to the file.
Each hook is a function that receives an array containing the file IDs that are
being uploaded, and returns a Promise to signal completion. Hooks are added and
removed through `Uppy` methods:
[`addPreProcessor`](/docs/uppy#addpreprocessorfn),
[`addUploader`](/docs/uppy#adduploaderfn),
[`addPostProcessor`](/docs/uppy#addpostprocessorfn), and their
[`remove*`](/docs/uppy#removepreprocessorremoveuploaderremovepostprocessorfn)
counterparts. Normally, hooks should be added during the plugin `install()`
method, and removed during the `uninstall()` method.
Additionally, upload hooks can fire events to signal progress.
When adding hooks, make sure to bind the hook `fn` beforehand! Otherwise, it
will be impossible to remove. For example:
```js
class MyPlugin extends BasePlugin {
constructor(uppy, opts) {
super(uppy, opts);
this.id = opts.id || 'MyPlugin';
this.type = 'example';
this.prepareUpload = this.prepareUpload.bind(this); // ← this!
}
prepareUpload(fileIDs) {
console.log(this); // `this` refers to the `MyPlugin` instance.
return Promise.resolve();
}
install() {
this.uppy.addPreProcessor(this.prepareUpload);
}
uninstall() {
this.uppy.removePreProcessor(this.prepareUpload);
}
}
```
Or you can define the method as a class field:
```js
class MyPlugin extends UIPlugin {
constructor(uppy, opts) {
super(uppy, opts);
this.id = opts.id || 'MyPlugin';
this.type = 'example';
}
prepareUpload = (fileIDs) => {
// ← this!
console.log(this); // `this` refers to the `MyPlugin` instance.
return Promise.resolve();
};
install() {
this.uppy.addPreProcessor(this.prepareUpload);
}
uninstall() {
this.uppy.removePreProcessor(this.prepareUpload);
}
}
```
## Progress events
Progress events can be fired for individual files to show feedback to the user.
For upload progress events, only emitting how many bytes are expected and how
many have been uploaded is enough. Uppy will handle calculating progress
percentages, upload speed, etc.
Preprocessing and postprocessing progress events are plugin-dependent and can
refer to anything, so Uppy doesnt try to be smart about them. Processing
progress events can be of two types: determinate or indeterminate. Some
processing does not have meaningful progress beyond “not done” and “done”. For
example, sending a request to initialize a server-side resource that will serve
as the upload destination. In those situations, indeterminate progress is
suitable. Other processing does have meaningful progress. For example,
encrypting a large file. In those situations, determinate progress is suitable.
Here are the relevant events:
- [`preprocess-progress`](/docs/uppy#preprocess-progress)
- [`upload-progress`](/docs/uppy#upload-progress)
- [`postprocess-progress`](/docs/uppy#postprocess-progress)
## JSX
Since Uppy uses Preact and not React, the default Babel configuration for JSX
elements does not work. You have to import the Preact `h` function and tell
Babel to use it by adding a `/** @jsx h */` comment at the top of the file.
See the Preact
[Getting Started Guide](https://preactjs.com/guide/getting-started) for more on
Babel and JSX.
<!-- eslint-disable jsdoc/check-tag-names -->
```jsx
/** @jsx h */
import { UIPlugin } from '@uppy/core';
import { h } from 'preact';
class NumFiles extends UIPlugin {
render() {
const numFiles = Object.keys(this.uppy.state.files).length;
return <div>Number of files: {numFiles}</div>;
}
}
```
## Locales
For any user facing language that you use while writing your Plugin, please
provide them as strings in the `defaultLocale` property like so:
```js
this.defaultLocale = {
strings: {
youCanOnlyUploadFileTypes: 'You can only upload: %{types}',
youCanOnlyUploadX: {
0: 'You can only upload %{smart_count} file',
1: 'You can only upload %{smart_count} files',
2: 'You can only upload %{smart_count} files',
},
},
};
```
This allows them to be overridden by Locale Packs, or directly when users pass
`locale: { strings: youCanOnlyUploadFileTypes: 'Something else completely about %{types}'} }`.
For this to work, its also required that you call `this.i18nInit()` in the
plugin constructor.
## Example of a custom plugin
Below is a full example of a
[small plugin](https://github.com/arturi/uppy-plugin-image-compressor) that
compresses images before uploading them. You can replace `compressorjs` method
with any other work you need to do. This works especially well for async stuff,
like calling an external API.
<!-- eslint-disable consistent-return -->
```js
import { UIPlugin } from '@uppy/core';
import Translator from '@uppy/utils/lib/Translator';
import Compressor from 'compressorjs/dist/compressor.esm.js';
class UppyImageCompressor extends UIPlugin {
constructor(uppy, opts) {
const defaultOptions = {
quality: 0.6,
};
super(uppy, { ...defaultOptions, ...opts });
this.id = this.opts.id || 'ImageCompressor';
this.type = 'modifier';
this.defaultLocale = {
strings: {
compressingImages: 'Compressing images...',
},
};
// we use those internally in `this.compress`, so they
// should not be overridden
delete this.opts.success;
delete this.opts.error;
this.i18nInit();
}
compress(blob) {
return new Promise(
(resolve, reject) =>
new Compressor(blob, {
...this.opts,
success(result) {
return resolve(result);
},
error(err) {
return reject(err);
},
}),
);
}
prepareUpload = (fileIDs) => {
const promises = fileIDs.map((fileID) => {
const file = this.uppy.getFile(fileID);
this.uppy.emit('preprocess-progress', file, {
mode: 'indeterminate',
message: this.i18n('compressingImages'),
});
if (!file.type.startsWith('image/')) {
return;
}
return this.compress(file.data)
.then((compressedBlob) => {
this.uppy.log(
`[Image Compressor] Image ${file.id} size before/after compression: ${file.data.size} / ${compressedBlob.size}`,
);
this.uppy.setFileState(fileID, { data: compressedBlob });
})
.catch((err) => {
this.uppy.log(
`[Image Compressor] Failed to compress ${file.id}:`,
'warning',
);
this.uppy.log(err, 'warning');
});
});
const emitPreprocessCompleteForAll = () => {
fileIDs.forEach((fileID) => {
const file = this.uppy.getFile(fileID);
this.uppy.emit('preprocess-complete', file);
});
};
// Why emit `preprocess-complete` for all files at once, instead of
// above when each is processed?
// Because it leads to StatusBar showing a weird “upload 6 files” button,
// while waiting for all the files to complete pre-processing.
return Promise.all(promises).then(emitPreprocessCompleteForAll);
};
install() {
this.uppy.addPreProcessor(this.prepareUpload);
}
uninstall() {
this.uppy.removePreProcessor(this.prepareUpload);
}
}
export default UppyImageCompressor;
```

View file

@ -1,11 +0,0 @@
---
sidebar_position: 4
---
# Building your own UI with Uppy
:::note
This guide is in progress.
:::

View file

@ -1,84 +0,0 @@
---
sidebar_position: 1
---
# Choosing the uploader you need
Versatile, reliable uploading is at the heart of Uppy. It has many configurable
plugins to suit your needs. In this guide we will explain the different plugins,
their strategies, and when to use them based on use cases.
## Use cases
### I want worry-free, plug-and-play uploads with Transloadit services
Transloadits strength is versatility. By doing video, audio, images, documents,
and more, you only need one vendor for [all your file processing
needs][transloadit-services]. The [`@uppy/transloadit`][] plugin directly
uploads to Transloadit so you only have to worry about creating a
[template][transloadit-concepts]. It uses
[Tus](#i-want-reliable-resumable-uploads) under the hood so you dont have to
sacrifice reliable, resumable uploads for convenience.
You should use [`@uppy/transloadit`][] if you dont want to host your own
server, (optionally) need file processing, and store it in the service (such as
S3 or GCS) of your liking. All with minimal effort.
### I want reliable, resumable uploads
[Tus][tus] is a new open protocol for resumable uploads built on HTTP. This
means accidentally closing your tab or losing connection lets you continue, for
instance, your 10GB upload instead of starting all over.
Tus supports any language, any platform, and any network. It requires a client
and server integration to work. You can checkout the client and server
[implementations][tus-implementations] to find the server in your preferred
language. You can store files on the Tus server itself, but you can also use
service integrations (such as S3) to store files externally.
If you want reliable, resumable uploads: use [`@uppy/tus`][] to connect to your
Tus server in a few lines of code.
:::tip
If you plan to let people upload _a lot_ of files, [`@uppy/tus`][] has
exponential backoff built-in. Meaning if your server (or proxy) returns HTTP 429
because its being overloaded, [`@uppy/tus`][] will find the ideal sweet spot to
keep uploading without overloading.
:::
### I want to upload to AWS S3 (or S3-compatible storage) directly
When you prefer a _client-to-storage_ over a _client-to-server-to-storage_ (such
as [Transloadit](/docs/transloadit) or [Tus](/docs/tus)) setup. This may in some
cases be preferable, for instance, to reduce costs or the complexity of running
a server and load balancer with [Tus](/docs/tus).
Uppy has two plugins to make this happen [`@uppy/aws-s3`][] and
[`@uppy/aws-s3-multipart`][], but we are planning to merge the two plugins in
the next major. You should use [`@uppy/aws-s3`][] with the new
`shouldUseMultipart` option.
:::info
You can also save files in S3 with the [`/s3/store`][s3-robot] robot while still
using the powers of Transloadit services.
:::
### I want to send regular HTTP uploads to my own server
[`@uppy/xhr-upload`][] handles classic HTML multipart form uploads as well as
uploads using the HTTP `PUT` method.
[s3-robot]: https://transloadit.com/services/file-exporting/s3-store/
[transloadit-services]: https://transloadit.com/services/
[transloadit-concepts]: https://transloadit.com/docs/getting-started/concepts/
[`@uppy/transloadit`]: /docs/transloadit
[`@uppy/tus`]: /docs/tus
[`@uppy/aws-s3-multipart`]: /docs/aws-s3-multipart
[`@uppy/aws-s3`]: /docs/aws-s3-multipart
[`@uppy/xhr-upload`]: /docs/xhr-upload
[tus]: https://tus.io/
[tus-implementations]: https://tus.io/implementations.html

View file

@ -1,136 +0,0 @@
# Custom stores
If your app uses a state management library such as
[Redux](https://redux.js.org), it can be useful to have Uppy store its state
there instead—that way, you could write custom uploader UI components in the
same way as the other components in the application.
Uppy comes with two state management solutions (stores):
- `@uppy/store-default`, a basic object-based store.
- `@uppy/store-redux`, a store that uses a key in a Redux store.
You can also use a third-party store:
- [uppy-store-ngrx](https://github.com/rimlin/uppy-store-ngrx/), keeping Uppy
state in a key in an [Ngrx](https://github.com/ngrx/platform) store for use
with Angular.
## Using stores
To use a store, pass an instance to the
[`store` option](/docs/uppy#store-defaultstore) in the Uppy constructor:
```js
import DefaultStore from '@uppy/store-default';
const uppy = new Uppy({
store: new DefaultStore(),
});
```
### `DefaultStore`
Uppy uses the `DefaultStore` by default! You do not need to do anything to use
it. It does not take any options.
### `ReduxStore`
The `ReduxStore` stores Uppy state on a key in an existing Redux store. The
`ReduxStore` dispatches `uppy/STATE_UPDATE` actions to update state. When the
state in Redux changes, it notifies Uppy. This way, you get most of the benefits
of Redux, including support for the Redux Devtools and time traveling!
Checkout our
[Redux example](https://github.com/transloadit/uppy/tree/main/examples/redux)
for a working demo.
#### `opts.store`
Pass a Redux store instance, from `Redux.createStore`. This instance should have
the Uppy reducer mounted somewhere already.
#### `opts.id`
By default, the `ReduxStore` assumes Uppy state is stored on a `state.uppy[id]`
key. `id` is randomly generated by the store constructor, but can be specified
by passing an `id` option if it should be predictable.
```js
ReduxStore({
store,
id: 'avatarUpload',
});
```
#### `opts.selector`
If youd rather not store the Uppy state under the `state.uppy` key at all, use
the `selector` option to the `ReduxStore` constructor to tell it where to find
state instead:
```js
const uppy = new Uppy({
store: ReduxStore({
store,
id: 'avatarUpload',
selector: (state) => state.pages.profile.uppy.avatarUpload,
}),
});
```
Note that when specifying a custom selector, you **must** also specify a custom
store ID. The store `id` tells the reducer in which property it should put
Uppys state. The selector must then take the state from that property. In the
example, we set the ID to `avatarUpload` and take the state from the
`[reducer mount path].avatarUpload`.
If your app uses [`reselect`](https://npmjs.com/package/reselect), its selectors
work well with this!
## Implementing Stores
An Uppy store is an object with three methods.
- `getState()` - Return the current state object.
- `setState(patch)` - Merge the object `patch` into the current state.
- `subscribe(listener)` - Call `listener` whenever the state changes. `listener`
is a function that should receive three parameters:
`(prevState, nextState, patch)`
The `subscribe()` method should return a function that “unsubscribes”
(removes) the `listener`.
The default store implementation, for example, looks a bit like this:
```js
function createDefaultStore() {
let state = {};
const listeners = new Set();
return {
getState: () => state,
setState: (patch) => {
const prevState = state;
const nextState = { ...prevState, ...patch };
state = nextState;
listeners.forEach((listener) => {
listener(prevState, nextState, patch);
});
},
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.remove(listener);
},
};
}
```
A pattern like this, where users can pass options via a function call if
necessary, is recommended.
See the
[@uppy/store-default](https://github.com/transloadit/uppy/tree/main/packages/%40uppy/store-default)
package for more inspiration.

View file

@ -1,865 +0,0 @@
# Migration guides
These cover all the major Uppy versions and how to migrate to them.
## Migrate from Companion 4.x to 5.x
- End-of-Life versions of Node.js are no longer supported (use latest 18.x LTS,
20.x LTS, or 22.x current).
- Setting the `corsOrigin` (`COMPANION_CLIENT_ORIGINS`) option is now required.
You should define the list of origins you expect your app to be served from,
otherwise it can be impersonated from a different origin you dont control.
Set it to `true` if you dont care about impersonating.
- `COMPANION_REDIS_EXPRESS_SESSION_PREFIX` now defaults to `companion-session:`
(before `sess:`). To revert keep backwards compatibility, set the environment
variable `COMPANION_REDIS_EXPRESS_SESSION_PREFIX=sess:`.
- The URL endpoint (used by the `Url`/`Link` plugin) is now turned off by
default and must be explicitly enabled with
`COMPANION_ENABLE_URL_ENDPOINT=true` or `enableUrlEndpoint: true`.
- The option `streamingUpload` / `COMPANION_STREAMING_UPLOAD` now defaults to
`true`.
- The option `getKey(req, filename, metadata)` has changed signature to
`getKey({ filename, metadata, req })`.
- The option `bucket(req, metadata)` has changed signature to
`bucketOrFn({ req, metadata, filename })`.
- Custom provider breaking changes. If you have not implemented a custom
provider, you should not be affected.
- The static `getExtraConfig` property has been renamed to
`getExtraGrantConfig`.
- The static `authProvider` property has been renamed to `oauthProvider`.
- Endpoint `GET /s3/params` now returns `{ method: "POST" }` instead of
`{ method: "post" }`. This will not affect most people.
- `access-control-allow-headers` is no longer included in
`Access-Control-Expose-Headers`, and `uppy-versions` is no longer an allowed
header. We are not aware of any issues this might cause.
- Internal refactoring (probably wont affect you)
- `getProtectedGot` parameter `blockLocalIPs` changed to `allowLocalIPs`
(inverted boolean).
- `getURLMeta` 2nd (boolean) argument inverted.
- `getProtectedHttpAgent` parameter `blockLocalIPs` changed to `allowLocalIPs`
(inverted boolean).
- `downloadURL` 2nd (boolean) argument inverted.
- `StreamHttpJsonError` renamed to `HttpError`.
- Removed the `oauthOrigin` option, as well as the (undocumented) option
`clients`. Use `corsOrigin` instead.
### `@uppy/companion-client`
:::info
Unless you built a custom provider, you dont use `@uppy/companion-client`
directly but through provider plugins such as `@uppy/google-drive`. In which
case you dont have to do anything.
:::
- `supportsRefreshToken` now defaults to `false` instead of `true`. If you have
implemented a custom provider, this might affect you.
- `Socket` class is no longer in use and has been removed. Unless you used this
class you dont need to do anything.
- Remove deprecated options `serverUrl` and `serverPattern` (they were merely
defined in Typescript, not in use).
- `RequestClient` methods `get`, `post`, `delete` no longer accepts a boolean as
the third argument. Instead, pass `{ skipPostResponse: true | false }`. This
wont affect you unless youve been using `RequestClient`.
- When pausing uploads, the WebSocket towards companion will no longer be
closed. This allows paused uploads to be cancelled, but once a file has been
paused it will still occupy its place in the concurrency queue.
## Migrate from Uppy 3.x to 4.x
### TypeScript rewrite
Almost all plugins have been completely rewritten in TypeScript! This means you
may run into type error all over the place, but the types now accurately show
Uppys state and files.
There are too many small changes to cover, so you have to upgrade and see where
TypeScript complains.
One important thing to note are the new generics on `@uppy/core`.
<!-- eslint-disable @typescript-eslint/no-unused-vars -->
<!-- eslint-disable @typescript-eslint/no-non-null-assertion -->
```ts
import Uppy from '@uppy/core';
// xhr-upload is for uploading to your own backend.
import XHRUpload from '@uppy/xhr-upload';
// Your own metadata on files
type Meta = { myCustomMetadata: string };
// The response from your server
type Body = { someThingMyBackendReturns: string };
const uppy = new Uppy<Meta, Body>().use(XHRUpload, {
endpoint: '/upload',
});
const id = uppy.addFile({
name: 'example.jpg',
data: new Blob(),
meta: { myCustomMetadata: 'foo' },
});
// This is now typed
const { myCustomMetadata } = uppy.getFile(id).meta;
await uppy.upload();
// This is strictly typed too
const { someThingMyBackendReturns } = uppy.getFile(id).response.body!;
```
### `@uppy/aws-s3` and `@uppy/aws-s3-multipart`
- `@uppy/aws-s3` and `@uppy/aws-s3-multipart` have been combined into a single
plugin. You should now only use `@uppy/aws-s3` with the new option,
[`shouldUseMultipart()`](/docs/aws-s3-multipart/#shouldusemultipartfile), to
allow you to switch between regular and multipart uploads. You can read more
about this in the
[plugin docs](https://uppy.io/docs/aws-s3-multipart/#when-should-i-use-it).
- Remove deprecated `prepareUploadParts` option.
- Companions options (`companionUrl`, `companionHeaders`, and
`companionCookieRules`) are renamed to more generic names (`endpoint`,
`headers`, and `cookieRules`).
Using Companion with the `@uppy/aws-s3` plugin only makes sense if you already
need Companion for remote providers (such as Google Drive). When using your
own backend, you can let Uppy do all the heavy lifting on the client which it
would normally do for Companion, so you dont have to implement that yourself.
As long as you return the JSON for the expected endpoints (see our
[server example](https://github.com/transloadit/uppy/blob/main/examples/aws-nodejs/index.js)),
you only need to set `endpoint`.
If you are using Companion, rename the options. If you have a lot of
client-side complexity (`createMultipartUpload`, `signPart`, etc), consider
letting Uppy do this for you.
### `@uppy/core`
- The `close()` method has been renamed to `destroy()` to more accurately
reflect you can not recover from it without creating a new `Uppy` instance.
- The `clearUploadedFiles()` method has been renamed to `clear()` as a
convenience method to clear all the state. This can be useful when a user
navigates away and you want to clear the state on success.
- `bytesUploaded`, in `file.progress.bytesUploaded`, is now always a `boolean`,
instead of a `boolean` or `number`.
### `@uppy/xhr-upload`
Before the plugin had the options `getResponseData`, `getResponseError`,
`validateStatus` and `responseUrlFieldName`. These were inflexible and too
specific. Now we have hooks similar to `@uppy/tus`:
- `onBeforeRequest` to manipulate the request before it is sent.
- `shouldRetry` to determine if a request should be retried. By default 3
retries with exponential backoff. After three attempts it will throw an error,
regardless of whether you returned `true`.
- `onAfterResponse` called after a successful response but before Uppy resolves
the upload.
Checkout the [docs](/docs/xhr-upload/) for more info.
### `@uppy/transloadit`
The options `signature`, `params`, `fields`, and `getAssemblyOptions` have been
removed in favor of [`assemblyOptions`](/docs/transloadit/#assemblyoptions),
which can be an object or an (async) function returning an object.
When using `assemblyOptions()` as a function, it is now called only once for all
files, instead of per file. Before `@uppy/transloadit` was trying to be too
smart, creating multiple assemblies in which each assembly has files with
identical `fields`. This was done so you can use `fields` dynamically in your
template per file, instead of per assembly.
Now we sent all metadata of a file inside the tus upload (which
`@uppy/transloadit` uses under the hood) and make it accessible in your
Transloadit template as `file_user_meta`. You should use `fields` for global
values in your template and `file_user_meta` for per file values.
Another benefit of running `assemblyOptions()` only once, is that when
generating a
[secret](https://transloadit.com/docs/topics/signature-authentication/) on your
server (which you should), a network request is made only once for all files,
instead of per file.
### CDN
- We no longer build and serve the legacy build, made for IE11, on our CDN.
### Miscellaneous
- All uploaders plugins now consistently use
[`allowedMetaFields`](/docs/xhr-upload/#allowedmetafields). Before there were
inconsistencies between plugins.
- All plugin `titles` (what you see in the Dashboard when you open a plugin) are
now set from the `locale` option. See the
[docs](/docs/locales/#overriding-locale-strings-for-a-specific-plugin) on how
to overide a string.
### `@uppy/angular`
- Upgrade to Angular 18.x (17.x is still supported too) and to TS 5.4
### `@uppy/react`
- Remove deprecated `useUppy`
- You can no longer set `inline` on the `Dashboard` component, use `Dashboard`
or `DashboardModal` components respectively.
### `@uppy/svelte`
- Make Svelte 5 the peer dependency
- Remove UMD output
### `@uppy/vue`
- Migrate to Composition API with TypeScript & drop Vue 2 support
- Drop Vue 2 support
## Migrate from Robodog to Uppy plugins
Uppy is flexible and extensible through plugins. But the integration code could
sometimes be daunting. This is what brought Robodog to life. An alternative with
the same features, but with a more ergonomic and minimal API.
But, it didnt come with its own set of new problems:
- It tries to do the exact same, but it looks like a different product.
- Its confusing for users whether they want to use Robodog or Uppy directly.
- Robodog is more ergonomic because its limited. When you hit such a limit, you
need to refactor everything to Uppy with plugins.
This has now led us to deprecating Robodog and embrace Uppy for its strong
suits; modularity and flexibility. At the same time, we also introduced
something to take away some repetitive integration code:
[`@uppy/remote-sources`](/docs/remote-sources).
To mimic the Robodog implementation with all its features, you can use the code
snippet below. But chances are Robodog did more than you need so feel free to
remove things or go through the [list of plugins](/docs/companion/) and install
and use the ones you need.
You can also checkout how we migrated the Robodog example ourselves in this
[commit](https://github.com/transloadit/uppy/commit/089aaed615c77bafaf905e291b6b4e82aaeb2f6f).
```js
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import RemoteSources from '@uppy/remote-sources';
import Webcam from '@uppy/webcam';
import ScreenCapture from '@uppy/screen-capture';
import GoldenRetriever from '@uppy/golden-retriever';
import ImageEditor from '@uppy/image-editor';
import Audio from '@uppy/audio';
import Transloadit, {
COMPANION_URL,
COMPANION_ALLOWED_HOSTS,
} from '@uppy/transloadit';
import '@uppy/core/dist/style.css';
import '@uppy/dashboard/dist/style.css';
import '@uppy/audio/dist/style.css';
import '@uppy/screen-capture/dist/style.css';
import '@uppy/image-editor/dist/style.css';
new Uppy()
.use(Dashboard, {
inline: true,
target: '#app',
showProgressDetails: true,
proudlyDisplayPoweredByUppy: true,
})
.use(RemoteSources, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
})
.use(Webcam, {
showVideoSourceDropdown: true,
showRecordingLength: true,
})
.use(Audio, {
showRecordingLength: true,
})
.use(ScreenCapture)
.use(ImageEditor)
.use(Transloadit, {
service: 'https://api2.transloadit.com',
async getAssemblyOptions(file) {
// This is where you configure your auth key, auth secret, and template ID
// /uppy/docs/transloadit/#getAssemblyOptions-file
//
// It is important to set the secret in production:
// https://transloadit.com/docs/topics/signature-authentication/
const response = await fetch('/some-endpoint');
return response.json();
},
});
```
## Migrate from Uppy 2.x to 3.x
### Uppy is pure ESM
Following the footsteps of many packages, we now only ship Uppy core and its
plugins as
[ECMAScript Modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
(ESM). On Uppy 2.x, we were shipping CommonJS.
If are already using ESM yourself, or are using the CDN builds, nothing changes
for you!
If you are using CommonJS, you might need to add some tooling for everything to
work, or you might want to refactor your codebase to ESM refer to the
[Pure ESM package](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c)
gist for added information and help on how to do that.
### Robodog is deprecated
See the [Robodog migration guide](#Migrate-from-Robodog-to-Uppy-plugins).
### `@uppy/core`
#### Remove `AggregateError` polyfill.
Its supported by most modern browsers and
[can be polyfilled by the user](https://github.com/transloadit/uppy/pull/3532#discussion_r818602636)
if needed.
To migrate: install a `AggregateError` polyfill or use `core-js`.
#### Remove `reset()` method.
Its a duplicate of `cancelAll`, but with a less intention revealing name.
To migrate: use `cancelAll`.
#### Remove backwards compatible exports (static properties on `Uppy`)\`
`Uppy`, `UIPlugin`, `BasePlugin`, and `debugLogger` used to also be accessible
on the `Uppy` export. This has now been removed due to the transition to ESM.
To migrate: import the `Uppy` class by default and/or use named exports for
everything else.
#### `uppy.validateRestrictions()` now returns a `RestrictionError`
This method used to return `{ result: false, reason: err.message }`, but that
felt strange as it tries to mimic an error. Instead it now return a
`RestrictionError`, which is extended `Error` class.
To migrate: check the return value, if its defined you have an error, otherwise
all went well. Note that the error is `return`ed, its not `throw`n, so you
dont have to `catch` it.
### `@uppy/transloadit`
Remove export of `ALLOWED_COMPANION_PATTERN`, `COMPANION`, and
`COMPANION_PATTERN` in favor of `COMPANION_URL` and `COMPANION_ALLOWED_HOSTS`.
This is to have more intention revealing names, `COMPANION` sounds like the
Companion instance, `COMPANION_URL` makes it more clear that its a URL.
These are properties can now be imported and used for remote sources plugins
when using Transloadit:
```js
import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
// ...
uppy.use(Dropbox, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
});
```
### `@uppy/aws-s3-multipart`
#### Make `headers` inside the return value of [`prepareUploadParts`](/docs/aws-s3-multipart/#prepareUploadParts-file-partData) part-indexed too.
This is to allow custom headers to be set per part. See this
[issue](https://github.com/transloadit/uppy/issues/3881) for details.
To migrate: make headers part indexed like `presignedUrls`:
`{ "headers": { "1": { "Content-MD5": "foo" } }}`.
#### Remove `client` getter and setter.
Its internal usage only.
To migrate: use exposed options only.
### `@uppy/tus/`, `@uppy/aws-s3`, `@uppy/xhr-upload`
Rename `metaFields` option to `allowedMetaFields`. Counter intuitively,
`metaFields` is for _filtering_ which `metaFields` to send along with the
request, not for adding extra meta fields to a request. As a lot of people were
confused by this, and the name overlaps with the
[`metaFields` option from Dashboard](/docs/dashboard/#metaFields), we renamed
it.
To migrate: use `allowedMetaFields`.
### `@uppy/react`
#### Uppy dependencies have become peer dependencies
`@uppy/dashboard`, `@uppy/drag-drop`, `@uppy/file-input`, `@uppy/progress-bar`,
and `@uppy/status-bar` are now peer dependencies. This means you dont install
all these packages if you only need one.
To migrate: install only the packages you need. If you use the Dashboard
component, you need `@uppy/dashboard`, and so onwards.
#### Dont expose `validProps` on the exported components.
Its internal usage only.
To migrate: use exposed options only.
### `@uppy/svelte`
`@uppy/dashboard`, `@uppy/drag-drop`, `@uppy/progress-bar`, and
`@uppy/status-bar` are now peer dependencies. This means you dont install all
these packages if you only need one.
To migrate: install only the packages you need. If you use the Dashboard
component, you need `@uppy/dashboard`, and so onwards.
### `@uppy/vue`
`@uppy/dashboard`, `@uppy/drag-drop`, `@uppy/file-input`, `@uppy/progress-bar`,
and `@uppy/status-bar` are now peer dependencies. This means you dont install
all these packages if you only need one.
To migrate: install only the packages you need. If you use the Dashboard
component, you need `@uppy/dashboard`, and so onwards.
### `@uppy/store-redux`
Remove backwards compatible exports (static properties on `ReduxStore`).
Exports, such as `reducer`, used to also be accessible on the `ReduxStore`
export. This has now been removed due to the transition to ESM.
To migrate: use named imports.
### `@uppy/thumbnail-generator`
Remove `rotateImage`, `protect`, and `canvasToBlob` from the plugin prototype.
They are internal usage only.
To migrate: use exposed options only.
### Known issues
- [`ERESOLVE could not resolve` on npm install](https://github.com/transloadit/uppy/issues/4057).
- [@uppy/svelte reports a broken dependency with the Vite bundler](https://github.com/transloadit/uppy/issues/4069).
## Migrate from Companion 3.x to 4.x
### Minimum required Node.js version is v14.20.0
Aligning with the Node.js
[Long Term Support (LTS) schedule](https://nodejs.org/en/about/releases/) and to
use modern syntax features.
### `companion.app()` returns `{ app, emitter }` instead of `app`
Companion 3.x provides the emitter as `companionEmitter` on `app`. As of 4.x, an
object is returned with an `app` property (express middleware) and an `emitter`
property (event emitter). This provides more flexibility in the future and
follows best practices.
### Removed `searchProviders` wrapper object inside `providerOptions`
To use [`@uppy/unsplash`](/docs/unsplash), you had to configure Unsplash in
Companion inside `providerOptions.searchProviders`. This is redundant, Unsplash
is a provider as well so we removed the wrapper object.
### Moved the `s3` options out of `providerOptions`
To use AWS S3 for storage, you configured the `s3` object inside
`providerOptions`. But as S3 is not a provider but a destination. To avoid
confusion we moved the `s3` settings to the root settings object.
### Removed compatibility for legacy Custom Provider implementations
[Custom Provider](/docs/companion/#Adding-custom-providers) implementations must
use the Promise API. The callback API is no longer supported.
### Default to no ACL for AWS S3
Default to no
[ACL](https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html)
for S3 uploads. Before the default was `public-read` but AWS now discourages
ACLs. The environment variable `COMPANION_AWS_DISABLE_ACL` is also removed,
instead Companion only uses `COMPANION_AWS_ACL`.
### `protocol` sent from Uppy in any `get` request is now required (before it would default to Multipart).
If you use any official Uppy plugins, then no migration is needed. For custom
plugins that talk to Companion, make to send along the `protocol` header with a
value of `multipart`, `s3Multipart`, or `tus`.
### `emitSuccess` and `emitError` are now private methods on the `Uploader` class.
Its unlikely youre using this, but its technically a breaking change. In
general, dont depend on implicitly internal methods, use exposed APIs instead.
### Removed `chunkSize` backwards compatibility for AWS S3 Multipart
`chunkSize` option will now be used as `partSize` in AWS multipart. Before only
valid values would be respected. Invalid values would be ignored. Now any value
will be passed on to the AWS SDK, possibly throwing an error on invalid values.
### Removed backwards compatibility for `/metrics` endpoint
The `metrics` option is a boolean flag to tell Companion whether to provide an
endpoint `/metrics` with Prometheus metrics. Metrics will now always be served
under `options.server.path`. Before v4.x, it would always be served under the
root.
For example: if `{ options: { metrics: true, server: { path: '/companion' }}}`,
metrics will now be served under `/companion/metrics`. In v3.x, the metrics
would be served under `/metrics`.
## Migrate from Uppy 1.x to 2.x
### New bundle requires manual polyfilling
With 2.0, following in the footsteps of Microsoft, we are dropping support for
IE11. As a result, we are able to remove all built-in polyfills, and the new
bundle size is **25% smaller**! If you want your app to still support older
browsers (such as IE11), you may need to add the following polyfills to your
bundle:
- [abortcontroller-polyfill](https://github.com/mo/abortcontroller-polyfill)
- [core-js](https://github.com/zloirock/core-js)
- [md-gum-polyfill](https://github.com/mozdevs/mediaDevices-getUserMedia-polyfill)
- [resize-observer-polyfill](https://github.com/que-etc/resize-observer-polyfill)
- [whatwg-fetch](https://github.com/github/fetch)
If youre using a bundler, you need import these before Uppy:
```js
import 'core-js';
import 'whatwg-fetch';
import 'abortcontroller-polyfill/dist/polyfill-patch-fetch';
// Order matters here: AbortController needs fetch, which needs Promise (provided by core-js).
import 'md-gum-polyfill';
import ResizeObserver from 'resize-observer-polyfill';
window.ResizeObserver ??= ResizeObserver;
export { default } from '@uppy/core';
export * from '@uppy/core';
```
If youre using Uppy from a CDN, we now provide two bundles: one for up-to-date
browsers that do not include polyfills and use modern syntax, and one for legacy
browsers. When migrating, be mindful about the types of browsers you want to
support:
```html
<!-- Modern browsers (recommended) -->
<script src="https://releases.transloadit.com/uppy/v3.17.0/uppy.min.js"></script>
<!-- Legacy browsers (IE11+) -->
<script
nomodule
src="https://releases.transloadit.com/uppy/v3.17.0/uppy.legacy.min.js"
></script>
<script type="module">
import 'https://releases.transloadit.com/uppy/v3.17.0/uppy.min.js';
</script>
```
Please note that while you may be able to get 2.0 to work in IE11 this way, we
do not officially support it anymore.
### Use `BasePlugin` or `UIPlugin` instead of `Plugin`
[`@uppy/core`][core] used to provide a `Plugin` class for creating plugins. This
was used for any official plugin, but also for users who want to create their
own custom plugin. But, `Plugin` always came bundled with Preact, even if the
plugin itself didnt add any UI elements.
`Plugin` has been replaced with `BasePlugin` and `UIPlugin`. `BasePlugin` is the
minimum you need to create a plugin and `UIPlugin` adds Preact for rendering
user interfaces.
You can import them from [`@uppy/core`][core]:
```js
import { BasePlugin, UIPlugin } from '@uppy/core';
```
**Note:** some bundlers will include `UIPlugin` (and thus Preact) if you import
from `@uppy/core`. To make sure this does not happen, you can import `Uppy` and
`BasePlugin` directly:
```js
import Uppy from '@uppy/core/lib/Uppy.js';
import BasePlugin from '@uppy/core/lib/BasePlugin.js';
```
### Use the latest Preact for your Uppy plugins
Official plugins have already been upgraded. If you are using any custom
plugins, upgrade Preact to the latest version. At the time of writing this is
`10.5.13`.
### Set plugin titles from locales
Titles for plugins used to be set with the `title` property in the plugin
options, but all other strings are set in `locale`. This has now been aligned.
You should set your plugin title from the `locale` property.
Before
```js
import Webcam from '@uppy/webcam';
uppy.use(Webcam, {
title: 'Some title',
});
```
After
```js
import Webcam from '@uppy/webcam';
uppy.use(Webcam, {
locale: {
strings: {
title: 'Some title',
},
},
});
```
### Initialize Uppy with the `new` keyword
The default export `Uppy` is no longer callable as a function. This means you
construct the `Uppy` instance using the `new` keyword.
```js
import Uppy from '@uppy/core';
const uppy = new Uppy(); // correct.
const otherUppy = Uppy(); // incorrect, will throw.
```
### Rename `allowMultipleUploads` to `allowMultipleUploadBatches`
[`allowMultipleUploadBatches`](/docs/uppy/#allowmultipleuploadbatches) means
allowing several calls to [`.upload()`](/docs/uppy/#upload), in other words, a
user can add more files after already having uploaded some.
<!--retext-simplify ignore multiple-->
We have renamed this to be more intention revealing that this is about uploads,
and not whether a user can choose multiple files for one upload.
```js
const uppy = new Uppy({
allowMultipleUploadBatches: true,
});
```
### New default limits for [`@uppy/xhr-upload`][xhr] and [`@uppy/tus`][tus]
The default limit has been changed from `0` to `5`. Setting this to `0` means no
limit on concurrent uploads.
You can change the limit on the Tus and XHR plugin options.
```js
uppy.use(Tus, {
// ...
limit: 10,
});
```
```js
uppy.use(XHRUpload, {
// ...
limit: 10,
});
```
### TypeScript changes
Uppy used to have loose types by default and strict types as an opt-in. The
default export was a function that returned the `Uppy` class, and the types came
bundled with the default export (`Uppy.SomeType`).
```ts
import Uppy from '@uppy/core';
import Tus from '@uppy/tus';
const uppy = Uppy<Uppy.StrictTypes>();
uppy.use(Tus, {
invalidOption: null, // this will make the compilation fail!
});
```
Uppy is now strictly typed by default and loose types have been removed.
```ts
// ...
const uppy = new Uppy();
uppy.use(Tus, {
invalidOption: null, // this will make the compilation fail!
});
```
Uppy types are now individual exports and should be imported separately.
<!-- eslint-disable @typescript-eslint/no-unused-vars -->
```ts
import type { PluginOptions, UIPlugin, PluginTarget } from '@uppy/core';
```
#### Event types
[`@uppy/core`][core] provides an [`.on`](/docs/uppy/#uppy-on-39-event-39-action)
method to listen to [events](/docs/uppy/#Events). The types for these events
were loose and allowed for invalid events to be passed, such as
`uppy.on('upload-errrOOOoooOOOOOrrrr')`.
<!-- eslint-disable @typescript-eslint/no-unused-vars -->
```ts
// Before:
type Meta = { myCustomMetadata: string };
// Invalid event
uppy.on<Meta>('upload-errrOOOoooOOOOOrrrr', () => {
// ...
});
// After:
// Normal event signature
uppy.on('complete', (result) => {
const successResults = result.successful;
});
// Custom signature
type Meta = { myCustomMetadata: string };
// Notice how the custom type has now become the second argument
uppy.on<'complete', Meta>('complete', (result) => {
// The passed type is now merged into the `meta` types.
const meta = result.successful[0].meta.myCustomMetadata;
});
```
Plugins that add their own events can merge with existing ones in `@uppy/core`
with `declare module '@uppy/core' { ... }`. This is a TypeScript pattern called
[module augmentation](https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation).
For instance, when using [`@uppy/dashboard`][dashboard]:
<!-- eslint-disable @typescript-eslint/no-unused-vars -->
```ts
uppy.on('dashboard:file-edit-start', (file) => {
const fileName = file.name;
});
```
### Changes to pre-signing URLs for [`@uppy/aws-s3-multipart`][aws-s3-multipart]
See the Uppy 2.0.0 announcement post about the batch
[pre-signing URLs change](/blog/2021/08/2.0/#Batch-pre-signing-URLs-for-AWS-S3-Multipart).
`prepareUploadPart` has been renamed to
[`prepareUploadParts`](/docs/aws-s3-multipart/#prepareUploadParts-file-partData)
(plural). See the documentation link on how to use this function.
### Removed the `.run` method from [`@uppy/core`][core]
The `.run` method on the `Uppy` instance has been removed. This method was
already obsolete and only logged a warning. As of this major version, it no
longer exists.
### Removed `resume` and `removeFingerprintOnSuccess` options from [`@uppy/tus`][tus]
Tus will now by default try to resume uploads if the upload has been started in
the past.
This also means tus will store some data in localStorage for each upload, which
will automatically be removed on success. Making `removeFingerprintOnSuccess`
obsolete too.
### Thats it!
Uppy 1.0 will continue to receive bug fixes for three more months (until
<time datetime="2021-12-01">1 December 2021</time>), security fixes for one more
year (until <time datetime="2022-09-01">1 September 2022</time>), but no more
new features after today. Exceptions are unlikely, but _can_ be made to
accommodate those with commercial support contracts, for example.
We hope youll waste no time in taking Uppy 2.0 out for a walk. When you do,
please let us know what you thought of it on
[Reddit](https://www.reddit.com/r/javascript/comments/penbr7/uppy_file_uploader_20_smaller_and_faster_modular/),
[HN](https://news.ycombinator.com/item?id=28359287), ProductHunt, or
[Twitter](https://twitter.com/uppy_io/status/1432399270846603264). Were howling
at the moon to hear from you!
## Migrate from Companion 1.x to 2.x
### Prerequisite
Since v2, you now need to be running `node.js >= v10.20.1` to use Companion.
### ProviderOptions
In v2 the `google` and `microsoft` [providerOptions](/docs/companion/#Options)
have been changed to `drive` and `onedrive` respectively.
### OAuth Redirect URIs
On your Providers respective developer platforms, the OAuth redirect URIs that
you should supply has now changed from:
`http(s)://$COMPANION_HOST_NAME/connect/$AUTH_PROVIDER/callback` in v1
to:
`http(s)://$COMPANION_HOST_NAME/$PROVIDER_NAME/redirect` in v2
#### New Redirect URIs
<div class="table-responsive">
| Provider | New Redirect URI |
| ------------- | ---------------------------------------------------- |
| Dropbox | `https://$COMPANION_HOST_NAME/dropbox/redirect` |
| Google Drive | `https://$COMPANION_HOST_NAME/drive/redirect` |
| Google Photos | `https://$COMPANION_HOST_NAME/googlephotos/redirect` |
| OneDrive | `https://$COMPANION_HOST_NAME/onedrive/redirect` |
| Box | `https://$YOUR_COMPANION_HOST_NAME/box/redirect` |
| Facebook | `https://$COMPANION_HOST_NAME/facebook/redirect` |
| Instagram | `https://$COMPANION_HOST_NAME/instagram/redirect` |
</div>
<!-- definitions -->
[core]: /docs/uppy/
[xhr]: /docs/xhr-upload/
[dashboard]: /docs/dashboard/
[aws-s3-multipart]: /docs/aws-s3-multipart/
[tus]: /docs/tus/

File diff suppressed because it is too large Load diff

View file

@ -1,4 +0,0 @@
{
"label": "Presets",
"position": 9
}

View file

@ -1,179 +0,0 @@
---
sidebar_position: 1
slug: /remote-sources
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import UppyCdnExample from '/src/components/UppyCdnExample';
# Remote sources
`@uppy/remote-sources` is a preset plugin (meaning it bundles and sets up other
plugins) to add all the available remote sources, such Instagram, Google Drive,
Dropbox, and others to Uppy Dashboard in one package.
:::note
Remote Sources requires Dashboard and automatically installs all its plugins to
it.
:::
## When should I use it?
`@uppy/remote-sources` aims to simplify the setup for adding Companion plugins,
when you want to share the configuration between plugins. If you want your users
to upload files from any of the remote sources that Uppy offers, this plugin is
recommended.
A [Companion](/docs/companion) instance is required for the Remote Sources
plugin to work. Companion handles authentication with the remote services (such
as Facebook, Dropbox, etc.), downloads the files, and uploads them to the
destination. This saves the user bandwidth, especially helpful if they are on a
mobile connection.
You can self-host Companion or get a hosted version with any
[Transloadit plan](https://transloadit.com/pricing/).
## Install
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/remote-sources
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/remote-sources
```
</TabItem>
<TabItem value="cdn" label="CDN">
<UppyCdnExample>
{`
import { RemoteSources } from "{{UPPY_JS_URL}}"
const RemoteSources = new Uppy().use(RemoteSources)
`}
</UppyCdnExample>
</TabItem>
</Tabs>
## Use
```js {10} showLineNumbers
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import RemoteSources from '@uppy/remote-sources';
import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';
new Uppy();
.use(Dashboard);
.use(RemoteSources, { companionUrl: 'https://your-companion-url' });
```
### Use with Transloadit
```js
import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
import RemoteSources from '@uppy/remote-sources';
uppy.use(RemoteSources, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
});
```
You may also hit rate limits, because the OAuth application is shared between
everyone using Transloadit.
To solve that, you can use your own OAuth keys with Transloadits hosted
Companion servers by using Transloadit Template Credentials. [Create a Template
Credential][template-credentials] on the Transloadit site. Select “Companion
OAuth” for the service, and enter the key and secret for the provider you want
to use. Then you can pass the name of the new credentials to that provider:
```js
import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
import RemoteSources from '@uppy/remote-sources';
uppy.use(RemoteSources, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
companionKeysParams: {
GoogleDrive: { key: '...', credentialsName: '...' },
Dropbox: { key: '...', credentialsName: '...' },
// ...etc
},
});
```
## API
### Options
#### `id`
A unique identifier for this plugin (`string`, default: `RemoteSources`).
#### `sources`
List of remote sources that will be enabled (`array`, default:
`['Box', 'Dropbox', 'Facebook', 'GoogleDrive','Instagram', 'OneDrive', 'Unsplash', 'Url', 'Zoom']`).
You dont need to specify them manually or change them, but if you want to alter
the order in which they appear in the Dashboard, or disable some sources, this
option is for you.
```js
uppy.use(RemoteSources, {
companionUrl: 'https://your-companion-url',
sources: ['Instagram', 'GoogleDrive', 'Unsplash', 'Url'],
});
```
#### `companionUrl`
URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
#### `companionHeaders`
Custom headers that should be sent along to [Companion](/docs/companion) on
every request (`object`, default: `{}`).
#### `companionAllowedHosts`
The valid and authorized URL(s)/URL pattern(s) from which OAuth responses should
be accepted (`string | RegExp | Array<string | RegExp>`, Default:
`companionUrl`).
This value can be a `string`, a `RegExp` object, or an array of both.
This is useful when you have your [Companion](/docs/companion) running on
several hosts. Otherwise, the default value, which is `companionUrl`, should do
fine.
#### `companionCookiesRule`
This option correlates to the [`Request.credentials` value][], which tells the plugin
whether to send cookies to [Companion](/docs/companion) (`string`, default: `same-origin`).
#### `target`
DOM element, CSS selector, or plugin to place the drag and drop area into
(`string`, `Element`, `Function`, or `UIPlugin`, default:
[`Dashboard`](/docs/dashboard)).
[`request.credentials` value]:
https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials
[template-credentials]:
https://transloadit.com/docs/#how-to-create-template-credentials

View file

@ -1,58 +0,0 @@
---
sidebar_position: 1
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import UppyCdnExample from '/src/components/UppyCdnExample';
import { QuickStartLinks } from '../src/components/QuickStartLinks/QuickStartLinks.tsx';
# Quick start
Uppy is a sleek, modular JavaScript file uploader that integrates seamlessly
with any application. Its fast, has a comprehensible API and lets you worry
about more important problems than building a file uploader.
:::tip
You can take Uppy for a walk inside StackBlitz with a
[minimal drag & drop](https://stackblitz.com/edit/vitejs-vite-yzbujq?file=main.js/g)
experience or a
[full featured dashboard](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
:::
<QuickStartLinks
items={[
{
name: 'I want a full featured, extendable UI',
description: 'Learn more about the Dashboard',
link: '/docs/dashboard',
},
{
name: 'I want a minimal drag & drop UI',
description: 'We have a lightweight plugin for that',
link: '/docs/drag-drop',
},
{
name: 'Which uploader do I need?',
description: 'Choosing the uploader you need',
link: '/docs/guides/choosing-uploader',
},
{
name: 'I want to add files from remote sources',
description: 'Such as Google Drive, Dropbox, Instagram',
link: '/docs/companion',
},
{
name: 'Id like a project example',
description: 'Try out one of our extensive example projects',
link: 'https://github.com/transloadit/uppy/tree/main/examples',
},
{
name: 'I have a question',
description: 'Our community forum is there to help',
link: 'https://community.transloadit.com/',
},
]}
/>

View file

@ -1,4 +0,0 @@
{
"label": "Sources",
"position": 6
}

View file

@ -1,133 +0,0 @@
---
sidebar_position: 3
slug: /audio
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import UppyCdnExample from '/src/components/UppyCdnExample';
# Audio
The `@uppy/audio` plugin lets you record audio using a built-in or external
microphone, or any other audio device, on desktop and mobile. The UI shows real
time sound wavelength when recording.
:::tip
[Try out the live example](/examples) or take it for a spin in
[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
:::
## When should I use this?
When you want users to record audio on their computer.
## Install
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/audio
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/audio
```
</TabItem>
<TabItem value="cdn" label="CDN">
<UppyCdnExample>
{`
import { Uppy, Dashboard, Audio } from "{{UPPY_JS_URL}}"
const uppy = new Uppy()
uppy.use(Dashboard, { inline: true, target: 'body' })
uppy.use(Audio)
`}
</UppyCdnExample>
</TabItem>
</Tabs>
## Use
```js {3,7,11} showLineNumbers
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import Audio from '@uppy/audio';
import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';
import '@uppy/audio/dist/style.min.css';
new Uppy().use(Dashboard, { inline: true, target: 'body' }).use(Audio);
```
### API
### Options
#### `id`
A unique identifier for this plugin (`string`, default: `'audio'`).
#### `title`
Configures the title / name shown in the UI, for instance, on Dashboard tabs
(`string`, default: `'Audio'`).
#### `target`
DOM element, CSS selector, or plugin to place the drag and drop area into
(`string`, `Element`, `Function`, or `UIPlugin`, default:
[`Dashboard`](/docs/dashboard)).
#### `showAudioSourceDropdown`
Configures whether to show a dropdown to select audio device (`boolean`,
default: `false`).
#### `locale`
```js
export default {
strings: {
pluginNameAudio: 'Audio',
// Used as the label for the button that starts an audio recording.
// This is not visibly rendered but is picked up by screen readers.
startAudioRecording: 'Begin audio recording',
// Used as the label for the button that stops an audio recording.
// This is not visibly rendered but is picked up by screen readers.
stopAudioRecording: 'Stop audio recording',
// Title on the “allow access” screen
allowAudioAccessTitle: 'Please allow access to your microphone',
// Description on the “allow access” screen
allowAudioAccessDescription:
'In order to record audio, please allow microphone access for this site.',
// Title on the “device not available” screen
noAudioTitle: 'Microphone Not Available',
// Description on the “device not available” screen
noAudioDescription:
'In order to record audio, please connect a microphone or another audio input device',
// Message about file size will be shown in an Informer bubble
recordingStoppedMaxSize:
'Recording stopped because the file size is about to exceed the limit',
// Used as the label for the counter that shows recording length (`1:25`).
// This is not visibly rendered but is picked up by screen readers.
recordingLength: 'Recording length %{recording_length}',
// Used as the label for the submit checkmark button.
// This is not visibly rendered but is picked up by screen readers.
submitRecordedFile: 'Submit recorded file',
// Used as the label for the discard cross button.
// This is not visibly rendered but is picked up by screen readers.
discardRecordedFile: 'Discard recorded file',
},
};
```

View file

@ -1,5 +0,0 @@
{
"label": "Companion plugins",
"position": 3,
"collapsed": false
}

View file

@ -1,226 +0,0 @@
---
slug: /box
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import UppyCdnExample from '/src/components/UppyCdnExample';
# Box
The `@uppy/box` plugin lets users import files from their
[Box](https://www.box.com/en-nl/home) account.
:::tip
[Try out the live example](/examples) or take it for a spin in
[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
:::
## When should I use this?
When you want to let users import files from their
[Box](https://www.box.com/en-nl/home) account.
A [Companion](/docs/companion) instance is required for the Box plugin to work.
Companion handles authentication with Box, downloads the files, and uploads them
to the destination. This saves the user bandwidth, especially helpful if they
are on a mobile connection.
You can self-host Companion or get a hosted version with any
[Transloadit plan](https://transloadit.com/pricing/).
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/box
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/box
```
</TabItem>
<TabItem value="cdn" label="CDN">
<UppyCdnExample>
{`
import { Uppy, Box } from "{{UPPY_JS_URL}}"
const uppy = new Uppy()
uppy.use(Box, {
// Options
})
`}
</UppyCdnExample>
</TabItem>
</Tabs>
## Use
Using Box requires setup in both Uppy and Companion.
### Use in Uppy
```js {10-13} showLineNumbers
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import Box from '@uppy/box';
import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';
new Uppy()
.use(Dashboard, { inline: true, target: '#dashboard' })
.use(Box, { companionUrl: 'https://your-companion.com' });
```
### Use with Transloadit
```js
import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
import Box from '@uppy/box';
uppy.use(Box, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
});
```
You may also hit rate limits, because the OAuth application is shared between
everyone using Transloadit.
To solve that, you can use your own OAuth keys with Transloadits hosted
Companion servers by using Transloadit Template Credentials. [Create a Template
Credential][template-credentials] on the Transloadit site. Select “Companion
OAuth” for the service, and enter the key and secret for the provider you want
to use. Then you can pass the name of the new credentials to that provider:
```js
import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
import Box from '@uppy/box';
uppy.use(Box, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
companionKeysParams: {
key: 'YOUR_TRANSLOADIT_API_KEY',
credentialsName: 'my_companion_dropbox_creds',
},
});
```
### Use in Companion
You can create a Box App on the
[Box Developers site](https://app.box.com/developers/console).
Things to note:
- Choose `Custom App` and select the `User Authentication (OAuth 2.0)` app type.
- You must enable full write access, or you will get
[403 when downloading files](https://support.box.com/hc/en-us/community/posts/360049195613-403-error-while-file-download-API-Call)
Youll be redirected to the app page. This page lists the client ID (app key)
and client secret (app secret), which you should use to configure Companion.
The app page has a `"Redirect URIs"` field. Here, add:
```
https://$YOUR_COMPANION_HOST_NAME/box/redirect
```
If you are using Transloadit hosted Companion:
```
https://api2.transloadit.com/companion/box/redirect
```
You can only use the integration with your own account initially. Make sure to
apply for production status on the app page before you publish your app, or your
users will not be able to sign in!
Configure the Box key and secret. With the standalone Companion server, specify
environment variables:
```shell
export COMPANION_BOX_KEY="Box API key"
export COMPANION_BOX_SECRET="Box API secret"
```
When using the Companion Node.js API, configure these options:
```js
companion.app({
providerOptions: {
box: {
key: 'Box API key',
secret: 'Box API secret',
},
},
});
```
## API
### Options
#### `id`
A unique identifier for this plugin (`string`, default: `'Box'`).
#### `title`
Title / name shown in the UI, such as Dashboard tabs (`string`, default:
`'Box'`).
#### `target`
DOM element, CSS selector, or plugin to place the drag and drop area into
(`string`, `Element`, `Function`, or `UIPlugin`, default:
[`Dashboard`](/docs/dashboard)).
#### `companionUrl`
URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
#### `companionHeaders`
Custom headers that should be sent along to [Companion](/docs/companion) on
every request (`Object`, default: `{}`).
#### `companionAllowedHosts`
The valid and authorised URL(s) from which OAuth responses should be accepted
(`string` or `RegExp` or `Array`, default: `companionUrl`).
This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
useful when you have your [Companion](/docs/companion) running on several hosts.
Otherwise, the default value should do fine.
#### `companionCookiesRule`
This option correlates to the
[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
(`string`, default: `'same-origin'`).
This tells the plugin whether to send cookies to [Companion](/docs/companion).
#### `locale`
```js
export default {
strings: {
pluginNameBox: 'Box',
},
};
```
[template-credentials]:
https://transloadit.com/docs/#how-to-create-template-credentials

View file

@ -1,226 +0,0 @@
---
slug: /dropbox
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import UppyCdnExample from '/src/components/UppyCdnExample';
# Dropbox
The `@uppy/dropbox` plugin lets users import files from their
[Dropbox](https://www.dropbox.com) account.
:::tip
[Try out the live example](/examples) or take it for a spin in
[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
:::
## When should I use this?
When you want to let users import files from their
[Dropbox](https://www.dropbox.com) account.
A [Companion](/docs/companion) instance is required for the Dropbox plugin to
work. Companion handles authentication with Dropbox, downloads the files, and
uploads them to the destination. This saves the user bandwidth, especially
helpful if they are on a mobile connection.
You can self-host Companion or get a hosted version with any
[Transloadit plan](https://transloadit.com/pricing/).
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/dropbox
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/dropbox
```
</TabItem>
<TabItem value="cdn" label="CDN">
<UppyCdnExample>
{`
import { Uppy, Dropbox } from "{{UPPY_JS_URL}}"
const uppy = new Uppy()
uppy.use(Dropbox, {
// Options
})
`}
</UppyCdnExample>
</TabItem>
</Tabs>
## Use
Using Dropbox requires setup in both Uppy and Companion.
### Use in Uppy
```js {10-13} showLineNumbers
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import Dropbox from '@uppy/dropbox';
import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';
new Uppy()
.use(Dashboard, { inline: true, target: '#dashboard' })
.use(Dropbox, { companionUrl: 'https://your-companion.com' });
```
### Use with Transloadit
```js
import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
import Dropbox from '@uppy/dropbox';
uppy.use(Dropbox, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
});
```
You may also hit rate limits, because the OAuth application is shared between
everyone using Transloadit.
To solve that, you can use your own OAuth keys with Transloadits hosted
Companion servers by using Transloadit Template Credentials. [Create a Template
Credential][template-credentials] on the Transloadit site. Select “Companion
OAuth” for the service, and enter the key and secret for the provider you want
to use. Then you can pass the name of the new credentials to that provider:
```js
import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
import Dropbox from '@uppy/dropbox';
uppy.use(Dropbox, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
companionKeysParams: {
key: 'YOUR_TRANSLOADIT_API_KEY',
credentialsName: 'my_companion_dropbox_creds',
},
});
```
### Use in Companion
You can create a Dropbox App on the
[Dropbox Developers site](https://www.dropbox.com/developers/apps/create).
Things to note:
- Choose the “Dropbox API”, not the business variant.
- Typically youll want “Full Dropbox” access, unless you are absolutely certain
that you need the other one.
Youll be redirected to the app page. This page lists the app key and app
secret, which you should use to configure Companion as shown above.
The app page has a “Redirect URIs” field. Here, add:
```
https://$YOUR_COMPANION_HOST_NAME/dropbox/redirect
```
If you are using Transloadit hosted Companion:
```
https://api2.transloadit.com/companion/dropbox/redirect
```
You can only use the integration with your own account initially. Make sure to
apply for production status on the app page before you publish your app, or your
users will not be able to sign in!
Configure the Dropbox key and secret. With the standalone Companion server,
specify environment variables:
```shell
export COMPANION_DROPBOX_KEY="Dropbox API key"
export COMPANION_DROPBOX_SECRET="Dropbox API secret"
```
When using the Companion Node.js API, configure these options:
```js
companion.app({
providerOptions: {
dropbox: {
key: 'Dropbox API key',
secret: 'Dropbox API secret',
},
},
});
```
## API
### Options
#### `id`
A unique identifier for this plugin (`string`, default: `'Dropbox'`).
#### `title`
Title / name shown in the UI, such as Dashboard tabs (`string`, default:
`'Dropbox'`).
#### `target`
DOM element, CSS selector, or plugin to place the drag and drop area into
(`string`, `Element`, `Function`, or `UIPlugin`, default:
[`Dashboard`](/docs/dashboard)).
#### `companionUrl`
URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
#### `companionHeaders`
Custom headers that should be sent along to [Companion](/docs/companion) on
every request (`Object`, default: `{}`).
#### `companionAllowedHosts`
The valid and authorised URL(s) from which OAuth responses should be accepted
(`string` or `RegExp` or `Array`, default: `companionUrl`).
This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
useful when you have your [Companion](/docs/companion) running on several hosts.
Otherwise, the default value should do fine.
#### `companionCookiesRule`
This option correlates to the
[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
(`string`, default: `'same-origin'`).
This tells the plugin whether to send cookies to [Companion](/docs/companion).
#### `locale`
```js
export default {
strings: {
pluginNameDropbox: 'Dropbox',
},
};
```
[template-credentials]:
https://transloadit.com/docs/#how-to-create-template-credentials

View file

@ -1,223 +0,0 @@
---
slug: /facebook
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import UppyCdnExample from '/src/components/UppyCdnExample';
# Facebook
The `@uppy/facebook` plugin lets users import files from their
[Facebook](https://www.facebook.com) account.
:::tip
[Try out the live example](/examples) or take it for a spin in
[StackBlitz](https://stackblitz.com/edit/vitejs-vite-zaqyaf?file=main.js).
:::
## When should I use this?
When you want to let users import files from their
[Facebook](https://www.facebook.com) account.
A [Companion](/docs/companion) instance is required for the Facebook plugin to
work. Companion handles authentication with Facebook, downloads the files, and
uploads them to the destination. This saves the user bandwidth, especially
helpful if they are on a mobile connection.
You can self-host Companion or get a hosted version with any
[Transloadit plan](https://transloadit.com/pricing/).
<Tabs>
<TabItem value="npm" label="NPM" default>
```shell
npm install @uppy/facebook
```
</TabItem>
<TabItem value="yarn" label="Yarn">
```shell
yarn add @uppy/facebook
```
</TabItem>
<TabItem value="cdn" label="CDN">
<UppyCdnExample>
{`
import { Uppy, Facebook } from "{{UPPY_JS_URL}}"
const uppy = new Uppy()
uppy.use(Facebook, {
// Options
})
`}
</UppyCdnExample>
</TabItem>
</Tabs>
## Use
Using Facebook requires setup in both Uppy and Companion.
### Use in Uppy
```js {10-13} showLineNumbers
import Uppy from '@uppy/core';
import Dashboard from '@uppy/dashboard';
import Facebook from '@uppy/facebook';
import '@uppy/core/dist/style.min.css';
import '@uppy/dashboard/dist/style.min.css';
new Uppy()
.use(Dashboard, { inline: true, target: '#dashboard' })
.use(Facebook, { companionUrl: 'https://your-companion.com' });
```
### Use with Transloadit
```js
import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
import Facebook from '@uppy/facebook';
uppy.use(Facebook, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
});
```
You may also hit rate limits, because the OAuth application is shared between
everyone using Transloadit.
To solve that, you can use your own OAuth keys with Transloadits hosted
Companion servers by using Transloadit Template Credentials. [Create a Template
Credential][template-credentials] on the Transloadit site. Select “Companion
OAuth” for the service, and enter the key and secret for the provider you want
to use. Then you can pass the name of the new credentials to that provider:
```js
import { COMPANION_URL, COMPANION_ALLOWED_HOSTS } from '@uppy/transloadit';
import Facebook from '@uppy/facebook';
uppy.use(Facebook, {
companionUrl: COMPANION_URL,
companionAllowedHosts: COMPANION_ALLOWED_HOSTS,
companionKeysParams: {
key: 'YOUR_TRANSLOADIT_API_KEY',
credentialsName: 'my_companion_dropbox_creds',
},
});
```
### Use in Companion
You can create a Facebook App on the
[Facebook Developers site](https://developers.facebook.com/apps).
The app page has a “Redirect URIs” field. Here, add:
```
https://$YOUR_COMPANION_HOST_NAME/facebook/redirect
```
If you are using Transloadit hosted Companion:
```
https://api2.transloadit.com/companion/facebook/redirect
```
You can only use the integration with your own account initially. Make sure to
apply for production status on the app page before you publish your app, or your
users will not be able to sign in!
You need to set up OAuth in your Facebook app for Companion to be able to
connect to users Facebook accounts. You have to enable “Advanced Access” for
the `user_photos` permission. A precondition of that is “Business Verification”
which involves setting up a Meta Business Account and submitting documents to
prove business ownership.
Configure the Facebook key and secret. With the standalone Companion server,
specify environment variables:
```shell
export COMPANION_FACEBOOK_KEY="Facebook API key"
export COMPANION_FACEBOOK_SECRET="Facebook API secret"
```
When using the Companion Node.js API, configure these options:
```js
companion.app({
providerOptions: {
facebook: {
key: 'Facebook API key',
secret: 'Facebook API secret',
},
},
});
```
## API
### Options
#### `id`
A unique identifier for this plugin (`string`, default: `'Facebook'`).
#### `title`
Title / name shown in the UI, such as Dashboard tabs (`string`, default:
`'Facebook'`).
#### `target`
DOM element, CSS selector, or plugin to place the drag and drop area into
(`string`, `Element`, `Function`, or `UIPlugin`, default:
[`Dashboard`](/docs/dashboard)).
#### `companionUrl`
URL to a [Companion](/docs/companion) instance (`string`, default: `null`).
#### `companionHeaders`
Custom headers that should be sent along to [Companion](/docs/companion) on
every request (`Object`, default: `{}`).
#### `companionAllowedHosts`
The valid and authorised URL(s) from which OAuth responses should be accepted
(`string` or `RegExp` or `Array`, default: `companionUrl`).
This value can be a `string`, a `RegExp` pattern, or an `Array` of both. This is
useful when you have your [Companion](/docs/companion) running on several hosts.
Otherwise, the default value should do fine.
#### `companionCookiesRule`
This option correlates to the
[RequestCredentials value](https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials)
(`string`, default: `'same-origin'`).
This tells the plugin whether to send cookies to [Companion](/docs/companion).
#### `locale`
```js
export default {
strings: {
pluginNameFacebook: 'Facebook',
},
};
```
[template-credentials]:
https://transloadit.com/docs/#how-to-create-template-credentials

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