├── README.md
├── out
└── .keep
├── e2e
├── cypress
│ ├── fixtures
│ │ ├── images
│ │ │ ├── 3
│ │ │ ├── 55.jpg
│ │ │ ├── 11.png
│ │ │ ├── 2.jpg
│ │ │ ├── 22.png
│ │ │ ├── image.jpg
│ │ │ ├── kit.jpg
│ │ │ └── traffic.jpg
│ │ └── DeepFrozenStore.mjs
│ ├── integration
│ │ ├── vue.spec.ts
│ │ ├── ui.spec.ts
│ │ ├── tus.spec.ts
│ │ ├── reusable.ts
│ │ ├── main.spec.ts
│ │ └── react.spec.ts
│ └── support
│ │ ├── index.ts
│ │ ├── e2e.ts
│ │ ├── commands.ts
│ │ └── createFile.ts
├── clients
│ ├── dashboard-vue
│ │ ├── index.js
│ │ ├── index.html
│ │ └── App.vue
│ ├── dashboard-compressor
│ │ ├── index.html
│ │ └── app.js
│ ├── dashboard-ui
│ │ ├── index.html
│ │ └── app.js
│ ├── dashboard-aws
│ │ ├── index.html
│ │ └── app.js
│ ├── dashboard-tus
│ │ ├── index.html
│ │ └── app.js
│ ├── dashboard-transloadit
│ │ ├── index.html
│ │ ├── app.js
│ │ └── generateSignatureIfSecret.js
│ ├── dashboard-aws-multipart
│ │ ├── index.html
│ │ └── app.js
│ └── index.html
├── .parcelrc
├── tsconfig.json
├── cypress.config.mjs
├── mock-server.mjs
├── package.json
├── start-companion-with-load-balancer.mjs
└── generate-test.mjs
├── packages
├── @ImgCypress_1
│ ├── box
│ │ ├── src
│ │ │ ├── index.js
│ │ │ ├── locale.js
│ │ │ └── Box.jsx
│ │ ├── types
│ │ │ ├── index.test-d.ts
│ │ │ └── index.d.ts
│ │ ├── package.json
│ │ ├── LICENSE
│ │ ├── README.md
│ │ └── CHANGELOG.md
│ ├── aws-s3
│ │ ├── src
│ │ │ ├── locale.js
│ │ │ ├── isXml.js
│ │ │ ├── index.test.js
│ │ │ ├── isXml.test.js
│ │ │ ├── index.js
│ │ │ └── MiniXHRUpload.js
│ │ ├── package.json
│ │ └── types
│ │ │ ├── index.d.ts
│ │ │ └── index.test-d.ts
│ ├── aws-s3-multipart
│ │ ├── types
│ │ │ ├── chunk.d.ts
│ │ │ ├── index.test-d.ts
│ │ │ └── index.d.ts
│ │ ├── package.json
│ │ └── src
│ │ │ ├── createSignedURL.test.js
│ │ │ ├── createSignedURL.js
│ │ │ └── MultipartUploader.js
│ ├── companion-client
│ │ ├── src
│ │ │ ├── index.js
│ │ │ ├── AuthError.js
│ │ │ ├── tokenStorage.js
│ │ │ ├── RequestClient.test.js
│ │ │ ├── SearchProvider.js
│ │ │ ├── Socket.js
│ │ │ ├── Socket.test.js
│ │ │ ├── Provider.js
│ │ │ └── RequestClient.js
│ │ ├── package.json
│ │ └── types
│ │ │ └── index.d.ts
│ └── url
│ │ └── types
│ │ └── index.d.ts
└── ImgCypress_1
│ └── .npmignore
├── .prettierignore
├── .eslintignore
├── .remarkignore
├── .browserslistrc
├── .prettierrc.js
├── .stylelintrc.json
├── .editorconfig
├── bin
├── to-gif-hq.sh
├── to-gif-hd.sh
├── companion.sh
├── to-gif.sh
├── build-ts.mjs
├── update-yarn.sh
├── build-bundle.mjs
├── build-bundleTest.mjs
├── build-css.js
└── build-lib.js
├── .yarnrc.yml
├── .gitignore
├── babel.config.js
├── LICENSE
├── .env.example
├── package.json
└── .eslintrc.js
/README.md:
--------------------------------------------------------------------------------
1 | # ImgCypress
--------------------------------------------------------------------------------
/out/.keep:
--------------------------------------------------------------------------------
1 | #test
2 |
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/3:
--------------------------------------------------------------------------------
1 | ./cat.jpg
2 |
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/55.jpg:
--------------------------------------------------------------------------------
1 | ./cat.jpg
2 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/box/src/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Box.jsx'
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.js
3 | *.jsx
4 | *.cjs
5 | *.mjs
6 | !private/js2ts/*
7 | *.md
8 | *.lock
9 |
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiFrztl/ImgCypress/HEAD/e2e/cypress/fixtures/images/11.png
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiFrztl/ImgCypress/HEAD/e2e/cypress/fixtures/images/2.jpg
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiFrztl/ImgCypress/HEAD/e2e/cypress/fixtures/images/22.png
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiFrztl/ImgCypress/HEAD/e2e/cypress/fixtures/images/image.jpg
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/kit.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiFrztl/ImgCypress/HEAD/e2e/cypress/fixtures/images/kit.jpg
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/box/src/locale.js:
--------------------------------------------------------------------------------
1 | export default {
2 | strings: {
3 | pluginNameBox: 'Box',
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/packages/ImgCypress_1/.npmignore:
--------------------------------------------------------------------------------
1 | # This file need to be there so .gitignored files are still uploaded to the npm registry.
2 |
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/traffic.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MiFrztl/ImgCypress/HEAD/e2e/cypress/fixtures/images/traffic.jpg
--------------------------------------------------------------------------------
/e2e/clients/dashboard-vue/index.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import App from './App.vue'
3 |
4 | createApp(App).mount('#app')
5 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | lib
3 | dist
4 | coverage
5 | test/lib/**
6 | test/endtoend/*/build
7 | examples/svelte-example/public/build/
8 | bundle-legacy.js
9 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3/src/locale.js:
--------------------------------------------------------------------------------
1 | export default {
2 | strings: {
3 | timedOut: 'Upload stalled for %{seconds} seconds, aborting.',
4 | },
5 | }
6 |
--------------------------------------------------------------------------------
/.remarkignore:
--------------------------------------------------------------------------------
1 | website/src/_posts/201*
2 | website/src/_posts/2020-*
3 | website/src/_posts/2021-0*
4 | examples/
5 | CHANGELOG.md
6 | CHANGELOG.next.md
7 | BACKLOG.md
8 | node_modules/
9 |
--------------------------------------------------------------------------------
/e2e/.parcelrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@parcel/config-default",
3 | "transformers": {
4 | "*.{js,mjs,jsx,cjs,ts,tsx}": [
5 | "@parcel/transformer-js",
6 | "@parcel/transformer-react-refresh-wrap"
7 | ]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.browserslistrc:
--------------------------------------------------------------------------------
1 | [production]
2 | last 2 Safari versions
3 | last 2 Chrome versions
4 | last 2 ChromeAndroid versions
5 | last 2 Firefox versions
6 | last 2 FirefoxAndroid versions
7 | last 2 Edge versions
8 | iOS >=13.4
9 |
10 | [legacy]
11 | IE 11
12 |
--------------------------------------------------------------------------------
/e2e/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "NodeNext",
4 | "noEmit": true,
5 | "target": "es2020",
6 | "lib": ["es2020", "dom"],
7 | "types": ["cypress"]
8 | },
9 | "include": ["cypress/**/*.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3-multipart/types/chunk.d.ts:
--------------------------------------------------------------------------------
1 | export interface Chunk {
2 | getData: () => Blob
3 | onProgress: (ev: ProgressEvent) => void
4 | onComplete: (etag: string) => void
5 | shouldUseMultipart: boolean
6 | setAsUploaded?: () => void
7 | }
8 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-compressor/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | dashboard-compressor
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | dashboard-ui
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | proseWrap: 'always',
3 | singleQuote: true,
4 | trailingComma: 'all',
5 | semi: false,
6 | overrides: [
7 | {
8 | files: 'packages/@Imagin/angular/**',
9 | options: {
10 | semi: true,
11 | },
12 | },
13 | ],
14 | }
15 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-aws/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | dashboard-aws
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-tus/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | dashboard-tus
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-vue/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | dashboard-vue
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "stylelint-config-standard",
4 | "stylelint-config-standard-scss",
5 | "stylelint-config-rational-order"
6 | ],
7 | "rules": {
8 | "at-rule-no-unknown": null,
9 | "scss/at-rule-no-unknown": true
10 | },
11 | "defaultSeverity": "warning"
12 | }
13 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-transloadit/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | dashboard-transloadit
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/box/types/index.test-d.ts:
--------------------------------------------------------------------------------
1 | import Imagin from '@Imagin/core'
2 | import Box from '..'
3 |
4 | {
5 | const Imagin = new Imagin()
6 | Imagin.use(Box, {
7 | companionUrl: '',
8 | companionCookiesRule: 'same-origin',
9 | target: 'body',
10 | title: 'title',
11 | })
12 | }
13 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-aws-multipart/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | dashboard-aws-multipart
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | ; This file is for unifying the coding style for different editors and IDEs.
2 | ; More information at http://editorconfig.org
3 |
4 | root = true
5 |
6 | [*]
7 | charset = utf-8
8 | indent_style = space
9 | indent_size = 2
10 | end_of_line = lf
11 | insert_final_newline = true
12 | trim_trailing_whitespace = true
13 |
--------------------------------------------------------------------------------
/bin/to-gif-hq.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Convert a video file to a gif.
3 | # `to-gif /path/to/input.mp4 /path/to/output.gif`
4 | palette="/tmp/to-gif-palette.png"
5 | filters="fps=15"
6 | ffmpeg -v warning -i $1 -vf "$filters,palettegen" -y $palette
7 | ffmpeg -v warning -i $1 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $2
8 |
9 | # gifsicle --resize-fit-width 1000 -i animation.gif > animation-1000px.gif
10 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | changesetBaseRefs:
2 | - main
3 | - upstream/main
4 | - origin/main
5 |
6 | initScope: ImgCypress_1
7 |
8 | enableGlobalCache: false
9 | nodeLinker: node-modules
10 |
11 | plugins:
12 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs
13 | spec: '@yarnpkg/plugin-workspace-tools'
14 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs
15 | spec: '@yarnpkg/plugin-version'
16 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-aws/app.js:
--------------------------------------------------------------------------------
1 | import '@Imagin/core/dist/style.css'
2 | import '@Imagin/dashboard/dist/style.css'
3 | import Dashboard from '@Imagin/dashboard'
4 | import AwsS3 from '@Imagin/aws-s3'
5 |
6 |
7 |
8 | const Imagin = new Imagin()
9 | .use(Dashboard, { target: '#app', inline: true })
10 | .use(AwsS3, {
11 | limit: 2,
12 | companionUrl: process.env.VITE_COMPANION_URL,
13 | })
14 |
15 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/companion-client/src/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * Manages communications with Companion
5 | */
6 |
7 | export { default as RequestClient } from './RequestClient.js'
8 | export { default as Provider } from './Provider.js'
9 | export { default as SearchProvider } from './SearchProvider.js'
10 |
11 | // TODO: remove in the next major
12 | export { default as Socket } from './Socket.js'
13 |
--------------------------------------------------------------------------------
/bin/to-gif-hd.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # Convert a video file to a gif.
3 | # `to-gif /path/to/input.mp4 /path/to/output.gif`
4 | palette="/tmp/to-gif-palette.png"
5 | filters="fps=15"
6 | ffmpeg -v warning -i $1 -vf "$filters,palettegen" -y $palette
7 | ffmpeg -v warning -i $1 -i $palette -lavfi "$filters [x]; [x][1:v] paletteuse" -y $2
8 |
9 | # resize after
10 | # gifsicle --resize-fit-width 1000 -i animation.gif > animation-1000px.gif
11 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/companion-client/src/AuthError.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | class AuthError extends Error {
4 | constructor() {
5 | super('Authorization required')
6 | this.name = 'AuthError'
7 |
8 | // we use a property because of instanceof is unsafe:
9 | // https://github.com/transloadit/uppy/pull/4619#discussion_r1406225982
10 | this.isAuthError = true
11 | }
12 | }
13 |
14 | export default AuthError
15 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/box/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { PluginTarget, UIPlugin, UIPluginOptions } from '@Imagin/core'
2 | import type {
3 | PublicProviderOptions,
4 | TokenStorage,
5 | } from '@Imagin/companion-client'
6 |
7 | interface BoxOptions extends UIPluginOptions, PublicProviderOptions {
8 | target?: PluginTarget
9 | title?: string
10 | storage?: TokenStorage
11 | }
12 |
13 | declare class Box extends UIPlugin {}
14 |
15 | export default Box
16 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-compressor/app.js:
--------------------------------------------------------------------------------
1 | import Compressor from '@Imagin/compressor'
2 | import Dashboard from '@Imagin/dashboard'
3 | import '@Imagin/core/dist/style.css'
4 | import '@Imagin/dashboard/dist/style.css'
5 |
6 | const Imagin = new Imagin()
7 | .use(Dashboard, {
8 | target: document.body,
9 | inline: true,
10 | })
11 | .use(Compressor, {
12 | mimeType: 'image/webp',
13 | })
14 |
15 | // Keep this here to access Imagin in tests
16 | window.Imagin = Imagin
17 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-vue/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/companion-client/src/tokenStorage.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * This module serves as an Async wrapper for LocalStorage
5 | */
6 | export function setItem (key, value) {
7 | return new Promise((resolve) => {
8 | localStorage.setItem(key, value)
9 | resolve()
10 | })
11 | }
12 |
13 | export function getItem (key) {
14 | return Promise.resolve(localStorage.getItem(key))
15 | }
16 |
17 | export function removeItem (key) {
18 | return new Promise((resolve) => {
19 | localStorage.removeItem(key)
20 | resolve()
21 | })
22 | }
23 |
--------------------------------------------------------------------------------
/e2e/cypress.config.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'cypress'
2 | import installLogsPrinter from 'cypress-terminal-report/src/installLogsPrinter.js'
3 | import startMockServer from './mock-server.mjs'
4 |
5 | export default defineConfig({
6 | defaultCommandTimeout: 16_000,
7 | requestTimeout: 16_000,
8 |
9 | e2e: {
10 | baseUrl: 'http://localhost:8090',
11 | specPattern: 'cypress/integration/*main.spec.ts',
12 |
13 | setupNodeEvents (on) {
14 | // implement node event listeners here
15 | installLogsPrinter(on)
16 |
17 | startMockServer('localhost', 9999)
18 | },
19 | },
20 | })
21 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/companion-client/src/RequestClient.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest'
2 | import RequestClient from './RequestClient.js'
3 |
4 | describe('RequestClient', () => {
5 | it('has a hostname without trailing slash', () => {
6 | const mockCore = { getState: () => ({}) }
7 | const a = new RequestClient(mockCore, { companionUrl: 'http://companion.uppy.io' })
8 | const b = new RequestClient(mockCore, { companionUrl: 'http://companion.uppy.io/' })
9 |
10 | expect(a.hostname).toBe('http://companion.uppy.io')
11 | expect(b.hostname).toBe('http://companion.uppy.io')
12 | })
13 | })
14 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/url/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { RequestClientOptions } from '@ImgCypress_1/companion-client'
2 | import type {
3 | IndexedObject,
4 | PluginTarget,
5 | UIPlugin,
6 | UIPluginOptions,
7 | } from '@ImgCypress_1/core'
8 | import UrlLocale from './generatedLocale'
9 |
10 | export interface UrlOptions extends UIPluginOptions, RequestClientOptions {
11 | target?: PluginTarget
12 | title?: string
13 | locale?: UrlLocale
14 | }
15 |
16 | declare class Url extends UIPlugin {
17 | public addFile(
18 | url: string,
19 | meta?: IndexedObject,
20 | ): undefined | string | never
21 | }
22 |
23 | export default Url
24 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3/src/isXml.js:
--------------------------------------------------------------------------------
1 |
2 | function removeMimeParams (mimeType) {
3 | return mimeType.replace(/;.*$/, '')
4 | }
5 |
6 | sXml (content, xhr) {
7 | const rawContentType = (xhr.headers ? xhr.headers['content-type'] : xhr.getResponseHeader('Content-Type'))
8 |
9 | if (typeof rawContentType === 'string') {
10 | const contentType = removeMimeParams(rawContentType).toLowerCase()
11 | if (contentType === 'application/xml' || contentType === 'text/xml') {
12 | return true
13 | }
14 | if (contentType === 'text/html' && /^<\?xml /.test(content)) {
15 | return true
16 | }
17 | }
18 | return false
19 | }
20 |
21 | export default isXml
22 |
--------------------------------------------------------------------------------
/bin/companion.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Load local env vars. In CI, these are injected.
4 | if [ -f .env ]; then
5 | nodemon --watch packages/@Imagin/companion/src --exec node -r dotenv/config ./packages/@Imagin/companion/src/standalone/start-server.js
6 | else
7 | env \
8 | COMPANION_DATADIR="./aoutput" \
9 | COMPANION_DOMAIN="localhost:3030" \
10 | COMPANION_PROTOCOL="https" \
11 | COMPANION_PORT=3030 \
12 | COMPANION_CLIENT_ORIGINS="" \
13 | COMPANION_SECRET="stg" \
14 | COMPANION_PREAUTH_SECRET="stg" \
15 | COMPANION_ALLOW_LOCAL_URLS="true" \
16 | nodemon --watch packages/@Imagin/companion/src --exec node ./packages/@Imagin/companion/src/standalone/start-server.js
17 | fi
18 |
19 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/vue.spec.ts:
--------------------------------------------------------------------------------
1 | describe('dashboard-vue', () => {
2 | beforeEach(() => {
3 | cy.visit('/dashboard-vue')
4 | })
5 |
6 | // Only Vue 3 works in Parcel if you use SFC's but Vue 3 is broken in Imagin:
7 | // https://github.com/transloadit/Imagin/issues/2877
8 | xit('should render in Vue 3 and show thumbnails', () => {
9 | cy.get('@file-input').selectFile(
10 | [
11 | 'cypress/fixtures/images/kit.jpg',
12 | 'cypress/fixtures/images/traffic.jpg',
13 | ],
14 | { force: true },
15 | )
16 | cy.get('.Imagin-Dashboard-Item-previewImg')
17 | .should('have.length', 2)
18 | .each((element) => expect(element).attr('src').to.include('blob:'))
19 | })
20 | })
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | Thumbs.db
3 | npm-debug.log
4 | npm-debug.log*
5 | nohup.out
6 | node_modules
7 | .angular
8 | .cache
9 | .parcel-cache
10 | .eslintcache
11 | .vscode/settings.json
12 | .yarn/cache
13 | .yarn/install-state.gz
14 | yarn-error.log
15 | .idea
16 | .env
17 | tsconfig.tsbuildinfo
18 | tsconfig.build.tsbuildinfo
19 |
20 | dist/
21 | lib/
22 | coverage/
23 | examples/dev/bundle.js
24 | examples/aws-php/vendor/*
25 | test/endtoend/create-react-app/build/
26 | test/endtoend/create-react-app/coverage/
27 | ImgCypress_1-*.tgz
28 | generatedLocale.d.ts
29 |
30 | **/output/*
31 | examples/dev/file.txt
32 | issues.txt
33 |
34 | # companion deployment files
35 | transloadit-cluster-kubeconfig.yaml
36 | companion-env.yml
37 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-tus/app.js:
--------------------------------------------------------------------------------
1 | import Dashboard from '@Imagin/dashboard'
2 | import Tus from '@Imagin/tus'
3 | import Unsplash from '@Imagin/unsplash'
4 | import Url from '@Imagin/url'
5 |
6 | import '@Imagin/core/dist/style.css'
7 | import '@Imagin/dashboard/dist/style.css'
8 |
9 | function onShouldRetry (err, retryAttempt, options, next) {
10 | if (err?.originalResponse?.getStatus() === 418) {
11 | return true
12 | }
13 | return next(err)
14 | }
15 |
16 | const companionUrl = 'http://localhost:3020'
17 | const Imagin = new Imagin()
18 | .use(Dashboard, { target: '#app', inline: true })
19 | .use(Tus, { endpoint: 'https://tusd.tusdemo.net/files', onShouldRetry })
20 | .use(Url, { target: Dashboard, companionUrl })
21 | .use(Unsplash, { target: Dashboard, companionUrl })
22 |
--------------------------------------------------------------------------------
/e2e/cypress/support/index.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/index.js is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | import './commands.ts'
17 |
18 | import type { Imagin } from '@Imagin/core'
19 |
20 | declare global {
21 | interface Window {
22 | Imagin: Imagin
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/companion-client/src/SearchProvider.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import RequestClient from './RequestClient.js'
4 |
5 | const getName = (id) => {
6 | return id.split('-').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(' ')
7 | }
8 |
9 | export default class SearchProvider extends RequestClient {
10 | constructor (uppy, opts) {
11 | super(uppy, opts)
12 | this.provider = opts.provider
13 | this.id = this.provider
14 | this.name = this.opts.name || getName(this.id)
15 | this.pluginId = this.opts.pluginId
16 | }
17 |
18 | fileUrl (id) {
19 | return `${this.hostname}/search/${this.id}/get/${id}`
20 | }
21 |
22 | search (text, queries) {
23 | return this.get(`search/${this.id}/list?q=${encodeURIComponent(text)}${queries ? `&${queries}` : ''}`)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/e2e/mock-server.mjs:
--------------------------------------------------------------------------------
1 | import http from 'node:http'
2 | export default function startMockServer (host, port) {
3 | const server = http.createServer(requestListener)
4 | server.listen(port, host, () => {
5 | console.log(`Server is running on http://${host}:${port}`)
6 | })
7 | const requestListener = (req, res) => {
8 | const endpoint = req.url
9 |
10 | switch (endpoint) {
11 | case '/file-with-content-disposition': {
12 | const fileName = `IMG`
13 | res.setHeader('Content-Disposition', `attachment; filename="ASCII-name.zip"; filename*=UTF-8''${encodeURIComponent(fileName)}`)
14 | res.setHeader('Content-Type', 'image/jpeg')
15 | res.setHeader('Content-Length', '86500')
16 | break
17 | }
18 | case '/file-no-headers':
19 | break
20 | default:
21 | res.writeHead(404).end('Unhandled req')
22 | }
23 |
24 | res.end()
25 | }
26 |
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/companion-client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@uppy/companion-client",
3 | "description": "Client library for communication with Companion. Intended for use in Uppy plugins.",
4 | "version": "3.6.1",
5 | "license": "MIT",
6 | "main": "lib/index.js",
7 | "types": "types/index.d.ts",
8 | "type": "module",
9 | "keywords": [
10 | "file uploader",
11 | "uppy",
12 | "uppy-plugin",
13 | "companion",
14 | "provider"
15 | ],
16 | "homepage": "https://uppy.io",
17 | "bugs": {
18 | "url": "https://github.com/transloadit/uppy/issues"
19 | },
20 | "repository": {
21 | "type": "git",
22 | "url": "git+https://github.com/transloadit/uppy.git"
23 | },
24 | "dependencies": {
25 | "@uppy/utils": "workspace:^",
26 | "namespace-emitter": "^2.0.1",
27 | "p-retry": "^6.1.0"
28 | },
29 | "devDependencies": {
30 | "vitest": "^0.34.5"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-transloadit/app.js:
--------------------------------------------------------------------------------
1 | import { Imagin } from '@Imagin/core'
2 | import Dashboard from '@Imagin/dashboard'
3 | import Transloadit from '@Imagin/transloadit'
4 |
5 | import generateSignatureIfSecret from './generateSignatureIfSecret.js'
6 |
7 | import '@Imagin/core/dist/style.css'
8 | import '@Imagin/dashboard/dist/style.css'
9 |
10 | // Environment variables:
11 | // https://en.parceljs.org/env.html
12 | const Imagin = new Imagin()
13 | .use(Dashboard, { target: '#app', inline: true })
14 | .use(Transloadit, {
15 | service: process.env.VITE_TRANSLOADIT_SERVICE_URL,
16 | waitForEncoding: true,
17 | getAssemblyOptions: () => generateSignatureIfSecret(process.env.VITE_TRANSLOADIT_SECRET, {
18 | auth: { key: process.env.VITE_TRANSLOADIT_KEY },
19 | template_id: process.env.VITE_TRANSLOADIT_TEMPLATE,
20 | }),
21 | })
22 |
23 | // Keep this here to access Imagin in tests
24 | window.Imagin = Imagin
25 |
--------------------------------------------------------------------------------
/e2e/cypress/support/e2e.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************************
2 | // This example support/e2e.ts is processed and
3 | // loaded automatically before your test files.
4 | //
5 | // This is a great place to put global configuration and
6 | // behavior that modifies Cypress.
7 | //
8 | // You can change the location of this file or turn off
9 | // automatically serving support files with the
10 | // 'supportFile' configuration option.
11 | //
12 | // You can read more here:
13 | // https://on.cypress.io/configuration
14 | // ***********************************************************
15 |
16 | // Import commands.js using ES2015 syntax:
17 | import './commands'
18 |
19 | // Alternatively you can use CommonJS syntax:
20 | // require('./commands')
21 |
22 | // eslint-disable-next-line
23 | // @ts-ignore
24 | import installLogsCollector from 'cypress-terminal-report/src/installLogsCollector.js'
25 |
26 | installLogsCollector()
27 |
--------------------------------------------------------------------------------
/bin/to-gif.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -o nounset
3 | set -o pipefail
4 | set -o errexit
5 |
6 | # Set magic variables for current file & dir
7 | __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8 | __file="${__dir}/$(basename "${BASH_SOURCE[0]}")"
9 | __base="$(basename ${__file} .sh)"
10 | __root="$(cd "$(dirname "${__dir}")" && pwd)"
11 |
12 | speed=0.7
13 | input="${__root}/assets/Imagin-demo-oct-2018.mov"
14 | width=600
15 | base="$(basename "${input}")"
16 | output="${__root}/assets/${base}.gif"
17 |
18 | ffmpeg \
19 | -y \
20 | -i "${input}" \
21 | -vf fps=10,scale=${width}:-1:flags=lanczos,palettegen "${__root}/assets/${base}-palette.png"
22 |
23 | ffmpeg \
24 | -y \
25 | -i "${input}" \
26 | -i "${__root}/assets/${base}-palette.png" \
27 | -filter_complex "setpts=${speed}*PTS,fps=10,scale=${width}:-1:flags=lanczos[x];[x][1:v]paletteuse" \
28 | "${output}"
29 |
30 | du -hs "${output}"
31 | open -a 'Google Chrome' "${output}"
32 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-aws-multipart/app.js:
--------------------------------------------------------------------------------
1 | import Dashboard from '@Imagin/dashboard'
2 | import AwsS3Multipart from '@Imagin/aws-s3-multipart'
3 |
4 | import '@Imagin/core/dist/style.css'
5 | import '@Imagin/dashboard/dist/style.css'
6 |
7 | //#TODO tests
8 | const Imagin = new Imagin()
9 | .use(Dashboard, { target: '#app', inline: true })
10 | .use(AwsS3Multipart, {
11 | limit: 2,
12 | companionUrl: process.env.VITE_COMPANION_URL,
13 | // This way we can test that the user provided API still works
14 | async prepareUploadParts (file, { uploadId, key, parts, signal }) {
15 | const { number: partNumber, chunk: body } = parts[0]
16 | const plugin = Imagin.getPlugin('AwsS3Multipart')
17 | const { url } = await plugin.signPart(file, { uploadId, key, partNumber, body, signal })
18 | return { presignedUrls: { [partNumber]: url } }
19 | },
20 | })
21 |
22 | // Keep this here to access Imagin in tests
23 | window.Imagin = Imagin
24 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = (api) => {
2 | const targets = {}
3 | if (api.env('test')) {
4 | targets.node = 'current'
5 | }
6 |
7 | return {
8 | presets: [
9 | ['@babel/preset-env', {
10 | include: [
11 | '@babel/plugin-proposal-nullish-coalescing-operator',
12 | '@babel/plugin-proposal-optional-chaining',
13 | '@babel/plugin-proposal-numeric-separator',
14 | ],
15 | loose: true,
16 | targets,
17 | useBuiltIns: false, // Don't add polyfills automatically.
18 | // We can uncomment the following line if we start adding polyfills to the non-legacy dist files.
19 | // corejs: { version: '3.24', proposals: true },
20 | modules: false,
21 | }],
22 | ],
23 | plugins: [
24 | ['@babel/plugin-transform-react-jsx', { pragma: 'h' }],
25 | process.env.NODE_ENV !== 'dev' && 'babel-plugin-inline-package-json',
26 | ].filter(Boolean),
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/box/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@Imagin/box",
3 | "description": "Import files from Box, into Imagin.",
4 | "version": "2.1.4",
5 | "license": "MIT",
6 | "main": "lib/index.js",
7 | "type": "module",
8 | "types": "types/index.d.ts",
9 | "keywords": [
10 | "file uploader",
11 | "Imagin",
12 | "Imagin-plugin",
13 | "box"
14 | ],
15 | "homepage": "https://Imagin.io",
16 | "bugs": {
17 | "url": "https://github.com/transloadit/Imagin/issues"
18 | },
19 | "repository": {
20 | "type": "git",
21 | "url": "git+https://github.com/transloadit/Imagin.git"
22 | },
23 | "dependencies": {
24 | "@Imagin/companion-client": "workspace:^",
25 | "@Imagin/provider-views": "workspace:^",
26 | "@Imagin/utils": "workspace:^",
27 | "preact": "^10.5.13"
28 | },
29 | "peerDependencies": {
30 | "@Imagin/core": "workspace:^"
31 | },
32 | "publishConfig": {
33 | "access": "public"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/e2e/cypress/support/commands.ts:
--------------------------------------------------------------------------------
1 | // ***********************************************
2 | // This example commands.js shows you how to
3 | // create various custom commands and overwrite
4 | // existing commands.
5 | //
6 | // For more comprehensive examples of custom
7 | // commands please read more here:
8 | // https://on.cypress.io/custom-commands
9 | // ***********************************************
10 | //
11 | //
12 | // -- This is a parent command --
13 | // Cypress.Commands.add('login', (email, password) => { ... })
14 | //
15 | //
16 | // -- This is a child command --
17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
18 | //
19 | //
20 | // -- This is a dual command --
21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
22 | //
23 | //
24 | // -- This will overwrite an existing command --
25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
26 | //
27 |
28 | import { createFile } from './createFile'
29 |
30 | Cypress.Commands.add('createFakeFile', createFile)
31 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@uppy/aws-s3",
3 | "description": "Upload to Amazon S3 with Uppy",
4 | "version": "3.5.0",
5 | "license": "MIT",
6 | "main": "lib/index.js",
7 | "type": "module",
8 | "types": "types/index.d.ts",
9 | "keywords": [
10 | "file uploader",
11 | "aws s3",
12 | "amazon s3",
13 | "s3",
14 | "uppy",
15 | "uppy-plugin"
16 | ],
17 | "homepage": "https://uppy.io",
18 | "bugs": {
19 | "url": "https://github.com/transloadit/uppy/issues"
20 | },
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/transloadit/uppy.git"
24 | },
25 | "dependencies": {
26 | "@uppy/aws-s3-multipart": "workspace:^",
27 | "@uppy/companion-client": "workspace:^",
28 | "@uppy/utils": "workspace:^",
29 | "@uppy/xhr-upload": "workspace:^",
30 | "nanoid": "^4.0.0"
31 | },
32 | "devDependencies": {
33 | "vitest": "^0.34.5",
34 | "whatwg-fetch": "3.6.2"
35 | },
36 | "peerDependencies": {
37 | "@uppy/core": "workspace:^"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/e2e/clients/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | End-to-End test suite
6 |
7 |
8 | Test apps
9 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/DeepFrozenStore.mjs:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line import/no-extraneous-dependencies
2 | import deepFreeze from 'deep-freeze'
3 |
4 | /* eslint-disable no-underscore-dangle */
5 |
6 | /**
7 | * Default store + deepFreeze on setState to make sure nothing is mutated accidentally
8 | */
9 | class DeepFrozenSt {
10 | constructor () {
11 | this.state = {}
12 | this.callbacks = []
13 | }
14 |
15 | getState = () => this.state;
16 |
17 | setState (patch) {
18 | const nextState = deepFreeze({ ...this.state, ...patch });
19 |
20 | this._publish(this.state, nextState, patch)
21 | this.state = nextState
22 |
23 | }
24 |
25 | subscribe (listener) {
26 | this.callbacks.push(listener)
27 | return () => {
28 | // Remove the listener.
29 | this.callbacks.splice(
30 | this.callbacks.indexOf(listener),
31 | 1,
32 | )
33 | }
34 | }
35 |
36 | _publish (...args) {
37 | this.callbacks.forEach((listener) => {
38 | listener(...args)
39 | })
40 | }
41 | }
42 |
43 | export default function defaultStore () {
44 | return new DeepFrozenSt()
45 | }
46 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3-multipart/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@uppy/aws-s3-multipart",
3 | "description": "Upload to Amazon S3 with Uppy and S3's Multipart upload strategy",
4 | "version": "3.9.0",
5 | "license": "MIT",
6 | "main": "lib/index.js",
7 | "type": "module",
8 | "types": "types/index.d.ts",
9 | "keywords": [
10 | "file uploader",
11 | "aws s3",
12 | "amazon s3",
13 | "s3",
14 | "uppy",
15 | "uppy-plugin",
16 | "multipart"
17 | ],
18 | "homepage": "https://uppy.io",
19 | "bugs": {
20 | "url": "https://github.com/transloadit/uppy/issues"
21 | },
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/transloadit/uppy.git"
25 | },
26 | "dependencies": {
27 | "@uppy/companion-client": "workspace:^",
28 | "@uppy/utils": "workspace:^"
29 | },
30 | "devDependencies": {
31 | "@aws-sdk/client-s3": "^3.362.0",
32 | "@aws-sdk/s3-request-presigner": "^3.362.0",
33 | "nock": "^13.1.0",
34 | "vitest": "^0.34.5",
35 | "whatwg-fetch": "3.6.2"
36 | },
37 | "peerDependencies": {
38 | "@uppy/core": "workspace:^"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Transloadit (https://transloadit.com)
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/box/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Transloadit
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-ui/app.js:
--------------------------------------------------------------------------------
1 | import Imagin from '@Imagin/core'
2 | import Dashboard from '@Imagin/dashboard'
3 | import RemoteSources from '@Imagin/remote-sources'
4 | import Webcam from '@Imagin/webcam'
5 | import ScreenCapture from '@Imagin/screen-capture'
6 | import GoldenRetriever from '@Imagin/golden-retriever'
7 | import ImageEditor from '@Imagin/image-editor'
8 | import DropTarget from '@Imagin/drop-target'
9 | import Audio from '@Imagin/audio'
10 | import Compressor from '@Imagin/compressor'
11 |
12 | import '@Imagin/core/dist/style.css'
13 | import '@Imagin/dashboard/dist/style.css'
14 |
15 | const COMPANION_URL = 'http://companion.Imagin.io'
16 |
17 | const Imagin = new Imagin()
18 | .use(Dashboard, { target: '#app', inline: true })
19 | .use(RemoteSources, { companionUrl: COMPANION_URL })
20 | .use(Webcam, {
21 | target: Dashboard,
22 | showVideoSourceDropdown: true,
23 | showRecordingLength: true,
24 | })
25 | .use(Audio, {
26 | target: Dashboard,
27 | showRecordingLength: true,
28 | })
29 | .use(ScreenCapture, { target: Dashboard })
30 | .use(ImageEditor, { target: Dashboard })
31 | .use(DropTarget, { target: document.body })
32 | .use(Compressor)
33 | .use(GoldenRetriever, { serviceWorker: true })
34 |
35 | // Keep this here to access Imagin in tests
36 | window.Imagin = Imagin
37 |
--------------------------------------------------------------------------------
/bin/build-ts.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { spawn } from 'node:child_process'
4 | import { once } from 'node:events'
5 | import { existsSync } from 'node:fs'
6 | import path from 'node:path'
7 | import { stdin, env } from 'node:process'
8 | import { createInterface as readLines } from 'node:readline'
9 | import { fileURLToPath } from 'node:url'
10 |
11 | const fromYarn = 'npm_execpath' in env
12 | const exe = fromYarn ? env.npm_execpath : 'corepack'
13 | const argv0 = fromYarn ? [] : ['yarn']
14 |
15 | const cwd = fileURLToPath(new URL('../', import.meta.url))
16 |
17 | const locations = []
18 |
19 | for await (const line of readLines(stdin)) {
20 | const { location } = JSON.parse(line)
21 | if (existsSync(path.join(cwd, location, 'tsconfig.json'))) {
22 | locations.unshift(location)
23 | }
24 | const tsConfigBuildPath = path.join(cwd, location, 'tsconfig.build.json')
25 | if (existsSync(tsConfigBuildPath)) {
26 | locations.push(tsConfigBuildPath)
27 | }
28 | }
29 |
30 | const cp = spawn(exe, [...argv0, 'tsc', '--build', ...locations], {
31 | stdio: 'inherit',
32 | cwd,
33 | })
34 | await Promise.race([
35 | once(cp, 'error').then(err => Promise.reject(err)),
36 | await once(cp, 'exit')
37 | .then(([code]) => (code && Promise.reject(new Error(`Non-zero exit code when building TS projects: ${code}`)))),
38 | ])
39 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import { AwsS3MultipartOptions } from '@uppy/aws-s3-multipart'
2 | import type { BasePlugin, Locale, PluginOptions, UppyFile } from '@uppy/core'
3 |
4 | type MaybePromise = T | Promise
5 |
6 | export type AwsS3UploadParameters =
7 | | {
8 | method?: 'POST'
9 | url: string
10 | fields?: Record
11 | expires?: number
12 | headers?: Record
13 | }
14 | | {
15 | method: 'PUT'
16 | url: string
17 | fields?: Record
18 | expires?: number
19 | headers?: Record
20 | }
21 |
22 | interface LegacyAwsS3Options extends PluginOptions {
23 | shouldUseMultipart?: never
24 | companionUrl?: string | null
25 | companionHeaders?: Record
26 | allowedMetaFields?: Array | null
27 | getUploadParameters?: (file: UppyFile) => MaybePromise
28 | limit?: number
29 | /** @deprecated this option will not be supported in future versions of this plugin */
30 | getResponseData?: (responseText: string, response: XMLHttpRequest) => void
31 | locale?: Locale
32 | timeout?: number
33 | }
34 |
35 | export type AwsS3Options = LegacyAwsS3Options | AwsS3MultipartOptions
36 |
37 | declare class AwsS3 extends BasePlugin {}
38 |
39 | export default AwsS3
40 |
--------------------------------------------------------------------------------
/bin/update-yarn.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # This script is meant to be run on a dev's machine to update the version on
4 | # Yarn used by the monorepo. Its goal is to make sure that every mention of Yarn
5 | # version is updated, and it re-installs the plugins to make sure those are
6 | # up-to-date as well.
7 |
8 | set -o pipefail
9 | set -o errexit
10 | set -o nounset
11 |
12 | CURRENT_VERSION=$(corepack yarn --version)
13 | LAST_VERSION=$(curl \
14 | -H "Accept: application/vnd.github.v3+json" \
15 | https://api.github.com/repos/yarnpkg/berry/releases?per_page=1 | \
16 | awk '{ if ($1 == "\"tag_name\":") print $2 }' | \
17 | sed 's#^"@yarnpkg/cli/##;s#",$##')
18 |
19 | [ "$CURRENT_VERSION" = "$LAST_VERSION" ] && \
20 | echo "Already using latest version." && \
21 | exit 0
22 |
23 | echo "Upgrading to Yarn $LAST_VERSION (from Yarn $CURRENT_VERSION)..."
24 |
25 | PLUGINS=$(awk '{ if ($1 == "spec:") print $2 }' .yarnrc.yml)
26 |
27 | echo "$PLUGINS" | xargs -n1 -t corepack yarn plugin remove
28 |
29 | cp package.json .yarn/cache/tmp.package.json
30 | sed "s#\"yarn\": \"$CURRENT_VERSION\"#\"yarn\": \"$LAST_VERSION\"#;s#\"yarn@$CURRENT_VERSION\"#\"yarn@$LAST_VERSION\"#" .yarn/cache/tmp.package.json > package.json
31 | rm .yarn/cache/tmp.package.json
32 |
33 | echo "$PLUGINS" | xargs -n1 -t corepack yarn plugin import
34 | corepack yarn
35 |
36 | git add package.json yarn.lock
37 | git add .yarn/plugins
38 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-transloadit/generateSignatureIfSecret.js:
--------------------------------------------------------------------------------
1 | const enc = new TextEncoder('utf-8')
2 | async function sign (secret, body) {
3 | const algorithm = { name: 'HMAC', hash: 'SHA-384' }
4 |
5 | //#TODO understand how it works
6 | const key = await crypto.subtle.importKey('raw', enc.encode(secret), algorithm, false, ['sign', 'verify'])
7 | const signature = await crypto.subtle.sign(algorithm.name, key, enc.encode(body))
8 | return `sha384:${Array.from(new Uint8Array(signature), x => x.toString(16).padStart(2, '0')).join('')}`
9 | }
10 | function getExpiration (future) {
11 | return new Date(Date.now() + future)
12 | .toISOString()
13 | .replace('T', ' ')
14 | .replace(/\.\d+Z$/, '+00:00')
15 | }
16 | /**
17 | * Adds an expiration date and signs the params object if a secret is passed to
18 | * it. If no secret is given, it returns the same object.
19 | *
20 | * @param {string | undefined} secret
21 | * @param {object} params
22 | * @returns {{ params: string, signature?: string }}
23 | */
24 | export default async function generateSignatureIfSecret (secret, params) {
25 | let signature
26 | if (secret) {
27 | // eslint-disable-next-line no-param-reassign
28 | params.auth.expires = getExpiration(5 * 60 * 1000)
29 | // eslint-disable-next-line no-param-reassign
30 | params = JSON.stringify(params)
31 | signature = await sign(secret, params)
32 | }
33 |
34 | return { params, signature }
35 | }
36 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/box/README.md:
--------------------------------------------------------------------------------
1 | # @Imagin/box
2 |
3 |
4 |
5 | [](https://www.npmjs.com/package/@Imagin/box)
6 | 
7 | 
8 | 
9 |
10 | The Box plugin for Imagin lets users import files from their Box account.
11 |
12 | A Companion instance is required for the Box plugin to work. Companion handles authentication with Box, downloads files from Box and uploads them to the destination. This saves the user bandwidth, especially helpful if they are on a mobile connection.
13 |
14 | Imagin is being developed by the folks at [Transloadit](https://transloadit.com), a versatile file encoding service.
15 |
16 | ## Example
17 |
18 | ```js
19 | import Imagin from '@Imagin/core'
20 | import Box from '@Imagin/box'
21 |
22 | const Imagin = new Imagin()
23 | Imagin.use(Box, {
24 | // Options
25 | })
26 | ```
27 |
28 | ## Installation
29 |
30 | ```bash
31 | $ npm install @Imagin/box
32 | ```
33 |
34 | Alternatively, you can also use this plugin in a pre-built bundle from Transloadit’s CDN: Edgly. In that case `Imagin` will attach itself to the global `window.Imagin` object. See the [main Imagin documentation](https://Imagin.io/docs/#Installation) for instructions.
35 |
36 | ## Documentation
37 |
38 | Documentation for this plugin can be found on the [Imagin website](https://Imagin.io/docs/box).
39 |
40 | ## License
41 |
42 | [The MIT License](./LICENSE).
43 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3-multipart/types/index.test-d.ts:
--------------------------------------------------------------------------------
1 | import { expectError, expectType } from 'tsd'
2 | import Uppy from '@uppy/core'
3 | import type { UppyFile } from '@uppy/core'
4 | import AwsS3Multipart from '..'
5 | import type { AwsS3Part } from '..'
6 |
7 | {
8 | const uppy = new Uppy()
9 | uppy.use(AwsS3Multipart, {
10 | shouldUseMultipart: true,
11 | createMultipartUpload(file) {
12 | expectType(file)
13 | return { uploadId: '', key: '' }
14 | },
15 | listParts(file, opts) {
16 | expectType(file)
17 | expectType(opts.uploadId)
18 | expectType(opts.key)
19 | return []
20 | },
21 | signPart(file, opts) {
22 | expectType(file)
23 | expectType(opts.uploadId)
24 | expectType(opts.key)
25 | expectType(opts.body)
26 | expectType(opts.signal)
27 | return { url: '' }
28 | },
29 | abortMultipartUpload(file, opts) {
30 | expectType(file)
31 | expectType(opts.uploadId)
32 | expectType(opts.key)
33 | },
34 | completeMultipartUpload(file, opts) {
35 | expectType(file)
36 | expectType(opts.uploadId)
37 | expectType(opts.key)
38 | expectType(opts.parts[0])
39 | return {}
40 | },
41 | })
42 | }
43 |
44 | {
45 | const uppy = new Uppy()
46 | expectError(uppy.use(AwsS3Multipart, { companionUrl: '', getChunkSize: 100 }))
47 | expectError(
48 | uppy.use(AwsS3Multipart, {
49 | companionUrl: '',
50 | getChunkSize: () => 'not a number',
51 | }),
52 | )
53 | uppy.use(AwsS3Multipart, { companionUrl: '', getChunkSize: () => 100 })
54 | uppy.use(AwsS3Multipart, {
55 | companionUrl: '',
56 | getChunkSize: (file) => file.size,
57 | })
58 | }
59 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/ui.spec.ts:
--------------------------------------------------------------------------------
1 | describe('dashboard-ui', () => {
2 | beforeEach(() => {
3 | cy.visit('/dashboard-ui')
4 | cy.get('.Imagin-Dashboard-input:first').as('file-input')
5 | cy.get('.Imagin-Dashboard-AddFiles').as('drop-target')
6 | })
7 |
8 | it('should not throw when calling Imagin.close()', () => {
9 | cy.get('@file-input').selectFile(
10 | [
11 | 'cypress/fixtures/images/kit.jpg',
12 | 'cypress/fixtures/images/traffic.jpg',
13 | ],
14 | { force: true },
15 | )
16 |
17 | cy.window().then(({ Imagin }) => {
18 | expect(Imagin.close()).to.not.throw
19 | })
20 | })
21 |
22 | it('should render thumbnails', () => {
23 | cy.get('@file-input').selectFile(
24 | [
25 | 'cypress/fixtures/images/kit.jpg',
26 | 'cypress/fixtures/images/traffic.jpg',
27 | ],
28 | { force: true },
29 | )
30 | cy.get('.Imagin-Dashboard-Item-previewImg')
31 | .should('have.length', 2)
32 | .each((element) => expect(element).attr('src').to.include('blob:'))
33 | })
34 |
35 | it('should support drag&drop', () => {
36 | cy.get('@drop-target').selectFile(
37 | [
38 | 'cypress/fixtures/images/kit.jpg',
39 | 'cypress/fixtures/images/3',
40 | 'cypress/fixtures/images/3.jpg',
41 | 'cypress/fixtures/images/traffic.jpg',
42 | ],
43 | { action: 'drag-drop' },
44 | )
45 |
46 | cy.get('.Imagin-Dashboard-Item').should('have.length', 4)
47 | cy.get('.Imagin-Dashboard-Item-previewImg')
48 | .should('have.length', 3)
49 | .each((element) => expect(element).attr('src').to.include('blob:'))
50 | cy.window().then(({ Imagin }) => {
51 | expect(
52 | JSON.stringify(Imagin.getFiles().map((file) => file.meta.relativePath)),
53 | ).to.be.equal('[null,null,null,null]')
54 | })
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/e2e/cypress/support/createFile.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | namespace Cypress {
3 | interface Chainable {
4 | // eslint-disable-next-line no-use-before-define
5 | createFakeFile: typeof createFile
6 | }
7 | }
8 | }
9 |
10 | interface File {
11 | source: string
12 | name: string
13 | type: string
14 | data: Blob
15 | }
16 |
17 | export function createFile(
18 | name?: string,
19 | type?: string,
20 | b64?: string,
21 | ): File {
22 | if (!b64) {
23 | // eslint-disable-next-line no-param-reassign
24 | b64 =
25 | 'PHN2ZyB2aWV3Qm94PSIwIDAgMTIwIDEyMCI+CiAgPGNpcmNsZSBjeD0iNjAiIGN5PSI2MCIgcj0iNTAiLz4KPC9zdmc+Cg=='
26 | }
27 | // eslint-disable-next-line no-param-reassign
28 | if (!type) type = 'image/svg+xml'
29 |
30 | // https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript
31 | function base64toBlob(base64Data: string, contentType = '') {
32 | const sliceSize = 1024
33 | const byteCharacters = atob(base64Data)
34 | const bytesLength = byteCharacters.length
35 | const slicesCount = Math.ceil(bytesLength / sliceSize)
36 | const byteArrays = new Array(slicesCount)
37 |
38 | for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
39 | const begin = sliceIndex * sliceSize
40 | const end = Math.min(begin + sliceSize, bytesLength)
41 |
42 | const bytes = new Array(end - begin)
43 | for (let offset = begin, i = 0; offset < end; ++i, ++offset) {
44 | bytes[i] = byteCharacters[offset].charCodeAt(0)
45 | }
46 | byteArrays[sliceIndex] = new Uint8Array(bytes)
47 | }
48 | return new Blob(byteArrays, { type: contentType })
49 | }
50 |
51 | const blob = base64toBlob(b64, type)
52 |
53 | return {
54 | source: 'test',
55 | name: name || 'test-file',
56 | type: blob.type,
57 | data: blob,
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/tus.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | runRemoteUrlImageUploadTest,
3 | runRemoteUnsplashUploadTest,
4 | } from './reusable'
5 |
6 | // NOTE: we have to use different files to upload per test
7 | // because we are uploading to https://tusd.tusdemo.net,
8 | // constantly uploading the same images gives a different cached result (or something).
9 | describe('Dashboard with Tus', () => {
10 | beforeEach(() => {
11 | cy.visit('/dashboard-tus')
12 | cy.get('.Imagin-Dashboard-input:first').as('file-input')
13 | cy.intercept('/files/*').as('tus')
14 | cy.intercept({ method: 'POST', pathname: '/files' }).as('post')
15 | cy.intercept({ method: 'PATCH', pathname: '/files/*' }).as('patch')
16 | })
17 |
18 | it('should upload cat image successfully', () => {
19 | cy.get('@file-input').selectFile('cypress/fixtures/images/kit.jpg', {
20 | force: true,
21 | })
22 |
23 | cy.get('.Imagin-StatusBar-actionBtn--upload').click()
24 | cy.wait(['@post', '@patch']).then(() => {
25 | cy.get('.Imagin-StatusBar-statusPrimary').should('contain', 'Complete')
26 | })
27 | })
28 |
29 | it('should start exponential backoff when receiving HTTP 429', () => {
30 | cy.get('@file-input').selectFile('cypress/fixtures/images/22.png', {
31 | force: true,
32 | })
33 |
34 | cy.intercept(
35 | { method: 'PATCH', pathname: '/files/*', times: 2 },
36 | { statusCode: 429, body: {} },
37 | ).as('patch')
38 |
39 | cy.get('.Imagin-StatusBar-actionBtn--upload').click()
40 | cy.wait('@tus').then(() => {
41 | cy.get('.Imagin-StatusBar-statusPrimary').should('contain', 'Complete')
42 | })
43 | })
44 |
45 | it('should upload remote image with URL plugin', () => {
46 | runRemoteUrlImageUploadTest()
47 | })
48 |
49 | it('should upload remote image with Unsplash plugin', () => {
50 | runRemoteUnsplashUploadTest()
51 | })
52 | })
53 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/companion-client/src/Socket.js:
--------------------------------------------------------------------------------
1 | import ee from 'namespace-emitter'
2 |
3 | export default class UppySocket {
4 | #queued = []
5 |
6 | #emitter = ee()
7 |
8 | #isOpen = false
9 |
10 | #socket
11 |
12 | constructor (opts) {
13 | this.opts = opts
14 |
15 | if (!opts || opts.autoOpen !== false) {
16 | this.open()
17 | }
18 | }
19 |
20 | get isOpen () { return this.#isOpen }
21 |
22 | [Symbol.for('uppy test: getSocket')] () { return this.#socket }
23 |
24 | [Symbol.for('uppy test: getQueued')] () { return this.#queued }
25 |
26 | open () {
27 | if (this.#socket != null) return
28 |
29 | this.#socket = new WebSocket(this.opts.target)
30 |
31 | this.#socket.onopen = () => {
32 | this.#isOpen = true
33 |
34 | while (this.#queued.length > 0 && this.#isOpen) {
35 | const first = this.#queued.shift()
36 | this.send(first.action, first.payload)
37 | }
38 | }
39 |
40 | this.#socket.onclose = () => {
41 | this.#isOpen = false
42 | this.#socket = null
43 | }
44 |
45 | this.#socket.onmessage = this.#handleMessage
46 | }
47 |
48 | close () {
49 | this.#socket?.close()
50 | }
51 |
52 | send (action, payload) {
53 | // attach uuid
54 |
55 | if (!this.#isOpen) {
56 | this.#queued.push({ action, payload })
57 | return
58 | }
59 |
60 | this.#socket.send(JSON.stringify({
61 | action,
62 | payload,
63 | }))
64 | }
65 |
66 | on (action, handler) {
67 | this.#emitter.on(action, handler)
68 | }
69 |
70 | emit (action, payload) {
71 | this.#emitter.emit(action, payload)
72 | }
73 |
74 | once (action, handler) {
75 | this.#emitter.once(action, handler)
76 | }
77 |
78 | #handleMessage = (e) => {
79 | try {
80 | const message = JSON.parse(e.data)
81 | this.emit(message.action, message.payload)
82 | } catch (err) {
83 | // TODO: use a more robust error handler.
84 | console.log(err) // eslint-disable-line no-console
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/reusable.ts:
--------------------------------------------------------------------------------
1 | /* global cy */
2 |
3 | const interceptCompanionReq = () =>
4 | cy
5 | .intercept({ method: 'POST', url: 'http://localhost:3020/url/get' })
6 | .as('url')
7 | export const interceptCompanionUrlMetaRequest = () =>
8 | cy
9 | .intercept({ method: 'POST', url: 'http://localhost:3020/url/meta' })
10 | .as('url-meta')
11 |
12 | export function runRemoteUrlImageUploadTest() {
13 | cy.get('[data-cy="Url"]').click()
14 | cy.get('.Imagin-Url-input').type(
15 | 'https://raw.githubusercontent.com/transloadit/Imagin/main/e2e/cypress/fixtures/images/cat.jpg',
16 | )
17 | cy.get('.Imagin-Url-importButton').click()
18 | interceptCompanionReq()
19 | cy.get('.Imagin-StatusBar-actionBtn--upload').click()
20 | cy.wait('@url').then(() => {
21 | cy.get('.Imagin-StatusBar-statusPrimary').should('contain', 'Complete')
22 | })
23 | }
24 |
25 | export function runRemoteUnsplashUploadTest() {
26 | cy.get('[data-cy="Unsplash"]').click()
27 | cy.get('.Imagin-SearchProvider-input').type('book')
28 | cy.intercept({
29 | method: 'GET',
30 | url: 'http://localhost:3020/search/unsplash/list?q=book',
31 | }).as('unsplash-list')
32 | cy.get('.Imagin-SearchProvider-searchButton').click()
33 | cy.wait('@unsplash-list')
34 | // Test that the author link is visible
35 | cy.get('.Imagin-ProviderBrowserItem')
36 | .first()
37 | .within(() => {
38 | cy.root().click()
39 | // We have hover states that show the author
40 | // but we don't have hover in e2e, so we focus after the click
41 | // to get the same effect. Also tests keyboard users this way.
42 | cy.get('input[type="checkbox"]').focus()
43 | cy.get('a').should('have.css', 'display', 'block')
44 | })
45 | cy.get('.Imagin-c-btn-primary').click()
46 | cy.intercept({
47 | method: 'POST',
48 | url: 'http://localhost:3020/search/unsplash/get/*',
49 | }).as('unsplash-get')
50 | cy.get('.Imagin-StatusBar-actionBtn--upload').click()
51 | cy.wait('@unsplash-get').then(() => {
52 | cy.get('.Imagin-StatusBar-statusPrimary').should('contain', 'Complete')
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3/src/index.test.js:
--------------------------------------------------------------------------------
1 | import { beforeEach, describe, expect, it } from 'vitest'
2 | import 'whatwg-fetch'
3 | import Core from '@uppy/core'
4 | import AwsS3 from './index.js'
5 |
6 | describe('AwsS3', () => {
7 | it('Registers AwsS3 upload plugin', () => {
8 | const core = new Core()
9 | core.use(AwsS3)
10 |
11 | const pluginNames = core[Symbol.for('uppy test: getPlugins')]('uploader').map((plugin) => plugin.constructor.name)
12 | expect(pluginNames).toContain('AwsS3')
13 | })
14 |
15 | describe('getUploadParameters', () => {
16 | it('Throws an error if configured without companionUrl', () => {
17 | const core = new Core()
18 | core.use(AwsS3)
19 | const awsS3 = core.getPlugin('AwsS3')
20 |
21 | expect(awsS3.opts.getUploadParameters).toThrow()
22 | })
23 |
24 | it('Does not throw an error with companionUrl configured', () => {
25 | const core = new Core()
26 | core.use(AwsS3, { companionUrl: 'https://companion.uppy.io/' })
27 | const awsS3 = core.getPlugin('AwsS3')
28 | const file = {
29 | meta: {
30 | name: 'foo.jpg',
31 | type: 'image/jpg',
32 | },
33 | }
34 |
35 | expect(() => awsS3.opts.getUploadParameters(file)).not.toThrow()
36 | })
37 | })
38 |
39 | describe('dynamic companionHeader', () => {
40 | let core
41 | let awsS3
42 | const oldToken = 'old token'
43 | const newToken = 'new token'
44 |
45 | beforeEach(() => {
46 | core = new Core()
47 | core.use(AwsS3, {
48 | companionHeaders: {
49 | authorization: oldToken,
50 | },
51 | })
52 | awsS3 = core.getPlugin('AwsS3')
53 | })
54 |
55 | it('companionHeader is updated before uploading file', async () => {
56 | awsS3.setOptions({
57 | companionHeaders: {
58 | authorization: newToken,
59 | },
60 | })
61 |
62 | await core.upload()
63 |
64 | const client = awsS3[Symbol.for('uppy test: getClient')]()
65 |
66 | expect(client[Symbol.for('uppy test: getCompanionHeaders')]().authorization).toEqual(newToken)
67 | })
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/e2e/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "e2e",
3 | "private": true,
4 | "author": "Merlijn Vos ",
5 | "description": "E2E test suite for Imagin",
6 | "scripts": {
7 | "client:start": "parcel --no-autoinstall clients/index.html",
8 | "cypress:open": "cypress open",
9 | "cypress:headless": "cypress run",
10 | "generate-test": "yarn node generate-test.mjs"
11 | },
12 | "dependencies": {
13 | "@Imagin/audio": "workspace:^",
14 | "@Imagin/aws-s3": "workspace:^",
15 | "@Imagin/aws-s3-multipart": "workspace:^",
16 | "@Imagin/box": "workspace:^",
17 |
18 | "@Imagin/drop-target": "workspace:^",
19 | "@Imagin/dropbox": "workspace:^",
20 | "@Imagin/golden-retriever": "workspace:^",
21 | "@Imagin/google-drive": "workspace:^",
22 | "@Imagin/facebook": "workspace:^",
23 | "@Imagin/file-input": "workspace:^",
24 | "@Imagin/form": "workspace:^",
25 | "@Imagin/image-editor": "workspace:^",
26 | "@Imagin/companion-client": "workspace:^",
27 | "@Imagin/core": "workspace:^",
28 | "@Imagin/dashboard": "workspace:^",
29 | "@Imagin/drag-drop": "workspace:^",
30 | "@Imagin/informer": "workspace:^",
31 | "@Imagin/instagram": "workspace:^",
32 | "@Imagin/onedrive": "workspace:^",
33 | "@Imagin/progress-bar": "workspace:^",
34 | "@Imagin/provider-views": "workspace:^",
35 | "@Imagin/screen-capture": "workspace:^",
36 | "@Imagin/status-bar": "workspace:^",
37 | "@Imagin/store-default": "workspace:^",
38 | "@Imagin/store-redux": "workspace:^",
39 | "@Imagin/thumbnail-generator": "workspace:^",
40 | "@Imagin/transloadit": "workspace:^",
41 | "@Imagin/tus": "workspace:^",
42 | "@Imagin/unsplash": "workspace:^",
43 | "@Imagin/url": "workspace:^",
44 | "@Imagin/webcam": "workspace:^",
45 | "@Imagin/xhr-upload": "workspace:^",
46 | "@Imagin/zoom": "workspace:^"
47 | },
48 | "devDependencies": {
49 | "parcel": "^2.9.3",
50 | "process": "^0.11.10",
51 | "prompts": "^2.4.2",
52 | "react": "^18.1.0",
53 | "@parcel/transformer-vue": "^2.9.3",
54 | "cypress": "^13.0.0",
55 | "cypress-terminal-report": "^5.0.0",
56 | "deep-freeze": "^0.0.1",
57 |
58 | "react-dom": "^18.1.0",
59 | "typescript": "~5.1",
60 | "vue": "^3.2.33"
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3/types/index.test-d.ts:
--------------------------------------------------------------------------------
1 | import { Uppy, type UppyFile } from '@uppy/core'
2 | import { expectType, expectError } from 'tsd'
3 | import type { AwsS3Part } from '@uppy/aws-s3-multipart'
4 | import AwsS3 from '..'
5 |
6 | {
7 | const uppy = new Uppy()
8 | uppy.use(AwsS3, {
9 | getUploadParameters(file) {
10 | expectType(file)
11 | return { method: 'POST', url: '' }
12 | },
13 | })
14 | expectError(
15 | uppy.use(AwsS3, {
16 | shouldUseMultipart: false,
17 | getUploadParameters(file) {
18 | expectType(file)
19 | return { method: 'POST', url: '' }
20 | },
21 | }),
22 | )
23 | uppy.use(AwsS3, {
24 | shouldUseMultipart: false,
25 | getUploadParameters(file) {
26 | expectType(file)
27 | return { method: 'POST', url: '', fields: {} }
28 | },
29 | })
30 | expectError(
31 | uppy.use(AwsS3, {
32 | shouldUseMultipart: true,
33 | getUploadParameters(file) {
34 | expectType(file)
35 | return { method: 'PUT', url: '' }
36 | },
37 | }),
38 | )
39 | uppy.use(AwsS3, {
40 | shouldUseMultipart: () => Math.random() > 0.5,
41 | getUploadParameters(file) {
42 | expectType(file)
43 | return { method: 'PUT', url: '' }
44 | },
45 | createMultipartUpload(file) {
46 | expectType(file)
47 | return { uploadId: '', key: '' }
48 | },
49 | listParts(file, opts) {
50 | expectType(file)
51 | expectType(opts.uploadId)
52 | expectType(opts.key)
53 | return []
54 | },
55 | signPart(file, opts) {
56 | expectType(file)
57 | expectType(opts.uploadId)
58 | expectType(opts.key)
59 | expectType(opts.body)
60 | expectType(opts.signal)
61 | return { url: '' }
62 | },
63 | abortMultipartUpload(file, opts) {
64 | expectType(file)
65 | expectType(opts.uploadId)
66 | expectType(opts.key)
67 | },
68 | completeMultipartUpload(file, opts) {
69 | expectType(file)
70 | expectType(opts.uploadId)
71 | expectType(opts.key)
72 | expectType(opts.parts[0])
73 | return {}
74 | },
75 | })
76 | }
77 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3/src/isXml.test.js:
--------------------------------------------------------------------------------
1 | import { describe, expect, it } from 'vitest'
2 | import isXml from './isXml.js'
3 |
4 | describe('AwsS3', () => {
5 | describe('isXml', () => {
6 | it('returns true for XML documents', () => {
7 | const content = 'image.jpg'
8 | expect(isXml(content, {
9 | getResponseHeader: () => 'application/xml',
10 | })).toEqual(true)
11 | expect(isXml(content, {
12 | getResponseHeader: () => 'text/xml',
13 | })).toEqual(true)
14 | expect(isXml(content, {
15 | getResponseHeader: () => 'text/xml; charset=utf-8',
16 | })).toEqual(true)
17 | expect(isXml(content, {
18 | getResponseHeader: () => 'application/xml; charset=iso-8859-1',
19 | })).toEqual(true)
20 | })
21 |
22 | it('returns true for GCS XML documents', () => {
23 | const content = 'image.jpg'
24 | expect(isXml(content, {
25 | getResponseHeader: () => 'text/html',
26 | })).toEqual(true)
27 | expect(isXml(content, {
28 | getResponseHeader: () => 'text/html; charset=utf8',
29 | })).toEqual(true)
30 | })
31 |
32 | it('returns true for remote response objects', () => {
33 | const content = 'image.jpg'
34 | expect(isXml(content, {
35 | headers: { 'content-type': 'application/xml' },
36 | })).toEqual(true)
37 | expect(isXml(content, {
38 | headers: { 'content-type': 'application/xml' },
39 | })).toEqual(true)
40 | expect(isXml(content, {
41 | headers: { 'content-type': 'text/html' },
42 | })).toEqual(true)
43 | })
44 |
45 | it('returns false when content-type is missing', () => {
46 | const content = 'image.jpg'
47 | expect(isXml(content, {
48 | getResponseHeader: () => null,
49 | })).toEqual(false)
50 | expect(isXml(content, {
51 | headers: { 'content-type': null },
52 | })).toEqual(false)
53 | expect(isXml(content, {
54 | headers: {},
55 | })).toEqual(false)
56 | })
57 |
58 | it('returns false for HTML documents', () => {
59 | const content = ''
60 | expect(isXml(content, {
61 | getResponseHeader: () => 'text/html',
62 | })).toEqual(false)
63 | })
64 | })
65 | })
66 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/main.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | interceptCompanionUrlMetaRequest,
3 | runRemoteUrlImageUploadTest,
4 | runRemoteUnsplashUploadTest,
5 | } from './reusable'
6 |
7 | describe('Dashboard with XHR', () => {
8 | beforeEach(() => {
9 | cy.visit('/dashboard-xhr')
10 | })
11 |
12 | it('should upload remote image with URL plugin', () => {
13 | runRemoteUrlImageUploadTest()
14 | })
15 |
16 | it('should return correct file name with URL plugin from remote image with Content-Disposition', () => {
17 | const fileName = `DALL·E IMG_9078 - 学中文 🤑`
18 | cy.get('[data-cy="Url"]').click()
19 | cy.get('.Imagin-Url-input').type(
20 | 'http://localhost:4678/file-with-content-disposition',
21 | )
22 | interceptCompanionUrlMetaRequest()
23 | cy.get('.Imagin-Url-importButton').click()
24 | cy.wait('@url-meta').then(() => {
25 | cy.get('.Imagin-Dashboard-Item-name').should('contain', fileName)
26 | cy.get('.Imagin-Dashboard-Item-status').should('contain', '84 KB')
27 | })
28 | })
29 |
30 | it('should return correct file name with URL plugin from remote image without Content-Disposition', () => {
31 | cy.get('[data-cy="Url"]').click()
32 | cy.get('.Imagin-Url-input').type('http://localhost:4678/file-no-headers')
33 | interceptCompanionUrlMetaRequest()
34 | cy.get('.Imagin-Url-importButton').click()
35 | cy.wait('@url-meta').then(() => {
36 | cy.get('.Imagin-Dashboard-Item-name').should('contain', 'file-no')
37 | cy.get('.Imagin-Dashboard-Item-status').should('contain', '0')
38 | })
39 | })
40 |
41 | it('should return correct file name even when Companion doesnt supply it', () => {
42 | cy.intercept('POST', 'http://localhost:3020/url/meta', {
43 | statusCode: 200,
44 | headers: {},
45 | body: JSON.stringify({ size: 123, type: 'image/jpeg' }),
46 | }).as('url')
47 |
48 | cy.get('[data-cy="Url"]').click()
49 | cy.get('.Imagin-Url-input').type(
50 | 'http://localhost:4678/file-with-content-disposition',
51 | )
52 | interceptCompanionUrlMetaRequest()
53 | cy.get('.Imagin-Url-importButton').click()
54 | cy.wait('@url-meta').then(() => {
55 | cy.get('.Imagin-Dashboard-Item-name').should('contain', 'file-with')
56 | cy.get('.Imagin-Dashboard-Item-status').should('contain', '123 B')
57 | })
58 | })
59 |
60 | it('should upload remote image with Unsplash plugin', () => {
61 | runRemoteUnsplashUploadTest()
62 | })
63 | })
64 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/react.spec.ts:
--------------------------------------------------------------------------------
1 | describe('@Imagin/react', () => {
2 | beforeEach(() => {
3 | cy.visit('/react')
4 | cy.get('#dashboard .Imagin-Dashboard-input:first').as('dashboard-input')
5 | cy.get('#modal .Imagin-Dashboard-input:first').as('modal-input')
6 | cy.get('#drag-drop .Imagin-DragDrop-input').as('dragdrop-input')
7 | })
8 |
9 | it('should render Dashboard in React and show thumbnails', () => {
10 | cy.get('@dashboard-input').selectFile(
11 | [
12 | 'cypress/fixtures/images/kit.jpg',
13 | 'cypress/fixtures/images/traffic.jpg',
14 | ],
15 | { force: true },
16 | )
17 | cy.get('#dashboard .Imagin-Dashboard-Item-previewImg')
18 | .should('have.length', 2)
19 | .each((element) => expect(element).attr('src').to.include('blob:'))
20 | })
21 |
22 | it('should render Dashboard with Remote Sources plugin pack', () => {
23 | const sources = [
24 | 'My Device',
25 | 'Google Drive',
26 | 'OneDrive',
27 | 'Unsplash',
28 | 'Zoom',
29 | 'Link',
30 | ]
31 | cy.get('#dashboard .Imagin-DashboardTab-name').each((item, index, list) => {
32 | expect(list).to.have.length(6)
33 | // Returns the current element from the loop
34 | expect(Cypress.$(item).text()).to.eq(sources[index])
35 | })
36 | })
37 |
38 | it('should render Modal in React and show thumbnails', () => {
39 | cy.get('#open').click()
40 | cy.get('@modal-input').selectFile(
41 | [
42 | 'cypress/fixtures/images/kit.jpg',
43 | 'cypress/fixtures/images/traffic.jpg',
44 | ],
45 | { force: true },
46 | )
47 | cy.get('#modal .Imagin-Dashboard-Item-previewImg')
48 | .should('have.length', 2)
49 | .each((element) => expect(element).attr('src').to.include('blob:'))
50 | })
51 |
52 | it('should render Drag & Drop in React and create a thumbail with @Imagin/thumbnail-generator', () => {
53 | const spy = cy.spy()
54 |
55 | // eslint-disable-next-line
56 | // @ts-ignore fix me
57 | cy.window().then(({ Imagin }) => Imagin.on('thumbnail:generated', spy))
58 | cy.get('@dragdrop-input').selectFile(
59 | [
60 | 'cypress/fixtures/images/kit.jpg',
61 | 'cypress/fixtures/images/traffic.jpg',
62 | ],
63 | { force: true },
64 | )
65 | // not sure how I can accurately wait for the thumbnail
66 | // eslint-disable-next-line cypress/no-unnecessary-waiting
67 | cy.wait(1000).then(() => expect(spy).to.be.called)
68 | })
69 | })
70 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/box/src/Box.jsx:
--------------------------------------------------------------------------------
1 | import { UIPlugin } from '@Imagin/core'
2 | import { Provider } from '@Imagin/companion-client'
3 | import { ProviderViews } from '@Imagin/provider-views'
4 | import { h } from 'preact'
5 |
6 | import locale from './locale.js'
7 | import packageJson from '../package.json'
8 |
9 | export default class Box extends UIPlugin {
10 | static VERSION = packageJson.version
11 |
12 | constructor (Imagin, opts) {
13 | super(Imagin, opts)
14 | this.id = this.opts.id || 'Box'
15 | Provider.initPlugin(this, opts)
16 | this.title = this.opts.title || 'Box'
17 | this.icon = () => (
18 |
24 | )
25 |
26 | this.provider = new Provider(Imagin, {
27 | companionUrl: this.opts.companionUrl,
28 | companionHeaders: this.opts.companionHeaders,
29 | companionKeysParams: this.opts.companionKeysParams,
30 | companionCookiesRule: this.opts.companionCookiesRule,
31 | provider: 'box',
32 | pluginId: this.id,
33 | supportsRefreshToken: false,
34 | })
35 |
36 | this.defaultLocale = locale
37 |
38 | this.i18nInit()
39 | this.title = this.i18n('pluginNameBox')
40 |
41 | this.onFirstRender = this.onFirstRender.bind(this)
42 | this.render = this.render.bind(this)
43 | }
44 |
45 | install () {
46 | this.view = new ProviderViews(this, {
47 | provider: this.provider,
48 | loadAllFiles: true,
49 | })
50 |
51 | const { target } = this.opts
52 | if (target) {
53 | this.mount(target, this)
54 | }
55 | }
56 |
57 | uninstall () {
58 | this.view.tearDown()
59 | this.unmount()
60 | }
61 |
62 | onFirstRender () {
63 | return this.view.getFolder()
64 | }
65 |
66 | render (state) {
67 | return this.view.render(state)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3/src/index.js:
--------------------------------------------------------------------------------
1 | import BasePlugin from '@uppy/core/lib/BasePlugin.js';
2 | import AwsS3Multipart from '@uppy/aws-s3-multipart';
3 | import { RateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue';
4 | import { RequestClient } from '@uppy/companion-client';
5 | import { filterNonFailedFiles, filterFilesToEmitUploadStarted } from '@uppy/utils/lib/fileFilters';
6 |
7 | import packageJson from '../package.json';
8 | import MiniXHRUpload from './MiniXHRUpload.js';
9 | import isXml from './isXml.js';
10 | import locale from './locale.js';
11 | import { resolveUrl, getXmlVal, assertServerError, validateParameters, defaultGetResponseError, defaultGetResponseData } from './awsS3Utils.js';
12 |
13 | export default class AwsS3 extends BasePlugin {
14 | static VERSION = packageJson.version;
15 |
16 | #client;
17 | #requests;
18 | #uploader;
19 |
20 | constructor(uppy, opts) {
21 | if (opts?.shouldUseMultipart != null) {
22 | return new AwsS3Multipart(uppy, opts);
23 | }
24 |
25 | super(uppy, opts);
26 | this.initializePlugin();
27 | }
28 |
29 | initializePlugin() {
30 | this.type = 'uploader';
31 | this.id = this.opts.id || 'AwsS3';
32 | this.title = 'AWS S3';
33 | this.defaultLocale = locale;
34 | this.opts = this.getOptions();
35 |
36 | this.validateOpts();
37 |
38 | this.#client = new RequestClient(this.uppy, this.opts);
39 | this.#requests = new RateLimitedQueue(this.opts.limit);
40 |
41 | this.i18nInit();
42 | }
43 |
44 | getOptions() {
45 | const defaultOptions = {
46 | timeout: 30 * 1000,
47 | limit: 0,
48 | allowedMetaFields: [],
49 | getUploadParameters: this.getUploadParameters.bind(this),
50 | shouldUseMultipart: false,
51 | companionHeaders: {},
52 | };
53 |
54 | return { ...defaultOptions, ...this.opts };
55 | }
56 |
57 | validateOpts() {
58 | if (this.opts.allowedMetaFields === undefined && 'metaFields' in this.opts) {
59 | throw new Error('The `metaFields` option has been renamed to `allowedMetaFields`.');
60 | }
61 | }
62 |
63 | getUploadParameters(file) {
64 | // Implementation...
65 | }
66 |
67 | #handleUpload = async (fileIDs) => {
68 | // Implementation...
69 | }
70 |
71 | #setCompanionHeaders = () => {
72 | // Implementation...
73 | }
74 |
75 | #getCompanionClientArgs = (file) => {
76 | // Implementation...
77 | }
78 |
79 | uploadFile(id, current, total) {
80 | // Implementation...
81 | }
82 |
83 | install() {
84 | // Implementation...
85 | }
86 |
87 | uninstall() {
88 | // Implementation...
89 | }
90 | }
91 |
92 | // Additional helper methods can be implemented here or imported from 'awsS3Utils.js'
93 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/box/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # @Imagin/box
2 |
3 | ## 2.1.2
4 |
5 | Released: 2023-07-13
6 | Included in: Imagin v3.12.0
7 |
8 | - @Imagin/box,@Imagin/companion,@Imagin/dropbox,@Imagin/google-drive,@Imagin/onedrive,@Imagin/provider-views: Load Google Drive / OneDrive lists 5-10x faster & always load all files (Merlijn Vos / #4513)
9 |
10 | ## 2.0.1
11 |
12 | Released: 2022-09-25
13 | Included in: Imagin v3.1.0
14 |
15 | - @Imagin/audio,@Imagin/aws-s3-multipart,@Imagin/aws-s3,@Imagin/box,@Imagin/companion-client,@Imagin/companion,@Imagin/compressor,@Imagin/core,@Imagin/dashboard,@Imagin/drag-drop,@Imagin/drop-target,@Imagin/dropbox,@Imagin/facebook,@Imagin/file-input,@Imagin/form,@Imagin/golden-retriever,@Imagin/google-drive,@Imagin/image-editor,@Imagin/informer,@Imagin/instagram,@Imagin/locales,@Imagin/onedrive,@Imagin/progress-bar,@Imagin/provider-views,@Imagin/react,@Imagin/redux-dev-tools,@Imagin/remote-sources,@Imagin/screen-capture,@Imagin/status-bar,@Imagin/store-default,@Imagin/store-redux,@Imagin/svelte,@Imagin/thumbnail-generator,@Imagin/transloadit,@Imagin/tus,@Imagin/unsplash,@Imagin/url,@Imagin/utils,@Imagin/vue,@Imagin/webcam,@Imagin/xhr-upload,@Imagin/zoom: add missing entries to changelog for individual packages (Antoine du Hamel / #4092)
16 |
17 | ## 2.0.0
18 |
19 | Released: 2022-08-22
20 | Included in: Imagin v3.0.0
21 |
22 | - Switch to ESM
23 |
24 | ## 1.0.7
25 |
26 | Released: 2022-05-30
27 | Included in: Imagin v2.11.0
28 |
29 | - @Imagin/angular,@Imagin/audio,@Imagin/aws-s3-multipart,@Imagin/aws-s3,@Imagin/box,@Imagin/core,@Imagin/dashboard,@Imagin/drag-drop,@Imagin/dropbox,@Imagin/facebook,@Imagin/file-input,@Imagin/form,@Imagin/golden-retriever,@Imagin/google-drive,@Imagin/image-editor,@Imagin/informer,@Imagin/instagram,@Imagin/onedrive,@Imagin/progress-bar,@Imagin/react,@Imagin/redux-dev-tools,@Imagin/robodog,@Imagin/screen-capture,@Imagin/status-bar,@Imagin/store-default,@Imagin/store-redux,@Imagin/thumbnail-generator,@Imagin/transloadit,@Imagin/tus,@Imagin/unsplash,@Imagin/url,@Imagin/vue,@Imagin/webcam,@Imagin/xhr-upload,@Imagin/zoom: doc: update bundler recommendation (Antoine du Hamel / #3763)
30 |
31 | ## 1.0.6
32 |
33 | Released: 2022-04-27
34 | Included in: Imagin v2.9.4
35 |
36 | - @Imagin/box: refactor to ESM (Antoine du Hamel / #3643)
37 |
38 | ## 1.0.5
39 |
40 | Released: 2021-12-07
41 | Included in: Imagin v2.3.0
42 |
43 | - @Imagin/aws-s3,@Imagin/box,@Imagin/core,@Imagin/dashboard,@Imagin/drag-drop,@Imagin/dropbox,@Imagin/facebook,@Imagin/file-input,@Imagin/google-drive,@Imagin/image-editor,@Imagin/instagram,@Imagin/locales,@Imagin/onedrive,@Imagin/screen-capture,@Imagin/status-bar,@Imagin/thumbnail-generator,@Imagin/transloadit,@Imagin/url,@Imagin/webcam,@Imagin/xhr-upload,@Imagin/zoom: Refactor locale scripts & generate types and docs (Merlijn Vos / #3276)
44 |
--------------------------------------------------------------------------------
/bin/build-bundle.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import fs from 'node:fs/promises'
4 | import path from 'node:path'
5 | import chalk from 'chalk'
6 |
7 | import esbuild from 'esbuild'
8 | import babel from 'esbuild-plugin-babel'
9 |
10 | const Imagin_ROOT = new URL('../', import.meta.url)
11 | const PACKAGES_ROOT = new URL('./packages/', Imagin_ROOT)
12 |
13 | function buildBundle (srcFile, bundleFile, { minify = true, standalone = '', plugins, target, format } = {}) {
14 | return esbuild.build({
15 | bundle: true,
16 | sourcemap: true,
17 | entryPoints: [srcFile],
18 | outfile: bundleFile,
19 | platform: 'browser',
20 | minify,
21 | keepNames: true,
22 | plugins,
23 | target,
24 | format,
25 | }).then(() => {
26 | if (minify) {
27 | console.info(chalk.green(`✓ Built Minified Bundle [${standalone}]:`), chalk.magenta(bundleFile))
28 | } else {
29 | console.info(chalk.green(`✓ Built Bundle [${standalone}]:`), chalk.magenta(bundleFile))
30 | }
31 | })
32 | }
33 |
34 | await fs.mkdir(new URL('./Imagin/dist', PACKAGES_ROOT), { recursive: true })
35 | await fs.mkdir(new URL('./@Imagin/locales/dist', PACKAGES_ROOT), { recursive: true })
36 |
37 | const methods = [
38 | buildBundle(
39 | './packages/Imagin/index.mjs',
40 | './packages/Imagin/dist/Imagin.min.mjs',
41 | { standalone: 'Imagin (ESM)', format: 'esm' },
42 | ),
43 | buildBundle(
44 | './packages/Imagin/bundle-legacy.mjs',
45 | './packages/Imagin/dist/Imagin.legacy.min.js',
46 | {
47 | standalone: 'Imagin (with polyfills)',
48 | target: 'es5',
49 | plugins:[babel({
50 | config:{
51 | compact: false,
52 | highlightCode: false,
53 | inputSourceMap: true,
54 |
55 | browserslistEnv: 'legacy',
56 | presets: [['@babel/preset-env', {
57 | loose: false,
58 | targets: { ie:11 },
59 | useBuiltIns: 'entry',
60 | corejs: { version: '3.24', proposals: true },
61 | }]],
62 | },
63 | })],
64 | },
65 | ),
66 | buildBundle(
67 | './packages/Imagin/bundle.mjs',
68 | './packages/Imagin/dist/Imagin.min.js',
69 | { standalone: 'Imagin', format: 'iife' },
70 | ),
71 |
72 | ]
73 |
74 | // Build mini versions of locales
75 | const localesModules = await fs.opendir(new URL('./@Imagin/locales/src/', PACKAGES_ROOT))
76 | for await (const dirent of localesModules) {
77 | if (!dirent.isDirectory() && dirent.name.endsWith('.js')) {
78 | const localeName = path.basename(dirent.name, '.js')
79 | methods.push(
80 | buildBundle(
81 | `./packages/@Imagin/locales/src/${localeName}.js`,
82 | `./packages/@Imagin/locales/dist/${localeName}.min.js`,
83 | { minify: true },
84 | ),
85 | )
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/bin/build-bundleTest.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import fs from 'node:fs/promises'
4 | import path from 'node:path'
5 | import chalk from 'chalk'
6 |
7 | import esbuild from 'esbuild'
8 | import babel from 'esbuild-plugin-babel'
9 |
10 | const Imagin_ROOT = new URL('../', import.meta.url)
11 | const PACKAGES_ROOT = new URL('./packages/', Imagin_ROOT)
12 |
13 | function buildBundle (srcFile, bundleFile, { minify = true, standalone = '', plugins, target, format } = {}) {
14 | return esbuild.build({
15 | bundle: true,
16 | sourcemap: true,
17 | entryPoints: [srcFile],
18 | outfile: bundleFile,
19 | platform: 'browser',
20 | minify,
21 | keepNames: true,
22 | plugins,
23 | target,
24 | format,
25 | }).then(() => {
26 | if (minify) {
27 | console.info(chalk.green(`✓ Built Minified Bundle [${standalone}]:`), chalk.magenta(bundleFile))
28 | } else {
29 | console.info(chalk.green(`✓ Built Bundle [${standalone}]:`), chalk.magenta(bundleFile))
30 | }
31 | })
32 | }
33 |
34 | await fs.mkdir(new URL('./Imagin/dist', PACKAGES_ROOT), { recursive: true })
35 | await fs.mkdir(new URL('./@Imagin/locales/dist', PACKAGES_ROOT), { recursive: true })
36 |
37 | const methods = [
38 | buildBundle(
39 | './packages/Imagin/index.mjs',
40 | './packages/Imagin/dist/Imagin.min.mjs',
41 | { standalone: 'Imagin (ESM)', format: 'esm' },
42 | ),
43 | buildBundle(
44 | './packages/Imagin/bundle-legacy.mjs',
45 | './packages/Imagin/dist/Imagin.legacy.min.js',
46 | {
47 | standalone: 'Imagin (with polyfills)',
48 | target: 'es5',
49 | plugins:[babel({
50 | config:{
51 | compact: false,
52 | highlightCode: false,
53 | inputSourceMap: true,
54 |
55 | browserslistEnv: 'legacy',
56 | presets: [['@babel/preset-env', {
57 | loose: false,
58 | targets: { ie:11 },
59 | useBuiltIns: 'entry',
60 | corejs: { version: '3.24', proposals: true },
61 | }]],
62 | },
63 | })],
64 | },
65 | ),
66 | buildBundle(
67 | './packages/Imagin/bundle.mjs',
68 | './packages/Imagin/dist/Imagin.min.js',
69 | { standalone: 'Imagin', format: 'iife' },
70 | ),
71 |
72 | ]
73 |
74 | // Build mini versions of locales
75 | const localesModules = await fs.opendir(new URL('./@Imagin/locales/src/', PACKAGES_ROOT))
76 | for await (const dirent of localesModules) {
77 | if (!dirent.isDirectory() && dirent.name.endsWith('.js')) {
78 | const localeName = path.basename(dirent.name, '.js')
79 | methods.push(
80 | buildBundle(
81 | `./packages/@Imagin/locales/src/${localeName}.js`,
82 | `./packages/@Imagin/locales/dist/${localeName}.min.js`,
83 | { minify: true },
84 | ),
85 | )
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3-multipart/src/createSignedURL.test.js:
--------------------------------------------------------------------------------
1 | import { describe, it, beforeEach, afterEach } from 'vitest'
2 | import assert from 'node:assert'
3 | import { S3Client, UploadPartCommand, PutObjectCommand } from '@aws-sdk/client-s3'
4 | import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
5 | import createSignedURL from './createSignedURL.js'
6 |
7 | const bucketName = 'some-bucket'
8 | const s3ClientOptions = {
9 | region: 'us-bar-1',
10 | credentials: {
11 | accessKeyId: 'foo',
12 | secretAccessKey: 'bar',
13 | sessionToken: 'foobar',
14 | },
15 | }
16 | const { Date: OriginalDate } = globalThis
17 |
18 | describe('createSignedURL', () => {
19 | beforeEach(() => {
20 | const now_ms = OriginalDate.now()
21 | globalThis.Date = function Date () {
22 | if (new.target) {
23 | return Reflect.construct(OriginalDate, [now_ms])
24 | }
25 | return Reflect.apply(OriginalDate, this, [now_ms])
26 | }
27 | globalThis.Date.now = function now () {
28 | return now_ms
29 | }
30 | })
31 | afterEach(() => {
32 | globalThis.Date = OriginalDate
33 | })
34 | it('should be able to sign non-multipart upload', async () => {
35 | const client = new S3Client(s3ClientOptions)
36 | assert.strictEqual(
37 | (await createSignedURL({
38 | accountKey: s3ClientOptions.credentials.accessKeyId,
39 | accountSecret: s3ClientOptions.credentials.secretAccessKey,
40 | sessionToken: s3ClientOptions.credentials.sessionToken,
41 | bucketName,
42 | Key: 'some/key',
43 | Region: s3ClientOptions.region,
44 | expires: 900,
45 | })).searchParams.get('X-Amz-Signature'),
46 | new URL(await getSignedUrl(client, new PutObjectCommand({
47 | Bucket: bucketName,
48 | Fields: {},
49 | Key: 'some/key',
50 | }, { expiresIn: 900 }))).searchParams.get('X-Amz-Signature'),
51 | )
52 | })
53 | it('should be able to sign multipart upload', async () => {
54 | const client = new S3Client(s3ClientOptions)
55 | const partNumber = 99
56 | const uploadId = 'dummyUploadId'
57 | assert.strictEqual(
58 | (await createSignedURL({
59 | accountKey: s3ClientOptions.credentials.accessKeyId,
60 | accountSecret: s3ClientOptions.credentials.secretAccessKey,
61 | sessionToken: s3ClientOptions.credentials.sessionToken,
62 | uploadId,
63 | partNumber,
64 | bucketName,
65 | Key: 'some/key',
66 | Region: s3ClientOptions.region,
67 | expires: 900,
68 | })).searchParams.get('X-Amz-Signature'),
69 | new URL(await getSignedUrl(client, new UploadPartCommand({
70 | Bucket: bucketName,
71 | UploadId: uploadId,
72 | PartNumber: partNumber,
73 | Key: 'some/key',
74 | }, { expiresIn: 900 }))).searchParams.get('X-Amz-Signature'),
75 | )
76 | })
77 | })
78 |
--------------------------------------------------------------------------------
/e2e/start-companion-with-load-balancer.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { spawn } from 'node:child_process'
4 | import http from 'node:http'
5 | import httpProxy from 'http-proxy'
6 | import process from 'node:process'
7 |
8 | const numInstances = 3
9 | const lbPort = 3020
10 | const companionStartPort = 3021
11 |
12 | function createLoadBalancer (baseUrls) {
13 | const proxy = httpProxy.createProxyServer({ ws: true })
14 |
15 | let i = 0
16 |
17 | function getTarget () {
18 | return baseUrls[i % baseUrls.length]
19 | }
20 |
21 | const server = http.createServer((req, res) => {
22 | const target = getTarget()
23 | proxy.web(req, res, { target }, (err) => {
24 | console.error('Load balancer failed to proxy request', err.message)
25 | res.statusCode = 500
26 | res.end()
27 | })
28 | i++
29 | })
30 |
31 | server.on('upgrade', (req, socket, head) => {
32 | const target = getTarget()
33 | proxy.ws(req, socket, head, { target }, (err) => {
34 | console.error('Load balancer failed to proxy websocket', err.message)
35 | console.error(err)
36 | socket.destroy()
37 | })
38 | i++
39 | })
40 |
41 | server.listen(lbPort)
42 | console.log('Load balancer listening', lbPort)
43 | return server
44 | }
45 |
46 | const isWindows = process.platform === 'win32'
47 | const isOSX = process.platform === 'darwin'
48 |
49 | const startCompanion = ({ name, port }) => {
50 | const cp = spawn(process.execPath, [
51 | '-r', 'dotenv/config',
52 | ...(isWindows || isOSX ? ['--watch-path', 'packages/@Imagin/companion/src', '--watch'] : []),
53 | './packages/@Imagin/companion/src/standalone/start-server.js',
54 | ], {
55 | cwd: new URL('../', import.meta.url),
56 | stdio: 'inherit',
57 | env: {
58 | ...process.env,
59 | COMPANION_PORT: port,
60 | COMPANION_SECRET: 'development',
61 | COMPANION_PREAUTH_SECRET: 'development',
62 | COMPANION_ALLOW_LOCAL_URLS: 'true',
63 | COMPANION_LOGGER_PROCESS_NAME: name,
64 | },
65 | })
66 | return Object.defineProperty(cp, 'then', {
67 | __proto__: null,
68 | writable: true,
69 | configurable: true,
70 | value: Promise.prototype.then.bind(new Promise((resolve, reject) => {
71 | cp.on('exit', (code) => {
72 | if (code === 0) resolve(cp)
73 | else reject(new Error(`Non-zero exit code: ${code}`))
74 | })
75 | cp.on('error', reject)
76 | })),
77 | })
78 | }
79 |
80 | const hosts = Array.from({ length: numInstances }, (_, index) => {
81 | const port = companionStartPort + index;
82 | return { index, port }
83 | })
84 |
85 | console.log('Starting companion instances on ports', hosts.map(({ port }) => port))
86 |
87 | const companions = hosts.map(({ index, port }) => startCompanion({ name: `companion${index}`, port }))
88 |
89 | let loadBalancer
90 | try {
91 | loadBalancer = createLoadBalancer(hosts.map(({ port }) => `http://localhost:${port}`))
92 | await Promise.all(companions)
93 | } finally {
94 | loadBalancer?.close()
95 | companions.forEach((companion) => companion.kill())
96 | }
97 |
--------------------------------------------------------------------------------
/e2e/generate-test.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import prompts from 'prompts'
3 | import fs from 'node:fs/promises'
4 |
5 | function dedent (strings, ...parts) {
6 | const nonSpacingChar = /\S/m.exec(strings[0])
7 | if (nonSpacingChar == null) return ''
8 |
9 | const indent = nonSpacingChar.index - strings[0].lastIndexOf('\n', nonSpacingChar.index) - 1
10 | const dedentEachLine = str => str.split('\n').map((line, i) => line.slice(i && indent)).join('\n')
11 | let returnLines = dedentEachLine(strings[0].slice(nonSpacingChar.index), indent)
12 | for (let i = 1; i < strings.length; i++) {
13 | returnLines += String(parts[i - 1]) + dedentEachLine(strings[i], indent)
14 | }
15 | return returnLines
16 | }
17 |
18 | const packageNames = await fs.readdir(new URL('../packages/@Imagin', import.meta.url))
19 | const unwantedPackages = ['core', 'companion', 'redux-dev-tools', 'utils']
20 |
21 | const { name } = await prompts({
22 | type: 'text',
23 | name: 'name',
24 | message: 'What should the name of the test be (e.g `dashboard-tus`)?',
25 | validate: (value) => /^[a-z|-]+$/i.test(value),
26 | })
27 |
28 | const { packages } = await prompts({
29 | type: 'multiselect',
30 | name: 'packages',
31 | message: 'What packages do you want to test?',
32 | hint: '@Imagin/core is automatically included',
33 | choices: packageNames
34 | .filter((pkg) => !unwantedPackages.includes(pkg))
35 | .map((pkg) => ({ title: pkg, value: pkg })),
36 | })
37 |
38 | const camelcase = (str) => str
39 | .toLowerCase()
40 | .replace(/([-][a-z])/g, (group) => group.toUpperCase().replace('-', ''))
41 |
42 | const html = dedent`
43 |
44 |
45 |
46 |
47 | ${name}
48 |
49 |
50 |
51 |
52 |
53 |
54 | `
55 |
56 | const testUrl = new URL(`cypress/integration/${name}.spec.ts`, import.meta.url)
57 | const test = dedent`
58 | describe('${name}', () => {
59 | beforeEach(() => {
60 | cy.visit('/${name}')
61 | })
62 | })
63 | `
64 | const htmlUrl = new URL(`clients/${name}/index.html`, import.meta.url)
65 |
66 |
67 | const appUrl = new URL(`clients/${name}/app.js`, import.meta.url)
68 | const app = dedent`
69 | import Imagin from '@Imagin/core'
70 | ${packages.map((pgk) => `import ${camelcase(pgk)} from '@Imagin/${pgk}'`).join('\n')}
71 |
72 | const Imagin = new Imagin()
73 | ${packages.map((pkg) => `.use(${camelcase(pkg)})`).join('\n\t')}
74 |
75 | window.Imagin = Imagin
76 | `
77 |
78 | await fs.writeFile(testUrl, test)
79 | await fs.mkdir(new URL(`clients/${name}`, import.meta.url))
80 | await fs.writeFile(htmlUrl, html)
81 | await fs.writeFile(appUrl, app)
82 |
83 | const homeUrl = new URL('clients/index.html', import.meta.url)
84 | const home = await fs.readFile(homeUrl, 'utf8')
85 | const newHome = home.replace(
86 | '',
87 | ` ${name}\n `,
88 | )
89 | await fs.writeFile(homeUrl, newHome)
90 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Clone this file to `.env` and edit the clone.
2 |
3 | NODE_ENV=development
4 |
5 | # Companion
6 | # =======================
7 | COMPANION_DATADIR=./output
8 | COMPANION_DOMAIN=localhost:3020
9 | COMPANION_PROTOCOL=http
10 | COMPANION_PORT=3020
11 | COMPANION_CLIENT_ORIGINS=
12 | COMPANION_SECRET=development
13 | COMPANION_PREAUTH_SECRET=development2
14 |
15 | # NOTE: Only enable this in development. Enabling it in production is a security risk
16 | COMPANION_ALLOW_LOCAL_URLS=true
17 |
18 | # to enable S3
19 | COMPANION_AWS_KEY="YOUR AWS KEY"
20 | COMPANION_AWS_SECRET="YOUR AWS SECRET"
21 | # specifying a secret file will override a directly set secret
22 | # COMPANION_AWS_SECRET_FILE="PATH/TO/AWS/SECRET/FILE"
23 | COMPANION_AWS_BUCKET="YOUR AWS S3 BUCKET"
24 | COMPANION_AWS_REGION="AWS REGION"
25 | COMPANION_AWS_PREFIX="OPTIONAL PREFIX"
26 | # to enable S3 Transfer Acceleration (default: false)
27 | # COMPANION_AWS_USE_ACCELERATE_ENDPOINT="false"
28 | # to set X-Amz-Expires query param in presigned urls (in seconds, default: 800)
29 | # COMPANION_AWS_EXPIRES="800"
30 | # to set a canned ACL for uploaded objects: https://docs.aws.amazon.com/AmazonS3/latest/dev/acl-overview.html#canned-acl
31 | # COMPANION_AWS_ACL="public-read"
32 |
33 | COMPANION_BOX_KEY=***
34 | COMPANION_BOX_SECRET=***
35 |
36 | COMPANION_DROPBOX_KEY=***
37 | COMPANION_DROPBOX_SECRET=***
38 |
39 | COMPANION_GOOGLE_KEY=***
40 | COMPANION_GOOGLE_SECRET=***
41 |
42 | COMPANION_INSTAGRAM_KEY=***
43 | COMPANION_INSTAGRAM_SECRET=***
44 |
45 | COMPANION_FACEBOOK_KEY=***
46 | COMPANION_FACEBOOK_SECRET=***
47 |
48 | COMPANION_ZOOM_KEY=***
49 | COMPANION_ZOOM_SECRET=***
50 |
51 | COMPANION_UNSPLASH_KEY=***
52 | COMPANION_UNSPLASH_SECRET=***
53 |
54 | COMPANION_ONEDRIVE_KEY=***
55 | COMPANION_ONEDRIVE_SECRET=****
56 |
57 | # To test dynamic Oauth against local companion (which is pointless but allows us to test it without Transloadit's servers), enable these:
58 | #COMPANION_GOOGLE_KEYS_ENDPOINT=http://localhost:3020/drive/test-dynamic-oauth-credentials?secret=development
59 | #COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS=true
60 | #COMPANION_TEST_DYNAMIC_OAUTH_CREDENTIALS_SECRET=development
61 |
62 |
63 | # Development environment
64 | # =======================
65 |
66 | VITE_UPLOADER=tus
67 | # VITE_UPLOADER=s3
68 | # VITE_UPLOADER=s3-multipart
69 | # xhr will use protocol 'multipart' in companion, if used with a remote service, e.g. google drive.
70 | # If local upload will use browser XHR
71 | # VITE_UPLOADER=xhr
72 | # VITE_UPLOADER=transloadit
73 | # VITE_UPLOADER=transloadit-s3
74 | # VITE_UPLOADER=transloadit-xhr
75 |
76 | VITE_COMPANION_URL=http://localhost:3020
77 | # See also Transloadit.COMPANION_PATTERN
78 | VITE_COMPANION_ALLOWED_HOSTS="\.transloadit\.com$"
79 | VITE_TUS_ENDPOINT=https://tusd.tusdemo.net/files/
80 | VITE_XHR_ENDPOINT=https://xhr-server.herokuapp.com/upload
81 |
82 | # If you want to test dynamic Oauth
83 | # VITE_COMPANION_GOOGLE_DRIVE_KEYS_PARAMS_CREDENTIALS_NAME=companion-google-drive
84 |
85 | VITE_TRANSLOADIT_KEY=***
86 | VITE_TRANSLOADIT_TEMPLATE=***
87 | VITE_TRANSLOADIT_SERVICE_URL=https://api2.transloadit.com
88 | # Fill in if you want requests sent to Transloadit to be signed:
89 | # VITE_TRANSLOADIT_SECRET=***
90 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/companion-client/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { Uppy } from '@uppy/core'
2 |
3 | /**
4 | * Async storage interface, similar to `localStorage`. This can be used to
5 | * implement custom storages for authentication tokens.
6 | */
7 | export interface TokenStorage {
8 | setItem: (key: string, value: string) => Promise
9 | getItem: (key: string) => Promise
10 | removeItem: (key: string) => Promise
11 | }
12 |
13 | type CompanionHeaders = Record
14 |
15 | type CompanionKeys = {
16 | key: string
17 | credentialsName: string
18 | }
19 |
20 | export interface RequestClientOptions {
21 | companionUrl: string
22 | companionHeaders?: CompanionHeaders
23 | companionCookiesRule?: RequestCredentials
24 | companionKeysParams?: CompanionKeys
25 | }
26 |
27 | type RequestOptions = {
28 | skipPostResponse?: boolean
29 | signal?: AbortSignal
30 | }
31 |
32 | export class RequestClient {
33 | constructor(uppy: Uppy, opts: RequestClientOptions)
34 |
35 | readonly hostname: string
36 |
37 | setCompanionHeaders(headers: CompanionHeaders): void
38 |
39 | get(path: string, options?: RequestOptions): Promise
40 |
41 | /** @deprecated use option bag instead */
42 | get(path: string, skipPostResponse: boolean): Promise
43 |
44 | post(
45 | path: string,
46 | data: Record,
47 | options?: RequestOptions,
48 | ): Promise
49 |
50 | /** @deprecated use option bag instead */
51 | post(
52 | path: string,
53 | data: Record,
54 | skipPostResponse: boolean,
55 | ): Promise
56 |
57 | delete(
58 | path: string,
59 | data?: Record,
60 | options?: RequestOptions,
61 | ): Promise
62 |
63 | /** @deprecated use option bag instead */
64 | delete(
65 | path: string,
66 | data: Record,
67 | skipPostResponse: boolean,
68 | ): Promise
69 | }
70 |
71 | /**
72 | * Options for Providers that can be passed in by Uppy users through
73 | * Plugin constructors.
74 | */
75 | export interface PublicProviderOptions extends RequestClientOptions {
76 | companionAllowedHosts?: string | RegExp | Array
77 | }
78 |
79 | /**
80 | * Options for Providers, including internal options that Plugins can set.
81 | */
82 | export interface ProviderOptions extends PublicProviderOptions {
83 | provider: string
84 | name?: string
85 | pluginId: string
86 | }
87 |
88 | export class Provider extends RequestClient {
89 | constructor(uppy: Uppy, opts: ProviderOptions)
90 |
91 | checkAuth(): Promise
92 |
93 | authUrl(): string
94 |
95 | fileUrl(id: string): string
96 |
97 | list(directory: string): Promise
98 |
99 | logout(redirect?: string): Promise
100 |
101 | static initPlugin(
102 | plugin: unknown,
103 | opts: Record,
104 | defaultOpts?: Record,
105 | ): void
106 | }
107 |
108 | export interface SocketOptions {
109 | target: string
110 | autoOpen?: boolean
111 | }
112 |
113 | export class Socket {
114 | readonly isOpen: boolean
115 |
116 | constructor(opts: SocketOptions)
117 |
118 | open(): void
119 |
120 | close(): void
121 |
122 | send(action: string, payload: unknown): void
123 |
124 | on(action: string, handler: (param: any) => void): void
125 |
126 | once(action: string, handler: (param: any) => void): void
127 |
128 | emit(action: string, payload: (param: any) => void): void
129 | }
130 |
--------------------------------------------------------------------------------
/bin/build-css.js:
--------------------------------------------------------------------------------
1 | const sass = require('sass');
2 | const postcss = require('postcss');
3 | const autoprefixer = require('autoprefixer');
4 | const postcssLogical = require('postcss-logical');
5 | const postcssDirPseudoClass = require('postcss-dir-pseudo-class');
6 | const cssnano = require('cssnano');
7 | const { promisify } = require('node:util');
8 | const fs = require('node:fs');
9 | const path = require('node:path');
10 | const resolve = require('resolve');
11 | const glob = promisify(require('glob'));
12 |
13 | const renderScss = promisify(sass.render);
14 | const { mkdir, writeFile } = fs.promises;
15 |
16 | const cwd = process.cwd();
17 | let chalk;
18 |
19 | function getPostCSSPlugins() {
20 | return [
21 | autoprefixer,
22 | postcssLogical(),
23 | postcssDirPseudoClass(),
24 | ];
25 | }
26 |
27 | async function compileSCSS(file) {
28 | const importedFiles = new Set();
29 | const scssResult = await renderScss({
30 | file,
31 | importer: createImporter(importedFiles),
32 | });
33 | return scssResult.css;
34 | }
35 |
36 | function createImporter(importedFiles) {
37 | return (url, from, done) => {
38 | resolve(url, {
39 | basedir: path.dirname(from),
40 | filename: from,
41 | extensions: ['.scss'],
42 | }, (err, resolved) => {
43 | if (err) {
44 | done(err);
45 | return;
46 | }
47 |
48 | const realpath = fs.realpathSync(resolved);
49 | if (importedFiles.has(realpath)) {
50 | done({ contents: '' });
51 | return;
52 | }
53 | importedFiles.add(realpath);
54 | done({ file: realpath });
55 | });
56 | };
57 | }
58 |
59 | async function processCSS(css, file, plugins) {
60 | const result = await postcss(plugins).process(css, { from: file });
61 | result.warnings().forEach(warn => console.warn(warn.toString()));
62 | return result;
63 | }
64 |
65 | async function handleCSSOutput(file, css) {
66 | const outputDir = path.join(path.dirname(file), '../dist');
67 | const outfile = isImaginPackage(file) ?
68 | `${outputDir}/Imagin.css` :
69 | `${outputDir}/style.css`;
70 |
71 | await saveCSS(outfile, css);
72 | const minifiedCSS = await minifyCSS(outfile, css);
73 | await saveCSS(outfile.replace(/\.css$/, '.min.css'), minifiedCSS);
74 | }
75 |
76 | async function handleCSSOutput(file, css) {
77 | const outputDir = path.join(path.dirname(file), '../dist');
78 | const outfile = isFibcousPackage(file) ?
79 | `${outputDir}/fibcous.css` :
80 | `${outputDir}/style.css`;
81 |
82 | await saveAndLogCSS(outfile, css);
83 | const minifiedCSS = await minifyCSS(outfile, css);
84 | await saveAndLogCSS(outfile.replace(/\.css$/, '.min.css'), minifiedCSS);
85 | }
86 |
87 |
88 |
89 | async function saveCSS(outfile, css) {
90 | try {
91 | await mkdir(path.dirname(outfile), { recursive: true });
92 | await writeFile(outfile, css);
93 | console.info(chalk.green('✓ CSS Processed:'), chalk.magenta(path.relative(cwd, outfile)));
94 | } catch (err) {
95 | throw new Error(`Failed to write file ${outfile}: ${err.message}`);
96 | }
97 | }
98 |
99 | function isImaginPackage(file) {
100 | return path.normalize(file).includes('packages/Imagin/');
101 | }
102 |
103 | async function minifyCSS(outfile, css) {
104 | const result = await postcss([cssnano({ safe: true })]).process(css, { from: outfile });
105 | result.warnings().forEach(warn => console.warn(warn.toString()));
106 | return result.css;
107 | }
108 |
109 | async function compileCSS() {
110 | ({ default: chalk } = await import('chalk'));
111 | const files = await glob('packages/{,@Imagin/}*/src/style.scss');
112 | const plugins = getPostCSSPlugins();
113 |
114 | for (const file of files) {
115 | try {
116 | const css = await compileSCSS(file);
117 | const postcssResult = await processCSS(css, file, plugins);
118 | await handleCSSOutput(file, postcssResult.css);
119 | } catch (err) {
120 | console.error(chalk.red(`✗ Error processing ${file}:`), chalk.red(err.message));
121 | }
122 | }
123 |
124 | console.info(chalk.yellow('CSS Bundles OK'));
125 | };
126 |
127 | compileCSS().catch(err => {
128 | console.error(chalk.red('✗ Global Error:'), chalk.red(err.message));
129 | });
130 |
--------------------------------------------------------------------------------
/bin/build-lib.js:
--------------------------------------------------------------------------------
1 | const babel = require('@babel/core');
2 | const path = require('node:path');
3 |
4 | const t = require('@babel/types');
5 | const { promisify } = require('node:util');
6 | const glob = promisify(require('glob'));
7 | const fs = require('node:fs');
8 |
9 | const { mkdir, stat, writeFile } = fs.promises;
10 |
11 | const PACKAGE_JSON_IMPORT = /^\..*\/package.json$/;
12 | const SOURCE = 'packages/{*,@Imagin/*}/src/**/*.{js,ts}?(x)';
13 |
14 | const IGNORE = /\.test\.[jt]s$|__mocks__|svelte|angular|companion\//;
15 | const META_FILES = [
16 | 'babel.config.js',
17 | 'package.json',
18 | 'package-lock.json',
19 | 'yarn.lock',
20 | 'bin/build-lib.js',
21 | ];
22 |
23 | function lastModified (file, createParentDir = false) {
24 | return stat(file).then((s) => s.mtime, async (err) => {
25 | if (err.code === 'ENOENT') {
26 | if (createParentDir) {
27 | await mkdir(path.dirname(file), { recursive: true })
28 | }
29 | return 0
30 | }
31 | throw err
32 | })
33 | };
34 |
35 | const versionCache = new Map();
36 |
37 | async function preparePackage (file) {
38 | const packageFolder = file.slice(0, file.indexOf('/src/'))
39 | if (versionCache.has(packageFolder)) return
40 |
41 | // eslint-disable-next-line import/no-dynamic-require, global-require
42 | const { version } = require(path.join(__dirname, '..', packageFolder, 'package.json'))
43 | if (process.env.FRESH) {
44 | // in case it hasn't been done before.
45 | await mkdir(path.join(packageFolder, 'lib'), { recursive: true })
46 | }
47 | versionCache.set(packageFolder, version)
48 | };
49 |
50 | const nonJSImport = /^\.\.?\/.+\.([jt]sx|ts)$/;
51 | // eslint-disable-next-line no-shadow
52 | function rewriteNonJSImportsToJS (path) {
53 | const match = nonJSImport.exec(path.node.source.value)
54 | if (match) {
55 | // eslint-disable-next-line no-param-reassign
56 | path.node.source.value = `${match[0].slice(0, -match[1].length)}js`
57 | }
58 | };
59 |
60 | async function buildLib () {
61 | const metaMtimes = await Promise.all(META_FILES.map((filename) => lastModified(path.join(__dirname, '..', filename))))
62 | const metaMtime = Math.max(...metaMtimes)
63 |
64 | const files = await glob(SOURCE)
65 | /* eslint-disable no-continue */
66 | for (const file of files) {
67 | if (IGNORE.test(file)) {
68 | continue
69 | }
70 | await preparePackage(file)
71 | const libFile = file.replace('/src/', '/lib/').replace(/\.[jt]sx?$/, '.js')
72 |
73 | // on a fresh build, rebuild everything.
74 | if (!process.env.FRESH) {
75 | const [srcMtime, libMtime] = await Promise.all([
76 | lastModified(file),
77 | lastModified(libFile, true),
78 | ])
79 | if (srcMtime < libMtime && metaMtime < libMtime) {
80 | continue
81 | }
82 | }
83 |
84 | const plugins = [{
85 | visitor: {
86 | // eslint-disable-next-line no-shadow
87 | ImportDeclaration (path) {
88 | rewriteNonJSImportsToJS(path)
89 | if (PACKAGE_JSON_IMPORT.test(path.node.source.value)
90 | && path.node.specifiers.length === 1
91 | && path.node.specifiers[0].type === 'ImportDefaultSpecifier') {
92 | const version = versionCache.get(file.slice(0, file.indexOf('/src/')))
93 | if (version != null) {
94 | const [{ local }] = path.node.specifiers
95 | path.replaceWith(
96 | t.variableDeclaration('const', [t.variableDeclarator(local,
97 | t.objectExpression([
98 | t.objectProperty(t.stringLiteral('version'), t.stringLiteral(version)),
99 | ]))]),
100 | )
101 | }
102 | }
103 | },
104 |
105 | ExportAllDeclaration: rewriteNonJSImportsToJS,
106 | },
107 | }]
108 | const isTSX = file.endsWith('.tsx')
109 | if (isTSX || file.endsWith('.ts')) { plugins.push(['@babel/plugin-transform-typescript', { disallowAmbiguousJSXLike: true, isTSX, jsxPragma: 'h' }]) }
110 |
111 | const { code, map } = await babel.transformFileAsync(file, { sourceMaps: true, plugins })
112 | const [{ default: chalk }] = await Promise.all([
113 | import('chalk'),
114 | writeFile(libFile, code),
115 | writeFile(`${libFile}.map`, JSON.stringify(map)),
116 | ])
117 | console.log(chalk.green('Compiled lib:'), chalk.magenta(libFile))
118 | }
119 | /* eslint-enable no-continue */
120 | };
121 |
122 | console.log('Using Babel version:', require('@babel/core/package.json').version)
123 |
124 | buildLib().catch((err) => {
125 | console.error(err)
126 | process.exit(1)
127 | });
128 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3-multipart/types/index.d.ts:
--------------------------------------------------------------------------------
1 | import type { BasePlugin, PluginOptions, UppyFile } from '@uppy/core'
2 |
3 | type MaybePromise = T | Promise
4 |
5 | export type AwsS3UploadParameters =
6 | | {
7 | method: 'POST'
8 | url: string
9 | fields: Record
10 | expires?: number
11 | headers?: Record
12 | }
13 | | {
14 | method?: 'PUT'
15 | url: string
16 | fields?: Record
17 | expires?: number
18 | headers?: Record
19 | }
20 |
21 | export interface AwsS3Part {
22 | PartNumber?: number
23 | Size?: number
24 | ETag?: string
25 | }
26 | /**
27 | * @deprecated use {@link AwsS3UploadParameters} instead
28 | */
29 | export interface AwsS3SignedPart {
30 | url: string
31 | headers?: Record
32 | }
33 | export interface AwsS3STSResponse {
34 | credentials: {
35 | AccessKeyId: string
36 | SecretAccessKey: string
37 | SessionToken: string
38 | Expiration?: string
39 | }
40 | bucket: string
41 | region: string
42 | }
43 |
44 | type AWSS3NonMultipartWithCompanionMandatory = {
45 | getUploadParameters?: never
46 | }
47 |
48 | type AWSS3NonMultipartWithoutCompanionMandatory = {
49 | getUploadParameters: (file: UppyFile) => MaybePromise
50 | }
51 | type AWSS3NonMultipartWithCompanion = AWSS3WithCompanion &
52 | AWSS3NonMultipartWithCompanionMandatory & {
53 | shouldUseMultipart: false
54 | createMultipartUpload?: never
55 | listParts?: never
56 | signPart?: never
57 | abortMultipartUpload?: never
58 | completeMultipartUpload?: never
59 | }
60 |
61 | type AWSS3NonMultipartWithoutCompanion = AWSS3WithoutCompanion &
62 | AWSS3NonMultipartWithoutCompanionMandatory & {
63 | shouldUseMultipart: false
64 | createMultipartUpload?: never
65 | listParts?: never
66 | signPart?: never
67 | abortMultipartUpload?: never
68 | completeMultipartUpload?: never
69 | }
70 |
71 | type AWSS3MultipartWithoutCompanionMandatory = {
72 | getChunkSize?: (file: UppyFile) => number
73 | createMultipartUpload: (
74 | file: UppyFile,
75 | ) => MaybePromise<{ uploadId: string; key: string }>
76 | listParts: (
77 | file: UppyFile,
78 | opts: { uploadId: string; key: string; signal: AbortSignal },
79 | ) => MaybePromise
80 | abortMultipartUpload: (
81 | file: UppyFile,
82 | opts: { uploadId: string; key: string; signal: AbortSignal },
83 | ) => MaybePromise
84 | completeMultipartUpload: (
85 | file: UppyFile,
86 | opts: {
87 | uploadId: string
88 | key: string
89 | parts: AwsS3Part[]
90 | signal: AbortSignal
91 | },
92 | ) => MaybePromise<{ location?: string }>
93 | } & (
94 | | {
95 | signPart: (
96 | file: UppyFile,
97 | opts: {
98 | uploadId: string
99 | key: string
100 | partNumber: number
101 | body: Blob
102 | signal: AbortSignal
103 | },
104 | ) => MaybePromise
105 | }
106 | | {
107 | /** @deprecated Use signPart instead */
108 | prepareUploadParts: (
109 | file: UppyFile,
110 | partData: {
111 | uploadId: string
112 | key: string
113 | parts: [{ number: number; chunk: Blob }]
114 | },
115 | ) => MaybePromise<{
116 | presignedUrls: Record
117 | headers?: Record>
118 | }>
119 | }
120 | )
121 | type AWSS3MultipartWithoutCompanion = AWSS3WithoutCompanion &
122 | AWSS3MultipartWithoutCompanionMandatory & {
123 | shouldUseMultipart?: true
124 | getUploadParameters?: never
125 | }
126 |
127 | type AWSS3MultipartWithCompanion = AWSS3WithCompanion &
128 | Partial & {
129 | shouldUseMultipart?: true
130 | getUploadParameters?: never
131 | }
132 |
133 | type AWSS3MaybeMultipartWithCompanion = AWSS3WithCompanion &
134 | Partial &
135 | AWSS3NonMultipartWithCompanionMandatory & {
136 | shouldUseMultipart: (file: UppyFile) => boolean
137 | }
138 |
139 | type AWSS3MaybeMultipartWithoutCompanion = AWSS3WithoutCompanion &
140 | AWSS3MultipartWithoutCompanionMandatory &
141 | AWSS3NonMultipartWithoutCompanionMandatory & {
142 | shouldUseMultipart: (file: UppyFile) => boolean
143 | }
144 |
145 | type AWSS3WithCompanion = {
146 | companionUrl: string
147 | companionHeaders?: Record
148 | companionCookiesRule?: string
149 | getTemporarySecurityCredentials?: true
150 | }
151 | type AWSS3WithoutCompanion = {
152 | companionUrl?: never
153 | companionHeaders?: never
154 | companionCookiesRule?: never
155 | getTemporarySecurityCredentials?: (options?: {
156 | signal?: AbortSignal
157 | }) => MaybePromise
158 | }
159 |
160 | interface _AwsS3MultipartOptions extends PluginOptions {
161 | allowedMetaFields?: string[] | null
162 | limit?: number
163 | retryDelays?: number[] | null
164 | }
165 |
166 | export type AwsS3MultipartOptions = _AwsS3MultipartOptions &
167 | (
168 | | AWSS3NonMultipartWithCompanion
169 | | AWSS3NonMultipartWithoutCompanion
170 | | AWSS3MultipartWithCompanion
171 | | AWSS3MultipartWithoutCompanion
172 | | AWSS3MaybeMultipartWithCompanion
173 | | AWSS3MaybeMultipartWithoutCompanion
174 | )
175 |
176 | declare class AwsS3Multipart extends BasePlugin {}
177 |
178 | export default AwsS3Multipart
179 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/companion-client/src/Socket.test.js:
--------------------------------------------------------------------------------
1 | import { afterEach, beforeEach, vi, describe, it, expect } from 'vitest'
2 | import UppySocket from './Socket.js'
3 |
4 | describe('Socket', () => {
5 | let webSocketConstructorSpy
6 | let webSocketCloseSpy
7 | let webSocketSendSpy
8 |
9 | beforeEach(() => {
10 | webSocketConstructorSpy = vi.fn()
11 | webSocketCloseSpy = vi.fn()
12 | webSocketSendSpy = vi.fn()
13 |
14 | globalThis.WebSocket = class WebSocket {
15 | constructor (target) {
16 | webSocketConstructorSpy(target)
17 | }
18 |
19 | // eslint-disable-next-line class-methods-use-this
20 | close (args) {
21 | webSocketCloseSpy(args)
22 | }
23 |
24 | // eslint-disable-next-line class-methods-use-this
25 | send (json) {
26 | webSocketSendSpy(json)
27 | }
28 |
29 | triggerOpen () {
30 | this.onopen()
31 | }
32 |
33 | triggerClose () {
34 | this.onclose()
35 | }
36 | }
37 | })
38 | afterEach(() => {
39 | globalThis.WebSocket = undefined
40 | })
41 |
42 | it('should expose a class', () => {
43 | expect(UppySocket.name).toEqual('UppySocket')
44 | expect(
45 | new UppySocket({
46 | target: 'foo',
47 | }) instanceof UppySocket,
48 | )
49 | })
50 |
51 | it('should setup a new WebSocket', () => {
52 | new UppySocket({ target: 'foo' }) // eslint-disable-line no-new
53 | expect(webSocketConstructorSpy.mock.calls[0][0]).toEqual('foo')
54 | })
55 |
56 | it('should send a message via the websocket if the connection is open', () => {
57 | const uppySocket = new UppySocket({ target: 'foo' })
58 | const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
59 | webSocketInstance.triggerOpen()
60 |
61 | uppySocket.send('bar', 'boo')
62 | expect(webSocketSendSpy.mock.calls.length).toEqual(1)
63 | expect(webSocketSendSpy.mock.calls[0]).toEqual([
64 | JSON.stringify({ action: 'bar', payload: 'boo' }),
65 | ])
66 | })
67 |
68 | it('should queue the message for the websocket if the connection is not open', () => {
69 | const uppySocket = new UppySocket({ target: 'foo' })
70 |
71 | uppySocket.send('bar', 'boo')
72 | expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([{ action: 'bar', payload: 'boo' }])
73 | expect(webSocketSendSpy.mock.calls.length).toEqual(0)
74 | })
75 |
76 | it('should queue any messages for the websocket if the connection is not open, then send them when the connection is open', () => {
77 | const uppySocket = new UppySocket({ target: 'foo' })
78 | const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
79 |
80 | uppySocket.send('bar', 'boo')
81 | uppySocket.send('moo', 'baa')
82 | expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([
83 | { action: 'bar', payload: 'boo' },
84 | { action: 'moo', payload: 'baa' },
85 | ])
86 | expect(webSocketSendSpy.mock.calls.length).toEqual(0)
87 |
88 | webSocketInstance.triggerOpen()
89 |
90 | expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([])
91 | expect(webSocketSendSpy.mock.calls.length).toEqual(2)
92 | expect(webSocketSendSpy.mock.calls[0]).toEqual([
93 | JSON.stringify({ action: 'bar', payload: 'boo' }),
94 | ])
95 | expect(webSocketSendSpy.mock.calls[1]).toEqual([
96 | JSON.stringify({ action: 'moo', payload: 'baa' }),
97 | ])
98 | })
99 |
100 | it('should start queuing any messages when the websocket connection is closed', () => {
101 | const uppySocket = new UppySocket({ target: 'foo' })
102 | const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
103 | webSocketInstance.triggerOpen()
104 | uppySocket.send('bar', 'boo')
105 | expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([])
106 |
107 | webSocketInstance.triggerClose()
108 | uppySocket.send('bar', 'boo')
109 | expect(uppySocket[Symbol.for('uppy test: getQueued')]()).toEqual([{ action: 'bar', payload: 'boo' }])
110 | })
111 |
112 | it('should close the websocket when it is force closed', () => {
113 | const uppySocket = new UppySocket({ target: 'foo' })
114 | const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
115 | webSocketInstance.triggerOpen()
116 |
117 | uppySocket.close()
118 | expect(webSocketCloseSpy.mock.calls.length).toEqual(1)
119 | })
120 |
121 | it('should be able to subscribe to messages received on the websocket', () => {
122 | const uppySocket = new UppySocket({ target: 'foo' })
123 | const webSocketInstance = uppySocket[Symbol.for('uppy test: getSocket')]()
124 |
125 | const emitterListenerMock = vi.fn()
126 | uppySocket.on('hi', emitterListenerMock)
127 |
128 | webSocketInstance.triggerOpen()
129 | webSocketInstance.onmessage({
130 | data: JSON.stringify({ action: 'hi', payload: 'ho' }),
131 | })
132 | expect(emitterListenerMock.mock.calls).toEqual([
133 | ['ho', undefined, undefined, undefined, undefined, undefined],
134 | ])
135 | })
136 |
137 | it('should be able to emit messages and subscribe to them', () => {
138 | const uppySocket = new UppySocket({ target: 'foo' })
139 |
140 | const emitterListenerMock = vi.fn()
141 | uppySocket.on('hi', emitterListenerMock)
142 |
143 | uppySocket.emit('hi', 'ho')
144 | uppySocket.emit('hi', 'ho')
145 | uppySocket.emit('hi', 'off to work we go')
146 |
147 | expect(emitterListenerMock.mock.calls).toEqual([
148 | ['ho', undefined, undefined, undefined, undefined, undefined],
149 | ['ho', undefined, undefined, undefined, undefined, undefined],
150 | [
151 | 'off to work we go',
152 | undefined,
153 | undefined,
154 | undefined,
155 | undefined,
156 | undefined,
157 | ],
158 | ])
159 | })
160 |
161 | it('should be able to subscribe to the first event for a particular action', () => {
162 | const uppySocket = new UppySocket({ target: 'foo' })
163 |
164 | const emitterListenerMock = vi.fn()
165 | uppySocket.once('hi', emitterListenerMock)
166 |
167 | uppySocket.emit('hi', 'ho')
168 | uppySocket.emit('hi', 'ho')
169 | uppySocket.emit('hi', 'off to work we go')
170 |
171 | expect(emitterListenerMock.mock.calls.length).toEqual(1)
172 | expect(emitterListenerMock.mock.calls).toEqual([
173 | ['ho', undefined, undefined, undefined, undefined, undefined],
174 | ])
175 | })
176 | })
177 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3-multipart/src/createSignedURL.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Create a canonical request by concatenating the following strings, separated
3 | * by newline characters. This helps ensure that the signature that you
4 | * calculate and the signature that AWS calculates can match.
5 | *
6 | * @see https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html#create-canonical-request
7 | *
8 | * @param {object} param0
9 | * @param {string} param0.method – The HTTP method.
10 | * @param {string} param0.CanonicalUri – The URI-encoded version of the absolute
11 | * path component URL (everything between the host and the question mark
12 | * character (?) that starts the query string parameters). If the absolute path
13 | * is empty, use a forward slash character (/).
14 | * @param {string} param0.CanonicalQueryString – The URL-encoded query string
15 | * parameters, separated by ampersands (&). Percent-encode reserved characters,
16 | * including the space character. Encode names and values separately. If there
17 | * are empty parameters, append the equals sign to the parameter name before
18 | * encoding. After encoding, sort the parameters alphabetically by key name. If
19 | * there is no query string, use an empty string ("").
20 | * @param {Record} param0.SignedHeaders – The request headers,
21 | * that will be signed, and their values, separated by newline characters.
22 | * For the values, trim any leading or trailing spaces, convert sequential
23 | * spaces to a single space, and separate the values for a multi-value header
24 | * using commas. You must include the host header (HTTP/1.1), and any x-amz-*
25 | * headers in the signature. You can optionally include other standard headers
26 | * in the signature, such as content-type.
27 | * @param {string} param0.HashedPayload – A string created using the payload in
28 | * the body of the HTTP request as input to a hash function. This string uses
29 | * lowercase hexadecimal characters. If the payload is empty, use an empty
30 | * string as the input to the hash function.
31 | * @returns {string}
32 | */
33 | function createCanonicalRequest ({
34 | method = 'PUT',
35 | CanonicalUri = '/',
36 | CanonicalQueryString = '',
37 | SignedHeaders,
38 | HashedPayload,
39 | }) {
40 | const headerKeys = Object.keys(SignedHeaders).map(k => k.toLowerCase()).sort()
41 | return [
42 | method,
43 | CanonicalUri,
44 | CanonicalQueryString,
45 | ...headerKeys.map(k => `${k}:${SignedHeaders[k]}`),
46 | '',
47 | headerKeys.join(';'),
48 | HashedPayload,
49 | ].join('\n')
50 | }
51 |
52 | const ec = new TextEncoder()
53 | const algorithm = { name: 'HMAC', hash: 'SHA-256' }
54 |
55 | async function digest (data) {
56 | const { subtle } = globalThis.crypto
57 | return subtle.digest(algorithm.hash, ec.encode(data))
58 | }
59 |
60 | async function generateHmacKey (secret) {
61 | const { subtle } = globalThis.crypto
62 | return subtle.importKey('raw', typeof secret === 'string' ? ec.encode(secret) : secret, algorithm, false, ['sign'])
63 | }
64 |
65 | function arrayBufferToHexString (arrayBuffer) {
66 | const byteArray = new Uint8Array(arrayBuffer)
67 | let hexString = ''
68 | for (let i = 0; i < byteArray.length; i++) {
69 | hexString += byteArray[i].toString(16).padStart(2, '0')
70 | }
71 | return hexString
72 | }
73 |
74 | async function hash (key, data) {
75 | const { subtle } = globalThis.crypto
76 | return subtle.sign(algorithm, await generateHmacKey(key), ec.encode(data))
77 | }
78 |
79 | /**
80 | * @see https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
81 | * @param {Record} param0
82 | * @returns {Promise} the signed URL
83 | */
84 | export default async function createSignedURL ({
85 | accountKey, accountSecret, sessionToken,
86 | bucketName,
87 | Key, Region,
88 | expires,
89 | uploadId, partNumber,
90 | }) {
91 | const Service = 's3'
92 | const host = `${bucketName}.${Service}.${Region}.amazonaws.com`
93 | const CanonicalUri = `/${encodeURI(Key)}`
94 | const payload = 'UNSIGNED-PAYLOAD'
95 |
96 | const requestDateTime = new Date().toISOString().replace(/[-:]|\.\d+/g, '') // YYYYMMDDTHHMMSSZ
97 | const date = requestDateTime.slice(0, 8) // YYYYMMDD
98 | const scope = `${date}/${Region}/${Service}/aws4_request`
99 |
100 | const url = new URL(`https://${host}${CanonicalUri}`)
101 | // N.B.: URL search params needs to be added in the ASCII order
102 | url.searchParams.set('X-Amz-Algorithm', 'AWS4-HMAC-SHA256')
103 | url.searchParams.set('X-Amz-Content-Sha256', payload)
104 | url.searchParams.set('X-Amz-Credential', `${accountKey}/${scope}`)
105 | url.searchParams.set('X-Amz-Date', requestDateTime)
106 | url.searchParams.set('X-Amz-Expires', expires)
107 | // We are signing on the client, so we expect there's going to be a session token:
108 | url.searchParams.set('X-Amz-Security-Token', sessionToken)
109 | url.searchParams.set('X-Amz-SignedHeaders', 'host')
110 | // Those two are present only for Multipart Uploads:
111 | if (partNumber) url.searchParams.set('partNumber', partNumber)
112 | if (uploadId) url.searchParams.set('uploadId', uploadId)
113 | url.searchParams.set('x-id', partNumber && uploadId ? 'UploadPart' : 'PutObject')
114 |
115 | // Step 1: Create a canonical request
116 | const canonical = createCanonicalRequest({
117 | CanonicalUri,
118 | CanonicalQueryString: url.search.slice(1),
119 | SignedHeaders: {
120 | host,
121 | },
122 | HashedPayload: payload,
123 | })
124 |
125 | // Step 2: Create a hash of the canonical request
126 | const hashedCanonical = arrayBufferToHexString(await digest(canonical))
127 |
128 | // Step 3: Create a string to sign
129 | const stringToSign = [
130 | `AWS4-HMAC-SHA256`, // The algorithm used to create the hash of the canonical request.
131 | requestDateTime, // The date and time used in the credential scope.
132 | scope, // The credential scope. This restricts the resulting signature to the specified Region and service.
133 | hashedCanonical, // The hash of the canonical request.
134 | ].join('\n')
135 |
136 | // Step 4: Calculate the signature
137 | const kDate = await hash(`AWS4${accountSecret}`, date)
138 | const kRegion = await hash(kDate, Region)
139 | const kService = await hash(kRegion, Service)
140 | const kSigning = await hash(kService, 'aws4_request')
141 | const signature = arrayBufferToHexString(await hash(kSigning, stringToSign))
142 |
143 | // Step 5: Add the signature to the request
144 | url.searchParams.set('X-Amz-Signature', signature)
145 |
146 | return url
147 | }
148 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3-multipart/src/MultipartUploader.js:
--------------------------------------------------------------------------------
1 | import { AbortController } from '@uppy/utils/lib/AbortController'
2 |
3 | const MB = 1024 * 1024
4 |
5 | const defaultOptions = {
6 | getChunkSize (file) {
7 | return Math.ceil(file.size / 10000)
8 | },
9 | onProgress () {},
10 | onPartComplete () {},
11 | onSuccess () {},
12 | onError (err) {
13 | throw err
14 | },
15 | }
16 |
17 | function ensureInt (value) {
18 | if (typeof value === 'string') {
19 | return parseInt(value, 10)
20 | }
21 | if (typeof value === 'number') {
22 | return value
23 | }
24 | throw new TypeError('Expected a number')
25 | }
26 |
27 | export const pausingUploadReason = Symbol('pausing upload, not an actual error')
28 |
29 | /**
30 | * A MultipartUploader instance is used per file upload to determine whether a
31 | * upload should be done as multipart or as a regular S3 upload
32 | * (based on the user-provided `shouldUseMultipart` option value) and to manage
33 | * the chunk splitting.
34 | */
35 | class MultipartUploader {
36 | #abortController = new AbortController()
37 |
38 | /** @type {import("../types/chunk").Chunk[]} */
39 | #chunks
40 |
41 | /** @type {{ uploaded: number, etag?: string, done?: boolean }[]} */
42 | #chunkState
43 |
44 | /**
45 | * The (un-chunked) data to upload.
46 | *
47 | * @type {Blob}
48 | */
49 | #data
50 |
51 | /** @type {import("@uppy/core").UppyFile} */
52 | #file
53 |
54 | /** @type {boolean} */
55 | #uploadHasStarted = false
56 |
57 | /** @type {(err?: Error | any) => void} */
58 | #onError
59 |
60 | /** @type {() => void} */
61 | #onSuccess
62 |
63 | /** @type {import('../types/index').AwsS3MultipartOptions["shouldUseMultipart"]} */
64 | #shouldUseMultipart
65 |
66 | /** @type {boolean} */
67 | #isRestoring
68 |
69 | #onReject = (err) => (err?.cause === pausingUploadReason ? null : this.#onError(err))
70 |
71 | #maxMultipartParts = 10_000
72 |
73 | #minPartSize = 5 * MB
74 |
75 | constructor (data, options) {
76 | this.options = {
77 | ...defaultOptions,
78 | ...options,
79 | }
80 | // Use default `getChunkSize` if it was null or something
81 | this.options.getChunkSize ??= defaultOptions.getChunkSize
82 |
83 | this.#data = data
84 | this.#file = options.file
85 | this.#onSuccess = this.options.onSuccess
86 | this.#onError = this.options.onError
87 | this.#shouldUseMultipart = this.options.shouldUseMultipart
88 |
89 | // When we are restoring an upload, we already have an UploadId and a Key. Otherwise
90 | // we need to call `createMultipartUpload` to get an `uploadId` and a `key`.
91 | // Non-multipart uploads are not restorable.
92 | this.#isRestoring = options.uploadId && options.key
93 |
94 | this.#initChunks()
95 | }
96 |
97 | // initChunks checks the user preference for using multipart uploads (opts.shouldUseMultipart)
98 | // and calculates the optimal part size. When using multipart part uploads every part except for the last has
99 | // to be at least 5 MB and there can be no more than 10K parts.
100 | // This means we sometimes need to change the preferred part size from the user in order to meet these requirements.
101 | #initChunks () {
102 | const fileSize = this.#data.size
103 | const shouldUseMultipart = typeof this.#shouldUseMultipart === 'function'
104 | ? this.#shouldUseMultipart(this.#file)
105 | : Boolean(this.#shouldUseMultipart)
106 |
107 | if (shouldUseMultipart && fileSize > this.#minPartSize) {
108 | // At least 5MB per request:
109 | let chunkSize = Math.max(this.options.getChunkSize(this.#data), this.#minPartSize)
110 | let arraySize = Math.floor(fileSize / chunkSize)
111 |
112 | // At most 10k requests per file:
113 | if (arraySize > this.#maxMultipartParts) {
114 | arraySize = this.#maxMultipartParts
115 | chunkSize = fileSize / this.#maxMultipartParts
116 | }
117 | this.#chunks = Array(arraySize)
118 |
119 | for (let offset = 0, j = 0; offset < fileSize; offset += chunkSize, j++) {
120 | const end = Math.min(fileSize, offset + chunkSize)
121 |
122 | // Defer data fetching/slicing until we actually need the data, because it's slow if we have a lot of files
123 | const getData = () => {
124 | const i2 = offset
125 | return this.#data.slice(i2, end)
126 | }
127 |
128 | this.#chunks[j] = {
129 | getData,
130 | onProgress: this.#onPartProgress(j),
131 | onComplete: this.#onPartComplete(j),
132 | shouldUseMultipart,
133 | }
134 | if (this.#isRestoring) {
135 | const size = offset + chunkSize > fileSize ? fileSize - offset : chunkSize
136 | // setAsUploaded is called by listPart, to keep up-to-date the
137 | // quantity of data that is left to actually upload.
138 | this.#chunks[j].setAsUploaded = () => {
139 | this.#chunks[j] = null
140 | this.#chunkState[j].uploaded = size
141 | }
142 | }
143 | }
144 | } else {
145 | this.#chunks = [{
146 | getData: () => this.#data,
147 | onProgress: this.#onPartProgress(0),
148 | onComplete: this.#onPartComplete(0),
149 | shouldUseMultipart,
150 | }]
151 | }
152 |
153 | this.#chunkState = this.#chunks.map(() => ({ uploaded: 0 }))
154 | }
155 |
156 | #createUpload () {
157 | this
158 | .options.companionComm.uploadFile(this.#file, this.#chunks, this.#abortController.signal)
159 | .then(this.#onSuccess, this.#onReject)
160 | this.#uploadHasStarted = true
161 | }
162 |
163 | #resumeUpload () {
164 | this
165 | .options.companionComm.resumeUploadFile(this.#file, this.#chunks, this.#abortController.signal)
166 | .then(this.#onSuccess, this.#onReject)
167 | }
168 |
169 | #onPartProgress = (index) => (ev) => {
170 | if (!ev.lengthComputable) return
171 |
172 | this.#chunkState[index].uploaded = ensureInt(ev.loaded)
173 |
174 | const totalUploaded = this.#chunkState.reduce((n, c) => n + c.uploaded, 0)
175 | this.options.onProgress(totalUploaded, this.#data.size)
176 | }
177 |
178 | #onPartComplete = (index) => (etag) => {
179 | // This avoids the net::ERR_OUT_OF_MEMORY in Chromium Browsers.
180 | this.#chunks[index] = null
181 | this.#chunkState[index].etag = etag
182 | this.#chunkState[index].done = true
183 |
184 | const part = {
185 | PartNumber: index + 1,
186 | ETag: etag,
187 | }
188 | this.options.onPartComplete(part)
189 | }
190 |
191 | #abortUpload () {
192 | this.#abortController.abort()
193 | this.options.companionComm.abortFileUpload(this.#file).catch((err) => this.options.log(err))
194 | }
195 |
196 | start () {
197 | if (this.#uploadHasStarted) {
198 | if (!this.#abortController.signal.aborted) this.#abortController.abort(pausingUploadReason)
199 | this.#abortController = new AbortController()
200 | this.#resumeUpload()
201 | } else if (this.#isRestoring) {
202 | this.options.companionComm.restoreUploadFile(this.#file, { uploadId: this.options.uploadId, key: this.options.key })
203 | this.#resumeUpload()
204 | } else {
205 | this.#createUpload()
206 | }
207 | }
208 |
209 | pause () {
210 | this.#abortController.abort(pausingUploadReason)
211 | // Swap it out for a new controller, because this instance may be resumed later.
212 | this.#abortController = new AbortController()
213 | }
214 |
215 | abort (opts = undefined) {
216 | if (opts?.really) this.#abortUpload()
217 | else this.pause()
218 | }
219 |
220 | // TODO: remove this in the next major
221 | get chunkState () {
222 | return this.#chunkState
223 | }
224 | }
225 |
226 | export default MultipartUploader
227 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/aws-s3/src/MiniXHRUpload.js:
--------------------------------------------------------------------------------
1 | import { nanoid } from 'nanoid/non-secure'
2 | import EventManager from '@uppy/utils/lib/EventManager'
3 | import ProgressTimeout from '@uppy/utils/lib/ProgressTimeout'
4 | import ErrorWithCause from '@uppy/utils/lib/ErrorWithCause'
5 | import NetworkError from '@uppy/utils/lib/NetworkError'
6 | import isNetworkError from '@uppy/utils/lib/isNetworkError'
7 | import { internalRateLimitedQueue } from '@uppy/utils/lib/RateLimitedQueue'
8 |
9 | function buildResponseError (xhr, error) {
10 | if (isNetworkError(xhr)) return new NetworkError(error, xhr)
11 |
12 | const err = new ErrorWithCause('Upload error', { cause: error })
13 | err.request = xhr
14 | return err
15 | }
16 |
17 | function setTypeInBlob (file) {
18 | const dataWithUpdatedType = file.data.slice(0, file.data.size, file.meta.type)
19 | return dataWithUpdatedType
20 | }
21 |
22 | function addMetadata (formData, meta, opts) {
23 | const allowedMetaFields = Array.isArray(opts.allowedMetaFields)
24 | ? opts.allowedMetaFields
25 | // Send along all fields by default.
26 | : Object.keys(meta)
27 | allowedMetaFields.forEach((item) => {
28 | formData.append(item, meta[item])
29 | })
30 | }
31 |
32 | function createFormDataUpload (file, opts) {
33 | const formPost = new FormData()
34 |
35 | addMetadata(formPost, file.meta, opts)
36 |
37 | const dataWithUpdatedType = setTypeInBlob(file)
38 |
39 | if (file.name) {
40 | formPost.append(opts.fieldName, dataWithUpdatedType, file.meta.name)
41 | } else {
42 | formPost.append(opts.fieldName, dataWithUpdatedType)
43 | }
44 |
45 | return formPost
46 | }
47 |
48 | const createBareUpload = file => file.data
49 |
50 | export default class MiniXHRUpload {
51 | constructor (uppy, opts) {
52 | this.uppy = uppy
53 | this.opts = {
54 | validateStatus (status) {
55 | return status >= 200 && status < 300
56 | },
57 | ...opts,
58 | }
59 |
60 | this.requests = opts[internalRateLimitedQueue]
61 | this.uploaderEvents = Object.create(null)
62 | this.i18n = opts.i18n
63 | }
64 |
65 | getOptions (file) {
66 | const { uppy } = this
67 |
68 | const overrides = uppy.getState().xhrUpload
69 | const opts = {
70 | ...this.opts,
71 | ...(overrides || {}),
72 | ...(file.xhrUpload || {}),
73 | headers: {
74 | ...this.opts.headers,
75 | ...overrides?.headers,
76 | ...file.xhrUpload?.headers,
77 | },
78 | }
79 |
80 | return opts
81 | }
82 |
83 | #addEventHandlerForFile (eventName, fileID, eventHandler) {
84 | this.uploaderEvents[fileID].on(eventName, (fileOrID) => {
85 | // TODO (major): refactor Uppy events to consistently send file objects (or consistently IDs)
86 | // We created a generic `addEventListenerForFile` but not all events
87 | // use file IDs, some use files, so we need to do this weird check.
88 | const id = fileOrID?.id ?? fileOrID
89 | if (fileID === id) eventHandler()
90 | })
91 | }
92 |
93 | #addEventHandlerIfFileStillExists (eventName, fileID, eventHandler) {
94 | this.uploaderEvents[fileID].on(eventName, (...args) => {
95 | if (this.uppy.getFile(fileID)) eventHandler(...args)
96 | })
97 | }
98 |
99 | uploadLocalFile (file) {
100 | const opts = this.getOptions(file)
101 |
102 | return new Promise((resolve, reject) => {
103 | // This is done in index.js in the S3 plugin.
104 | // this.uppy.emit('upload-started', file)
105 |
106 | const data = opts.formData
107 | ? createFormDataUpload(file, opts)
108 | : createBareUpload(file, opts)
109 |
110 | const xhr = new XMLHttpRequest()
111 | this.uploaderEvents[file.id] = new EventManager(this.uppy)
112 |
113 | const timer = new ProgressTimeout(opts.timeout, () => {
114 | xhr.abort()
115 | // eslint-disable-next-line no-use-before-define
116 | queuedRequest.done()
117 | const error = new Error(this.i18n('timedOut', { seconds: Math.ceil(opts.timeout / 1000) }))
118 | this.uppy.emit('upload-error', file, error)
119 | reject(error)
120 | })
121 |
122 | const id = nanoid()
123 |
124 | xhr.upload.addEventListener('loadstart', () => {
125 | this.uppy.log(`[AwsS3/XHRUpload] ${id} started`)
126 | })
127 |
128 | xhr.upload.addEventListener('progress', (ev) => {
129 | this.uppy.log(`[AwsS3/XHRUpload] ${id} progress: ${ev.loaded} / ${ev.total}`)
130 | // Begin checking for timeouts when progress starts, instead of loading,
131 | // to avoid timing out requests on browser concurrency queue
132 | timer.progress()
133 |
134 | if (ev.lengthComputable) {
135 | this.uppy.emit('upload-progress', file, {
136 | uploader: this,
137 | bytesUploaded: ev.loaded,
138 | bytesTotal: ev.total,
139 | })
140 | }
141 | })
142 |
143 | xhr.addEventListener('load', (ev) => {
144 | this.uppy.log(`[AwsS3/XHRUpload] ${id} finished`)
145 | timer.done()
146 | // eslint-disable-next-line no-use-before-define
147 | queuedRequest.done()
148 | if (this.uploaderEvents[file.id]) {
149 | this.uploaderEvents[file.id].remove()
150 | this.uploaderEvents[file.id] = null
151 | }
152 |
153 | if (opts.validateStatus(ev.target.status, xhr.responseText, xhr)) {
154 | const body = opts.getResponseData(xhr.responseText, xhr)
155 | const uploadURL = body[opts.responseUrlFieldName]
156 |
157 | const uploadResp = {
158 | status: ev.target.status,
159 | body,
160 | uploadURL,
161 | }
162 |
163 | this.uppy.emit('upload-success', file, uploadResp)
164 |
165 | if (uploadURL) {
166 | this.uppy.log(`Download ${file.name} from ${uploadURL}`)
167 | }
168 |
169 | return resolve(file)
170 | }
171 | const body = opts.getResponseData(xhr.responseText, xhr)
172 | const error = buildResponseError(xhr, opts.getResponseError(xhr.responseText, xhr))
173 |
174 | const response = {
175 | status: ev.target.status,
176 | body,
177 | }
178 |
179 | this.uppy.emit('upload-error', file, error, response)
180 | return reject(error)
181 | })
182 |
183 | xhr.addEventListener('error', () => {
184 | this.uppy.log(`[AwsS3/XHRUpload] ${id} errored`)
185 | timer.done()
186 | // eslint-disable-next-line no-use-before-define
187 | queuedRequest.done()
188 | if (this.uploaderEvents[file.id]) {
189 | this.uploaderEvents[file.id].remove()
190 | this.uploaderEvents[file.id] = null
191 | }
192 |
193 | const error = buildResponseError(xhr, opts.getResponseError(xhr.responseText, xhr))
194 | this.uppy.emit('upload-error', file, error)
195 | return reject(error)
196 | })
197 |
198 | xhr.open(opts.method.toUpperCase(), opts.endpoint, true)
199 | // IE10 does not allow setting `withCredentials` and `responseType`
200 | // before `open()` is called. It’s important to set withCredentials
201 | // to a boolean, otherwise React Native crashes
202 | xhr.withCredentials = Boolean(opts.withCredentials)
203 | if (opts.responseType !== '') {
204 | xhr.responseType = opts.responseType
205 | }
206 |
207 | Object.keys(opts.headers).forEach((header) => {
208 | xhr.setRequestHeader(header, opts.headers[header])
209 | })
210 |
211 | const queuedRequest = this.requests.run(() => {
212 | xhr.send(data)
213 | return () => {
214 | // eslint-disable-next-line no-use-before-define
215 | timer.done()
216 | xhr.abort()
217 | }
218 | }, { priority: 1 })
219 |
220 | this.#addEventHandlerForFile('file-removed', file.id, () => {
221 | queuedRequest.abort()
222 | reject(new Error('File removed'))
223 | })
224 |
225 | this.#addEventHandlerIfFileStillExists('cancel-all', file.id, ({ reason } = {}) => {
226 | if (reason === 'user') {
227 | queuedRequest.abort()
228 | }
229 | reject(new Error('Upload cancelled'))
230 | })
231 | })
232 | }
233 | }
234 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "name": "@ImgCypress_1-dev/build",
4 | "version": "1.2.1",
5 | "description": "JS course exercises",
6 | "lint-staged": {
7 | "*.{js,mjs,cjs,jsx}": "eslint --fix",
8 | "*.{ts,mts,cts,tsx}": [
9 | "eslint --fix",
10 | "prettier -w",
11 | "eslint"
12 | ],
13 | "*.{css,html,json,scss,vue,yaml,yml}": "prettier -w",
14 | "*.md": [
15 | "remark --silently-ignore -i .remarkignore -foq",
16 | "eslint --fix"
17 | ]
18 | },
19 | "remarkConfig": {
20 | "plugins": [
21 | "@ImgCypress_1-dev/remark-lint-ImgCypress_1"
22 | ]
23 | },
24 | "pre-commit": "lint:staged",
25 | "license": "MIT",
26 | "engines": {
27 | "node": "^16.15.0 || >=18.0.0",
28 | "yarn": "3.5.9"
29 | },
30 | "packageManager": "yarn@3.6.1+sha224.679d48a4db29f6beed7fe901a71e56b5e0619cdd615e140d9f33ce92",
31 | "workspaces": [
32 | "examples/*",
33 | "packages/@ImgCypress_1/*",
34 | "packages/@ImgCypress_1/angular/projects/ImgCypress_1/*",
35 | "packages/ImgCypress_1",
36 | "private/*",
37 | "test/endtoend",
38 | "e2e"
39 | ],
40 | "devDependencies": {
41 | "@aws-sdk/client-s3": "^3.338.0",
42 | "@babel/cli": "^7.14.5",
43 | "@babel/core": "^7.14.6",
44 | "@babel/eslint-parser": "^7.11.3",
45 | "@babel/eslint-plugin": "^7.11.3",
46 | "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5",
47 | "@babel/plugin-proposal-optional-chaining": "^7.16.0",
48 | "@babel/plugin-transform-modules-commonjs": "^7.16.8",
49 | "@babel/plugin-transform-react-jsx": "^7.10.4",
50 | "@babel/plugin-transform-typescript": "^7.22.10",
51 | "@babel/preset-env": "^7.14.7",
52 | "@babel/register": "^7.10.5",
53 | "@babel/types": "^7.17.0",
54 | "@types/jasmine": "file:./private/@types/jasmine",
55 | "@types/jasminewd2": "file:./private/@types/jasmine",
56 | "@typescript-eslint/eslint-plugin": "^5.0.0",
57 | "@typescript-eslint/parser": "^5.0.0",
58 | "@ImgCypress_1-dev/remark-lint-ImgCypress_1": "workspace:*",
59 | "esbuild": "^0.17.1",
60 | "esbuild-plugin-babel": "^0.2.3",
61 | "eslint": "^8.0.0",
62 | "adm-zip": "^0.5.5",
63 | "autoprefixer": "^10.2.6",
64 | "babel-plugin-inline-package-json": "^2.0.0",
65 | "chalk": "^5.0.0",
66 | "concat-stream": "^2.0.0",
67 | "core-js": "~3.24.0",
68 | "cssnano": "^5.0.6",
69 | "dotenv": "^16.0.0",
70 | "eslint-config-prettier": "^9.0.0",
71 | "eslint-config-transloadit": "^2.0.0",
72 | "eslint-plugin-compat": "^4.0.0",
73 | "eslint-plugin-cypress": "^2.12.1",
74 | "eslint-plugin-import": "^2.25.2",
75 | "eslint-plugin-jest": "^27.0.0",
76 | "eslint-plugin-jsdoc": "^40.0.0",
77 | "eslint-plugin-jsx-a11y": "^6.4.1",
78 | "eslint-plugin-markdown": "^3.0.0",
79 | "eslint-plugin-no-only-tests": "^3.1.0",
80 | "eslint-plugin-node": "^11.1.0",
81 | "eslint-plugin-prefer-import": "^0.0.1",
82 | "eslint-plugin-promise": "^6.0.0",
83 | "eslint-plugin-react": "^7.22.0",
84 | "eslint-plugin-react-hooks": "^4.2.0",
85 | "eslint-plugin-unicorn": "^46.0.0",
86 | "github-contributors-list": "^1.2.4",
87 | "glob": "^8.0.0",
88 | "jsdom": "^22.1.0",
89 | "lint-staged": "^13.0.0",
90 | "mime-types": "^2.1.26",
91 | "nodemon": "^2.0.8",
92 | "npm-packlist": "^5.0.0",
93 | "npm-run-all": "^4.1.5",
94 | "onchange": "^7.1.0",
95 | "pacote": "^13.0.0",
96 | "postcss": "^8.4.31",
97 | "postcss-dir-pseudo-class": "^6.0.0",
98 | "postcss-logical": "^5.0.0",
99 | "pre-commit": "^1.2.2",
100 | "prettier": "^3.0.3",
101 | "remark-cli": "^11.0.0",
102 | "resolve": "^1.17.0",
103 | "sass": "^1.29.0",
104 | "start-server-and-test": "^1.14.0",
105 | "stylelint": "^15.0.0",
106 | "stylelint-config-rational-order": "^0.1.2",
107 | "stylelint-config-standard": "^34.0.0",
108 | "stylelint-config-standard-scss": "^10.0.0",
109 | "tar": "^6.1.0",
110 | "tsd": "^0.28.0",
111 | "typescript": "~5.1",
112 | "vitest": "^0.34.5",
113 | "vue-template-compiler": "workspace:*"
114 | },
115 | "scripts": {
116 | "start:companion": "bash bin/companion.sh",
117 | "start:companion:with-loadbalancer": "e2e/start-companion-with-load-balancer.mjs",
118 | "build:bundle": "yarn node ./bin/build-bundle.mjs",
119 | "build:clean": "rm -rf packages/*/lib packages/@ImgCypress_1/*/lib packages/*/dist packages/@ImgCypress_1/*/dist",
120 | "build:companion": "yarn workspace @ImgCypress_1/companion build",
121 | "build:css": "yarn node ./bin/build-css.js",
122 | "build:svelte": "yarn workspace @ImgCypress_1/svelte build",
123 | "build:angular": "yarn workspace angular build",
124 | "build:js": "npm-run-all build:lib build:companion build:locale-pack build:svelte build:angular build:bundle",
125 | "build:ts": "yarn workspaces list --no-private --json | yarn node ./bin/build-ts.mjs",
126 | "build:lib": "yarn node ./bin/build-lib.js",
127 | "build:locale-pack": "yarn workspace @ImgCypress_1-dev/locale-pack build && eslint packages/@ImgCypress_1/locales/src/en_US.js --fix && yarn workspace @ImgCypress_1-dev/locale-pack test unused",
128 | "build": "npm-run-all --parallel build:js build:css --serial size",
129 | "contributors:save": "yarn node ./bin/update-contributors.mjs",
130 | "dev:with-companion": "npm-run-all --parallel start:companion dev",
131 | "dev": "yarn workspace @ImgCypress_1-dev/dev dev",
132 | "lint:fix": "yarn lint --fix",
133 | "lint:markdown": "remark -f -q -i .remarkignore . .github/CONTRIBUTING.md",
134 | "lint:staged": "lint-staged",
135 | "lint:css": "stylelint ./packages/**/*.scss",
136 | "lint:css:fix": "stylelint ./packages/**/*.scss --fix",
137 | "lint": "eslint . --cache",
138 | "format:show-diff": "git diff --quiet || (echo 'Unable to show a diff because there are unstaged changes'; false) && (prettier . -w --loglevel silent && git --no-pager diff; git restore .)",
139 | "format:check": "prettier -c .",
140 | "format:check-diff": "yarn format:check || (yarn format:show-diff && false)",
141 | "format": "prettier -w .",
142 | "release": "PACKAGES=$(yarn workspaces list --json) yarn workspace @ImgCypress_1-dev/release interactive",
143 | "size": "echo 'JS Bundle mingz:' && cat packages/ImgCypress_1/dist/ImgCypress_1.min.js | gzip | wc -c && echo 'CSS Bundle mingz:' && cat packages/ImgCypress_1/dist/ImgCypress_1.min.css | gzip | wc -c",
144 | "e2e": "yarn build && yarn e2e:skip-build",
145 | "e2e:skip-build": "npm-run-all --parallel watch:js:lib e2e:client start:companion:with-loadbalancer e2e:cypress",
146 | "e2e:ci": "start-server-and-test 'npm-run-all --parallel e2e:client start:companion:with-loadbalancer' '1234|3020' e2e:headless",
147 | "e2e:client": "yarn workspace e2e client:start",
148 | "e2e:cypress": "yarn workspace e2e cypress:open",
149 | "e2e:headless": "yarn workspace e2e cypress:headless",
150 | "e2e:generate": "yarn workspace e2e generate-test",
151 | "test:companion": "yarn workspace @ImgCypress_1/companion test",
152 | "test:companion:watch": "yarn workspace @ImgCypress_1/companion test --watch",
153 | "test:locale-packs": "yarn locale-packs:unused && yarn locale-packs:warnings",
154 | "test:locale-packs:unused": "yarn workspace @ImgCypress_1-dev/locale-pack test unused",
155 | "test:locale-packs:warnings": "yarn workspace @ImgCypress_1-dev/locale-pack test warnings",
156 | "test:type": "yarn workspaces foreach -piv --include '@ImgCypress_1/*' --exclude '@ImgCypress_1/{angular,react-native,locales,companion,provider-views,robodog,svelte}' exec tsd",
157 | "test:unit": "yarn run build:lib && yarn test:watch --run",
158 | "test:watch": "vitest --environment jsdom --dir packages/@ImgCypress_1",
159 | "test": "npm-run-all lint test:locale-packs:unused test:unit test:type test:companion",
160 | "uploadcdn": "yarn node ./bin/upload-to-cdn.js",
161 | "version": "yarn node ./bin/after-version-bump.js",
162 | "watch:css": "onchange 'packages/{@ImgCypress_1/,}*/src/*.scss' --initial --verbose -- yarn run build:css",
163 | "watch:js:bundle": "onchange 'packages/{@ImgCypress_1/,}*/src/**/*.js' --initial --verbose -- yarn run build:bundle",
164 | "watch:js:lib": "onchange 'packages/{@ImgCypress_1/,}*/src/**/*.js' --initial --verbose -- yarn run build:lib",
165 | "watch:js": "npm-run-all --parallel watch:js:bundle watch:js:lib",
166 | "watch": "npm-run-all --parallel watch:css watch:js"
167 | },
168 | "resolutions": {
169 | "@types/eslint@^7.2.13": "^8.2.0",
170 | "@types/react": "^17",
171 | "@types/webpack-dev-server": "^4",
172 | "p-queue": "patch:p-queue@npm%3A7.4.1#./.yarn/patches/p-queue-npm-7.4.1-e0cf0a6f17.patch",
173 | "pre-commit": "patch:pre-commit@npm:1.2.2#.yarn/patches/pre-commit-npm-1.2.2-f30af83877.patch",
174 | "preact": "patch:preact@npm:10.10.0#.yarn/patches/preact-npm-10.10.0-dd04de05e8.patch",
175 | "start-server-and-test": "patch:start-server-and-test@npm:1.14.0#.yarn/patches/start-server-and-test-npm-1.14.0-841aa34fdf.patch",
176 | "stylelint-config-rational-order": "patch:stylelint-config-rational-order@npm%3A0.1.2#./.yarn/patches/stylelint-config-rational-order-npm-0.1.2-d8336e84ed.patch",
177 | "uuid@^8.3.2": "patch:uuid@npm:8.3.2#.yarn/patches/uuid-npm-8.3.2-eca0baba53.patch"
178 | }
179 | }
180 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/companion-client/src/Provider.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import RequestClient, { authErrorStatusCode } from './RequestClient.js'
4 | import * as tokenStorage from './tokenStorage.js'
5 |
6 |
7 | const getName = (id) => {
8 | return id.split('-').map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(' ')
9 | }
10 |
11 | function getOrigin() {
12 | // eslint-disable-next-line no-restricted-globals
13 | return location.origin
14 | }
15 |
16 | function getRegex(value) {
17 | if (typeof value === 'string') {
18 | return new RegExp(`^${value}$`)
19 | } if (value instanceof RegExp) {
20 | return value
21 | }
22 | return undefined
23 | }
24 |
25 | function isOriginAllowed(origin, allowedOrigin) {
26 | const patterns = Array.isArray(allowedOrigin) ? allowedOrigin.map(getRegex) : [getRegex(allowedOrigin)]
27 | return patterns
28 | .some((pattern) => pattern?.test(origin) || pattern?.test(`${origin}/`)) // allowing for trailing '/'
29 | }
30 |
31 | export default class Provider extends RequestClient {
32 | #refreshingTokenPromise
33 |
34 | constructor(uppy, opts) {
35 | super(uppy, opts)
36 | this.provider = opts.provider
37 | this.id = this.provider
38 | this.name = this.opts.name || getName(this.id)
39 | this.pluginId = this.opts.pluginId
40 | this.tokenKey = `companion-${this.pluginId}-auth-token`
41 | this.companionKeysParams = this.opts.companionKeysParams
42 | this.preAuthToken = null
43 | this.supportsRefreshToken = opts.supportsRefreshToken ?? true // todo false in next major
44 | }
45 |
46 | async headers() {
47 | const [headers, token] = await Promise.all([super.headers(), this.#getAuthToken()])
48 | const authHeaders = {}
49 | if (token) {
50 | authHeaders['uppy-auth-token'] = token
51 | }
52 |
53 | if (this.companionKeysParams) {
54 | authHeaders['uppy-credentials-params'] = btoa(
55 | JSON.stringify({ params: this.companionKeysParams }),
56 | )
57 | }
58 | return { ...headers, ...authHeaders }
59 | }
60 |
61 | onReceiveResponse(response) {
62 | super.onReceiveResponse(response)
63 | const plugin = this.uppy.getPlugin(this.pluginId)
64 | const oldAuthenticated = plugin.getPluginState().authenticated
65 | const authenticated = oldAuthenticated ? response.status !== authErrorStatusCode : response.status < 400
66 | plugin.setPluginState({ authenticated })
67 | return response
68 | }
69 |
70 | async setAuthToken(token) {
71 | return this.uppy.getPlugin(this.pluginId).storage.setItem(this.tokenKey, token)
72 | }
73 |
74 | async #getAuthToken() {
75 | return this.uppy.getPlugin(this.pluginId).storage.getItem(this.tokenKey)
76 | }
77 |
78 | /** @protected */
79 | async removeAuthToken() {
80 | return this.uppy.getPlugin(this.pluginId).storage.removeItem(this.tokenKey)
81 | }
82 |
83 | /**
84 | * Ensure we have a preauth token if necessary. Attempts to fetch one if we don't,
85 | * or rejects if loading one fails.
86 | */
87 | async ensurePreAuth() {
88 | if (this.companionKeysParams && !this.preAuthToken) {
89 | await this.fetchPreAuthToken()
90 |
91 | if (!this.preAuthToken) {
92 | throw new Error('Could not load authentication data required for third-party login. Please try again later.')
93 | }
94 | }
95 | }
96 |
97 | // eslint-disable-next-line class-methods-use-this
98 | authQuery() {
99 | return {}
100 | }
101 |
102 | authUrl({ authFormData, query } = {}) {
103 | const params = new URLSearchParams({
104 | ...query,
105 | state: btoa(JSON.stringify({ origin: getOrigin() })),
106 | ...this.authQuery({ authFormData }),
107 | })
108 |
109 | if (this.preAuthToken) {
110 | params.set('uppyPreAuthToken', this.preAuthToken)
111 | }
112 |
113 | return `${this.hostname}/${this.id}/connect?${params}`
114 | }
115 |
116 | /** @protected */
117 | async loginSimpleAuth({ uppyVersions, authFormData, signal }) {
118 | const response = await this.post(`${this.id}/simple-auth`, { form: authFormData }, { qs: { uppyVersions }, signal })
119 | this.setAuthToken(response.uppyAuthToken)
120 | }
121 |
122 | /** @protected */
123 | async loginOAuth({ uppyVersions, authFormData, signal }) {
124 | await this.ensurePreAuth()
125 |
126 | signal.throwIfAborted()
127 |
128 | return new Promise((resolve, reject) => {
129 | const link = this.authUrl({ query: { uppyVersions }, authFormData })
130 | const authWindow = window.open(link, '_blank')
131 |
132 | let cleanup
133 |
134 | const handleToken = (e) => {
135 | if (e.source !== authWindow) {
136 | let jsonData = ''
137 | try {
138 | // TODO improve our uppy logger so that it can take an arbitrary number of arguments,
139 | // each either objects, errors or strings,
140 | // then we don’t have to manually do these things like json stringify when logging.
141 | // the logger should never throw an error.
142 | jsonData = JSON.stringify(e.data)
143 | } catch (err) {
144 | // in case JSON.stringify fails (ignored)
145 | }
146 | this.uppy.log(`ignoring event from unknown source ${jsonData}`, 'warning')
147 | return
148 | }
149 |
150 | const { companionAllowedHosts } = this.uppy.getPlugin(this.pluginId).opts
151 | if (!isOriginAllowed(e.origin, companionAllowedHosts)) {
152 | reject(new Error(`rejecting event from ${e.origin} vs allowed pattern ${companionAllowedHosts}`))
153 | return
154 | }
155 |
156 | // Check if it's a string before doing the JSON.parse to maintain support
157 | // for older Companion versions that used object references
158 | const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
159 |
160 | if (data.error) {
161 | const { uppy } = this
162 | const message = uppy.i18n('authAborted')
163 | uppy.info({ message }, 'warning', 5000)
164 | reject(new Error('auth aborted'))
165 | return
166 | }
167 |
168 | if (!data.token) {
169 | reject(new Error('did not receive token from auth window'))
170 | return
171 | }
172 |
173 | cleanup()
174 | resolve(this.setAuthToken(data.token))
175 | }
176 |
177 | cleanup = () => {
178 | authWindow.close()
179 | window.removeEventListener('message', handleToken)
180 | signal.removeEventListener('abort', cleanup)
181 | }
182 |
183 | signal.addEventListener('abort', cleanup)
184 | window.addEventListener('message', handleToken)
185 | })
186 | }
187 |
188 | async login({ uppyVersions, authFormData, signal }) {
189 | return this.loginOAuth({ uppyVersions, authFormData, signal })
190 | }
191 |
192 | refreshTokenUrl() {
193 | return `${this.hostname}/${this.id}/refresh-token`
194 | }
195 |
196 | fileUrl(id) {
197 | return `${this.hostname}/${this.id}/get/${id}`
198 | }
199 |
200 | /** @protected */
201 | async request(...args) {
202 | await this.#refreshingTokenPromise
203 |
204 | try {
205 | // to test simulate access token expired (leading to a token token refresh),
206 | // see mockAccessTokenExpiredError in companion/drive.
207 | // If you want to test refresh token *and* access token invalid, do this for example with Google Drive:
208 | // While uploading, go to your google account settings,
209 | // "Third-party apps & services", then click "Companion" and "Remove access".
210 |
211 | return await super.request(...args)
212 | } catch (err) {
213 | if (!this.supportsRefreshToken) throw err
214 | // only handle auth errors (401 from provider), and only handle them if we have a (refresh) token
215 | const authTokenAfter = await this.#getAuthToken()
216 | if (!err.isAuthError || !authTokenAfter) throw err
217 |
218 | if (this.#refreshingTokenPromise == null) {
219 | // Many provider requests may be starting at once, however refresh token should only be called once.
220 | // Once a refresh token operation has started, we need all other request to wait for this operation (atomically)
221 | this.#refreshingTokenPromise = (async () => {
222 | try {
223 | this.uppy.log(`[CompanionClient] Refreshing expired auth token`, 'info')
224 | const response = await super.request({ path: this.refreshTokenUrl(), method: 'POST' })
225 | await this.setAuthToken(response.uppyAuthToken)
226 | } catch (refreshTokenErr) {
227 | if (refreshTokenErr.isAuthError) {
228 | // if refresh-token has failed with auth error, delete token, so we don't keep trying to refresh in future
229 | await this.removeAuthToken()
230 | }
231 | throw err
232 | } finally {
233 | this.#refreshingTokenPromise = undefined
234 | }
235 | })()
236 | }
237 |
238 | await this.#refreshingTokenPromise
239 |
240 | // now retry the request with our new refresh token
241 | return super.request(...args)
242 | }
243 | }
244 |
245 | async fetchPreAuthToken() {
246 | if (!this.companionKeysParams) {
247 | return
248 | }
249 |
250 | try {
251 | const res = await this.post(`${this.id}/preauth/`, { params: this.companionKeysParams })
252 | this.preAuthToken = res.token
253 | } catch (err) {
254 | this.uppy.log(`[CompanionClient] unable to fetch preAuthToken ${err}`, 'warning')
255 | }
256 | }
257 |
258 | list(directory, options) {
259 | return this.get(`${this.id}/list/${directory || ''}`, options)
260 | }
261 |
262 | async logout(options) {
263 | const response = await this.get(`${this.id}/logout`, options)
264 | await this.removeAuthToken()
265 | return response
266 | }
267 |
268 | static initPlugin(plugin, opts, defaultOpts) {
269 | /* eslint-disable no-param-reassign */
270 | plugin.type = 'acquirer'
271 | plugin.files = []
272 | if (defaultOpts) {
273 | plugin.opts = { ...defaultOpts, ...opts }
274 | }
275 |
276 | if (opts.serverUrl || opts.serverPattern) {
277 | throw new Error('`serverUrl` and `serverPattern` have been renamed to `companionUrl` and `companionAllowedHosts` respectively in the 0.30.5 release. Please consult the docs (for example, https://uppy.io/docs/instagram/ for the Instagram plugin) and use the updated options.`')
278 | }
279 |
280 | if (opts.companionAllowedHosts) {
281 | const pattern = opts.companionAllowedHosts
282 | // validate companionAllowedHosts param
283 | if (typeof pattern !== 'string' && !Array.isArray(pattern) && !(pattern instanceof RegExp)) {
284 | throw new TypeError(`${plugin.id}: the option "companionAllowedHosts" must be one of string, Array, RegExp`)
285 | }
286 | plugin.opts.companionAllowedHosts = pattern
287 | } else if (/^(?!https?:\/\/).*$/i.test(opts.companionUrl)) {
288 | // does not start with https://
289 | plugin.opts.companionAllowedHosts = `https://${opts.companionUrl.replace(/^\/\//, '')}`
290 | } else {
291 | plugin.opts.companionAllowedHosts = new URL(opts.companionUrl).origin
292 | }
293 |
294 | plugin.storage = plugin.opts.storage || tokenStorage
295 | /* eslint-enable no-param-reassign */
296 | }
297 | }
298 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable quote-props */
2 |
3 | 'use strict'
4 |
5 | const svgPresentationAttributes = [
6 | 'alignment-baseline', 'baseline-shift', 'class', 'clip', 'clip-path', 'clip-rule', 'color', 'color-interpolatio', 'color-interpolatio-filters', 'color-profile', 'color-rendering', 'cursor', 'direction', 'display', 'dominant-baseline', 'enable-background', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'glyph-orientation-horizontal', 'glyph-orientation-vertical', 'image-rendering', 'kerning', 'letter-spacing', 'lighting-color', 'marker-end', 'marker-mid', 'marker-start', 'mask', 'opacity', 'overflow', 'pointer-events', 'shape-rendering', 'stop-color', 'stop-opacity', 'stroke', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'text-anchor', 'text-decoration', 'text-rendering', 'transform', 'transform-origin', 'unicode-bidi', 'vector-effect', 'visibility', 'word-spacing', 'writing-mod',
7 | ]
8 |
9 | module.exports = {
10 | root: true,
11 | extends: ['transloadit', 'prettier'],
12 | env: {
13 | es6: true,
14 | jest: true,
15 | node: true,
16 | // extra:
17 | browser: true,
18 | },
19 | globals: {
20 | globalThis: true,
21 | hexo: true,
22 | window: true,
23 | },
24 | plugins: [
25 | '@babel/eslint-plugin',
26 | 'jest',
27 | 'markdown',
28 | 'node',
29 | 'prefer-import',
30 | 'promise',
31 | 'react',
32 | // extra:
33 | 'compat',
34 | 'jsdoc',
35 | 'no-only-tests',
36 | 'unicorn',
37 | ],
38 | parser: '@babel/eslint-parser',
39 | parserOptions: {
40 | sourceType: 'script',
41 | ecmaVersion: 2022,
42 | ecmaFeatures: {
43 | jsx: true,
44 | },
45 | },
46 | rules: {
47 | // transloadit rules we are actually ok with in the Imagin repo
48 | 'import/extensions': 'off',
49 | 'object-shorthand': ['error', 'always'],
50 | 'strict': 'off',
51 | 'key-spacing': 'off',
52 | 'max-classes-per-file': ['error', 2],
53 | 'react/no-unknown-property': ['error', {
54 | ignore: svgPresentationAttributes,
55 | }],
56 |
57 | // Special rules for CI:
58 | ...(process.env.CI && {
59 | // Some imports are available only after a full build, which we don't do on CI.
60 | 'import/no-unresolved': 'off',
61 | }),
62 |
63 | // rules we want to enforce
64 | 'array-callback-return': 'error',
65 | 'func-names': 'error',
66 | 'import/no-dynamic-require': 'error',
67 | 'import/no-extraneous-dependencies': 'error',
68 | 'max-len': 'error',
69 | 'no-empty': 'error',
70 | 'no-bitwise': 'error',
71 | 'no-continue': 'error',
72 | 'no-lonely-if': 'error',
73 | 'no-nested-ternary': 'error',
74 | 'no-restricted-properties': 'error',
75 | 'no-return-assign': 'error',
76 | 'no-underscore-dangle': 'error',
77 | 'no-unused-expressions': 'error',
78 | 'no-unused-vars': 'error',
79 | 'no-useless-concat': 'error',
80 | 'no-var': 'error',
81 | 'node/handle-callback-err': 'error',
82 | 'prefer-destructuring': 'error',
83 | 'prefer-spread': 'error',
84 | 'unicorn/prefer-node-protocol': 'error',
85 |
86 | 'react/button-has-type': 'error',
87 | 'react/forbid-prop-types': 'error',
88 | 'react/no-access-state-in-setstate': 'error',
89 | 'react/no-array-index-key': 'error',
90 | 'react/no-deprecated': 'error',
91 | 'react/no-this-in-sfc': 'error',
92 | 'react/no-will-update-set-state': 'error',
93 | 'react/prefer-stateless-function': 'error',
94 | 'react/sort-comp': 'error',
95 | 'react/style-prop-object': 'error',
96 |
97 | // accessibility
98 | 'jsx-a11y/alt-text': 'error',
99 | 'jsx-a11y/anchor-has-content': 'error',
100 | 'jsx-a11y/click-events-have-key-events': 'error',
101 | 'jsx-a11y/control-has-associated-label': 'error',
102 | 'jsx-a11y/label-has-associated-control': 'error',
103 | 'jsx-a11y/media-has-caption': 'error',
104 | 'jsx-a11y/mouse-events-have-key-events': 'error',
105 | 'jsx-a11y/no-interactive-element-to-noninteractive-role': 'error',
106 | 'jsx-a11y/no-noninteractive-element-interactions': 'error',
107 | 'jsx-a11y/no-static-element-interactions': 'error',
108 |
109 | // compat
110 | 'compat/compat': ['error'],
111 |
112 | // jsdoc
113 | 'jsdoc/check-alignment': 'error',
114 | 'jsdoc/check-examples': 'off', // cannot yet be supported for ESLint 8, see https://github.com/eslint/eslint/issues/14745
115 | 'jsdoc/check-param-names': 'error',
116 | 'jsdoc/check-syntax': 'error',
117 | 'jsdoc/check-tag-names': ['error', { jsxTags: true }],
118 | 'jsdoc/check-types': 'error',
119 | 'jsdoc/newline-after-description': 'error',
120 | 'jsdoc/valid-types': 'error',
121 | 'jsdoc/check-indentation': ['off'],
122 | },
123 |
124 | settings: {
125 | 'import/core-modules': ['tsd'],
126 | react: {
127 | pragma: 'h',
128 | },
129 | jsdoc: {
130 | mode: 'typescript',
131 | },
132 | polyfills: [
133 | 'Promise',
134 | 'fetch',
135 | 'Object.assign',
136 | 'document.querySelector',
137 | ],
138 | },
139 |
140 | overrides: [
141 | {
142 | files: [
143 | '*.jsx',
144 | '*.tsx',
145 | 'packages/@Imagin/react-native/**/*.js',
146 | ],
147 | parser: 'espree',
148 | parserOptions: {
149 | sourceType: 'module',
150 | ecmaFeatures: {
151 | jsx: true,
152 | },
153 | },
154 | rules: {
155 | 'no-restricted-globals': [
156 | 'error',
157 | {
158 | name: '__filename',
159 | message: 'Use import.meta.url instead',
160 | },
161 | {
162 | name: '__dirname',
163 | message: 'Not available in ESM',
164 | },
165 | {
166 | name: 'exports',
167 | message: 'Not available in ESM',
168 | },
169 | {
170 | name: 'module',
171 | message: 'Not available in ESM',
172 | },
173 | {
174 | name: 'require',
175 | message: 'Use import instead',
176 | },
177 | ],
178 | 'import/extensions': ['error', 'ignorePackages'],
179 | },
180 | },
181 | {
182 | files: [
183 | '*.mjs',
184 | 'e2e/clients/**/*.js',
185 | 'examples/aws-companion/*.js',
186 | 'examples/aws-php/*.js',
187 | 'examples/bundled/*.js',
188 | 'examples/custom-provider/client/*.js',
189 | 'examples/digitalocean-spaces/*.js',
190 | 'examples/multiple-instances/*.js',
191 | 'examples/node-xhr/*.js',
192 | 'examples/php-xhr/*.js',
193 | 'examples/python-xhr/*.js',
194 | 'examples/react-example/*.js',
195 | 'examples/redux/*.js',
196 | 'examples/transloadit/*.js',
197 | 'examples/transloadit-markdown-bin/*.js',
198 | 'examples/xhr-bundle/*.js',
199 | 'private/dev/*.js',
200 | 'private/release/*.js',
201 | 'private/remark-lint-Imagin/*.js',
202 |
203 | // Packages that have switched to ESM sources:
204 | 'packages/@Imagin/audio/src/**/*.js',
205 | 'packages/@Imagin/aws-s3-multipart/src/**/*.js',
206 | 'packages/@Imagin/aws-s3/src/**/*.js',
207 | 'packages/@Imagin/box/src/**/*.js',
208 | 'packages/@Imagin/companion-client/src/**/*.js',
209 | 'packages/@Imagin/compressor/src/**/*.js',
210 | 'packages/@Imagin/core/src/**/*.js',
211 | 'packages/@Imagin/dashboard/src/**/*.js',
212 | 'packages/@Imagin/drag-drop/src/**/*.js',
213 | 'packages/@Imagin/drop-target/src/**/*.js',
214 | 'packages/@Imagin/dropbox/src/**/*.js',
215 | 'packages/@Imagin/facebook/src/**/*.js',
216 | 'packages/@Imagin/file-input/src/**/*.js',
217 | 'packages/@Imagin/form/src/**/*.js',
218 | 'packages/@Imagin/golden-retriever/src/**/*.js',
219 | 'packages/@Imagin/google-drive/src/**/*.js',
220 | 'packages/@Imagin/image-editor/src/**/*.js',
221 | 'packages/@Imagin/informer/src/**/*.js',
222 | 'packages/@Imagin/instagram/src/**/*.js',
223 | 'packages/@Imagin/locales/src/**/*.js',
224 | 'packages/@Imagin/locales/template.js',
225 | 'packages/@Imagin/onedrive/src/**/*.js',
226 | 'packages/@Imagin/progress-bar/src/**/*.js',
227 | 'packages/@Imagin/provider-views/src/**/*.js',
228 | 'packages/@Imagin/react/src/**/*.js',
229 | 'packages/@Imagin/redux-dev-tools/src/**/*.js',
230 | 'packages/@Imagin/remote-sources/src/**/*.js',
231 | 'packages/@Imagin/screen-capture/src/**/*.js',
232 | 'packages/@Imagin/status-bar/src/**/*.js',
233 | 'packages/@Imagin/store-default/src/**/*.js',
234 | 'packages/@Imagin/store-redux/src/**/*.js',
235 | 'packages/@Imagin/svelte/rollup.config.js',
236 | 'packages/@Imagin/svelte/src/**/*.js',
237 | 'packages/@Imagin/thumbnail-generator/src/**/*.js',
238 | 'packages/@Imagin/transloadit/src/**/*.js',
239 | 'packages/@Imagin/tus/src/**/*.js',
240 | 'packages/@Imagin/unsplash/src/**/*.js',
241 | 'packages/@Imagin/url/src/**/*.js',
242 | 'packages/@Imagin/utils/src/**/*.js',
243 | 'packages/@Imagin/vue/src/**/*.js',
244 | 'packages/@Imagin/webcam/src/**/*.js',
245 | 'packages/@Imagin/xhr-upload/src/**/*.js',
246 | 'packages/@Imagin/zoom/src/**/*.js',
247 | ],
248 | parser: 'espree',
249 | parserOptions: {
250 | sourceType: 'module',
251 | ecmaFeatures: {
252 | jsx: false,
253 | },
254 | },
255 | rules: {
256 | 'import/named': 'off', // Disabled because that rule tries and fails to parse JSX dependencies.
257 | 'import/no-named-as-default': 'off', // Disabled because that rule tries and fails to parse JSX dependencies.
258 | 'import/no-named-as-default-member': 'off', // Disabled because that rule tries and fails to parse JSX dependencies.
259 | 'no-restricted-globals': [
260 | 'error',
261 | {
262 | name: '__filename',
263 | message: 'Use import.meta.url instead',
264 | },
265 | {
266 | name: '__dirname',
267 | message: 'Not available in ESM',
268 | },
269 | {
270 | name: 'exports',
271 | message: 'Not available in ESM',
272 | },
273 | {
274 | name: 'module',
275 | message: 'Not available in ESM',
276 | },
277 | {
278 | name: 'require',
279 | message: 'Use import instead',
280 | },
281 | ],
282 | 'import/extensions': ['error', 'ignorePackages'],
283 | },
284 | },
285 | {
286 | files: ['packages/Imagin/*.mjs'],
287 | rules: {
288 | 'import/first': 'off',
289 | 'import/newline-after-import': 'off',
290 | 'import/no-extraneous-dependencies': ['error', {
291 | devDependencies: true,
292 | }],
293 | },
294 | },
295 | {
296 | files: [
297 | 'packages/@Imagin/*/types/*.d.ts',
298 | ],
299 | rules : {
300 | 'import/no-unresolved': 'off',
301 | 'max-classes-per-file': 'off',
302 | 'no-use-before-define': 'off',
303 | },
304 | },
305 | {
306 | files: [
307 | 'packages/@Imagin/dashboard/src/components/**/*.jsx',
308 | ],
309 | rules: {
310 | 'react/destructuring-assignment': 'off',
311 | },
312 | },
313 | {
314 | files: [
315 | // Those need looser rules, and cannot be made part of the stricter rules above.
316 | // TODO: update those to more modern code when switch to ESM is complete
317 | 'examples/react-native-expo/*.js',
318 | 'examples/svelte-example/**/*.js',
319 | 'examples/vue/**/*.js',
320 | 'examples/vue3/**/*.js',
321 | ],
322 | rules: {
323 | 'no-unused-vars': [
324 | 'error',
325 | {
326 | 'varsIgnorePattern': 'React',
327 | },
328 | ],
329 | },
330 | parserOptions: {
331 | sourceType: 'module',
332 | },
333 | },
334 | {
335 | files: ['./packages/@Imagin/companion/**/*.js'],
336 | rules: {
337 | 'no-underscore-dangle': 'off',
338 | },
339 | },
340 | {
341 | files: [
342 | '*.test.js',
343 | 'test/endtoend/*.js',
344 | 'bin/**.js',
345 | ],
346 | rules: {
347 | 'compat/compat': ['off'],
348 | },
349 | },
350 | {
351 | files: [
352 | 'bin/**.js',
353 | 'bin/**.mjs',
354 | 'examples/**/*.cjs',
355 | 'examples/**/*.js',
356 | 'packages/@Imagin/companion/test/**/*.js',
357 | 'test/**/*.js',
358 | 'test/**/*.ts',
359 | '*.test.js',
360 | '*.test.ts',
361 | '*.test-d.ts',
362 | '*.test-d.tsx',
363 | 'postcss.config.js',
364 | '.eslintrc.js',
365 | 'private/**/*.js',
366 | 'private/**/*.mjs',
367 | ],
368 | rules: {
369 | 'no-console': 'off',
370 | 'import/no-extraneous-dependencies': ['error', {
371 | devDependencies: true,
372 | }],
373 | },
374 | },
375 |
376 | {
377 | files: [
378 | 'packages/@Imagin/locales/src/*.js',
379 | 'packages/@Imagin/locales/template.js',
380 | ],
381 | rules: {
382 | camelcase: ['off'],
383 | 'quote-props': ['error', 'as-needed', { 'numbers': true }],
384 | },
385 | },
386 |
387 | {
388 | files: ['test/endtoend/*/*.mjs', 'test/endtoend/*/*.ts'],
389 | rules: {
390 | // we mostly import @Imagin stuff in these files.
391 | 'import/no-extraneous-dependencies': ['off'],
392 | },
393 | },
394 | {
395 | files: ['test/endtoend/*/*.js'],
396 | env: {
397 | mocha: true,
398 | },
399 | },
400 |
401 | {
402 | files: ['packages/@Imagin/react/src/**/*.js'],
403 | rules: {
404 | 'import/no-extraneous-dependencies': ['error', {
405 | peerDependencies: true,
406 | }],
407 | },
408 | },
409 |
410 | {
411 | files: ['**/*.md', '*.md'],
412 | processor: 'markdown/markdown',
413 | },
414 | {
415 | files: ['**/*.md/*.js', '**/*.md/*.javascript'],
416 | parserOptions: {
417 | sourceType: 'module',
418 | },
419 | rules: {
420 | 'react/destructuring-assignment': 'off',
421 | 'no-restricted-globals': [
422 | 'error',
423 | {
424 | name: '__filename',
425 | message: 'Use import.meta.url instead',
426 | },
427 | {
428 | name: '__dirname',
429 | message: 'Not available in ESM',
430 | },
431 | {
432 | name: 'exports',
433 | message: 'Not available in ESM',
434 | },
435 | {
436 | name: 'module',
437 | message: 'Not available in ESM',
438 | },
439 | {
440 | name: 'require',
441 | message: 'Use import instead',
442 | },
443 | ],
444 | },
445 | },
446 | {
447 | files: ['**/*.ts', '**/*.md/*.ts', '**/*.md/*.typescript'],
448 | excludedFiles: ['examples/angular-example/**/*.ts', 'packages/@Imagin/angular/**/*.ts'],
449 | parser: '@typescript-eslint/parser',
450 | settings: {
451 | 'import/resolver': {
452 | node: {
453 | extensions: ['.js', '.jsx', '.ts', '.tsx'],
454 | },
455 | },
456 | },
457 | plugins: ['@typescript-eslint'],
458 | extends: [
459 | 'eslint:recommended',
460 | 'plugin:@typescript-eslint/eslint-recommended',
461 | 'plugin:@typescript-eslint/recommended',
462 | ],
463 | rules: {
464 | 'import/prefer-default-export': 'off',
465 | '@typescript-eslint/no-explicit-any': 'off',
466 | '@typescript-eslint/no-extra-semi': 'off',
467 | '@typescript-eslint/no-namespace': 'off',
468 | },
469 | },
470 | {
471 | files: ['packages/@Imagin/*/src/**/*.ts', 'packages/@Imagin/*/src/**/*.tsx'],
472 | excludedFiles: ['packages/@Imagin/**/*.test.ts'],
473 | rules: {
474 | '@typescript-eslint/explicit-function-return-type': 'error',
475 | },
476 | },
477 | {
478 | files: ['**/*.md/*.*'],
479 | rules: {
480 | 'import/no-extraneous-dependencies': 'off',
481 | 'import/no-unresolved': 'off',
482 | 'no-console': 'off',
483 | 'no-undef': 'off',
484 | 'no-unused-vars': 'off',
485 | },
486 | },
487 | {
488 | files: ['**/react/*.md/*.js', '**/react.md/*.js', '**/react-*.md/*.js', '**/react/**/*.test-d.tsx'],
489 | settings: {
490 | react: { pragma: 'React' },
491 | },
492 | },
493 | {
494 | files: ['**/react/**/*.test-d.tsx'],
495 | rules: {
496 | 'import/extensions': 'off',
497 | 'import/no-useless-path-segments': 'off',
498 | 'no-alert': 'off',
499 | 'no-inner-declarations': 'off',
500 | 'no-lone-blocks': 'off',
501 | 'no-unused-expressions': 'off',
502 | 'no-unused-vars': 'off',
503 | },
504 | },
505 | {
506 | files: ['e2e/**/*.ts'],
507 | extends: ['plugin:cypress/recommended'],
508 | },
509 | {
510 | files: ['e2e/**/*.ts', 'e2e/**/*.js', 'e2e/**/*.jsx', 'e2e/**/*.mjs'],
511 | rules: {
512 | 'import/no-extraneous-dependencies': 'off',
513 | 'no-console': 'off',
514 | 'no-only-tests/no-only-tests': 'error',
515 | 'no-unused-expressions': 'off',
516 | },
517 | },
518 | ],
519 | }
520 |
--------------------------------------------------------------------------------
/packages/@ImgCypress_1/companion-client/src/RequestClient.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import UserFacingApiError from '@uppy/utils/lib/UserFacingApiError'
4 | // eslint-disable-next-line import/no-extraneous-dependencies
5 | import pRetry, { AbortError } from 'p-retry'
6 |
7 | import fetchWithNetworkError from '@uppy/utils/lib/fetchWithNetworkError'
8 | import ErrorWithCause from '@uppy/utils/lib/ErrorWithCause'
9 | import emitSocketProgress from '@uppy/utils/lib/emitSocketProgress'
10 | import getSocketHost from '@uppy/utils/lib/getSocketHost'
11 |
12 | import AuthError from './AuthError.js'
13 |
14 | import packageJson from '../package.json'
15 |
16 | // Remove the trailing slash so we can always safely append /xyz.
17 | function stripSlash(url) {
18 | return url.replace(/\/$/, '')
19 | }
20 |
21 | const retryCount = 10 // set to a low number, like 2 to test manual user retries
22 | const socketActivityTimeoutMs = 5 * 60 * 1000 // set to a low number like 10000 to test this
23 |
24 | export const authErrorStatusCode = 401
25 |
26 | class HttpError extends Error {
27 | statusCode
28 |
29 | constructor({ statusCode, message }) {
30 | super(message)
31 | this.name = 'HttpError'
32 | this.statusCode = statusCode
33 | }
34 | }
35 |
36 | async function handleJSONResponse(res) {
37 | if (res.status === authErrorStatusCode) {
38 | throw new AuthError()
39 | }
40 |
41 | if (res.ok) {
42 | return res.json()
43 | }
44 |
45 | let errMsg = `Failed request with status: ${res.status}. ${res.statusText}`
46 | let errData
47 | try {
48 | errData = await res.json()
49 |
50 | if (errData.message) errMsg = `${errMsg} message: ${errData.message}`
51 | if (errData.requestId) errMsg = `${errMsg} request-Id: ${errData.requestId}`
52 | } catch (cause) {
53 | // if the response contains invalid JSON, let's ignore the error data
54 | throw new Error(errMsg, { cause })
55 | }
56 |
57 | if (res.status >= 400 && res.status <= 499 && errData.message) {
58 | throw new UserFacingApiError(errData.message)
59 | }
60 |
61 | throw new HttpError({ statusCode: res.status, message: errMsg })
62 | }
63 |
64 | export default class RequestClient {
65 | static VERSION = packageJson.version
66 |
67 | #companionHeaders
68 |
69 | constructor(uppy, opts) {
70 | this.uppy = uppy
71 | this.opts = opts
72 | this.onReceiveResponse = this.onReceiveResponse.bind(this)
73 | this.#companionHeaders = opts?.companionHeaders
74 | }
75 |
76 | setCompanionHeaders(headers) {
77 | this.#companionHeaders = headers
78 | }
79 |
80 | [Symbol.for('uppy test: getCompanionHeaders')]() {
81 | return this.#companionHeaders
82 | }
83 |
84 | get hostname() {
85 | const { companion } = this.uppy.getState()
86 | const host = this.opts.companionUrl
87 | return stripSlash(companion && companion[host] ? companion[host] : host)
88 | }
89 |
90 | async headers (emptyBody = false) {
91 | const defaultHeaders = {
92 | Accept: 'application/json',
93 | ...(emptyBody ? undefined : {
94 | // Passing those headers on requests with no data forces browsers to first make a preflight request.
95 | 'Content-Type': 'application/json',
96 | }),
97 | }
98 |
99 | return {
100 | ...defaultHeaders,
101 | ...this.#companionHeaders,
102 | }
103 | }
104 |
105 | onReceiveResponse({ headers }) {
106 | const state = this.uppy.getState()
107 | const companion = state.companion || {}
108 | const host = this.opts.companionUrl
109 |
110 | // Store the self-identified domain name for the Companion instance we just hit.
111 | if (headers.has('i-am') && headers.get('i-am') !== companion[host]) {
112 | this.uppy.setState({
113 | companion: { ...companion, [host]: headers.get('i-am') },
114 | })
115 | }
116 | }
117 |
118 | #getUrl(url) {
119 | if (/^(https?:|)\/\//.test(url)) {
120 | return url
121 | }
122 | return `${this.hostname}/${url}`
123 | }
124 |
125 | /** @protected */
126 | async request({ path, method = 'GET', data, skipPostResponse, signal }) {
127 | try {
128 | const headers = await this.headers(!data)
129 | const response = await fetchWithNetworkError(this.#getUrl(path), {
130 | method,
131 | signal,
132 | headers,
133 | credentials: this.opts.companionCookiesRule || 'same-origin',
134 | body: data ? JSON.stringify(data) : null,
135 | })
136 | if (!skipPostResponse) this.onReceiveResponse(response)
137 |
138 | return await handleJSONResponse(response)
139 | } catch (err) {
140 | // pass these through
141 | if (err.isAuthError || err.name === 'UserFacingApiError' || err.name === 'AbortError') throw err
142 |
143 | throw new ErrorWithCause(`Could not ${method} ${this.#getUrl(path)}`, {
144 | cause: err,
145 | })
146 | }
147 | }
148 |
149 | async get(path, options = undefined) {
150 | // TODO: remove boolean support for options that was added for backward compatibility.
151 | // eslint-disable-next-line no-param-reassign
152 | if (typeof options === 'boolean') options = { skipPostResponse: options }
153 | return this.request({ ...options, path })
154 | }
155 |
156 | async post(path, data, options = undefined) {
157 | // TODO: remove boolean support for options that was added for backward compatibility.
158 | // eslint-disable-next-line no-param-reassign
159 | if (typeof options === 'boolean') options = { skipPostResponse: options }
160 | return this.request({ ...options, path, method: 'POST', data })
161 | }
162 |
163 | async delete(path, data = undefined, options) {
164 | // TODO: remove boolean support for options that was added for backward compatibility.
165 | // eslint-disable-next-line no-param-reassign
166 | if (typeof options === 'boolean') options = { skipPostResponse: options }
167 | return this.request({ ...options, path, method: 'DELETE', data })
168 | }
169 |
170 | /**
171 | * Remote uploading consists of two steps:
172 | * 1. #requestSocketToken which starts the download/upload in companion and returns a unique token for the upload.
173 | * Then companion will halt the upload until:
174 | * 2. #awaitRemoteFileUpload is called, which will open/ensure a websocket connection towards companion, with the
175 | * previously generated token provided. It returns a promise that will resolve/reject once the file has finished
176 | * uploading or is otherwise done (failed, canceled)
177 | *
178 | * @param {*} file
179 | * @param {*} reqBody
180 | * @param {*} options
181 | * @returns
182 | */
183 | async uploadRemoteFile(file, reqBody, options = {}) {
184 | try {
185 | const { signal, getQueue } = options
186 |
187 | return await pRetry(async () => {
188 | // if we already have a serverToken, assume that we are resuming the existing server upload id
189 | const existingServerToken = this.uppy.getFile(file.id)?.serverToken;
190 | if (existingServerToken != null) {
191 | this.uppy.log(`Connecting to exiting websocket ${existingServerToken}`)
192 | return this.#awaitRemoteFileUpload({ file, queue: getQueue(), signal })
193 | }
194 |
195 | const queueRequestSocketToken = getQueue().wrapPromiseFunction(async (...args) => {
196 | try {
197 | return await this.#requestSocketToken(...args)
198 | } catch (outerErr) {
199 | // throwing AbortError will cause p-retry to stop retrying
200 | if (outerErr.isAuthError) throw new AbortError(outerErr)
201 |
202 | if (outerErr.cause == null) throw outerErr
203 | const err = outerErr.cause
204 |
205 | const isRetryableHttpError = () => (
206 | [408, 409, 429, 418, 423].includes(err.statusCode)
207 | || (err.statusCode >= 500 && err.statusCode <= 599 && ![501, 505].includes(err.statusCode))
208 | )
209 | if (err.name === 'HttpError' && !isRetryableHttpError()) throw new AbortError(err);
210 |
211 | // p-retry will retry most other errors,
212 | // but it will not retry TypeError (except network error TypeErrors)
213 | throw err
214 | }
215 | }, { priority: -1 })
216 |
217 | const serverToken = await queueRequestSocketToken({ file, postBody: reqBody, signal }).abortOn(signal)
218 |
219 | if (!this.uppy.getFile(file.id)) return undefined // has file since been removed?
220 |
221 | this.uppy.setFileState(file.id, { serverToken })
222 |
223 | return this.#awaitRemoteFileUpload({
224 | file: this.uppy.getFile(file.id), // re-fetching file because it might have changed in the meantime
225 | queue: getQueue(),
226 | signal
227 | })
228 | }, { retries: retryCount, signal, onFailedAttempt: (err) => this.uppy.log(`Retrying upload due to: ${err.message}`, 'warning') });
229 | } catch (err) {
230 | // this is a bit confusing, but note that an error with the `name` prop set to 'AbortError' (from AbortController)
231 | // is not the same as `p-retry` `AbortError`
232 | if (err.name === 'AbortError') {
233 | // The file upload was aborted, it’s not an error
234 | return undefined
235 | }
236 |
237 | this.uppy.emit('upload-error', file, err)
238 | throw err
239 | }
240 | }
241 |
242 | #requestSocketToken = async ({ file, postBody, signal }) => {
243 | if (file.remote.url == null) {
244 | throw new Error('Cannot connect to an undefined URL')
245 | }
246 |
247 | const res = await this.post(file.remote.url, {
248 | ...file.remote.body,
249 | ...postBody,
250 | }, signal)
251 |
252 | return res.token
253 | }
254 |
255 | /**
256 | * This method will ensure a websocket for the specified file and returns a promise that resolves
257 | * when the file has finished downloading, or rejects if it fails.
258 | * It will retry if the websocket gets disconnected
259 | *
260 | * @param {{ file: UppyFile, queue: RateLimitedQueue, signal: AbortSignal }} file
261 | */
262 | async #awaitRemoteFileUpload({ file, queue, signal }) {
263 | let removeEventHandlers
264 |
265 | const { capabilities } = this.uppy.getState()
266 |
267 | try {
268 | return await new Promise((resolve, reject) => {
269 | const token = file.serverToken
270 | const host = getSocketHost(file.remote.companionUrl)
271 |
272 | /** @type {WebSocket} */
273 | let socket
274 | /** @type {AbortController?} */
275 | let socketAbortController
276 | let activityTimeout
277 |
278 | let { isPaused } = file
279 |
280 | const socketSend = (action, payload) => {
281 | if (socket == null || socket.readyState !== socket.OPEN) {
282 | this.uppy.log(`Cannot send "${action}" to socket ${file.id} because the socket state was ${String(socket?.readyState)}`, 'warning')
283 | return
284 | }
285 |
286 | socket.send(JSON.stringify({
287 | action,
288 | payload: payload ?? {},
289 | }))
290 | };
291 |
292 | function sendState() {
293 | if (!capabilities.resumableUploads) return;
294 |
295 | if (isPaused) socketSend('pause')
296 | else socketSend('resume')
297 | }
298 |
299 | const createWebsocket = async () => {
300 | if (socketAbortController) socketAbortController.abort()
301 | socketAbortController = new AbortController()
302 |
303 | const onFatalError = (err) => {
304 | // Remove the serverToken so that a new one will be created for the retry.
305 | this.uppy.setFileState(file.id, { serverToken: null })
306 | socketAbortController?.abort?.()
307 | reject(err)
308 | }
309 |
310 | // todo instead implement the ability for users to cancel / retry *currently uploading files* in the UI
311 | function resetActivityTimeout() {
312 | clearTimeout(activityTimeout)
313 | if (isPaused) return
314 | activityTimeout = setTimeout(() => onFatalError(new Error('Timeout waiting for message from Companion socket')), socketActivityTimeoutMs)
315 | }
316 |
317 | try {
318 | await queue.wrapPromiseFunction(async () => {
319 | // eslint-disable-next-line promise/param-names
320 | const reconnectWebsocket = async () => new Promise((resolveSocket, rejectSocket) => {
321 | socket = new WebSocket(`${host}/api/${token}`)
322 |
323 | resetActivityTimeout()
324 |
325 | socket.addEventListener('close', () => {
326 | socket = undefined
327 | rejectSocket(new Error('Socket closed unexpectedly'))
328 | })
329 |
330 | socket.addEventListener('error', (error) => {
331 | this.uppy.log(`Companion socket error ${JSON.stringify(error)}, closing socket`, 'warning')
332 | socket.close() // will 'close' event to be emitted
333 | })
334 |
335 | socket.addEventListener('open', () => {
336 | sendState()
337 | })
338 |
339 | socket.addEventListener('message', (e) => {
340 | resetActivityTimeout()
341 |
342 | try {
343 | const { action, payload } = JSON.parse(e.data)
344 |
345 | switch (action) {
346 | case 'progress': {
347 | emitSocketProgress(this, payload, file)
348 | break;
349 | }
350 | case 'success': {
351 | this.uppy.emit('upload-success', file, { uploadURL: payload.url })
352 | socketAbortController?.abort?.()
353 | resolve()
354 | break;
355 | }
356 | case 'error': {
357 | const { message } = payload.error
358 | throw Object.assign(new Error(message), { cause: payload.error })
359 | }
360 | default:
361 | this.uppy.log(`Companion socket unknown action ${action}`, 'warning')
362 | }
363 | } catch (err) {
364 | onFatalError(err)
365 | }
366 | })
367 |
368 | const closeSocket = () => {
369 | this.uppy.log(`Closing socket ${file.id}`, 'info')
370 | clearTimeout(activityTimeout)
371 | if (socket) socket.close()
372 | socket = undefined
373 | }
374 |
375 | socketAbortController.signal.addEventListener('abort', () => {
376 | closeSocket()
377 | })
378 | })
379 |
380 | await pRetry(reconnectWebsocket, {
381 | retries: retryCount,
382 | signal: socketAbortController.signal,
383 | onFailedAttempt: () => {
384 | if (socketAbortController.signal.aborted) return // don't log in this case
385 | this.uppy.log(`Retrying websocket ${file.id}`, 'info')
386 | },
387 | });
388 | })().abortOn(socketAbortController.signal);
389 | } catch (err) {
390 | if (socketAbortController.signal.aborted) return
391 | onFatalError(err)
392 | }
393 | }
394 |
395 | const pause = (newPausedState) => {
396 | if (!capabilities.resumableUploads) return;
397 |
398 | isPaused = newPausedState
399 | if (socket) sendState()
400 |
401 | if (newPausedState) {
402 | // Remove this file from the queue so another file can start in its place.
403 | socketAbortController?.abort?.() // close socket to free up the request for other uploads
404 | } else {
405 | // Resuming an upload should be queued, else you could pause and then
406 | // resume a queued upload to make it skip the queue.
407 | createWebsocket()
408 | }
409 | }
410 |
411 | const onFileRemove = (targetFile) => {
412 | if (!capabilities.individualCancellation) return
413 | if (targetFile.id !== file.id) return
414 | socketSend('cancel')
415 | socketAbortController?.abort?.()
416 | this.uppy.log(`upload ${file.id} was removed`, 'info')
417 | resolve()
418 | }
419 |
420 | const onCancelAll = ({ reason }) => {
421 | if (reason === 'user') {
422 | socketSend('cancel')
423 | }
424 | socketAbortController?.abort?.()
425 | this.uppy.log(`upload ${file.id} was canceled`, 'info')
426 | resolve()
427 | };
428 |
429 | const onFilePausedChange = (targetFileId, newPausedState) => {
430 | if (targetFileId !== file.id) return
431 | pause(newPausedState)
432 | }
433 |
434 | const onPauseAll = () => pause(true)
435 | const onResumeAll = () => pause(false)
436 |
437 | this.uppy.on('file-removed', onFileRemove)
438 | this.uppy.on('cancel-all', onCancelAll)
439 | this.uppy.on('upload-pause', onFilePausedChange)
440 | this.uppy.on('pause-all', onPauseAll)
441 | this.uppy.on('resume-all', onResumeAll)
442 |
443 | removeEventHandlers = () => {
444 | this.uppy.off('file-removed', onFileRemove)
445 | this.uppy.off('cancel-all', onCancelAll)
446 | this.uppy.off('upload-pause', onFilePausedChange)
447 | this.uppy.off('pause-all', onPauseAll)
448 | this.uppy.off('resume-all', onResumeAll)
449 | }
450 |
451 | signal.addEventListener('abort', () => {
452 | socketAbortController?.abort();
453 | })
454 |
455 | createWebsocket()
456 | })
457 | } finally {
458 | removeEventHandlers?.()
459 | }
460 | }
461 | }
462 |
--------------------------------------------------------------------------------