mirror of
https://github.com/transloadit/uppy.git
synced 2026-07-17 16:50:22 +00:00
Merge branch 'main' into restore/pr-6224
This commit is contained in:
commit
0a423a45b9
409 changed files with 14096 additions and 11418 deletions
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
"@uppy/xhr-upload": minor
|
||||
"@uppy/utils": minor
|
||||
---
|
||||
|
||||
Introduce modern, minimal TaskQueue to replace RateLimitedQueue
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@uppy/transloadit": patch
|
||||
---
|
||||
|
||||
Add type re-export for `AssemblyInstructionsInput`
|
||||
45
.changeset/curvy-feet-chew.md
Normal file
45
.changeset/curvy-feet-chew.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
"@uppy/angular": patch
|
||||
"@uppy/google-photos-picker": patch
|
||||
"@uppy/google-drive-picker": patch
|
||||
"@uppy/thumbnail-generator": patch
|
||||
"@uppy/companion-client": patch
|
||||
"@uppy/golden-retriever": patch
|
||||
"@uppy/image-generator": patch
|
||||
"@uppy/provider-views": patch
|
||||
"@uppy/remote-sources": patch
|
||||
"@uppy/screen-capture": patch
|
||||
"@uppy/store-default": patch
|
||||
"@uppy/google-drive": patch
|
||||
"@uppy/image-editor": patch
|
||||
"@uppy/transloadit": patch
|
||||
"@uppy/components": patch
|
||||
"@uppy/compressor": patch
|
||||
"@uppy/status-bar": patch
|
||||
"@uppy/xhr-upload": patch
|
||||
"@uppy/companion": patch
|
||||
"@uppy/dashboard": patch
|
||||
"@uppy/drag-drop": patch
|
||||
"@uppy/facebook": patch
|
||||
"@uppy/onedrive": patch
|
||||
"@uppy/unsplash": patch
|
||||
"@uppy/dropbox": patch
|
||||
"@uppy/locales": patch
|
||||
"@uppy/aws-s3": patch
|
||||
"@uppy/svelte": patch
|
||||
"@uppy/webcam": patch
|
||||
"@uppy/webdav": patch
|
||||
"@uppy/audio": patch
|
||||
"@uppy/react": patch
|
||||
"@uppy/utils": patch
|
||||
"@uppy/core": patch
|
||||
"@uppy/form": patch
|
||||
"@uppy/zoom": patch
|
||||
"@uppy/box": patch
|
||||
"@uppy/tus": patch
|
||||
"@uppy/url": patch
|
||||
"@uppy/vue": patch
|
||||
"uppy": patch
|
||||
---
|
||||
|
||||
Bump shared runtime dependencies (preact, nanoid, lodash, classnames, shallow-equal, pretty-bytes, p-queue, tus-js-client, @transloadit/types @transloadit/prettier-bytes v1, is-mobile, exifr, compressorjs, rxjs, tslib). Also includes type-only fixes in `@uppy/companion`'s `jwt.ts` and `request.ts` to track `@types/jsonwebtoken` v9 and `@types/node`.
|
||||
5
.changeset/few-plants-pump.md
Normal file
5
.changeset/few-plants-pump.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/dashboard": patch
|
||||
---
|
||||
|
||||
fix "My Device" button in dashboard, it now respects the fileManagerSelectionType.
|
||||
9
.changeset/gold-rice-jog.md
Normal file
9
.changeset/gold-rice-jog.md
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
"@uppy/components": major
|
||||
"@uppy/companion": major
|
||||
"uppy": major
|
||||
"@uppy/transloadit": minor
|
||||
"@uppy/locales": minor
|
||||
---
|
||||
|
||||
Remove @uppy/instagram references from all the packages
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@uppy/companion": patch
|
||||
---
|
||||
|
||||
Fix bug with 429 not returning JSON response with message
|
||||
5
.changeset/happy-timers-kick.md
Normal file
5
.changeset/happy-timers-kick.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/aws-s3": patch
|
||||
---
|
||||
|
||||
Improve type
|
||||
5
.changeset/lazy-berries-brake.md
Normal file
5
.changeset/lazy-berries-brake.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/companion-client": patch
|
||||
---
|
||||
|
||||
uploadRemoteFile() now queues token request and websocket request as a single job in the request queue.
|
||||
5
.changeset/legal-drinks-marry.md
Normal file
5
.changeset/legal-drinks-marry.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/transloadit": minor
|
||||
---
|
||||
|
||||
Add assemblyStatus and lastAssemblyStatus to transloadit's plugin state
|
||||
8
.changeset/mighty-deers-sin.md
Normal file
8
.changeset/mighty-deers-sin.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
"@uppy/companion-client": major
|
||||
"@uppy/companion": major
|
||||
---
|
||||
|
||||
Send token using websocket instead of window.opener.
|
||||
Breaking in `@uppy/companion-client` because it needs newest version of Companion in order to work.
|
||||
Breaking in `@uppy/companion` because `companion.socket()` now requires `companionOptions` to be passed as the second argument.
|
||||
5
.changeset/proud-spies-train.md
Normal file
5
.changeset/proud-spies-train.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/transloadit": patch
|
||||
---
|
||||
|
||||
emit updated AssemblyState in 'transloadit:assembly-cancelled' event
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
---
|
||||
"@uppy/companion": patch
|
||||
---
|
||||
|
||||
upgrade @aws-sdk/ deps in @uppy/companion
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
---
|
||||
"@uppy/transloadit": patch
|
||||
"uppy": patch
|
||||
---
|
||||
|
||||
remove monkey patch from uppy bundle package
|
||||
5
.changeset/smooth-chefs-occur.md
Normal file
5
.changeset/smooth-chefs-occur.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/companion": major
|
||||
---
|
||||
|
||||
Upgrade to express 5 - Companion no longer works when used as a middleware with Express 4.
|
||||
5
.changeset/three-cobras-lick.md
Normal file
5
.changeset/three-cobras-lick.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
"@uppy/companion": patch
|
||||
---
|
||||
|
||||
Port Companion to TypeScript. Not really a breaking change but there could be some unexpected breakage.
|
||||
|
|
@ -44,9 +44,6 @@ COMPANION_DROPBOX_SECRET=***
|
|||
COMPANION_GOOGLE_KEY=***
|
||||
COMPANION_GOOGLE_SECRET=***
|
||||
|
||||
COMPANION_INSTAGRAM_KEY=***
|
||||
COMPANION_INSTAGRAM_SECRET=***
|
||||
|
||||
COMPANION_FACEBOOK_KEY=***
|
||||
COMPANION_FACEBOOK_SECRET=***
|
||||
|
||||
|
|
|
|||
4
.github/CONTRIBUTING.md
vendored
4
.github/CONTRIBUTING.md
vendored
|
|
@ -57,7 +57,7 @@ This is not needed for changing existing components.
|
|||
## Companion
|
||||
|
||||
If you’d like to work on features that the basic development version of Uppy
|
||||
doesn’t support, such as Uppy integrations with Instagram/Google Drive/Facebook
|
||||
doesn’t support, such as Uppy integrations with Google Drive/Facebook
|
||||
etc., you need to set up your `.env` file (copy the contents of `.env.example`
|
||||
and adjust them based on what you need to work on), and run:
|
||||
|
||||
|
|
@ -78,7 +78,7 @@ when files are changed.
|
|||
### How the Authentication and Token mechanism works
|
||||
|
||||
This section describes how Authentication works between Companion and Providers.
|
||||
While this behaviour is the same for all Providers (Dropbox, Instagram, Google
|
||||
While this behaviour is the same for all Providers (Dropbox, Google
|
||||
Drive, etc.), we are going to be referring to Dropbox in place of any Provider
|
||||
throughout this section.
|
||||
|
||||
|
|
|
|||
2
.github/workflows/bundlers.yml
vendored
2
.github/workflows/bundlers.yml
vendored
|
|
@ -42,7 +42,7 @@ jobs:
|
|||
node-version: lts/*
|
||||
- name: Install dependencies
|
||||
run:
|
||||
corepack yarn install
|
||||
corepack yarn install --immutable
|
||||
env:
|
||||
# https://docs.cypress.io/guides/references/advanced-installation#Skipping-installation
|
||||
CYPRESS_INSTALL_BINARY: 0
|
||||
|
|
|
|||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
|
@ -47,7 +47,7 @@ jobs:
|
|||
node-version: ${{matrix.node-version}}
|
||||
- name: Install dependencies
|
||||
run:
|
||||
corepack yarn install
|
||||
corepack yarn install --immutable
|
||||
- name: Install Playwright Browsers
|
||||
run: corepack yarn workspace @uppy/dashboard playwright install --with-deps
|
||||
- name: Build
|
||||
|
|
@ -84,7 +84,7 @@ jobs:
|
|||
node-version: lts/*
|
||||
- name: Install dependencies
|
||||
run:
|
||||
corepack yarn install
|
||||
corepack yarn install --immutable
|
||||
- run: corepack yarn run typecheck
|
||||
|
||||
lint_js:
|
||||
|
|
|
|||
67
.github/workflows/companion-deploy.yml
vendored
67
.github/workflows/companion-deploy.yml
vendored
|
|
@ -38,56 +38,49 @@ jobs:
|
|||
path: /tmp/companion-${{ github.sha }}.tar.gz
|
||||
|
||||
docker:
|
||||
name: DockerHub
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
DOCKER_BUILDKIT: 0
|
||||
COMPOSE_DOCKER_CLI_BUILD: 0
|
||||
name: DockerHub (${{ matrix.platform }})
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- runner: ubuntu-24.04
|
||||
platform: linux/amd64
|
||||
platform_tag: amd64
|
||||
- runner: ubuntu-24.04-arm
|
||||
platform: linux/arm64
|
||||
platform_tag: arm64
|
||||
runs-on: ${{ matrix.runner }}
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
- name: Docker meta
|
||||
id: docker_meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
with:
|
||||
images: transloadit/companion
|
||||
tags: |
|
||||
type=edge
|
||||
type=raw,value=latest,enable=false
|
||||
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{secrets.DOCKER_USERNAME}}
|
||||
password: ${{secrets.DOCKER_PASSWORD}}
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
with:
|
||||
push: true
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
platforms: ${{ matrix.platform }}
|
||||
file: Dockerfile
|
||||
tags: ${{ steps.docker_meta.outputs.tags }}
|
||||
labels: ${{ steps.docker_meta.outputs.labels }}
|
||||
tags: transloadit/companion:edge-${{ matrix.platform_tag }}
|
||||
|
||||
heroku:
|
||||
name: Heroku
|
||||
docker-manifest:
|
||||
name: DockerHub manifest
|
||||
needs: docker
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
- 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@e3eb99d45a8e2ec5dca08735e089607befa4bf28 # v3.14.15
|
||||
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
heroku_api_key: ${{secrets.HEROKU_API_KEY}}
|
||||
heroku_app_name: companion-demo
|
||||
heroku_email: ${{secrets.HEROKU_EMAIL}}
|
||||
usedocker: true
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Publish edge manifest
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
--tag transloadit/companion:edge \
|
||||
transloadit/companion:edge-amd64 \
|
||||
transloadit/companion:edge-arm64
|
||||
|
|
|
|||
12
.github/workflows/manual-cdn.yml
vendored
12
.github/workflows/manual-cdn.yml
vendored
|
|
@ -30,18 +30,6 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v6
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run:
|
||||
echo "dir=$(corepack yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v5
|
||||
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@v6
|
||||
with:
|
||||
|
|
|
|||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -50,7 +50,7 @@ jobs:
|
|||
|
||||
- name: Create Release Pull Request or Publish
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d
|
||||
with:
|
||||
version: corepack yarn run version
|
||||
publish: corepack yarn run release
|
||||
|
|
@ -102,7 +102,7 @@ jobs:
|
|||
# set latest tag for default branch
|
||||
type=raw,value=latest,enable=true
|
||||
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
- uses: docker/setup-buildx-action@v3
|
||||
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f
|
||||
- name: Log in to DockerHub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
|
|
|
|||
|
|
@ -9,5 +9,6 @@ initScope: uppy
|
|||
|
||||
nodeLinker: node-modules
|
||||
|
||||
npmPublishAccess: public
|
||||
npmMinimalAgeGate: 2880
|
||||
|
||||
npmPublishAccess: public
|
||||
|
|
|
|||
|
|
@ -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/v5.2.3/uppy.min.js"></script>`)
|
||||
(`<script src="https://releases.transloadit.com/uppy/v5.2.4/uppy.min.js"></script>`)
|
||||
or bundle it with your webapp.
|
||||
|
||||
Note that the recommended way to use Uppy is to install it with yarn/npm and use
|
||||
|
|
@ -83,7 +83,7 @@ inline into the page. This, and many more configuration options can be found
|
|||
here: <https://uppy.io/docs/dashboard/>.
|
||||
|
||||
Uppy has many more Plugins besides Xhr and the Dashboard. For example, you can
|
||||
enable Webcam, Instagram, or video encoding support. Note that for some Plugins,
|
||||
enable Webcam or video encoding support. Note that for some Plugins,
|
||||
you will need to run a server side component called: Companion. Those plugins
|
||||
are marked with a (c) symbol. Alternatively, you can sign up for a free
|
||||
Transloadit account. Transloadit runs Companion for you, tusd servers to handle
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ yarn workspace @uppy/[package-name] build
|
|||
- **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)
|
||||
- **Provider Plugins**: Google Drive, Dropbox, etc. (require Companion)
|
||||
- **Uploader Plugins**: Tus (resumable), XHR Upload, AWS S3, etc.
|
||||
- **Utility Plugins**: Golden Retriever (recovery), Thumbnail Generator, etc.
|
||||
|
||||
|
|
|
|||
|
|
@ -24,13 +24,12 @@ FROM node:22.22.1-alpine
|
|||
WORKDIR /app
|
||||
|
||||
# copy required files from build stage.
|
||||
COPY --from=build /app/packages/@uppy/companion/bin /app/bin
|
||||
COPY --from=build /app/packages/@uppy/companion/lib /app/lib
|
||||
COPY --from=build /app/packages/@uppy/companion/dist /app/dist
|
||||
COPY --from=build /app/packages/@uppy/companion/package.json /app/package.json
|
||||
COPY --from=build /app/packages/@uppy/companion/node_modules /app/node_modules
|
||||
|
||||
ENV PATH "${PATH}:/app/node_modules/.bin"
|
||||
|
||||
CMD ["node","/app/bin/companion"]
|
||||
CMD ["node","/app/dist/bin/companion.js"]
|
||||
# This can be overruled later
|
||||
EXPOSE 3020
|
||||
|
|
|
|||
20
README.md
20
README.md
|
|
@ -7,7 +7,7 @@ with any application. It’s fast, has a comprehensible API and lets you worry
|
|||
about more important problems than building a file uploader.
|
||||
|
||||
- **Fetch** files from local disk, remote URLs, Google Drive, Dropbox, Box,
|
||||
Instagram or snap and record selfies with a camera
|
||||
or snap and record selfies with a camera
|
||||
- **Preview** and edit metadata with a nice interface
|
||||
- **Upload** to the final destination, optionally process/encode
|
||||
|
||||
|
|
@ -82,7 +82,7 @@ For the supported frameworks (except Angular) Uppy offers three ways to build us
|
|||
- Lightweight, modular plugin-based architecture, light on dependencies :zap:
|
||||
- Resumable file uploads via the open [tus](https://tus.io/) standard, so large
|
||||
uploads survive network hiccups
|
||||
- Supports picking files from: Webcam, Dropbox, Box, Google Drive, Instagram,
|
||||
- Supports picking files from: Webcam, Dropbox, Box, Google Drive,
|
||||
bypassing the user’s device where possible, syncing between servers directly
|
||||
via [@uppy/companion](https://uppy.io/docs/companion)
|
||||
- Works great with file encoding and processing backends, such as
|
||||
|
|
@ -103,7 +103,7 @@ npm install @uppy/core @uppy/dashboard @uppy/tus
|
|||
```
|
||||
|
||||
Add CSS
|
||||
[uppy.min.css](https://releases.transloadit.com/uppy/v5.2.3/uppy.min.css),
|
||||
[uppy.min.css](https://releases.transloadit.com/uppy/v5.2.4/uppy.min.css),
|
||||
either to your HTML page’s `<head>` or include in JS, if your bundler of choice
|
||||
supports it.
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
|
|||
```html
|
||||
<!-- 1. Add CSS to `<head>` -->
|
||||
<link
|
||||
href="https://releases.transloadit.com/uppy/v5.2.3/uppy.min.css"
|
||||
href="https://releases.transloadit.com/uppy/v5.2.4/uppy.min.css"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
|
|||
Uppy,
|
||||
Dashboard,
|
||||
Tus,
|
||||
} from 'https://releases.transloadit.com/uppy/v5.2.3/uppy.min.mjs'
|
||||
} from 'https://releases.transloadit.com/uppy/v5.2.4/uppy.min.mjs'
|
||||
|
||||
const uppy = new Uppy()
|
||||
uppy.use(Dashboard, { target: '#files-drag-drop' })
|
||||
|
|
@ -140,7 +140,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
|
|||
|
||||
- [Uppy](https://uppy.io/docs/uppy/) — full list of options, methods and events
|
||||
- [Companion](https://uppy.io/docs/companion/) — setting up and running a
|
||||
Companion instance, which adds support for Instagram, Dropbox, Box, Google
|
||||
Companion instance, which adds support for Dropbox, Box, Google
|
||||
Drive and remote URLs
|
||||
- [React](https://uppy.io/docs/react/) — components to integrate Uppy UI plugins
|
||||
with React apps
|
||||
|
|
@ -153,7 +153,7 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` 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
|
||||
plugins like Webcam
|
||||
- Headless components ([react](https://uppy.io/docs/react/), [svelte](https://uppy.io/docs/svelte/), [vue](https://uppy.io/docs/vue/))
|
||||
|
||||
### Sources
|
||||
|
|
@ -166,8 +166,6 @@ CDN. In that case `Uppy` will attach itself to the global `window.Uppy` object.
|
|||
Google Drive
|
||||
- ⓒ [`Dropbox`](https://uppy.io/docs/dropbox/) — import files from Dropbox
|
||||
- ⓒ [`Box`](https://uppy.io/docs/box/) — import files from Box
|
||||
- ⓒ [`Instagram`](https://uppy.io/docs/instagram/) — import images and videos
|
||||
from Instagram
|
||||
- ⓒ [`Facebook`](https://uppy.io/docs/facebook/) — import images and videos from
|
||||
Facebook
|
||||
- ⓒ [`OneDrive`](https://uppy.io/docs/onedrive/) — import files from Microsoft
|
||||
|
|
@ -222,7 +220,7 @@ cases, the file input leaves some to be desired:
|
|||
- Uppy supports editing meta information before uploading.
|
||||
- Uppy allows cropping images before uploading.
|
||||
- There’s the situation where people are using their mobile devices and want to
|
||||
upload on the go, but they have their picture on Instagram, files in Dropbox
|
||||
upload on the go, but they have files in Dropbox
|
||||
or a plain file URL from anywhere on the open web. Uppy allows to pick files
|
||||
from those and push it to the destination without downloading it to your
|
||||
mobile device first.
|
||||
|
|
@ -272,7 +270,7 @@ If you want resumability with the Tus plugin, use
|
|||
[one of the tus server implementations](https://tus.io/implementations.html) 👌🏼
|
||||
|
||||
And you’ll need [`@uppy/companion`](https://uppy.io/docs/companion) if you’d
|
||||
like your users to be able to pick files from Instagram, Google Drive, Dropbox
|
||||
like your users to be able to pick files from Google Drive, Dropbox
|
||||
or via direct URLs (with more services coming).
|
||||
|
||||
## Contributions are welcome
|
||||
|
|
|
|||
68
biome.json
68
biome.json
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
|
|
@ -14,10 +14,11 @@
|
|||
"bin/**",
|
||||
"e2e/**",
|
||||
"private/**",
|
||||
"!packages/**/{dist,lib}/**",
|
||||
"!packages/**/{dist,lib}",
|
||||
"!node_modules",
|
||||
"!.svelte-kit",
|
||||
"!packages/@uppy/components/src/input.css"
|
||||
"!packages/@uppy/components/src/input.css",
|
||||
"!examples/angular/**/*.html"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
|
|
@ -25,20 +26,11 @@
|
|||
"indentStyle": "space",
|
||||
"formatWithErrors": true
|
||||
},
|
||||
"html": {
|
||||
"experimentalFullSupportEnabled": 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": {
|
||||
|
|
@ -48,22 +40,23 @@
|
|||
"useExhaustiveDependencies": "error",
|
||||
"useHookAtTopLevel": "error",
|
||||
"noUnusedFunctionParameters": "off",
|
||||
"noUnusedImports": "error",
|
||||
"noUnusedPrivateClassMembers": "error",
|
||||
"noUnusedVariables": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"ignoreRestSiblings": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"noNestedComponentDefinitions": "error",
|
||||
"noReactPropAssignments": "error"
|
||||
},
|
||||
"style": {
|
||||
"noNonNullAssertion": "off"
|
||||
},
|
||||
"a11y": {
|
||||
"noSvgWithoutTitle": "off"
|
||||
},
|
||||
"nursery": {
|
||||
"noNestedComponentDefinitions": "error",
|
||||
"noReactPropAssign": "error"
|
||||
"noSvgWithoutTitle": "off",
|
||||
"useSemanticElements": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -80,5 +73,36 @@
|
|||
"organizeImports": "on"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"includes": ["packages/@uppy/companion/**"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"complexity": {
|
||||
"useLiteralKeys": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["**/*.vue", "**/*.svelte", "packages/@uppy/vue/**/*.ts"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"useHookAtTopLevel": "off",
|
||||
"useExhaustiveDependencies": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["examples/nextjs/**/*.css"],
|
||||
"css": {
|
||||
"parser": {
|
||||
"tailwindDirectives": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@uppy/companion": "workspace:*",
|
||||
"body-parser": "^1.20.4",
|
||||
"body-parser": "^2.2.2",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.22.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.17.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"vite": "^7.1.11"
|
||||
|
|
|
|||
|
|
@ -66,4 +66,4 @@ const server = app.listen(3020, () => {
|
|||
console.log('listening on port 3020')
|
||||
})
|
||||
|
||||
companion.socket(server)
|
||||
companion.socket(server, options)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,6 @@
|
|||
"@aws-sdk/s3-request-presigner": "^3.338.0",
|
||||
"body-parser": "^1.20.4",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.22.0"
|
||||
"express": "^5.2.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Uppy – AWS upload example</title>
|
||||
|
|
@ -24,7 +24,8 @@
|
|||
</footer>
|
||||
<noscript>You need JavaScript to run this example.</noscript>
|
||||
<script type="module">
|
||||
import { Uppy, Dashboard, AwsS3 } from './uppy.min.mjs'
|
||||
import { AwsS3, Dashboard, Uppy } from './uppy.min.mjs'
|
||||
|
||||
function onUploadComplete(result) {
|
||||
console.log(
|
||||
'Upload complete! We’ve uploaded these files:',
|
||||
|
|
@ -34,7 +35,7 @@
|
|||
function onUploadSuccess(file, data) {
|
||||
console.log(
|
||||
'Upload success! We’ve uploaded this file:',
|
||||
file.meta['name'],
|
||||
file.meta.name,
|
||||
)
|
||||
}
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Uppy – AWS upload example</title>
|
||||
|
|
@ -16,7 +16,8 @@
|
|||
</footer>
|
||||
<noscript>You need JavaScript to run this example.</noscript>
|
||||
<script type="module">
|
||||
import { Uppy, Dashboard, AwsS3 } from './uppy.min.mjs'
|
||||
import { AwsS3, Dashboard, Uppy } from './uppy.min.mjs'
|
||||
|
||||
/**
|
||||
* This generator transforms a deep object into URL-encodable pairs
|
||||
* to work with `URLSearchParams` on the client and `body-parser` on the server.
|
||||
|
|
@ -258,7 +259,7 @@
|
|||
uppy.on('upload-success', (file, data) => {
|
||||
console.log(
|
||||
'Upload success! We’ve uploaded this file:',
|
||||
file.meta['name'],
|
||||
file.meta.name,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@
|
|||
</head>
|
||||
<body>
|
||||
<noscript>You need JavaScript enabled for this example to work.</noscript>
|
||||
<button id="uppyModalOpener">Open Modal</button>
|
||||
<button type="button" id="uppyModalOpener">Open Modal</button>
|
||||
|
||||
<script type="module">
|
||||
import {
|
||||
Uppy,
|
||||
Dashboard,
|
||||
Webcam,
|
||||
Tus,
|
||||
Uppy,
|
||||
Webcam,
|
||||
} from 'https://releases.transloadit.com/uppy/v4.18.0/uppy.min.mjs'
|
||||
|
||||
const uppy = new Uppy({ debug: true, autoProceed: false })
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -10,16 +10,16 @@
|
|||
"@uppy/google-drive": "workspace:*",
|
||||
"@uppy/provider-views": "workspace:*",
|
||||
"@uppy/tus": "workspace:*",
|
||||
"preact": "^10.26.10"
|
||||
"preact": "^10.29.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@uppy/companion": "workspace:*",
|
||||
"body-parser": "^1.20.4",
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.22.0",
|
||||
"body-parser": "^2.2.2",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.15.6",
|
||||
"npm-run-all": "^4.1.2",
|
||||
"vite": "^7.1.11"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@
|
|||
"cors": "^2.8.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dotenv": "^16.0.1",
|
||||
"express": "^4.22.0",
|
||||
"dotenv": "^17.4.2",
|
||||
"express": "^5.2.1",
|
||||
"vite": "^7.1.11"
|
||||
},
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -10,23 +10,18 @@
|
|||
/>
|
||||
</head>
|
||||
<body>
|
||||
<button id="uppyModalOpener">Open Modal</button>
|
||||
<button type="button" id="uppyModalOpener">Open Modal</button>
|
||||
<noscript>This web page requires JavaScript to work properly.</noscript>
|
||||
<script type="module">
|
||||
import {
|
||||
Uppy,
|
||||
Dashboard,
|
||||
Instagram,
|
||||
GoogleDrive,
|
||||
Tus,
|
||||
Uppy,
|
||||
} from 'https://releases.transloadit.com/uppy/v4.18.0/uppy.min.mjs'
|
||||
|
||||
const uppy = new Uppy({ debug: true, autoProceed: false })
|
||||
.use(Dashboard, { trigger: '#uppyModalOpener' })
|
||||
.use(Instagram, {
|
||||
target: Dashboard,
|
||||
companionUrl: 'http://localhost:3020',
|
||||
})
|
||||
.use(GoogleDrive, {
|
||||
target: Dashboard,
|
||||
companionUrl: 'http://localhost:3020',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
"dependencies": {
|
||||
"@uppy/companion": "workspace:*",
|
||||
"body-parser": "^1.20.4",
|
||||
"express": "^4.22.0",
|
||||
"express": "^5.2.1",
|
||||
"express-session": "^1.15.6",
|
||||
"light-server": "^2.4.0"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -32,10 +32,6 @@ const companionOptions = {
|
|||
key: 'your google key',
|
||||
secret: 'your google secret',
|
||||
},
|
||||
instagram: {
|
||||
key: 'your instagram key',
|
||||
secret: 'your instagram secret',
|
||||
},
|
||||
dropbox: {
|
||||
key: 'your dropbox key',
|
||||
secret: 'your dropbox secret',
|
||||
|
|
@ -75,7 +71,7 @@ app.use((err, req, res) => {
|
|||
res.status(500).json({ message: err.message, error: err })
|
||||
})
|
||||
|
||||
companion.socket(app.listen(3020))
|
||||
companion.socket(app.listen(3020), companionOptions)
|
||||
|
||||
console.log('Welcome to Companion!')
|
||||
console.log(`Listening on http://0.0.0.0:${3020}`)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"serve": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tailwindcss/vite": "^4.3.0",
|
||||
"@uppy/core": "workspace:*",
|
||||
"@uppy/react": "workspace:*",
|
||||
"@uppy/remote-sources": "workspace:*",
|
||||
|
|
@ -23,13 +23,14 @@
|
|||
"devDependencies": {
|
||||
"@types/react": "^19.0.10",
|
||||
"@types/react-dom": "^19.0.4",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@vitest/browser": "^4.1.6",
|
||||
"@vitest/browser-playwright": "^4.1.6",
|
||||
"msw": "^2.10.4",
|
||||
"playwright": "1.57.0",
|
||||
"playwright": "1.60.0",
|
||||
"typescript": "^5.7.3",
|
||||
"vite": "^7.1.11",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest-browser-react": "^1.0.0"
|
||||
"vite": "^8.0.14",
|
||||
"vitest": "^4.1.6",
|
||||
"vitest-browser-react": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
/** biome-ignore-all lint/nursery/useUniqueElementIds: it's fine */
|
||||
/** biome-ignore-all lint/correctness/useUniqueElementIds: it's fine */
|
||||
import Uppy, { type UppyFile } from '@uppy/core'
|
||||
import UppyImageEditor from '@uppy/image-editor'
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { userEvent } from '@vitest/browser/context'
|
||||
import { setupWorker } from 'msw/browser'
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||
import { userEvent } from 'vitest/browser'
|
||||
import { render } from 'vitest-browser-react'
|
||||
import { tusHandlers } from '../../shared/tusHandlers.js'
|
||||
import App from '../src/App'
|
||||
|
|
@ -19,7 +19,7 @@ const createMockFile = (name: string, type: string, size: number = 1024) => {
|
|||
|
||||
describe('App', () => {
|
||||
test('renders all main sections and upload button is initially disabled', async () => {
|
||||
const screen = render(<App />)
|
||||
const screen = await render(<App />)
|
||||
|
||||
await expect.element(screen.getByText('With list')).toBeInTheDocument()
|
||||
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
|
||||
|
|
@ -33,7 +33,7 @@ describe('App', () => {
|
|||
})
|
||||
|
||||
test('can add and remove files and upload', async () => {
|
||||
const screen = render(<App />)
|
||||
const screen = await render(<App />)
|
||||
|
||||
const fileInput = document.getElementById(
|
||||
'uppy-dropzone-file-input-uppy',
|
||||
|
|
@ -59,7 +59,7 @@ describe('App', () => {
|
|||
|
||||
describe('ScreenCapture Component', () => {
|
||||
test('renders with title, control buttons, and close functionality works', async () => {
|
||||
const screen = render(<App />)
|
||||
const screen = await render(<App />)
|
||||
|
||||
await screen.getByRole('button', { name: 'Screen Capture' }).click()
|
||||
|
||||
|
|
@ -90,7 +90,7 @@ describe('ScreenCapture Component', () => {
|
|||
|
||||
describe('Webcam Component', () => {
|
||||
test('renders with title, control buttons, and close functionality works', async () => {
|
||||
const screen = render(<App />)
|
||||
const screen = await render(<App />)
|
||||
|
||||
await screen.getByRole('button', { name: 'Webcam' }).click()
|
||||
|
||||
|
|
@ -121,7 +121,7 @@ describe('Webcam Component', () => {
|
|||
|
||||
describe('RemoteSource Component', () => {
|
||||
test('renders login button and login interaction works', async () => {
|
||||
const screen = render(<App />)
|
||||
const screen = await render(<App />)
|
||||
|
||||
await screen.getByRole('button', { name: 'Dropbox' }).click()
|
||||
|
||||
|
|
@ -129,6 +129,5 @@ describe('RemoteSource Component', () => {
|
|||
await expect.element(loginButton).toBeInTheDocument()
|
||||
|
||||
await loginButton.click()
|
||||
await expect.element(loginButton).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -6,4 +6,11 @@ import { defineConfig } from 'vite'
|
|||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
// @vitejs/plugin-react v5+ no longer adds react/react-dom to resolve.dedupe
|
||||
// automatically (it did in v4). Without deduping, the linked @uppy/react
|
||||
// workspace pulls in a second React copy, causing "Invalid hook call".
|
||||
// https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md (v5.0.0)
|
||||
resolve: {
|
||||
dedupe: ['react', 'react-dom'],
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
import tailwindcss from '@tailwindcss/vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
// @vitejs/plugin-react v5+ no longer adds react/react-dom to resolve.dedupe
|
||||
// automatically (it did in v4). Without deduping, the linked @uppy/react
|
||||
// workspace pulls in a second React copy, causing "Invalid hook call".
|
||||
// https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md (v5.0.0)
|
||||
resolve: {
|
||||
dedupe: ['react', 'react-dom'],
|
||||
},
|
||||
test: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: 'playwright',
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -20,14 +20,14 @@
|
|||
"@uppy/transloadit": "workspace:*",
|
||||
"@uppy/tus": "workspace:*",
|
||||
"@uppy/xhr-upload": "workspace:*",
|
||||
"express": "^4.22.0",
|
||||
"express": "^5.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router": "^7.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@react-router/dev": "^7.12.0",
|
||||
"@types/express": "^4.17.20",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/react": "^18.2.20",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"tsx": "^4.0.0",
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@ async function startServer() {
|
|||
|
||||
// TUS upload endpoints (before React Router)
|
||||
app.all('/api/upload/tus', (req, res) => tusServer.handle(req, res))
|
||||
app.all('/api/upload/tus/*', (req, res) => tusServer.handle(req, res))
|
||||
app.all('/api/upload/tus/{*splat}', (req, res) =>
|
||||
tusServer.handle(req, res),
|
||||
)
|
||||
|
||||
// Handle Chrome DevTools requests silently
|
||||
app.get('/.well-known/appspecific/com.chrome.devtools.json', (req, res) => {
|
||||
|
|
@ -49,7 +51,7 @@ async function startServer() {
|
|||
})
|
||||
|
||||
// React Router handles all other routes
|
||||
app.all('*', reactRouterHandler)
|
||||
app.all('/{*splat}', reactRouterHandler)
|
||||
|
||||
const port = process.env.PORT || 3000
|
||||
const server = app.listen(port, () => {
|
||||
|
|
|
|||
|
|
@ -26,15 +26,16 @@
|
|||
"@sveltejs/kit": "^2.49.5",
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"@tailwindcss/vite": "^4.0.0",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
"@vitest/browser": "^4.1.6",
|
||||
"@vitest/browser-playwright": "^4.1.6",
|
||||
"msw": "^2.10.4",
|
||||
"playwright": "1.57.0",
|
||||
"playwright": "1.60.0",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^6.2.5",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest-browser-svelte": "^1.0.0"
|
||||
"vitest": "^4.1.6",
|
||||
"vitest-browser-svelte": "^2.1.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
|||
>
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<input {...getFileInputProps()} class="hidden" />
|
||||
<button
|
||||
<button type="button"
|
||||
{...getButtonProps()}
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
|
|
@ -30,7 +30,7 @@ const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
|||
Device
|
||||
</button>
|
||||
|
||||
<button
|
||||
<button type="button"
|
||||
onclick={() => openModal('webcam')}
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
|
|
@ -40,7 +40,7 @@ const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
|||
Webcam
|
||||
</button>
|
||||
|
||||
<button
|
||||
<button type="button"
|
||||
onclick={() => openModal('screen-capture')}
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
|
|
@ -50,7 +50,7 @@ const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
|
|||
Screen Capture
|
||||
</button>
|
||||
|
||||
<button
|
||||
<button type="button"
|
||||
onclick={() => openModal('dropbox')}
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ const editor = useImageEditor({ file })
|
|||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<!-- biome-ignore lint/a11y/useAltText: alt is provided via editor.getImageProps() spread -->
|
||||
<img
|
||||
class="w-full max-h-[400px] rounded-lg border-2"
|
||||
{...editor.getImageProps()}
|
||||
|
|
@ -41,31 +42,31 @@ const editor = useImageEditor({ file })
|
|||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-wrap mb-4">
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
{...editor.getRotateButtonProps(-90)}
|
||||
>
|
||||
↶ -90°
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
{...editor.getRotateButtonProps(90)}
|
||||
>
|
||||
↷ +90°
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
{...editor.getFlipHorizontalButtonProps()}
|
||||
>
|
||||
⇆ Flip
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
{...editor.getZoomButtonProps(0.1)}
|
||||
>
|
||||
+ Zoom
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
{...editor.getZoomButtonProps(-0.1)}
|
||||
>
|
||||
|
|
@ -74,25 +75,25 @@ const editor = useImageEditor({ file })
|
|||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-wrap mb-4">
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
{...editor.getCropSquareButtonProps()}
|
||||
>
|
||||
1:1
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
{...editor.getCropLandscapeButtonProps()}
|
||||
>
|
||||
16:9
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
{...editor.getCropPortraitButtonProps()}
|
||||
>
|
||||
9:16
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
{...editor.getResetButtonProps()}
|
||||
>
|
||||
|
|
@ -101,13 +102,13 @@ const editor = useImageEditor({ file })
|
|||
</div>
|
||||
|
||||
<div class="flex gap-4 justify-end">
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-500 text-white px-4 py-2 rounded-md"
|
||||
{...editor.getCancelButtonProps({ onClick: close })}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||
{...editor.getSaveButtonProps({ onClick: close })}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -42,36 +42,37 @@ const {
|
|||
<p>{mediaError.message ? `Camera error: ${mediaError.message}` : 'An unknown camera error occurred.'}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<!-- biome-ignore lint/a11y/useMediaCaption: live camera preview does not require captions -->
|
||||
<video
|
||||
class="border-2 w-full rounded-lg data-[uppy-mirrored=true]:scale-x-[-1]"
|
||||
{...videoProps}
|
||||
></video>
|
||||
<div class="flex gap-4 flex-wrap mt-4">
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
{...primaryActionButtonProps}
|
||||
>
|
||||
{primaryActionButtonLabel}
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
{...recordButtonProps}
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
||||
{...stopRecordingButtonProps}
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||
{...submitButtonProps}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
||||
{...discardButtonProps}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ const dtf = new Intl.DateTimeFormat('en-US', {
|
|||
timeStyle: 'short',
|
||||
})
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedVariables: used as a Svelte action via use:setFolderCheckboxIndeterminate
|
||||
function setFolderCheckboxIndeterminate(
|
||||
node: HTMLInputElement,
|
||||
item: PartialTreeFolderNode,
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ let hideUploadButton = false
|
|||
</script>
|
||||
|
||||
<main class="p-5 max-w-xl mx-auto">
|
||||
<button onclick={() => (disabled = !disabled)}>Toggle dashboard</button>
|
||||
<button type="button" onclick={() => (disabled = !disabled)}>Toggle dashboard</button>
|
||||
<Dashboard props={{disabled: disabled}} uppy={uppy} />
|
||||
<button onclick={() => (hideUploadButton = !hideUploadButton)}>Toggle statusbar</button>
|
||||
<button type="button" onclick={() => (hideUploadButton = !hideUploadButton)}>Toggle statusbar</button>
|
||||
<div id="statusbar-container">
|
||||
<StatusBar props={{ hideUploadButton }} uppy={uppy} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { userEvent } from '@vitest/browser/context'
|
||||
import { setupWorker } from 'msw/browser'
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||
import { userEvent } from 'vitest/browser'
|
||||
import { render } from 'vitest-browser-svelte'
|
||||
import { tusHandlers } from '../../shared/tusHandlers.js'
|
||||
import PropsReactivity from '../src/components/test/props-reactivity.svelte'
|
||||
|
|
@ -20,7 +20,7 @@ const createMockFile = (name: string, type: string, size: number = 1024) => {
|
|||
|
||||
describe('App', () => {
|
||||
test('renders all main sections and upload button is initially disabled', async () => {
|
||||
const screen = render(App)
|
||||
const screen = await render(App)
|
||||
|
||||
await expect.element(screen.getByText('With list')).toBeInTheDocument()
|
||||
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
|
||||
|
|
@ -34,7 +34,7 @@ describe('App', () => {
|
|||
})
|
||||
|
||||
test('can add and remove files and upload', async () => {
|
||||
const screen = render(App)
|
||||
const screen = await render(App)
|
||||
|
||||
const fileInput = document.getElementById(
|
||||
'uppy-dropzone-file-input-uppy',
|
||||
|
|
@ -60,7 +60,7 @@ describe('App', () => {
|
|||
|
||||
describe('ScreenCapture Component', () => {
|
||||
test('renders with title, control buttons, and close functionality works', async () => {
|
||||
const screen = render(App)
|
||||
const screen = await render(App)
|
||||
|
||||
await screen
|
||||
.getByRole('button', { name: 'Screen Capture', exact: true })
|
||||
|
|
@ -93,7 +93,7 @@ describe('ScreenCapture Component', () => {
|
|||
|
||||
describe('Webcam Component', () => {
|
||||
test('renders with title, control buttons, and close functionality works', async () => {
|
||||
const screen = render(App)
|
||||
const screen = await render(App)
|
||||
|
||||
await screen.getByRole('button', { name: 'Webcam', exact: true }).click()
|
||||
|
||||
|
|
@ -124,7 +124,7 @@ describe('Webcam Component', () => {
|
|||
|
||||
describe('RemoteSource Component', () => {
|
||||
test('renders login button and login interaction works', async () => {
|
||||
const screen = render(App)
|
||||
const screen = await render(App)
|
||||
|
||||
await screen.getByRole('button', { name: 'Dropbox', exact: true }).click()
|
||||
|
||||
|
|
@ -132,12 +132,11 @@ describe('RemoteSource Component', () => {
|
|||
await expect.element(loginButton).toBeInTheDocument()
|
||||
|
||||
await loginButton.click()
|
||||
await expect.element(loginButton).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
test('Dashboard reacts to prop changes', async () => {
|
||||
const screen = render(PropsReactivity)
|
||||
const screen = await render(PropsReactivity)
|
||||
const toggleButton = screen.getByText('Toggle dashboard')
|
||||
const dashboard = screen.container.querySelector('.uppy-Dashboard')
|
||||
|
||||
|
|
@ -148,7 +147,7 @@ test('Dashboard reacts to prop changes', async () => {
|
|||
})
|
||||
|
||||
test('StatusBar reacts to prop changes', async () => {
|
||||
const screen = render(PropsReactivity)
|
||||
const screen = await render(PropsReactivity)
|
||||
const toggleButton = screen.getByText('Toggle statusbar')
|
||||
|
||||
expect(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { sveltekit } from '@sveltejs/kit/vite'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
|
|
@ -7,7 +8,7 @@ export default defineConfig({
|
|||
test: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: 'playwright',
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
@ -140,7 +140,7 @@
|
|||
This API is a one-shot upload UI using a modal overlay. Call the
|
||||
function and receive a listen to an event with upload results ✌️
|
||||
</p>
|
||||
<button onclick="openModal()">Open</button>
|
||||
<button type="button" onclick="openModal()">Open</button>
|
||||
|
||||
<h2>uppy.upload()</h2>
|
||||
<p>An <input type=file> backed by <code>uppy.upload()</code>:</p>
|
||||
|
|
|
|||
|
|
@ -50,9 +50,11 @@ const formUppy = new Uppy({
|
|||
})
|
||||
.use(Transloadit, {
|
||||
waitForEncoding: true,
|
||||
params: {
|
||||
auth: { key: TRANSLOADIT_KEY },
|
||||
template_id: TEMPLATE_ID,
|
||||
assemblyOptions: {
|
||||
params: {
|
||||
auth: { key: TRANSLOADIT_KEY },
|
||||
template_id: TEMPLATE_ID,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -100,9 +102,11 @@ const formUppyWithDashboard = new Uppy({
|
|||
})
|
||||
.use(Transloadit, {
|
||||
waitForEncoding: true,
|
||||
params: {
|
||||
auth: { key: TRANSLOADIT_KEY },
|
||||
template_id: TEMPLATE_ID,
|
||||
assemblyOptions: {
|
||||
params: {
|
||||
auth: { key: TRANSLOADIT_KEY },
|
||||
template_id: TEMPLATE_ID,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -129,9 +133,11 @@ const dashboard = new Uppy({
|
|||
.use(ImageEditor, { target: Dashboard })
|
||||
.use(Transloadit, {
|
||||
waitForEncoding: true,
|
||||
params: {
|
||||
auth: { key: TRANSLOADIT_KEY },
|
||||
template_id: TEMPLATE_ID,
|
||||
assemblyOptions: {
|
||||
params: {
|
||||
auth: { key: TRANSLOADIT_KEY },
|
||||
template_id: TEMPLATE_ID,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -151,9 +157,11 @@ const dashboardModal = new Uppy({
|
|||
.use(ImageEditor, { target: Dashboard })
|
||||
.use(Transloadit, {
|
||||
waitForEncoding: true,
|
||||
params: {
|
||||
auth: { key: TRANSLOADIT_KEY },
|
||||
template_id: TEMPLATE_ID,
|
||||
assemblyOptions: {
|
||||
params: {
|
||||
auth: { key: TRANSLOADIT_KEY },
|
||||
template_id: TEMPLATE_ID,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -182,9 +190,11 @@ const uppyWithoutUI = new Uppy({
|
|||
},
|
||||
}).use(Transloadit, {
|
||||
waitForEncoding: true,
|
||||
params: {
|
||||
auth: { key: TRANSLOADIT_KEY },
|
||||
template_id: TEMPLATE_ID,
|
||||
assemblyOptions: {
|
||||
params: {
|
||||
auth: { key: TRANSLOADIT_KEY },
|
||||
template_id: TEMPLATE_ID,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
"@uppy/remote-sources": "workspace:*",
|
||||
"@uppy/transloadit": "workspace:*",
|
||||
"@uppy/webcam": "workspace:*",
|
||||
"express": "^4.22.0",
|
||||
"express": "^5.2.1",
|
||||
"he": "^1.2.0"
|
||||
},
|
||||
"private": true,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ function Footer() {
|
|||
}
|
||||
|
||||
function FormFields(fields) {
|
||||
// biome-ignore lint/nursery/noNestedComponentDefinitions: not a react component
|
||||
// biome-ignore lint/correctness/noNestedComponentDefinitions: not a react component
|
||||
function Field([name, value]) {
|
||||
if (name === 'transloadit') return ''
|
||||
let isValueJSON = false
|
||||
|
|
@ -76,7 +76,7 @@ function FormFields(fields) {
|
|||
}
|
||||
|
||||
function UploadsList(uploads) {
|
||||
// biome-ignore lint/nursery/noNestedComponentDefinitions: not a react component
|
||||
// biome-ignore lint/correctness/noNestedComponentDefinitions: not a react component
|
||||
function Upload(upload) {
|
||||
return `<li>${e(upload.name)}</li>`
|
||||
}
|
||||
|
|
@ -89,12 +89,12 @@ function UploadsList(uploads) {
|
|||
}
|
||||
|
||||
function ResultsList(results) {
|
||||
// biome-ignore lint/nursery/noNestedComponentDefinitions: not a react component
|
||||
// biome-ignore lint/correctness/noNestedComponentDefinitions: not a react component
|
||||
function Result(result) {
|
||||
return `<li>${e(result.name)} <a href="${result.ssl_url}" target="_blank">View</a></li>`
|
||||
}
|
||||
|
||||
// biome-ignore lint/nursery/noNestedComponentDefinitions: not a react component
|
||||
// biome-ignore lint/correctness/noNestedComponentDefinitions: not a react component
|
||||
function ResultsSection(stepName) {
|
||||
return `
|
||||
<h2>${e(stepName)}</h2>
|
||||
|
|
|
|||
|
|
@ -22,12 +22,13 @@
|
|||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"@vitest/browser": "^3.2.4",
|
||||
"@vitest/browser": "^4.1.6",
|
||||
"@vitest/browser-playwright": "^4.1.6",
|
||||
"msw": "^2.10.4",
|
||||
"playwright": "1.57.0",
|
||||
"playwright": "1.60.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"vite": "^7.1.11",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest-browser-vue": "^1.0.0"
|
||||
"vitest": "^4.1.6",
|
||||
"vitest-browser-vue": "^2.1.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
>
|
||||
<div class="flex items-center justify-center gap-4">
|
||||
<input v-bind="getFileInputProps()" class="hidden" />
|
||||
<button
|
||||
<button type="button"
|
||||
v-bind="getButtonProps()"
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
|
|
@ -17,7 +17,7 @@
|
|||
Device
|
||||
</button>
|
||||
|
||||
<button
|
||||
<button type="button"
|
||||
@click="() => props.openModal('webcam')"
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
Webcam
|
||||
</button>
|
||||
|
||||
<button
|
||||
<button type="button"
|
||||
@click="() => props.openModal('screen-capture')"
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
|
|
@ -37,7 +37,7 @@
|
|||
Screen Capture
|
||||
</button>
|
||||
|
||||
<button
|
||||
<button type="button"
|
||||
@click="() => props.openModal('dropbox')"
|
||||
class="hover:bg-gray-100 transition-colors p-2 rounded-md flex flex-col items-center gap-2 text-sm"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<!-- biome-ignore lint/a11y/useAltText: alt is provided via editor.getImageProps() spread -->
|
||||
<img
|
||||
class="w-full max-h-[400px] rounded-lg border-2"
|
||||
v-bind="editor.getImageProps()"
|
||||
|
|
@ -29,31 +30,31 @@
|
|||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-wrap mb-4">
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
v-bind="editor.getRotateButtonProps(-90)"
|
||||
>
|
||||
↶ -90°
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
v-bind="editor.getRotateButtonProps(90)"
|
||||
>
|
||||
↷ +90°
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
v-bind="editor.getFlipHorizontalButtonProps()"
|
||||
>
|
||||
⇆ Flip
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
v-bind="editor.getZoomButtonProps(0.1)"
|
||||
>
|
||||
+ Zoom
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
v-bind="editor.getZoomButtonProps(-0.1)"
|
||||
>
|
||||
|
|
@ -62,25 +63,25 @@
|
|||
</div>
|
||||
|
||||
<div class="flex gap-2 flex-wrap mb-4">
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
v-bind="editor.getCropSquareButtonProps()"
|
||||
>
|
||||
1:1
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
v-bind="editor.getCropLandscapeButtonProps()"
|
||||
>
|
||||
16:9
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
v-bind="editor.getCropPortraitButtonProps()"
|
||||
>
|
||||
9:16
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-200 px-3 py-1 rounded disabled:opacity-50"
|
||||
v-bind="editor.getResetButtonProps()"
|
||||
>
|
||||
|
|
@ -89,13 +90,13 @@
|
|||
</div>
|
||||
|
||||
<div class="flex gap-4 justify-end">
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-500 text-white px-4 py-2 rounded-md"
|
||||
v-bind="editor.getCancelButtonProps({ onClick: close })"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||
v-bind="editor.getSaveButtonProps({ onClick: close })"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -24,31 +24,31 @@
|
|||
<track kind="captions" />
|
||||
</video>
|
||||
<div class="flex gap-4 flex-wrap mt-4">
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
v-bind="primaryActionButtonProps"
|
||||
>
|
||||
{{ primaryActionButtonLabel }}
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-blue-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-blue-300"
|
||||
v-bind="recordButtonProps"
|
||||
>
|
||||
Record
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-red-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-red-300"
|
||||
v-bind="stopRecordingButtonProps"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-green-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-green-300"
|
||||
v-bind="submitButtonProps"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
class="bg-gray-500 text-white px-4 py-2 rounded-md disabled:opacity-50 disabled:bg-gray-300"
|
||||
v-bind="discardButtonProps"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { userEvent } from '@vitest/browser/context'
|
||||
import { setupWorker } from 'msw/browser'
|
||||
import { afterAll, beforeAll, describe, expect, test } from 'vitest'
|
||||
import { userEvent } from 'vitest/browser'
|
||||
import { render } from 'vitest-browser-vue'
|
||||
import { tusHandlers } from '../../shared/tusHandlers.js'
|
||||
import App from '../src/App.vue'
|
||||
|
|
@ -19,7 +19,7 @@ const createMockFile = (name: string, type: string, size: number = 1024) => {
|
|||
|
||||
describe('App', () => {
|
||||
test('renders all main sections and upload button is initially disabled', async () => {
|
||||
const screen = render(App)
|
||||
const screen = await render(App)
|
||||
|
||||
await expect.element(screen.getByText('With list')).toBeInTheDocument()
|
||||
await expect.element(screen.getByText('With grid')).toBeInTheDocument()
|
||||
|
|
@ -33,7 +33,7 @@ describe('App', () => {
|
|||
})
|
||||
|
||||
test('can add and remove files and upload', async () => {
|
||||
const screen = render(App)
|
||||
const screen = await render(App)
|
||||
|
||||
const fileInput = document.getElementById(
|
||||
'uppy-dropzone-file-input-uppy',
|
||||
|
|
@ -59,7 +59,7 @@ describe('App', () => {
|
|||
|
||||
describe('ScreenCapture Component', () => {
|
||||
test('renders with title, control buttons, and close functionality works', async () => {
|
||||
const screen = render(App)
|
||||
const screen = await render(App)
|
||||
|
||||
await screen
|
||||
.getByRole('button', { name: 'Screen Capture', exact: true })
|
||||
|
|
@ -92,7 +92,7 @@ describe('ScreenCapture Component', () => {
|
|||
|
||||
describe('Webcam Component', () => {
|
||||
test('renders with title, control buttons, and close functionality works', async () => {
|
||||
const screen = render(App)
|
||||
const screen = await render(App)
|
||||
|
||||
await screen.getByRole('button', { name: 'Webcam', exact: true }).click()
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ describe('Webcam Component', () => {
|
|||
|
||||
describe('RemoteSource Component', () => {
|
||||
test('renders login button and login interaction works', async () => {
|
||||
const screen = render(App)
|
||||
const screen = await render(App)
|
||||
|
||||
await screen.getByRole('button', { name: 'Dropbox', exact: true }).click()
|
||||
|
||||
|
|
@ -131,6 +131,5 @@ describe('RemoteSource Component', () => {
|
|||
await expect.element(loginButton).toBeInTheDocument()
|
||||
|
||||
await loginButton.click()
|
||||
await expect.element(loginButton).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import tailwindcss from '@tailwindcss/vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { playwright } from '@vitest/browser-playwright'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
|
|
@ -7,7 +8,7 @@ export default defineConfig({
|
|||
test: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
provider: 'playwright',
|
||||
provider: playwright(),
|
||||
instances: [{ browser: 'chromium' }],
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
"@uppy/dashboard": "workspace:*",
|
||||
"@uppy/xhr-upload": "workspace:*",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.22.0",
|
||||
"express": "^5.2.1",
|
||||
"multer": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "@uppy-dev/build",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Instagram, Dropbox, Google Drive, S3 and more :dog:",
|
||||
"description": "Extensible JavaScript file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox, Google Drive, S3 and more :dog:",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"examples/*",
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
"release": "changeset publish"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "2.0.5",
|
||||
"@biomejs/biome": "2.4.15",
|
||||
"@changesets/changelog-github": "^0.5.1",
|
||||
"@changesets/cli": "patch:@changesets/cli@npm%3A2.29.5#~/.yarn/patches/@changesets-cli-npm-2.29.5-68d8030bf3.patch",
|
||||
"npm-run-all": "^4.1.5",
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
{
|
||||
"ignorePatterns": ["projects/**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
// eslint-disable-line import/newline-after-import
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": ["error", "never"],
|
||||
"import/no-unresolved": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
],
|
||||
"semi": ["error", "never"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended",
|
||||
"plugin:@angular-eslint/template/accessibility"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -17,22 +17,15 @@
|
|||
"@angular/platform-browser": "^19.2.17",
|
||||
"@angular/platform-browser-dynamic": "^19.2.17",
|
||||
"@angular/router": "^19.2.17",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"rxjs": "~7.8.2",
|
||||
"tslib": "^2.8.1",
|
||||
"zone.js": "~0.15.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.0.0",
|
||||
"@angular-eslint/eslint-plugin": "^18.0.1",
|
||||
"@angular-eslint/eslint-plugin-template": "^18.0.1",
|
||||
"@angular/cli": "^19.0.0",
|
||||
"@angular/compiler-cli": "^19.2.17",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@typescript-eslint/types": "^7.2.0",
|
||||
"@typescript-eslint/utils": "^7.2.0",
|
||||
"eslint": "^8.57.0",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
{
|
||||
"extends": "../../../.eslintrc.json",
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"packages/@uppy/angular/projects/angular/tsconfig.lib.json",
|
||||
"packages/@uppy/angular/projects/angular/tsconfig.spec.json"
|
||||
],
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
"rules": {
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "uppy",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "uppy",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"dot-notation": "error",
|
||||
"indent": "error",
|
||||
"no-empty-function": "off",
|
||||
"no-shadow": "error",
|
||||
"no-unused-expressions": "error",
|
||||
"no-use-before-define": "off",
|
||||
"quotes": "error",
|
||||
"semi": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
"prepublishOnly": "rm -fr * && cp -r ../../../dist/uppy/angular .."
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": "^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0",
|
||||
|
|
|
|||
|
|
@ -41,16 +41,16 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@uppy/utils": "workspace:^",
|
||||
"preact": "^10.26.10"
|
||||
"preact": "^10.29.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cssnano": "^7.0.7",
|
||||
"jsdom": "^26.1.0",
|
||||
"postcss": "^8.5.6",
|
||||
"cssnano": "^8.0.1",
|
||||
"jsdom": "^29.1.1",
|
||||
"postcss": "^8.5.15",
|
||||
"postcss-cli": "^11.0.1",
|
||||
"sass": "^1.89.2",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.1.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@uppy/core": "workspace:^"
|
||||
|
|
|
|||
|
|
@ -284,7 +284,9 @@ export default class Audio<M extends Meta, B extends Body> extends UIPlugin<
|
|||
#stop = async () => {
|
||||
if (this.#stream) {
|
||||
const audioTracks = this.#stream.getAudioTracks()
|
||||
audioTracks.forEach((track) => track.stop())
|
||||
audioTracks.forEach((track) => {
|
||||
track.stop()
|
||||
})
|
||||
}
|
||||
|
||||
if (this.#recorder) {
|
||||
|
|
|
|||
|
|
@ -44,11 +44,12 @@
|
|||
"devDependencies": {
|
||||
"@aws-sdk/client-s3": "^3.362.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.362.0",
|
||||
"@types/node": "^20.19.0",
|
||||
"@uppy/core": "workspace:^",
|
||||
"jsdom": "^26.1.0",
|
||||
"nock": "^13.1.0",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest": "^4.1.6",
|
||||
"whatwg-fetch": "3.6.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
|
|
|||
|
|
@ -263,9 +263,9 @@ export class HTTPCommunicationQueue<M extends Meta, B extends Body> {
|
|||
const data = chunk.getData()
|
||||
if (method.toUpperCase() === 'POST') {
|
||||
const formData = new FormData()
|
||||
Object.entries(fields!).forEach(([key, value]) =>
|
||||
formData.set(key, value),
|
||||
)
|
||||
Object.entries(fields!).forEach(([key, value]) => {
|
||||
formData.set(key, value)
|
||||
})
|
||||
formData.set('file', data)
|
||||
body = formData
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -256,6 +256,7 @@ class MultipartUploader<M extends Meta, B extends Body> {
|
|||
else this.pause()
|
||||
}
|
||||
|
||||
// biome-ignore lint/correctness/noUnusedPrivateClassMembers: accessed via Symbol in tests
|
||||
private [Symbol.for('uppy test: getChunkState')]() {
|
||||
return this.#chunkState
|
||||
}
|
||||
|
|
|
|||
|
|
@ -662,10 +662,11 @@ export default class AwsS3Multipart<
|
|||
querify: true,
|
||||
})
|
||||
|
||||
const query = new URLSearchParams({ filename, type, ...metadata } as Record<
|
||||
string,
|
||||
string
|
||||
>)
|
||||
const query = new URLSearchParams({
|
||||
filename,
|
||||
...(type != null && { type }),
|
||||
...metadata,
|
||||
})
|
||||
|
||||
return this.#client.get(`s3/params?${query}`, options)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
"extends": "../../../tsconfig.shared",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": false,
|
||||
"noEmit": true
|
||||
"noEmit": true,
|
||||
"types": ["preact", "node"]
|
||||
},
|
||||
"include": ["./package.json", "./src/**/*.*"],
|
||||
"references": [
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@
|
|||
"@uppy/companion-client": "workspace:^",
|
||||
"@uppy/provider-views": "workspace:^",
|
||||
"@uppy/utils": "workspace:^",
|
||||
"preact": "^10.26.10"
|
||||
"preact": "^10.29.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@uppy/core": "workspace:^"
|
||||
|
|
|
|||
|
|
@ -41,9 +41,9 @@
|
|||
"p-retry": "^6.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsdom": "^26.1.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"typescript": "^5.8.3",
|
||||
"vitest": "^3.2.4"
|
||||
"vitest": "^4.1.6"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@uppy/core": "workspace:^"
|
||||
|
|
|
|||
|
|
@ -5,8 +5,11 @@ import type {
|
|||
UnknownProviderPlugin,
|
||||
Uppy,
|
||||
} from '@uppy/core'
|
||||
import type { CompanionClientProvider, RequestOptions } from '@uppy/utils'
|
||||
import { isOriginAllowed } from './getAllowedHosts.js'
|
||||
import {
|
||||
type CompanionClientProvider,
|
||||
getSocketHost,
|
||||
type RequestOptions,
|
||||
} from '@uppy/utils'
|
||||
import type { CompanionPluginOptions } from './index.js'
|
||||
import RequestClient, { authErrorStatusCode } from './RequestClient.js'
|
||||
|
||||
|
|
@ -135,22 +138,27 @@ export default class Provider<M extends Meta, B extends Body>
|
|||
authUrl({
|
||||
authFormData,
|
||||
query,
|
||||
authCallbackToken,
|
||||
}: {
|
||||
authFormData: unknown
|
||||
query: Record<string, string>
|
||||
authCallbackToken?: string | undefined
|
||||
}): string {
|
||||
const params = new URLSearchParams({
|
||||
const searchParams = new URLSearchParams({
|
||||
...query,
|
||||
// This is only used for Companion instances configured to accept multiple origins.
|
||||
// `origin` is only used for Companion instances configured to accept multiple origins.
|
||||
state: btoa(JSON.stringify({ origin: getOrigin() })),
|
||||
...this.authQuery({ authFormData }),
|
||||
})
|
||||
|
||||
if (this.preAuthToken) {
|
||||
params.set('uppyPreAuthToken', this.preAuthToken)
|
||||
searchParams.set('uppyPreAuthToken', this.preAuthToken)
|
||||
}
|
||||
if (authCallbackToken) {
|
||||
searchParams.set('authCallbackToken', authCallbackToken)
|
||||
}
|
||||
|
||||
return `${this.hostname}/${this.id}/connect?${params}`
|
||||
return `${this.hostname}/${this.id}/connect?${searchParams}`
|
||||
}
|
||||
|
||||
protected async loginSimpleAuth({
|
||||
|
|
@ -181,67 +189,62 @@ export default class Provider<M extends Meta, B extends Body>
|
|||
signal: AbortSignal
|
||||
}): Promise<void> {
|
||||
await this.ensurePreAuth()
|
||||
|
||||
signal.throwIfAborted()
|
||||
|
||||
const link = this.authUrl({ query: { uppyVersions }, authFormData })
|
||||
// Important: We need to do this synchronously, or else browsers might block the popup.
|
||||
// (We cannot wait until the websocket connection is established).
|
||||
// This opens up a race condtition if the websocket is not connected before the user
|
||||
// completes(or cancels) authentication, but that’s a small compromose we gotta make.
|
||||
const authCallbackToken = crypto.randomUUID()
|
||||
|
||||
const link = this.authUrl({
|
||||
query: { uppyVersions },
|
||||
authFormData,
|
||||
authCallbackToken,
|
||||
})
|
||||
const authWindow = window.open(link, '_blank')
|
||||
let interval: number | undefined
|
||||
let handleMessage: ((e: MessageEvent<any>) => void) | undefined
|
||||
let webSocket: WebSocket | undefined
|
||||
|
||||
try {
|
||||
return await new Promise((resolve, reject) => {
|
||||
handleMessage = (e: MessageEvent<any>) => {
|
||||
if (e.source !== authWindow) {
|
||||
let jsonData = ''
|
||||
try {
|
||||
// TODO improve our uppy logger so that it can take an arbitrary number of arguments,
|
||||
// each either objects, errors or strings,
|
||||
// then we don’t have to manually do these things like json stringify when logging.
|
||||
// the logger should never throw an error.
|
||||
jsonData = JSON.stringify(e.data)
|
||||
} catch (_err) {
|
||||
// in case JSON.stringify fails (ignored)
|
||||
const host = getSocketHost(this.opts.companionUrl)
|
||||
|
||||
// Note that this promise is not guaranteed to settle in all cases
|
||||
const token = await new Promise<string>((resolve, reject) => {
|
||||
webSocket = new WebSocket(
|
||||
`${host}/api2/auth-callback/token/${authCallbackToken}`,
|
||||
)
|
||||
|
||||
webSocket.addEventListener('close', () => {
|
||||
reject(new Error('Socket closed'))
|
||||
})
|
||||
|
||||
webSocket.addEventListener('error', (error) => {
|
||||
this.uppy.log(
|
||||
`Companion socket error ${JSON.stringify(error)}, closing socket`,
|
||||
'warning',
|
||||
)
|
||||
webSocket?.close() // 'close' event will be emitted
|
||||
})
|
||||
|
||||
webSocket.addEventListener('message', (e) => {
|
||||
try {
|
||||
const { token, error } = JSON.parse(e.data)
|
||||
if (error) {
|
||||
reject(new Error('Authentication reported error'))
|
||||
} else if (!token) {
|
||||
reject(new Error('Authentication did not return a token'))
|
||||
} else {
|
||||
resolve(token)
|
||||
}
|
||||
this.uppy.log(
|
||||
`ignoring event from unknown source ${jsonData}`,
|
||||
'warning',
|
||||
)
|
||||
return
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
|
||||
const { companionAllowedHosts } = this.#getPlugin().opts
|
||||
if (!isOriginAllowed(e.origin, companionAllowedHosts)) {
|
||||
this.uppy.log(
|
||||
`ignoring event from ${e.origin} vs allowed pattern ${companionAllowedHosts}`,
|
||||
'warning',
|
||||
)
|
||||
// We cannot reject here because the page might send events from other origins
|
||||
// before sending the "real" auth completed event.
|
||||
// for example Box has a "Pendo" tool that sends events to the opener
|
||||
// https://github.com/transloadit/uppy/pull/5719
|
||||
return
|
||||
}
|
||||
|
||||
// Check if it's a string before doing the JSON.parse to maintain support
|
||||
// for older Companion versions that used object references
|
||||
const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
|
||||
|
||||
if (data.error) {
|
||||
const { uppy } = this
|
||||
const message = uppy.i18n('authAborted')
|
||||
uppy.info({ message }, 'warning', 5000)
|
||||
reject(new Error('auth aborted'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!data.token) {
|
||||
reject(new Error('did not receive token from auth window'))
|
||||
return
|
||||
}
|
||||
|
||||
resolve(this.setAuthToken(data.token))
|
||||
}
|
||||
signal.addEventListener('abort', () =>
|
||||
reject(new Error('Authentication was aborted')),
|
||||
)
|
||||
|
||||
// poll for user closure of the window, so we can reject when it happens
|
||||
if (authWindow) {
|
||||
|
|
@ -251,15 +254,21 @@ export default class Provider<M extends Meta, B extends Body>
|
|||
}
|
||||
}, 500)
|
||||
}
|
||||
|
||||
signal.addEventListener('abort', () => reject(new Error('Aborted')))
|
||||
window.addEventListener('message', handleMessage)
|
||||
})
|
||||
|
||||
this.setAuthToken(token)
|
||||
} catch (err) {
|
||||
const message = this.uppy.i18n('authAborted')
|
||||
this.uppy.info({ message }, 'warning', 5000)
|
||||
this.uppy.log(`Authentication failed: ${err.message}`, 'warning')
|
||||
throw err
|
||||
} finally {
|
||||
// cleanup:
|
||||
authWindow?.close()
|
||||
window.clearInterval(interval)
|
||||
if (handleMessage) window.removeEventListener('message', handleMessage)
|
||||
// if we don't setTimeout, the window doesn't really close (I don't know why).
|
||||
setTimeout(() => authWindow?.close(), 1)
|
||||
this.uppy.log(`Closing auth callback socket ${authCallbackToken}`)
|
||||
webSocket?.close()
|
||||
clearInterval(interval)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -234,12 +234,13 @@ export default class RequestClient<M extends Meta, B extends Body> {
|
|||
}
|
||||
|
||||
/**
|
||||
* Remote uploading consists of two steps:
|
||||
* 1. #requestSocketToken which starts the download/upload in companion and returns a unique token for the upload.
|
||||
* Then companion will halt the upload until:
|
||||
* 2. #awaitRemoteFileUpload is called, which will open/ensure a websocket connection towards companion, with the
|
||||
* previously generated token provided. It returns a promise that will resolve/reject once the file has finished
|
||||
* uploading or is otherwise done (failed, canceled)
|
||||
* Remote uploading uses a single queue admission per attempt:
|
||||
* 1. Acquire one queue slot.
|
||||
* 2. Reuse an existing serverToken or request a new one from Companion.
|
||||
* 3. Open/maintain the websocket and wait for Companion to finish.
|
||||
*
|
||||
* This prevents socket tokens from being created long before their websocket
|
||||
* session is admitted by the queue.
|
||||
*/
|
||||
async uploadRemoteFile(
|
||||
file: RemoteUppyFile<M, B>,
|
||||
|
|
@ -248,72 +249,46 @@ export default class RequestClient<M extends Meta, B extends Body> {
|
|||
): Promise<void> {
|
||||
try {
|
||||
const { signal, getQueue } = options || {}
|
||||
const queue = getQueue()
|
||||
|
||||
return await pRetry(
|
||||
async () => {
|
||||
// if we already have a serverToken, assume that we are resuming the existing server upload id
|
||||
const existingServerToken = this.uppy.getFile(file.id)?.serverToken
|
||||
if (existingServerToken != null) {
|
||||
this.uppy.log(
|
||||
`Connecting to exiting websocket ${existingServerToken}`,
|
||||
)
|
||||
return this.#awaitRemoteFileUpload({
|
||||
file,
|
||||
queue: getQueue(),
|
||||
signal,
|
||||
})
|
||||
}
|
||||
const queueRemoteUploadAttempt = queue.wrapPromiseFunction(
|
||||
async () => {
|
||||
const currentFile = this.uppy.getFile(file.id) as
|
||||
| RemoteUppyFile<M, B>
|
||||
| undefined
|
||||
if (currentFile == null) return undefined
|
||||
|
||||
const queueRequestSocketToken = getQueue().wrapPromiseFunction(
|
||||
async (
|
||||
...args: [
|
||||
{
|
||||
file: RemoteUppyFile<M, B>
|
||||
postBody: Record<string, unknown>
|
||||
signal: AbortSignal
|
||||
},
|
||||
]
|
||||
) => {
|
||||
try {
|
||||
return await this.#requestSocketToken(...args)
|
||||
} catch (outerErr) {
|
||||
// throwing AbortError will cause p-retry to stop retrying
|
||||
if (outerErr.isAuthError) throw new AbortError(outerErr)
|
||||
let serverToken = currentFile.serverToken
|
||||
|
||||
if (outerErr.cause == null) throw outerErr
|
||||
const err = outerErr.cause
|
||||
if (serverToken != null) {
|
||||
this.uppy.log(`Connecting to exiting websocket ${serverToken}`)
|
||||
} else {
|
||||
serverToken = await this.#requestSocketTokenWithRetryStrategy({
|
||||
file: currentFile,
|
||||
postBody: reqBody,
|
||||
signal,
|
||||
})
|
||||
|
||||
const isRetryableHttpError = () =>
|
||||
[408, 409, 429, 418, 423].includes(err.statusCode) ||
|
||||
(err.statusCode >= 500 &&
|
||||
err.statusCode <= 599 &&
|
||||
![501, 505].includes(err.statusCode))
|
||||
if (err.name === 'HttpError' && !isRetryableHttpError())
|
||||
throw new AbortError(err)
|
||||
if (!this.uppy.getFile(file.id)) return undefined
|
||||
|
||||
// p-retry will retry most other errors,
|
||||
// but it will not retry TypeError (except network error TypeErrors)
|
||||
throw err
|
||||
this.uppy.setFileState(file.id, { serverToken })
|
||||
}
|
||||
|
||||
const latestFile = this.uppy.getFile(file.id) as
|
||||
| RemoteUppyFile<M, B>
|
||||
| undefined
|
||||
if (latestFile == null) return undefined
|
||||
|
||||
return this.#awaitRemoteFileUpload({
|
||||
file: latestFile,
|
||||
signal,
|
||||
})
|
||||
},
|
||||
{ priority: -1 },
|
||||
)
|
||||
|
||||
const serverToken = await queueRequestSocketToken({
|
||||
file,
|
||||
postBody: reqBody,
|
||||
signal,
|
||||
}).abortOn(signal)
|
||||
|
||||
if (!this.uppy.getFile(file.id)) return undefined // has file since been removed?
|
||||
|
||||
this.uppy.setFileState(file.id, { serverToken })
|
||||
|
||||
return this.#awaitRemoteFileUpload({
|
||||
file: this.uppy.getFile(file.id) as RemoteUppyFile<M, B>, // re-fetching file because it might have changed in the meantime
|
||||
queue: getQueue(),
|
||||
signal,
|
||||
})
|
||||
return queueRemoteUploadAttempt().abortOn(signal)
|
||||
},
|
||||
{
|
||||
retries: retryCount,
|
||||
|
|
@ -360,6 +335,39 @@ export default class RequestClient<M extends Meta, B extends Body> {
|
|||
return res.token
|
||||
}
|
||||
|
||||
#requestSocketTokenWithRetryStrategy = async ({
|
||||
file,
|
||||
postBody,
|
||||
signal,
|
||||
}: {
|
||||
file: RemoteUppyFile<M, B>
|
||||
postBody: Record<string, unknown>
|
||||
signal: AbortSignal
|
||||
}): Promise<string> => {
|
||||
try {
|
||||
return await this.#requestSocketToken({ file, postBody, signal })
|
||||
} catch (outerErr) {
|
||||
// throwing AbortError will cause p-retry to stop retrying
|
||||
if (outerErr.isAuthError) throw new AbortError(outerErr)
|
||||
|
||||
if (outerErr.cause == null) throw outerErr
|
||||
const err = outerErr.cause
|
||||
|
||||
const isRetryableHttpError = () =>
|
||||
[408, 409, 429, 418, 423].includes(err.statusCode) ||
|
||||
(err.statusCode >= 500 &&
|
||||
err.statusCode <= 599 &&
|
||||
![501, 505].includes(err.statusCode))
|
||||
if (err.name === 'HttpError' && !isRetryableHttpError()) {
|
||||
throw new AbortError(err)
|
||||
}
|
||||
|
||||
// p-retry will retry most other errors,
|
||||
// but it will not retry TypeError (except network error TypeErrors)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will ensure a websocket for the specified file and returns a promise that resolves
|
||||
* when the file has finished downloading, or rejects if it fails.
|
||||
|
|
@ -367,11 +375,9 @@ export default class RequestClient<M extends Meta, B extends Body> {
|
|||
*/
|
||||
async #awaitRemoteFileUpload({
|
||||
file,
|
||||
queue,
|
||||
signal,
|
||||
}: {
|
||||
file: RemoteUppyFile<M, B>
|
||||
queue: any
|
||||
signal: AbortSignal
|
||||
}): Promise<void> {
|
||||
let removeEventHandlers: () => void
|
||||
|
|
@ -430,126 +436,113 @@ export default class RequestClient<M extends Meta, B extends Body> {
|
|||
function resetActivityTimeout() {
|
||||
clearTimeout(activityTimeout)
|
||||
if (isPaused) return
|
||||
activityTimeout = setTimeout(
|
||||
() =>
|
||||
onFatalError(
|
||||
new Error(
|
||||
'Timeout waiting for message from Companion socket',
|
||||
),
|
||||
),
|
||||
socketActivityTimeoutMs,
|
||||
)
|
||||
activityTimeout = setTimeout(() => {
|
||||
onFatalError(
|
||||
new Error('Timeout waiting for message from Companion socket'),
|
||||
)
|
||||
}, socketActivityTimeoutMs)
|
||||
}
|
||||
|
||||
try {
|
||||
await queue
|
||||
.wrapPromiseFunction(async () => {
|
||||
const reconnectWebsocket = async () =>
|
||||
new Promise((_, rejectSocket) => {
|
||||
socket = new WebSocket(`${host}/api/${token}`)
|
||||
const reconnectWebsocket = async () =>
|
||||
new Promise((_, rejectSocket) => {
|
||||
socket = new WebSocket(`${host}/api/${token}`)
|
||||
|
||||
resetActivityTimeout()
|
||||
resetActivityTimeout()
|
||||
|
||||
socket.addEventListener('close', () => {
|
||||
socket = undefined
|
||||
rejectSocket(new Error('Socket closed unexpectedly'))
|
||||
})
|
||||
|
||||
socket.addEventListener('error', (error) => {
|
||||
this.uppy.log(
|
||||
`Companion socket error ${JSON.stringify(
|
||||
error,
|
||||
)}, closing socket`,
|
||||
'warning',
|
||||
)
|
||||
socket?.close() // will 'close' event to be emitted
|
||||
})
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
sendState()
|
||||
})
|
||||
|
||||
socket.addEventListener('message', (e) => {
|
||||
resetActivityTimeout()
|
||||
|
||||
try {
|
||||
const { action, payload } = JSON.parse(e.data)
|
||||
|
||||
switch (action) {
|
||||
case 'progress': {
|
||||
emitSocketProgress(
|
||||
this,
|
||||
payload,
|
||||
this.uppy.getFile(file.id),
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'success': {
|
||||
// payload.response is sent from companion for xhr-upload (aka uploadMultipart in companion) and
|
||||
// s3 multipart (aka uploadS3Multipart)
|
||||
// but not for tus/transloadit (aka uploadTus)
|
||||
// responseText is a string which may or may not be in JSON format
|
||||
// this means that an upload destination of xhr or s3 multipart MUST respond with valid JSON
|
||||
// to companion, or the JSON.parse will crash
|
||||
const text = payload.response?.responseText
|
||||
|
||||
this.uppy.emit(
|
||||
'upload-success',
|
||||
this.uppy.getFile(file.id),
|
||||
{
|
||||
uploadURL: payload.url,
|
||||
status: payload.response?.status ?? 200,
|
||||
body: text
|
||||
? (JSON.parse(text) as B)
|
||||
: undefined,
|
||||
},
|
||||
)
|
||||
socketAbortController?.abort?.()
|
||||
resolve()
|
||||
break
|
||||
}
|
||||
case 'error': {
|
||||
const { message } = payload.error
|
||||
throw Object.assign(new Error(message), {
|
||||
cause: payload.error,
|
||||
})
|
||||
}
|
||||
default:
|
||||
this.uppy.log(
|
||||
`Companion socket unknown action ${action}`,
|
||||
'warning',
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
onFatalError(err)
|
||||
}
|
||||
})
|
||||
|
||||
const closeSocket = () => {
|
||||
this.uppy.log(`Closing socket ${file.id}`)
|
||||
clearTimeout(activityTimeout)
|
||||
if (socket) socket.close()
|
||||
socket = undefined
|
||||
}
|
||||
|
||||
socketAbortController.signal.addEventListener(
|
||||
'abort',
|
||||
() => {
|
||||
closeSocket()
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
await pRetry(reconnectWebsocket, {
|
||||
retries: retryCount,
|
||||
signal: socketAbortController.signal,
|
||||
onFailedAttempt: () => {
|
||||
if (socketAbortController.signal.aborted) return // don't log in this case
|
||||
this.uppy.log(`Retrying websocket ${file.id}`)
|
||||
},
|
||||
socket.addEventListener('close', () => {
|
||||
socket = undefined
|
||||
rejectSocket(new Error('Socket closed unexpectedly'))
|
||||
})
|
||||
})()
|
||||
.abortOn(socketAbortController.signal)
|
||||
|
||||
socket.addEventListener('error', (error) => {
|
||||
this.uppy.log(
|
||||
`Companion socket error ${JSON.stringify(
|
||||
error,
|
||||
)}, closing socket`,
|
||||
'warning',
|
||||
)
|
||||
socket?.close() // will 'close' event to be emitted
|
||||
})
|
||||
|
||||
socket.addEventListener('open', () => {
|
||||
sendState()
|
||||
})
|
||||
|
||||
socket.addEventListener('message', (e) => {
|
||||
resetActivityTimeout()
|
||||
|
||||
try {
|
||||
const { action, payload } = JSON.parse(e.data)
|
||||
|
||||
switch (action) {
|
||||
case 'progress': {
|
||||
emitSocketProgress(
|
||||
this,
|
||||
payload,
|
||||
this.uppy.getFile(file.id),
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'success': {
|
||||
// payload.response is sent from companion for xhr-upload (aka uploadMultipart in companion) and
|
||||
// s3 multipart (aka uploadS3Multipart)
|
||||
// but not for tus/transloadit (aka uploadTus)
|
||||
// responseText is a string which may or may not be in JSON format
|
||||
// this means that an upload destination of xhr or s3 multipart MUST respond with valid JSON
|
||||
// to companion, or the JSON.parse will crash
|
||||
const text = payload.response?.responseText
|
||||
|
||||
this.uppy.emit(
|
||||
'upload-success',
|
||||
this.uppy.getFile(file.id),
|
||||
{
|
||||
uploadURL: payload.url,
|
||||
status: payload.response?.status ?? 200,
|
||||
body: text ? (JSON.parse(text) as B) : undefined,
|
||||
},
|
||||
)
|
||||
socketAbortController?.abort?.()
|
||||
resolve()
|
||||
break
|
||||
}
|
||||
case 'error': {
|
||||
const { message } = payload.error
|
||||
throw Object.assign(new Error(message), {
|
||||
cause: payload.error,
|
||||
})
|
||||
}
|
||||
default:
|
||||
this.uppy.log(
|
||||
`Companion socket unknown action ${action}`,
|
||||
'warning',
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
onFatalError(err)
|
||||
}
|
||||
})
|
||||
|
||||
const closeSocket = () => {
|
||||
this.uppy.log(`Closing socket ${file.id}`)
|
||||
clearTimeout(activityTimeout)
|
||||
if (socket) socket.close()
|
||||
socket = undefined
|
||||
}
|
||||
|
||||
socketAbortController.signal.addEventListener('abort', () => {
|
||||
closeSocket()
|
||||
})
|
||||
})
|
||||
|
||||
await pRetry(reconnectWebsocket, {
|
||||
retries: retryCount,
|
||||
signal: socketAbortController.signal,
|
||||
onFailedAttempt: () => {
|
||||
if (socketAbortController.signal.aborted) return // don't log in this case
|
||||
this.uppy.log(`Retrying websocket ${file.id}`)
|
||||
},
|
||||
})
|
||||
} catch (err) {
|
||||
if (socketAbortController.signal.aborted) return
|
||||
onFatalError(err)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import getAllowedHosts, { isOriginAllowed } from './getAllowedHosts.js'
|
||||
import getAllowedHosts from './getAllowedHosts.js'
|
||||
|
||||
describe('getAllowedHosts', () => {
|
||||
it('can convert companionAllowedHosts', () => {
|
||||
|
|
@ -30,20 +30,3 @@ describe('getAllowedHosts', () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isOriginAllowed', () => {
|
||||
it('should check origin', () => {
|
||||
expect(isOriginAllowed('a', [/^.+$/])).toBeTruthy()
|
||||
expect(isOriginAllowed('a', ['^.+$'])).toBeTruthy()
|
||||
expect(
|
||||
isOriginAllowed('www.transloadit.com', ['^www\\.transloadit\\.com$']),
|
||||
).toBeTruthy()
|
||||
expect(
|
||||
isOriginAllowed('www.transloadit.com', ['^transloadit\\.com$']),
|
||||
).toBeFalsy()
|
||||
expect(isOriginAllowed('match', ['fail', 'match'])).toBeTruthy()
|
||||
expect(
|
||||
isOriginAllowed('www.transloadit.com', ['\\.transloadit\\.com$']),
|
||||
).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -47,15 +47,3 @@ export default function getAllowedHosts(
|
|||
ret = escapeRegex(ret)
|
||||
return ret
|
||||
}
|
||||
|
||||
export function isOriginAllowed(
|
||||
origin: string,
|
||||
allowedOrigin: string | RegExp | Array<string | RegExp> | undefined,
|
||||
): boolean {
|
||||
const patterns = Array.isArray(allowedOrigin)
|
||||
? allowedOrigin.map(wrapInRegex)
|
||||
: [wrapInRegex(allowedOrigin)]
|
||||
return patterns.some(
|
||||
(pattern) => pattern?.test(origin) || pattern?.test(`${origin}/`),
|
||||
) // allowing for trailing '/'
|
||||
}
|
||||
|
|
|
|||
2
packages/@uppy/companion/.gitignore
vendored
2
packages/@uppy/companion/.gitignore
vendored
|
|
@ -34,6 +34,6 @@ test/output/*
|
|||
.DS_Store
|
||||
|
||||
# Transpiled
|
||||
./lib/
|
||||
./dist/
|
||||
infra/kube/companion/uppy-env.yaml
|
||||
scripts/.tl-deploy-hosts-danger.txt
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
# @uppy/companion
|
||||
|
||||
## 6.2.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- 49db42d: Fix bug with 429 not returning JSON response with message
|
||||
- 4652dc6: upgrade @aws-sdk/ deps in @uppy/companion
|
||||
|
||||
## 6.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ data:
|
|||
COMPANION_BOX_SECRET: 'YOUR BOX SECRET'
|
||||
COMPANION_GOOGLE_KEY: 'YOUR GOOGLE KEY'
|
||||
COMPANION_GOOGLE_SECRET: 'YOUR GOOGLE SECRET'
|
||||
COMPANION_INSTAGRAM_KEY: 'YOUR INSTAGRAM KEY'
|
||||
COMPANION_INSTAGRAM_SECRET: 'YOUR INSTAGRAM SECRET'
|
||||
COMPANION_AWS_KEY: 'YOUR AWS KEY'
|
||||
COMPANION_AWS_SECRET: 'YOUR AWS SECRET'
|
||||
COMPANION_AWS_BUCKET: 'YOUR AWS S3 BUCKET'
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ Companion is a server integration for
|
|||
[Uppy](https://github.com/transloadit/uppy) file uploader.
|
||||
|
||||
It handles the server-to-server communication between your server and file
|
||||
storage providers such as Google Drive, Dropbox, Instagram, etc. **Companion is
|
||||
storage providers such as Google Drive, Dropbox, etc. **Companion is
|
||||
not a target to upload files to**. For this, use a <https://tus.io> server (if
|
||||
you want resumable) or your existing Apache/Nginx server (if you don’t).
|
||||
[See here for full documentation](https://uppy.io/docs/companion/)
|
||||
|
|
@ -68,7 +68,10 @@ on, you call the `socket` method like so.
|
|||
// ...
|
||||
const server = app.listen(PORT)
|
||||
|
||||
companion.socket(server)
|
||||
// Pass the same `options` object you passed to `companion.app(options)` —
|
||||
// `companion.socket` needs `options.server` to compute the external base path
|
||||
// for incoming WS URLs (important behind reverse proxies).
|
||||
companion.socket(server, options)
|
||||
```
|
||||
|
||||
### Run as standalone server
|
||||
|
|
|
|||
|
|
@ -3,7 +3,14 @@ class Gauge {
|
|||
}
|
||||
|
||||
export default function () {
|
||||
const middleware = (req, res, next) => {
|
||||
type Req = { url?: string }
|
||||
type Res = {
|
||||
setHeader: (key: string, value: string) => void
|
||||
end: (s?: string) => void
|
||||
}
|
||||
type Next = () => void
|
||||
|
||||
const middleware = (req: Req, res: Res, next: Next) => {
|
||||
// simulate prometheus metrics endpoint:
|
||||
if (req.url === '/metrics') {
|
||||
res.setHeader('Content-Type', 'text/plain')
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
export class Upload {
|
||||
constructor(file, options) {
|
||||
this.url = 'https://tus.endpoint/files/foo-bar'
|
||||
this.options = options
|
||||
}
|
||||
|
||||
_triggerProgressThenSuccess() {
|
||||
this.options.onProgress(this.options.uploadSize, this.options.uploadSize)
|
||||
setTimeout(() => this.options.onSuccess(), 100)
|
||||
}
|
||||
|
||||
start() {
|
||||
setTimeout(this._triggerProgressThenSuccess.bind(this), 100)
|
||||
}
|
||||
|
||||
abort() {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
export default { Upload }
|
||||
42
packages/@uppy/companion/__mocks__/tus-js-client.ts
Normal file
42
packages/@uppy/companion/__mocks__/tus-js-client.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
type UploadOptions = {
|
||||
uploadSize: number
|
||||
onProgress: (bytesUploaded: number, bytesTotal: number) => void
|
||||
onSuccess: () => void
|
||||
} & Record<string, unknown>
|
||||
|
||||
let lastUploadFile: unknown
|
||||
|
||||
export function __getLastUploadFile(): unknown {
|
||||
return lastUploadFile
|
||||
}
|
||||
|
||||
export function __resetTusMockState(): void {
|
||||
lastUploadFile = undefined
|
||||
}
|
||||
|
||||
export class Upload {
|
||||
url: string
|
||||
|
||||
options: UploadOptions
|
||||
|
||||
constructor(file: unknown, options: UploadOptions) {
|
||||
lastUploadFile = file
|
||||
this.url = 'https://tus.endpoint/files/foo-bar'
|
||||
this.options = options
|
||||
}
|
||||
|
||||
_triggerProgressThenSuccess() {
|
||||
this.options.onProgress(this.options.uploadSize, this.options.uploadSize)
|
||||
setTimeout(() => this.options.onSuccess(), 100)
|
||||
}
|
||||
|
||||
start() {
|
||||
setTimeout(this._triggerProgressThenSuccess.bind(this), 100)
|
||||
}
|
||||
|
||||
abort() {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
export default { Upload }
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
#!/usr/bin/env node
|
||||
|
||||
import '../lib/standalone/start-server.js'
|
||||
|
|
@ -26,10 +26,6 @@ COMPANION_GOOGLE_KEY=
|
|||
COMPANION_GOOGLE_SECRET=
|
||||
COMPANION_GOOGLE_SECRET_FILE=
|
||||
|
||||
COMPANION_INSTAGRAM_KEY=
|
||||
COMPANION_INSTAGRAM_SECRET=
|
||||
COMPANION_INSTAGRAM_SECRET_FILE=
|
||||
|
||||
COMPANION_FACEBOOK_KEY=
|
||||
COMPANION_FACEBOOK_SECRET=
|
||||
COMPANION_FACEBOOK_SECRET_FILE=
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue