├── SECURITY.md
├── README.md
├── examples
├── aws-companion
│ ├── .gitignore
│ ├── index.html
│ ├── README.md
│ ├── main.js
│ ├── package.json
│ └── server.cjs
├── aws-php
│ ├── .gitignore
│ ├── composer.json
│ ├── index.html
│ ├── package.json
│ ├── serve.php
│ ├── main.js
│ ├── readme.md
│ └── s3-sign.php
├── node-xhr
│ ├── .gitignore
│ ├── index.html
│ ├── README.md
│ ├── package.json
│ ├── main.js
│ └── server.js
├── php-xhr
│ ├── .gitignore
│ ├── index.html
│ ├── README.md
│ ├── package.json
│ ├── main.js
│ └── server.php
├── ExpTestImg-with-companion
│ ├── output
│ │ └── .empty
│ ├── .gitignore
│ ├── README.md
│ ├── package.json
│ ├── client
│ │ └── index.html
│ └── server
│ │ └── index.js
├── angular-example
│ ├── src
│ │ ├── assets
│ │ │ └── .gitkeep
│ │ ├── app
│ │ │ ├── app.component.css
│ │ │ ├── app.module.ts
│ │ │ └── app.component.ts
│ │ ├── main.ts
│ │ ├── styles.css
│ │ └── index.html
│ ├── .vscode
│ │ ├── extensions.json
│ │ ├── launch.json
│ │ └── tasks.json
│ ├── tsconfig.spec.json
│ ├── tsconfig.app.json
│ ├── .editorconfig
│ ├── .gitignore
│ ├── tsconfig.json
│ ├── .eslintrc.json
│ ├── README.md
│ ├── package.json
│ └── angular.json
├── python-xhr
│ ├── .gitignore
│ ├── package.json
│ └── README.md
├── digitalocean-spaces
│ ├── .gitignore
│ ├── main.js
│ ├── package.json
│ ├── index.html
│ ├── setcors.xml
│ ├── README.md
│ └── server.cjs
├── cdn-example
│ ├── package.json
│ ├── README.md
│ └── index.html
├── custom-provider
│ ├── index.html
│ ├── client
│ │ ├── main.js
│ │ └── MyCustomProvider.jsx
│ ├── README.md
│ ├── package.json
│ └── server
│ │ ├── CustomProvider.cjs
│ │ └── index.cjs
├── multiple-instances
│ ├── package.json
│ ├── README.md
│ ├── index.html
│ └── main.js
├── aws-nodejs
│ ├── package.json
│ ├── README.md
│ ├── public
│ │ ├── drag.html
│ │ └── index.html
│ └── index.js
└── bundled
│ ├── package.json
│ ├── index.html
│ ├── sw.js
│ └── index.js
├── e2e
├── cypress
│ ├── fixtures
│ │ ├── images
│ │ │ ├── 3
│ │ │ ├── 4.jpg
│ │ │ ├── 1.png
│ │ │ ├── 2.jpg
│ │ │ ├── kit.jpg
│ │ │ ├── image.jpg
│ │ │ ├── papagai.png
│ │ │ └── traffic.jpg
│ │ └── DeepFrozenStore.mjs
│ ├── integration
│ │ ├── dashboard-aws.spec.ts
│ │ ├── dashboard-vue.spec.ts
│ │ ├── dashboard-ui.spec.ts
│ │ ├── dashboard-tus.spec.ts
│ │ ├── reusable-tests.ts
│ │ ├── dashboard-xhr.spec.ts
│ │ └── react.spec.ts
│ └── support
│ │ ├── index.ts
│ │ ├── e2e.ts
│ │ ├── commands.ts
│ │ └── createFakeFile.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
├── .dockerignore
├── assets
├── Ec5l9JgXgAI6RVQ.jpg
├── photo_2020-06-10_15-03-37.jpg
└── stringutils.ps1
├── .prettierignore
├── .eslintignore
├── docker-compose-test.yml
├── .remarkignore
├── .browserslistrc
├── .prettierrc.js
├── .stylelintrc.json
├── .editorconfig
├── Dockerfile.test
├── docker-compose.yml
├── bin
├── to-gif-hq.sh
├── to-gif-hd.sh
├── companion.sh
├── to-gif.sh
├── update-contributors.mjs
├── build-ts.mjs
├── update-yarn.sh
├── build-bundle.mjs
├── build-css.js
└── build-lib.js
├── .yarnrc.yml
├── docker-compose-dev.yml
├── .gitignore
├── babel.config.js
├── LICENSE
├── Dockerfile
├── Makefile
├── .env.example
├── BUNDLE-README.md
└── .eslintrc.js
/SECURITY.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ExpTestImg
2 | Learning JS
3 |
--------------------------------------------------------------------------------
/examples/aws-companion/.gitignore:
--------------------------------------------------------------------------------
1 | tmp
2 |
--------------------------------------------------------------------------------
/examples/aws-php/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 |
--------------------------------------------------------------------------------
/examples/node-xhr/.gitignore:
--------------------------------------------------------------------------------
1 | uploads/
2 |
--------------------------------------------------------------------------------
/examples/php-xhr/.gitignore:
--------------------------------------------------------------------------------
1 | uploads/
2 |
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/3:
--------------------------------------------------------------------------------
1 | ./cat.jpg
2 |
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/4.jpg:
--------------------------------------------------------------------------------
1 | ./cat.jpg
2 |
--------------------------------------------------------------------------------
/examples/ExpTestImg-with-companion/output/.empty:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/angular-example/src/assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/python-xhr/.gitignore:
--------------------------------------------------------------------------------
1 | uploads/
2 |
--------------------------------------------------------------------------------
/examples/angular-example/src/app/app.component.css:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/digitalocean-spaces/.gitignore:
--------------------------------------------------------------------------------
1 | tmp/
2 |
--------------------------------------------------------------------------------
/examples/ExpTestImg-with-companion/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | output/*
3 | !output/.empty
4 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | .git
3 | website
4 | assets
5 | private
6 | e2e
7 | .env
8 |
--------------------------------------------------------------------------------
/assets/Ec5l9JgXgAI6RVQ.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderAlejo/ExpTestImg/HEAD/assets/Ec5l9JgXgAI6RVQ.jpg
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.js
3 | *.jsx
4 | *.cjs
5 | *.mjs
6 | !private/js2ts/*
7 | *.md
8 | *.lock
9 |
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderAlejo/ExpTestImg/HEAD/e2e/cypress/fixtures/images/1.png
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderAlejo/ExpTestImg/HEAD/e2e/cypress/fixtures/images/2.jpg
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/kit.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderAlejo/ExpTestImg/HEAD/e2e/cypress/fixtures/images/kit.jpg
--------------------------------------------------------------------------------
/assets/photo_2020-06-10_15-03-37.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderAlejo/ExpTestImg/HEAD/assets/photo_2020-06-10_15-03-37.jpg
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderAlejo/ExpTestImg/HEAD/e2e/cypress/fixtures/images/image.jpg
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/papagai.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderAlejo/ExpTestImg/HEAD/e2e/cypress/fixtures/images/papagai.png
--------------------------------------------------------------------------------
/e2e/cypress/fixtures/images/traffic.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/CoderAlejo/ExpTestImg/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 |
--------------------------------------------------------------------------------
/docker-compose-test.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | uppy:
5 | image: companion
6 | build:
7 | context: .
8 | dockerfile: Dockerfile.test
9 |
--------------------------------------------------------------------------------
/examples/aws-php/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transloadit/ExpTestImg-aws-demo",
3 | "type": "project",
4 | "require": {
5 | "aws/aws-sdk-php": "^3.31"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/examples/angular-example/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3 | "recommendations": ["angular.ng-template"]
4 | }
5 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-compressor/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | dashboard-compressor
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/examples/cdn-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/cdn",
3 | "version": "0.0.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts || python3 -m http.server || php -S localhost:8000"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | dashboard-ui
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/angular-example/src/main.ts:
--------------------------------------------------------------------------------
1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'
2 |
3 | import { AppModule } from './app/app.module'
4 |
5 | platformBrowserDynamic()
6 | .bootstrapModule(AppModule)
7 | .catch((err) => console.error(err)) // eslint-disable-line no-console
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | proseWrap: 'always',
3 | singleQuote: true,
4 | trailingComma: 'all',
5 | semi: false,
6 | overrides: [
7 | {
8 | files: 'packages/@ExpTestImg/angular/**',
9 | options: {
10 | semi: true,
11 | },
12 | },
13 | ],
14 | }
15 |
--------------------------------------------------------------------------------
/examples/angular-example/src/styles.css:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 | @import '@ExpTestImg/core/dist/style.css';
3 | @import '@ExpTestImg/dashboard/dist/style.css';
4 | @import '@ExpTestImg/drag-drop/dist/style.css';
5 | @import '@ExpTestImg/progress-bar/dist/style.css';
6 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-aws-multipart/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | dashboard-aws-multipart
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/angular-example/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/spec",
6 | "types": ["jasmine"]
7 | },
8 | "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/examples/angular-example/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": []
7 | },
8 | "files": ["src/main.ts"],
9 | "include": ["src/**/*.d.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/examples/php-xhr/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | PHP + ExpTestImg Example
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/aws-companion/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Companion + AWS Example
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/cdn-example/README.md:
--------------------------------------------------------------------------------
1 | # CDN example
2 |
3 | To run the example, open the `index.html` file in your browser.
4 |
5 | If you want to spawn a local webserver, you can use the following commands
6 | (requires Deno, Python, or PHP):
7 |
8 | ```sh
9 | corepack yarn install
10 | corepack yarn workspace @ExpTestImg-example/cdn dev
11 | ```
12 |
--------------------------------------------------------------------------------
/examples/angular-example/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AngularExample
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/examples/custom-provider/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ExpTestImg Custom provider Example
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/Dockerfile.test:
--------------------------------------------------------------------------------
1 | FROM node:18.17.1-alpine
2 |
3 | COPY package.json /app/package.json
4 |
5 | WORKDIR /app
6 |
7 | RUN apk --update add --virtual native-dep \
8 | make gcc g++ python3 libgcc libstdc++ git && \
9 | corepack yarn install && \
10 | apk del native-dep
11 | RUN apk add bash
12 |
13 | COPY . /app
14 | RUN npm install -g nodemon
15 | CMD ["npm","test"]
16 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | ExpTestImg:
5 | image: transloadit/companion
6 | build:
7 | context: .
8 | dockerfile: Dockerfile
9 | volumes:
10 | - /app/node_modules
11 | - /mnt/ExpTestImg-server-data:/mnt/ExpTestImg-server-data
12 | ports:
13 | - '3020:3020'
14 | env_file:
15 | - .env
16 |
--------------------------------------------------------------------------------
/examples/angular-example/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/aws-php/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ExpTestImg AWS Presigned URL Example
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | changesetBaseRefs:
2 | - main
3 | - upstream/main
4 | - origin/main
5 |
6 | initScope: ExpTestImg
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 |
--------------------------------------------------------------------------------
/examples/node-xhr/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Node.js + ExpTestImg Example
7 |
8 |
9 |
10 |
11 |
12 |
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 |
--------------------------------------------------------------------------------
/examples/multiple-instances/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/multiple-instances",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "dependencies": {
6 | "@ExpTestImg/core": "workspace:*",
7 | "@ExpTestImg/dashboard": "workspace:*",
8 | "@ExpTestImg/golden-retriever": "workspace:*"
9 | },
10 | "devDependencies": {
11 | "vite": "^4.0.0"
12 | },
13 | "private": true,
14 | "scripts": {
15 | "start": "vite"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-aws/app.js:
--------------------------------------------------------------------------------
1 | import { ExpTestImg } from '@ExpTestImg/core'
2 | import Dashboard from '@ExpTestImg/dashboard'
3 | import AwsS3 from '@ExpTestImg/aws-s3'
4 |
5 | import '@ExpTestImg/core/dist/style.css'
6 | import '@ExpTestImg/dashboard/dist/style.css'
7 |
8 | const ExpTestImg = new ExpTestImg()
9 | .use(Dashboard, { target: '#app', inline: true })
10 | .use(AwsS3, {
11 | limit: 2,
12 | companionUrl: process.env.VITE_COMPANION_URL,
13 | })
14 |
15 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-compressor/app.js:
--------------------------------------------------------------------------------
1 | import Compressor from '@ExpTestImg/compressor'
2 | import Dashboard from '@ExpTestImg/dashboard'
3 | import '@ExpTestImg/core/dist/style.css'
4 | import '@ExpTestImg/dashboard/dist/style.css'
5 |
6 | const ExpTestImg = new ExpTestImg()
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 ExpTestImg in tests
16 | window.ExpTestImg = ExpTestImg
17 |
--------------------------------------------------------------------------------
/docker-compose-dev.yml:
--------------------------------------------------------------------------------
1 | version: '3.9'
2 |
3 | services:
4 | ExpTestImg:
5 | image: transloadit/companion
6 | build:
7 | context: .
8 | dockerfile: Dockerfile
9 | environment:
10 | - NODE_ENV=development
11 | volumes:
12 | - ./:/app
13 | - /app/node_modules
14 | - /mnt/ExpTestImg-server-data:/mnt/ExpTestImg-server-data
15 | ports:
16 | - '3020:3020'
17 | command: '/app/src/standalone/start-server.js --config nodemon.json'
18 | env_file:
19 | - .env
20 |
--------------------------------------------------------------------------------
/examples/digitalocean-spaces/main.js:
--------------------------------------------------------------------------------
1 | import ExpTestImg from '@ExpTestImg/core'
2 | import Dashboard from '@ExpTestImg/dashboard'
3 | import AwsS3 from '@ExpTestImg/aws-s3'
4 |
5 | import '@ExpTestImg/core/dist/style.css'
6 | import '@ExpTestImg/dashboard/dist/style.css'
7 |
8 | const ExpTestImg = new ExpTestImg({
9 | debug: true,
10 | })
11 |
12 | ExpTestImg.use(Dashboard, {
13 | inline: true,
14 | target: 'body',
15 | })
16 |
17 | // No client side changes needed!
18 | ExpTestImg.use(AwsS3, { companionUrl: '/companion' })
19 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-vue/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/examples/php-xhr/README.md:
--------------------------------------------------------------------------------
1 | # ExpTestImg + PHP Example
2 |
3 | This example uses PHP server and `@ExpTestImg/xhr-upload` to upload files to the local file system.
4 |
5 | ## Run it
6 |
7 | To run this example, make sure you've correctly installed the **repository root**:
8 |
9 | ```sh
10 | corepack yarn install
11 | corepack yarn build
12 | ```
13 |
14 | That will also install the dependencies for this example.
15 |
16 | Then, again in the **repository root**, start this example by doing:
17 |
18 | ```sh
19 | corepack yarn workspace @ExpTestImg-example/php-xhr start
20 | ```
21 |
--------------------------------------------------------------------------------
/examples/node-xhr/README.md:
--------------------------------------------------------------------------------
1 | # ExpTestImg + Node Example
2 |
3 | This example uses Node server and `@ExpTestImg/xhr-upload` to upload files to the local file system.
4 |
5 | ## Run it
6 |
7 | To run this example, make sure you've correctly installed the **repository root**:
8 |
9 | ```sh
10 | corepack yarn install
11 | corepack yarn build
12 | ```
13 |
14 | That will also install the dependencies for this example.
15 |
16 | Then, again in the **repository root**, start this example by doing:
17 |
18 | ```sh
19 | corepack yarn workspace @ExpTestImg-example/node-xhr start
20 | ```
21 |
--------------------------------------------------------------------------------
/examples/aws-php/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/aws-php",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@ExpTestImg/aws-s3": "workspace:*",
6 | "@ExpTestImg/core": "workspace:*",
7 | "@ExpTestImg/dashboard": "workspace:*",
8 | "ExpTestImg": "workspace:*"
9 | },
10 | "devDependencies": {
11 | "esbuild": "^0.17.1"
12 | },
13 | "private": true,
14 | "type": "module",
15 | "scripts": {
16 | "start": "php -S localhost:8080 serve.php",
17 | "outputBundle": "esbuild --format=esm --sourcemap=inline --bundle ./main.js"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/angular-example/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "ng serve",
7 | "type": "chrome",
8 | "request": "launch",
9 | "preLaunchTask": "npm: start",
10 | "url": "http://localhost:4200/"
11 | },
12 | {
13 | "name": "ng test",
14 | "type": "chrome",
15 | "request": "launch",
16 | "preLaunchTask": "npm: test",
17 | "url": "http://localhost:9876/debug.html"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/examples/digitalocean-spaces/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/digitalocean-spaces",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "dependencies": {
6 | "@ExpTestImg/aws-s3": "workspace:*",
7 | "@ExpTestImg/core": "workspace:*",
8 | "@ExpTestImg/dashboard": "workspace:*",
9 | "body-parser": "^1.18.3",
10 | "cors": "^2.8.5"
11 | },
12 | "devDependencies": {
13 | "dotenv": "^16.0.1",
14 | "express": "^4.16.2",
15 | "vite": "^4.0.0"
16 | },
17 | "private": true,
18 | "scripts": {
19 | "start": "node ./server.cjs"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/digitalocean-spaces/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ExpTestImg DigitalOcean Example
7 |
8 |
9 | DigitalOcean Spaces
10 |
11 | Using the AwsS3 plugin to upload
12 | to DigitalOcean Spaces ✌️
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/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:1234',
11 | specPattern: 'cypress/integration/*.spec.ts',
12 |
13 | setupNodeEvents (on) {
14 | // implement node event listeners here
15 | installLogsPrinter(on)
16 |
17 | startMockServer('localhost', 4678)
18 | },
19 | },
20 | })
21 |
--------------------------------------------------------------------------------
/examples/ExpTestImg-with-companion/README.md:
--------------------------------------------------------------------------------
1 | # @ExpTestImg/companion example
2 |
3 | This is a simple, lean example that combines the usage of @ExpTestImg/companion and ExpTestImg client.
4 |
5 | ## Test it
6 |
7 | To run this example, make sure you've correctly installed the **repository root**:
8 |
9 | ```bash
10 | corepack yarn install
11 | corepack yarn build
12 | ```
13 |
14 | That will also install the dependencies for this example.
15 |
16 | Then, again in the **repository root**, start this example by doing:
17 |
18 | ```bash
19 | corepack yarn workspace @ExpTestImg-example/ExpTestImg-with-companion start
20 | ```
21 |
--------------------------------------------------------------------------------
/examples/aws-nodejs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/aws-nodejs",
3 | "version": "1.0.0",
4 | "description": "ExpTestImg for AWS S3 with a custom Node.js backend for signing URLs",
5 | "main": "index.js",
6 | "scripts": {
7 | "dev": "node --watch index.js",
8 | "start": "node index.js"
9 | },
10 | "private": true,
11 | "license": "MIT",
12 | "dependencies": {
13 | "@aws-sdk/client-s3": "^3.338.0",
14 | "@aws-sdk/client-sts": "^3.338.0",
15 | "@aws-sdk/s3-request-presigner": "^3.338.0",
16 | "body-parser": "^1.20.0",
17 | "dotenv": "^16.0.0",
18 | "express": "^4.18.1"
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/examples/node-xhr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/node-xhr",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "dependencies": {
6 | "@ExpTestImg/core": "workspace:*",
7 | "@ExpTestImg/dashboard": "workspace:*",
8 | "@ExpTestImg/webcam": "workspace:*",
9 | "@ExpTestImg/xhr-upload": "workspace:*",
10 | "formidable": "^3.2.4"
11 | },
12 | "devDependencies": {
13 | "npm-run-all": "^4.1.3",
14 | "vite": "^4.0.0"
15 | },
16 | "private": true,
17 | "scripts": {
18 | "start": "npm-run-all --parallel start:server start:client",
19 | "start:client": "vite",
20 | "start:server": "node server.js"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/php-xhr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/php-xhr",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "dependencies": {
6 | "@ExpTestImg/core": "workspace:*",
7 | "@ExpTestImg/dashboard": "workspace:*",
8 | "@ExpTestImg/webcam": "workspace:*",
9 | "@ExpTestImg/xhr-upload": "workspace:*"
10 | },
11 | "devDependencies": {
12 | "npm-run-all": "^4.1.3",
13 | "vite": "^4.0.0"
14 | },
15 | "private": true,
16 | "scripts": {
17 | "start": "npm-run-all --parallel start:server start:client",
18 | "start:client": "vite",
19 | "start:server": "mkdir -p uploads && php -S 0.0.0.0:3020 server.php"
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/examples/bundled/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/bundled",
3 | "version": "0.0.0",
4 | "dependencies": {
5 | "@ExpTestImg/core": "workspace:*",
6 | "@ExpTestImg/dashboard": "workspace:*",
7 | "@ExpTestImg/google-drive": "workspace:*",
8 | "@ExpTestImg/instagram": "workspace:*",
9 | "@ExpTestImg/tus": "workspace:*",
10 | "@ExpTestImg/url": "workspace:*",
11 | "@ExpTestImg/webcam": "workspace:*"
12 | },
13 | "devDependencies": {
14 | "parcel": "^2.0.0"
15 | },
16 | "private": true,
17 | "type": "module",
18 | "scripts": {
19 | "build": "parcel build index.html",
20 | "dev": "parcel index.html"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/examples/node-xhr/main.js:
--------------------------------------------------------------------------------
1 | import ExpTestImg from '@ExpTestImg/core'
2 | import Dashboard from '@ExpTestImg/dashboard'
3 | import XHRUpload from '@ExpTestImg/xhr-upload'
4 | import Webcam from '@ExpTestImg/webcam'
5 |
6 | import '@ExpTestImg/core/dist/style.css'
7 | import '@ExpTestImg/dashboard/dist/style.css'
8 | import '@ExpTestImg/webcam/dist/style.css'
9 |
10 | const ExpTestImg = new ExpTestImg({
11 | debug: true,
12 | autoProceed: false,
13 | })
14 |
15 | ExpTestImg.use(Webcam)
16 | ExpTestImg.use(Dashboard, {
17 | inline: true,
18 | target: 'body',
19 | plugins: ['Webcam'],
20 | })
21 | ExpTestImg.use(XHRUpload, {
22 | endpoint: 'http://localhost:3020/upload',
23 | })
24 |
--------------------------------------------------------------------------------
/examples/php-xhr/main.js:
--------------------------------------------------------------------------------
1 | import ExpTestImg from '@ExpTestImg/core'
2 | import Webcam from '@ExpTestImg/webcam'
3 | import Dashboard from '@ExpTestImg/dashboard'
4 | import XHRUpload from '@ExpTestImg/xhr-upload'
5 |
6 | import '@ExpTestImg/core/dist/style.css'
7 | import '@ExpTestImg/dashboard/dist/style.css'
8 | import '@ExpTestImg/webcam/dist/style.css'
9 |
10 | const ExpTestImg = new ExpTestImg({
11 | debug: true,
12 | autoProceed: false,
13 | })
14 |
15 | ExpTestImg.use(Webcam)
16 | ExpTestImg.use(Dashboard, {
17 | inline: true,
18 | target: 'body',
19 | plugins: ['Webcam'],
20 | })
21 | ExpTestImg.use(XHRUpload, {
22 | endpoint: 'http://localhost:3020/upload.php',
23 | })
24 |
--------------------------------------------------------------------------------
/examples/aws-php/serve.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | http://localhost:3452
5 | GET
6 | POST
7 | PUT
8 | 3000
9 | ETag
10 | Authorization
11 | Content-Type
12 | x-amz-date
13 | x-content-sha256
14 |
15 |
16 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/dashboard-aws.spec.ts:
--------------------------------------------------------------------------------
1 | describe('Dashboard with @ExpTestImg/aws-s3', () => {
2 | beforeEach(() => {
3 | cy.visit('/dashboard-aws')
4 | cy.get('.ExpTestImg-Dashboard-input:first').as('file-input')
5 | cy.intercept({ method: 'GET', pathname: '/s3/params' }).as('get')
6 | cy.intercept({ method: 'POST' }).as('post')
7 | })
8 |
9 | it('should upload cat image successfully', () => {
10 | cy.get('@file-input').selectFile('cypress/fixtures/images/kit.jpg', {
11 | force: true,
12 | })
13 |
14 | cy.get('.ExpTestImg-StatusBar-actionBtn--upload').click()
15 | cy.wait(['@post', '@get'])
16 | cy.get('.ExpTestImg-StatusBar-statusPrimary').should('contain', 'Complete')
17 | })
18 | })
19 |
--------------------------------------------------------------------------------
/examples/python-xhr/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/python-xhr",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "dependencies": {
6 | "@ExpTestImg/core": "workspace:*",
7 | "@ExpTestImg/dashboard": "workspace:*",
8 | "@ExpTestImg/webcam": "workspace:*",
9 | "@ExpTestImg/xhr-upload": "workspace:*"
10 | },
11 | "devDependencies": {
12 | "npm-run-all": "^4.1.3",
13 | "vite": "^4.0.0"
14 | },
15 | "private": true,
16 | "scripts": {
17 | "installPythonDeps": "python3 -m pip install -r requirements.txt",
18 | "start": "npm-run-all --parallel start:server start:client",
19 | "start:client": "vite",
20 | "start:server": "mkdir -p uploads && python3 server.py"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/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/@ExpTestImg/companion/src --exec node -r dotenv/config ./packages/@ExpTestImg/companion/src/standalone/start-server.js
6 | else
7 | env \
8 | COMPANION_DATADIR="./output" \
9 | COMPANION_DOMAIN="localhost:3020" \
10 | COMPANION_PROTOCOL="http" \
11 | COMPANION_PORT=3020 \
12 | COMPANION_CLIENT_ORIGINS="" \
13 | COMPANION_SECRET="development" \
14 | COMPANION_PREAUTH_SECRET="development2" \
15 | COMPANION_ALLOW_LOCAL_URLS="true" \
16 | nodemon --watch packages/@ExpTestImg/companion/src --exec node ./packages/@ExpTestImg/companion/src/standalone/start-server.js
17 | fi
18 |
19 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/dashboard-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 ExpTestImg:
7 | // https://github.com/transloadit/ExpTestImg/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('.ExpTestImg-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 | ExpTestImg-*.tgz
28 | generatedLocale.d.ts
29 |
30 | **/output/*
31 | !output/.keep
32 | examples/dev/file.txt
33 | issues.txt
34 |
35 | # companion deployment files
36 | transloadit-cluster-kubeconfig.yaml
37 | companion-env.yml
38 |
--------------------------------------------------------------------------------
/examples/aws-companion/README.md:
--------------------------------------------------------------------------------
1 | # ExpTestImg + AWS S3 Example
2 |
3 | This example uses @ExpTestImg/companion with a custom AWS S3 configuration.
4 | Files are uploaded to a randomly named directory inside the `whatever/`
5 | directory in a bucket.
6 |
7 | ## Run it
8 |
9 | First, set up the `COMPANION_AWS_KEY`, `COMPANION_AWS_SECRET`,
10 | `COMPANION_AWS_REGION`, and `COMPANION_AWS_BUCKET` environment variables for
11 | `@ExpTestImg/companion` in a `.env` file. You may find useful to first copy the
12 | `.env.example` file:
13 |
14 | ```sh
15 | [ -f .env ] || cp .env.example .env
16 | ```
17 |
18 | To run this example, from the **repository root**, run:
19 |
20 | ```sh
21 | corepack yarn install
22 | corepack yarn workspace @ExpTestImg-example/aws-companion start
23 | ```
24 |
--------------------------------------------------------------------------------
/examples/angular-example/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Miscellaneous
32 | /.angular/cache
33 | .sass-cache/
34 | /connect.lock
35 | /coverage
36 | /libpeerconnection.log
37 | testem.log
38 | /typings
39 |
40 | # System files
41 | .DS_Store
42 | Thumbs.db
43 |
--------------------------------------------------------------------------------
/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 { ExpTestImg } from '@ExpTestImg/core'
19 |
20 | declare global {
21 | interface Window {
22 | ExpTestImg: ExpTestImg
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/bundled/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ExpTestImg
7 |
8 |
9 |
21 |
22 | ExpTestImg
23 |
24 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-tus/app.js:
--------------------------------------------------------------------------------
1 | import Dashboard from '@ExpTestImg/dashboard'
2 | import Tus from '@ExpTestImg/tus'
3 | import Unsplash from '@ExpTestImg/unsplash'
4 | import Url from '@ExpTestImg/url'
5 |
6 | import '@ExpTestImg/core/dist/style.css'
7 | import '@ExpTestImg/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 ExpTestImg = new ExpTestImg()
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 |
--------------------------------------------------------------------------------
/examples/python-xhr/README.md:
--------------------------------------------------------------------------------
1 | # ExpTestImg + Python Example
2 |
3 | This example uses a Python Flask server and `@ExpTestImg/xhr-upload` to upload files to the local file system.
4 |
5 | ## Run it
6 |
7 | To run this example, make sure you've correctly installed the **repository root**:
8 |
9 | ```sh
10 | corepack yarn install
11 | corepack yarn build
12 | ```
13 |
14 | That will also install the npm dependencies for this example.
15 |
16 | Additionally, this example uses python dependencies. Move into this directory, and install them using pip:
17 |
18 | ```sh
19 | corepack yarn workspace @ExpTestImg-example/python-xhr installPythonDeps
20 | ```
21 |
22 | Then, again in the **repository root**, start this example by doing:
23 |
24 | ```sh
25 | corepack yarn workspace @ExpTestImg-example/python-xhr start
26 | ```
27 |
--------------------------------------------------------------------------------
/examples/aws-companion/main.js:
--------------------------------------------------------------------------------
1 | import AwsS3 from '@ExpTestImg/aws-s3'
2 | import ExpTestImg from '@ExpTestImg/core'
3 | import Dashboard from '@ExpTestImg/dashboard'
4 | import GoogleDrive from '@ExpTestImg/google-drive'
5 | import Webcam from '@ExpTestImg/webcam'
6 |
7 | import '@ExpTestImg/core/dist/style.css'
8 | import '@ExpTestImg/dashboard/dist/style.css'
9 | import '@ExpTestImg/webcam/dist/style.css'
10 |
11 | const ExpTestImg = new ExpTestImg({
12 | debug: true,
13 | autoProceed: false,
14 | })
15 |
16 | ExpTestImg.use(GoogleDrive, {
17 | companionUrl: 'http://localhost:3020',
18 | })
19 | ExpTestImg.use(Webcam)
20 | ExpTestImg.use(Dashboard, {
21 | inline: true,
22 | target: 'body',
23 | plugins: ['GoogleDrive', 'Webcam'],
24 | })
25 | ExpTestImg.use(AwsS3, {
26 | companionUrl: 'http://localhost:3020',
27 | })
28 |
--------------------------------------------------------------------------------
/examples/custom-provider/client/main.js:
--------------------------------------------------------------------------------
1 | import ExpTestImg from '@ExpTestImg/core'
2 | import GoogleDrive from '@ExpTestImg/google-drive'
3 | import Tus from '@ExpTestImg/tus'
4 | import Dashboard from '@ExpTestImg/dashboard'
5 | import MyCustomProvider from './MyCustomProvider.jsx'
6 |
7 | import '@ExpTestImg/core/dist/style.css'
8 | import '@ExpTestImg/dashboard/dist/style.css'
9 |
10 | const ExpTestImg = new ExpTestImg({
11 | debug: true,
12 | })
13 |
14 | ExpTestImg.use(GoogleDrive, {
15 | companionUrl: 'http://localhost:3020',
16 | })
17 |
18 | ExpTestImg.use(MyCustomProvider, {
19 | companionUrl: 'http://localhost:3020',
20 | })
21 |
22 | ExpTestImg.use(Dashboard, {
23 | inline: true,
24 | target: 'body',
25 | plugins: ['GoogleDrive', 'MyCustomProvider'],
26 | })
27 |
28 | ExpTestImg.use(Tus, { endpoint: 'https://tusd.tusdemo.net/files/' })
29 |
--------------------------------------------------------------------------------
/examples/angular-example/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core'
2 | import { BrowserModule } from '@angular/platform-browser'
3 |
4 | import {
5 | ExpTestImgAngularDashboardModule,
6 | ExpTestImgAngularStatusBarModule,
7 | ExpTestImgAngularDragDropModule,
8 | ExpTestImgAngularProgressBarModule,
9 | ExpTestImgAngularDashboardModalModule,
10 | } from '@ExpTestImg' +
11 | /angular'
12 | import { AppComponent } from './app.component'
13 |
14 | @NgModule({
15 | declarations: [AppComponent],
16 | imports: [
17 | BrowserModule,
18 | ExpTestImgAngularDashboardModule,
19 | ExpTestImgAngularStatusBarModule,
20 | ExpTestImgAngularDashboardModalModule,
21 | ExpTestImgAngularDragDropModule,
22 | ExpTestImgAngularProgressBarModule,
23 | ],
24 | providers: [],
25 | bootstrap: [AppComponent],
26 | })
27 | export class AppModule {}
28 |
--------------------------------------------------------------------------------
/examples/multiple-instances/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | ExpTestImg example: Multiple instances
7 |
17 |
18 |
19 |
20 |
21 |
Instance A
22 |
23 |
24 |
25 |
Instance B
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/examples/multiple-instances/main.js:
--------------------------------------------------------------------------------
1 | import ExpTestImg from '@ExpTestImg/core'
2 | import Dashboard from '@ExpTestImg/dashboard'
3 | import GoldenRetriever from '@ExpTestImg/golden-retriever'
4 |
5 | import '@ExpTestImg/core/dist/style.css'
6 | import '@ExpTestImg/dashboard/dist/style.css'
7 |
8 | // Initialise two ExpTestImg instances with the GoldenRetriever plugin,
9 | // but with different `id`s.
10 | const a = new ExpTestImg({
11 | id: 'a',
12 | debug: true,
13 | })
14 | .use(Dashboard, {
15 | target: '#a',
16 | inline: true,
17 | width: 400,
18 | })
19 | .use(GoldenRetriever, { serviceWorker: false })
20 |
21 | const b = new ExpTestImg({
22 | id: 'b',
23 | debug: true,
24 | })
25 | .use(Dashboard, {
26 | target: '#b',
27 | inline: true,
28 | width: 400,
29 | })
30 | .use(GoldenRetriever, { serviceWorker: false })
31 |
32 | window.a = a
33 | window.b = b
34 |
--------------------------------------------------------------------------------
/examples/custom-provider/README.md:
--------------------------------------------------------------------------------
1 | # ExpTestImg + Companion + Custom Provider Example
2 |
3 | This example uses @ExpTestImg/companion with a dummy custom provider.
4 | This serves as an illustration on how integrating custom providers would work
5 |
6 | ## Run it
7 |
8 | **Note**: this example is using `fetch`, which is only available on Node.js 18+.
9 |
10 | First, you want to set up your environment variable. You can copy the content of
11 | `.env.example` and save it in a file named `.env`. You can modify in there all
12 | the information needed for the app to work that should not be committed
13 | (Google keys, Unsplash keys, etc.).
14 |
15 | ```sh
16 | [ -f .env ] || cp .env.example .env
17 | ```
18 |
19 | To run the example, from the root directory of this repo, run the following commands:
20 |
21 | ```sh
22 | corepack yarn install
23 | corepack yarn build
24 | corepack yarn workspace @ExpTestImg-example/custom-provider start
25 | ```
26 |
--------------------------------------------------------------------------------
/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/ExpTestImg-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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-transloadit/app.js:
--------------------------------------------------------------------------------
1 | import { ExpTestImg } from '@ExpTestImg/core'
2 | import Dashboard from '@ExpTestImg/dashboard'
3 | import Transloadit from '@ExpTestImg/transloadit'
4 |
5 | import generateSignatureIfSecret from './generateSignatureIfSecret.js'
6 |
7 | import '@ExpTestImg/core/dist/style.css'
8 | import '@ExpTestImg/dashboard/dist/style.css'
9 |
10 | // Environment variables:
11 | // https://en.parceljs.org/env.html
12 | const ExpTestImg = new ExpTestImg()
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 ExpTestImg in tests
24 | window.ExpTestImg = ExpTestImg
25 |
--------------------------------------------------------------------------------
/e2e/mock-server.mjs:
--------------------------------------------------------------------------------
1 | import http from 'node:http'
2 |
3 | const requestListener = (req, res) => {
4 | const endpoint = req.url
5 |
6 | switch (endpoint) {
7 | case '/file-with-content-disposition': {
8 | const fileName = `DALL·E IMG_9078 - 学中文 🤑`
9 | res.setHeader('Content-Disposition', `attachment; filename="ASCII-name.zip"; filename*=UTF-8''${encodeURIComponent(fileName)}`)
10 | res.setHeader('Content-Type', 'image/jpeg')
11 | res.setHeader('Content-Length', '86500')
12 | break
13 | }
14 | case '/file-no-headers':
15 | break
16 | default:
17 | res.writeHead(404).end('Unhandled request')
18 | }
19 |
20 | res.end()
21 | }
22 |
23 | export default function startMockServer (host, port) {
24 | const server = http.createServer(requestListener)
25 | server.listen(port, host, () => {
26 | console.log(`Server is running on http://${host}:${port}`)
27 | })
28 | }
29 |
30 | // startMockServer('localhost', 4678)
31 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-aws-multipart/app.js:
--------------------------------------------------------------------------------
1 | import Dashboard from '@ExpTestImg/dashboard'
2 | import AwsS3Multipart from '@ExpTestImg/aws-s3-multipart'
3 |
4 | import '@ExpTestImg/core/dist/style.css'
5 | import '@ExpTestImg/dashboard/dist/style.css'
6 |
7 | //#TODO tests
8 | const ExpTestImg = new ExpTestImg()
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 = ExpTestImg.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 ExpTestImg in tests
23 | window.ExpTestImg = ExpTestImg
24 |
--------------------------------------------------------------------------------
/examples/aws-companion/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/aws-companion",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "dependencies": {
6 | "@ExpTestImg/aws-s3": "workspace:*",
7 | "@ExpTestImg/core": "workspace:*",
8 | "@ExpTestImg/dashboard": "workspace:*",
9 | "@ExpTestImg/google-drive": "workspace:*",
10 | "@ExpTestImg/webcam": "workspace:*"
11 | },
12 | "devDependencies": {
13 | "@ExpTestImg/companion": "workspace:*",
14 | "body-parser": "^1.20.0",
15 | "cookie-parser": "^1.4.6",
16 | "cors": "^2.8.5",
17 | "dotenv": "^16.0.1",
18 | "express": "^4.18.1",
19 | "express-session": "^1.17.3",
20 | "npm-run-all": "^4.1.5",
21 | "vite": "^4.0.0"
22 | },
23 | "private": true,
24 | "engines": {
25 | "node": ">=16.15.0"
26 | },
27 | "scripts": {
28 | "dev": "vite",
29 | "start": "npm-run-all --parallel start:client start:server",
30 | "start:client": "vite",
31 | "start:server": "node server.cjs"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/examples/custom-provider/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/custom-provider",
3 | "version": "0.0.0",
4 | "type": "module",
5 | "dependencies": {
6 | "@ExpTestImg/companion-client": "workspace:*",
7 | "@ExpTestImg/core": "workspace:*",
8 | "@ExpTestImg/dashboard": "workspace:*",
9 | "@ExpTestImg/google-drive": "workspace:*",
10 | "@ExpTestImg/provider-views": "workspace:*",
11 | "@ExpTestImg/tus": "workspace:*",
12 | "preact": "^10.5.13"
13 | },
14 | "engines": {
15 | "node": ">=18.0.0"
16 | },
17 | "devDependencies": {
18 | "@ExpTestImg/companion": "workspace:*",
19 | "body-parser": "^1.18.2",
20 | "dotenv": "^16.0.1",
21 | "express": "^4.16.2",
22 | "express-session": "^1.15.6",
23 | "npm-run-all": "^4.1.2",
24 | "vite": "^4.0.0"
25 | },
26 | "private": true,
27 | "scripts": {
28 | "start": "npm-run-all --parallel start:server start:client",
29 | "start:client": "vite",
30 | "start:server": "node server/index.cjs"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/examples/angular-example/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "noImplicitOverride": true,
10 | "noPropertyAccessFromIndexSignature": true,
11 | "noImplicitReturns": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "sourceMap": true,
14 | "declaration": false,
15 | "downlevelIteration": true,
16 | "experimentalDecorators": true,
17 | "moduleResolution": "node",
18 | "importHelpers": true,
19 | "target": "ES2022",
20 | "module": "ES2022",
21 | "useDefineForClassFields": false,
22 | "lib": ["ES2022", "dom"]
23 | },
24 | "angularCompilerOptions": {
25 | "enableI18nLegacyMessageIdFormat": false,
26 | "strictInjectionParameters": true,
27 | "strictInputAccessModifiers": true,
28 | "strictTemplates": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/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 { createFakeFile } from './createFakeFile'
29 |
30 | Cypress.Commands.add('createFakeFile', createFakeFile)
31 |
--------------------------------------------------------------------------------
/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 DeepFrozenStore {
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 DeepFrozenStore()
45 | }
46 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/angular-example/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
3 | "version": "2.0.0",
4 | "tasks": [
5 | {
6 | "type": "npm",
7 | "script": "start",
8 | "isBackground": true,
9 | "problemMatcher": {
10 | "owner": "typescript",
11 | "pattern": "$tsc",
12 | "background": {
13 | "activeOnStart": true,
14 | "beginsPattern": {
15 | "regexp": "(.*?)"
16 | },
17 | "endsPattern": {
18 | "regexp": "bundle generation complete"
19 | }
20 | }
21 | }
22 | },
23 | {
24 | "type": "npm",
25 | "script": "test",
26 | "isBackground": true,
27 | "problemMatcher": {
28 | "owner": "typescript",
29 | "pattern": "$tsc",
30 | "background": {
31 | "activeOnStart": true,
32 | "beginsPattern": {
33 | "regexp": "(.*?)"
34 | },
35 | "endsPattern": {
36 | "regexp": "bundle generation complete"
37 | }
38 | }
39 | }
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/examples/aws-php/main.js:
--------------------------------------------------------------------------------
1 | import ExpTestImg from '@ExpTestImg/core'
2 | import Dashboard from '@ExpTestImg/dashboard'
3 | import AwsS3 from '@ExpTestImg/aws-s3'
4 |
5 | const ExpTestImg = new ExpTestImg({
6 | debug: true,
7 | })
8 |
9 | ExpTestImg.use(Dashboard, {
10 | inline: true,
11 | target: 'body',
12 | })
13 | ExpTestImg.use(AwsS3, {
14 | shouldUseMultipart: false, // The PHP backend only supports non-multipart uploads
15 |
16 | getUploadParameters (file) {
17 | // Send a request to our PHP signing endpoint.
18 | return fetch('/s3-sign.php', {
19 | method: 'post',
20 | // Send and receive JSON.
21 | headers: {
22 | accept: 'application/json',
23 | 'content-type': 'application/json',
24 | },
25 | body: JSON.stringify({
26 | filename: file.name,
27 | contentType: file.type,
28 | }),
29 | }).then((response) => {
30 | // Parse the JSON response.
31 | return response.json()
32 | }).then((data) => {
33 | // Return an object in the correct shape.
34 | return {
35 | method: data.method,
36 | url: data.url,
37 | fields: data.fields,
38 | headers: data.headers,
39 | }
40 | })
41 | },
42 | })
43 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:18.17.1-alpine as build
2 |
3 | # Create link to node on amd64 so that corepack can find it
4 | RUN if [ "$(uname -m)" == "aarch64" ]; then mkdir -p /usr/local/sbin/ && ln -s /usr/local/bin/node /usr/local/sbin/node; fi
5 |
6 | WORKDIR /app
7 |
8 | COPY . /app/
9 |
10 | RUN apk --update add --virtual native-dep \
11 | make gcc g++ python3 libgcc libstdc++ git && \
12 | (cd /app && corepack yarn workspaces focus @ExpTestImg/companion) && \
13 | apk del native-dep
14 |
15 | RUN cd /app && corepack yarn workspace @ExpTestImg/companion build
16 |
17 | # Now remove all non-prod dependencies for a leaner image
18 | RUN cd /app && corepack yarn workspaces focus @ExpTestImg/companion --production
19 |
20 | FROM node:18.17.1-alpine
21 |
22 | WORKDIR /app
23 |
24 | # copy required files from build stage.
25 | COPY --from=build /app/packages/@ExpTestImg/companion/bin /app/bin
26 | COPY --from=build /app/packages/@ExpTestImg/companion/lib /app/lib
27 | COPY --from=build /app/packages/@ExpTestImg/companion/package.json /app/package.json
28 | COPY --from=build /app/packages/@ExpTestImg/companion/node_modules /app/node_modules
29 |
30 | ENV PATH "${PATH}:/app/node_modules/.bin"
31 |
32 | CMD ["node","/app/bin/companion"]
33 | # This can be overruled later
34 | EXPOSE 3020
35 |
--------------------------------------------------------------------------------
/examples/angular-example/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignorePatterns": ["projects/**/*"],
3 | "overrides": [
4 | {
5 | "files": ["*.ts"],
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended",
9 | "plugin:@angular-eslint/recommended",
10 | "plugin:@angular-eslint/template/process-inline-templates"
11 | ],
12 | "rules": {
13 | "@angular-eslint/directive-selector": [
14 | "error",
15 | {
16 | "type": "attribute",
17 | "prefix": "app",
18 | "style": "camelCase"
19 | }
20 | ],
21 | "@typescript-eslint/semi": ["error", "never"],
22 | "import/no-unresolved": "off",
23 | "import/prefer-default-export": "off",
24 | "@angular-eslint/component-selector": [
25 | "error",
26 | {
27 | "type": "element",
28 | "prefix": "app",
29 | "style": "kebab-case"
30 | }
31 | ],
32 | "semi": ["error", "never"]
33 | }
34 | },
35 | {
36 | "files": ["star.html"],
37 | "extends": [
38 | "plugin:@angular-eslint/template/recommended",
39 | "plugin:@angular-eslint/template/accessibility"
40 | ],
41 | "rules": {}
42 | }
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/bin/update-contributors.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import fs from 'node:fs/promises';
4 |
5 | const README_FILE_NAME = new URL('../README.md', import.meta.url);
6 |
7 | async function updateReadmeWithContributors() {
8 | try {
9 | const readme = await fs.open(README_FILE_NAME, 'r+');
10 | try {
11 | const readme = await readme.readFile();
12 | const contributors = await getContributors();
13 |
14 | if (contributors.length === 0) {
15 | console.log('Empty response from githubcontrib. GitHub’s rate limit?');
16 | return;
17 | }
18 |
19 | const updatedReadmeContent = insertContributors(readme, contributors);
20 | await readme.write(updatedReadmeContent, 0, 'utf-8');
21 | } finally {
22 | await readme.close();
23 | }
24 | } catch (err) {
25 | console.error(err);
26 | process.exit(1);
27 | }
28 | }
29 |
30 | function insertContributors(readmeContent, contributors) {
31 | const startTag = '\n';
32 | const endTag = '';
33 | const startIndex = readmeContent.indexOf(startTag) + startTag.length;
34 | const endIndex = readmeContent.indexOf(endTag);
35 |
36 | return readmeContent.slice(0, startIndex) +
37 | contributors +
38 | readmeContent.slice(endIndex);
39 | }
40 |
41 | updateReadmeWithContributors();
42 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/angular-example/README.md:
--------------------------------------------------------------------------------
1 | # AngularExample
2 |
3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.2.0.
4 |
5 | ## Development server
6 |
7 | ```
8 | corepack yarn install
9 | corepack yarn build
10 | corepack yarn workspace @ExpTestImg/angular prepublishOnly
11 | corepack yarn workspace @ExpTestImg-example/angular start
12 | ```
13 |
14 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files.
15 |
16 | ## Code scaffolding
17 |
18 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
19 |
20 | ## Build
21 |
22 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
23 |
24 | ## Running unit tests
25 |
26 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
27 |
28 | ## Running end-to-end tests
29 |
30 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
31 |
32 | ## Further help
33 |
34 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
35 |
--------------------------------------------------------------------------------
/e2e/clients/dashboard-ui/app.js:
--------------------------------------------------------------------------------
1 | import ExpTestImg from '@ExpTestImg/core'
2 | import Dashboard from '@ExpTestImg/dashboard'
3 | import RemoteSources from '@ExpTestImg/remote-sources'
4 | import Webcam from '@ExpTestImg/webcam'
5 | import ScreenCapture from '@ExpTestImg/screen-capture'
6 | import GoldenRetriever from '@ExpTestImg/golden-retriever'
7 | import ImageEditor from '@ExpTestImg/image-editor'
8 | import DropTarget from '@ExpTestImg/drop-target'
9 | import Audio from '@ExpTestImg/audio'
10 | import Compressor from '@ExpTestImg/compressor'
11 |
12 | import '@ExpTestImg/core/dist/style.css'
13 | import '@ExpTestImg/dashboard/dist/style.css'
14 |
15 | const COMPANION_URL = 'http://companion.ExpTestImg.io'
16 |
17 | const ExpTestImg = new ExpTestImg()
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 ExpTestImg in tests
36 | window.ExpTestImg = ExpTestImg
37 |
--------------------------------------------------------------------------------
/assets/stringutils.ps1:
--------------------------------------------------------------------------------
1 | # Define the path to your Node.js project
2 | $projectPath = "E:\JavaProjects\ExpTestImg"
3 |
4 | # Define the strings for search and replace
5 | $searchString = "ExpTestImg"
6 | $replaceString = "ExpTestImg"
7 |
8 | # Function to rename directories
9 | Function Rename-Directories {
10 | param (
11 | [string]$path
12 | )
13 |
14 | # Get all directories in the path, excluding the root
15 | $directories = Get-ChildItem -Path $path -Recurse -Directory | Sort-Object FullName -Descending
16 |
17 | foreach ($dir in $directories) {
18 | $newName = $dir.Name -replace $searchString, $replaceString
19 | if ($newName -ne $dir.Name) {
20 | $newPath = Join-Path $dir.Parent.FullName $newName
21 | Rename-Item -Path $dir.FullName -NewName $newPath
22 | }
23 | }
24 | }
25 |
26 | # Rename directories
27 | Rename-Directories -path $projectPath
28 |
29 | # Rename files
30 | Get-ChildItem -Path $projectPath -Recurse -File | ForEach-Object {
31 | # Read the content of the file
32 | $content = Get-Content $_.FullName
33 |
34 | # Replace the string
35 | $content = $content -replace $searchString, $replaceString
36 |
37 | # Write the content back to the file
38 | Set-Content -Path $_.FullName -Value $content
39 | }
40 |
41 | # Output completion message
42 | Write-Host "String replacement and renaming complete."
43 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/examples/ExpTestImg-with-companion/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
15 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/examples/aws-php/readme.md:
--------------------------------------------------------------------------------
1 | # ExpTestImg + AWS S3 Example
2 |
3 | This example uses a server-side PHP endpoint to sign uploads to S3.
4 |
5 | ## Running It
6 |
7 | To run this example, make sure you've correctly installed the **repository root**:
8 |
9 | ```bash
10 | yarn || corepack yarn install
11 | yarn build || corepack yarn build
12 | ```
13 |
14 | That will also install the npm dependencies for this example.
15 |
16 | This example also uses the AWS PHP SDK.
17 | To install it, [get composer](https://getcomposer.org) and run `composer update` in this folder.
18 |
19 | ```bash
20 | corepack yarn workspace @ExpTestImg-example/aws-php exec "composer update"
21 | ```
22 |
23 | Configure AWS S3 credentials using [environment variables](https://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/credentials.html#environment-credentials) or a [credentials file in `~/.aws/credentials`](https://docs.aws.amazon.com/aws-sdk-php/v3/guide/guide/credentials.html#credential-profiles).
24 | Configure a bucket name and region in the `s3-sign.php` file.
25 |
26 | Then, again in the **repository root**, start this example by doing:
27 |
28 | ```bash
29 | corepack yarn workspace @ExpTestImg-example/aws-php start
30 | ```
31 |
32 | The demo should now be available at http://localhost:8080.
33 |
34 | You can use a different S3-compatible service like GCS by configuring that service in `~/.aws/config` and `~/.aws/credentials`, and then providing appropriate environment variables:
35 |
36 | ```bash
37 | AWS_PROFILE="gcs" \
38 | COMPANION_AWS_ENDPOINT="https://storage.googleapis.com" \
39 | COMPANION_AWS_BUCKET="test-bucket-name" \
40 | corepack yarn run example aws-php
41 | ```
42 |
--------------------------------------------------------------------------------
/examples/aws-php/s3-sign.php:
--------------------------------------------------------------------------------
1 | 'latest',
17 | 'endpoint' => $awsEndpoint,
18 | 'region' => $awsRegion,
19 | ]);
20 |
21 | // Retrieve data about the file to be uploaded from the request body.
22 | $body = json_decode(file_get_contents('php://input'));
23 | $filename = $body->filename;
24 | $contentType = $body->contentType;
25 |
26 | // Prepare a PutObject command.
27 | $command = $s3->getCommand('putObject', [
28 | 'Bucket' => $bucket,
29 | 'Key' => "{$directory}/{$filename}",
30 | 'ContentType' => $contentType,
31 | 'Body' => '',
32 | ]);
33 |
34 | $request = $s3->createPresignedRequest($command, '+5 minutes');
35 |
36 | header('content-type: application/json');
37 | echo json_encode([
38 | 'method' => $request->getMethod(),
39 | 'url' => (string) $request->getUri(),
40 | 'fields' => [],
41 | // Also set the content-type header on the request, to make sure that it is the same as the one we used to generate the signature.
42 | // Else, the browser picks a content-type as it sees fit.
43 | 'headers' => [
44 | 'content-type' => $contentType,
45 | ],
46 | ]);
47 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Licensed under MIT.
2 | # Copyright (2016) by Kevin van Zonneveld https://twitter.com/kvz
3 | #
4 | # https://www.npmjs.com/package/fakefile
5 | #
6 | # Please do not edit this file directly, but propose changed upstream instead:
7 | # https://github.com/kvz/fakefile/blob/master/Makefile
8 | #
9 | # This Makefile offers convience shortcuts into any Node.js project that utilizes npm scripts.
10 | # It functions as a wrapper around the actual listed in `package.json`
11 | # So instead of typing:
12 | #
13 | # $ npm script build:assets
14 | #
15 | # you could also type:
16 | #
17 | # $ make build-assets
18 | #
19 | # Notice that colons (:) are replaced by dashes for Makefile compatibility.
20 | #
21 | # The benefits of this wrapper are:
22 | #
23 | # - You get to keep the the scripts package.json, which is more portable
24 | # (Makefiles & Windows are harder to mix)
25 | # - Offer a polite way into the project for developers coming from different
26 | # languages (npm scripts is obviously very Node centric)
27 | # - Profit from better autocomplete (make ) than npm currently offers.
28 | # OSX users will have to install bash-completion
29 | # (http://davidalger.com/development/bash-completion-on-os-x-with-brew/)
30 |
31 | define npm_script_targets
32 | TARGETS := $(shell node -e 'for (var k in require("./package.json").scripts) {console.log(k.replace(/:/g, "-"));}')
33 | $$(TARGETS):
34 | npm run $(subst -,:,$(MAKECMDGOALS))
35 |
36 | .PHONY: $$(TARGETS)
37 | endef
38 |
39 | $(eval $(call npm_script_targets))
40 |
41 | # These npm run scripts are available, without needing to be mentioned in `package.json`
42 | install:
43 | npm install
44 |
--------------------------------------------------------------------------------
/examples/digitalocean-spaces/README.md:
--------------------------------------------------------------------------------
1 | # Uploading to DigitalOcean Spaces
2 |
3 | This example uses ExpTestImg to upload files to a [DigitalOcean Space](https://digitaloceanspaces.com/).
4 | DigitalOcean Spaces has an identical API to S3, so we can use the
5 | [AwsS3](https://ExpTestImg.io/docs/aws-s3-multipart) plugin. We use @ExpTestImg/companion with a
6 | [custom `endpoint` configuration](./server.cjs#L39) that points to DigitalOcean.
7 |
8 | ## Running it
9 |
10 | To run this example, make sure you've correctly installed the **repository root**:
11 |
12 | ```bash
13 | corepack yarn install
14 | corepack yarn build
15 | ```
16 |
17 | That will also install the dependencies for this example.
18 |
19 | First, set up the `COMPANION_AWS_KEY`, `COMPANION_AWS_SECRET`,
20 | `COMPANION_AWS_REGION` (use a DigitalOcean region name for `COMPANION_AWS_REGION`,
21 | e.g. `nyc3`), and `COMPANION_AWS_BUCKET` environment variables for
22 | `@ExpTestImg/companion` in a `.env` file. You may find useful to first copy the
23 | `.env.example` file:
24 |
25 | ```sh
26 | [ -f .env ] || cp .env.example .env
27 | ```
28 |
29 | To setup the CORS settings of your Spaces bucket in accordance with
30 | [the plugin docs](https://ExpTestImg.io/docs/aws-s3-multipart/#setting-up-your-s3-bucket),
31 | you can use the [example XML config file](./setcors.xml) with the
32 | [`s3cmd` CLI](https://docs.digitalocean.com/products/spaces/reference/s3cmd/):
33 |
34 | ```sh
35 | s3cmd setcors examples/digitalocean-spaces/setcors.xml "s3://$COMPANION_AWS_BUCKET"
36 | ```
37 |
38 | Then you can start the dev server:
39 |
40 | ```bash
41 | corepack yarn workspace @ExpTestImg-example/digitalocean-spaces start
42 | ```
43 |
--------------------------------------------------------------------------------
/examples/node-xhr/server.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /* eslint-disable no-console */
4 |
5 | import http from 'node:http'
6 | import { fileURLToPath } from 'node:url'
7 | import { mkdir } from 'node:fs/promises'
8 |
9 | import formidable from 'formidable'
10 |
11 | const UPLOAD_DIR = new URL('./uploads/', import.meta.url)
12 |
13 | await mkdir(UPLOAD_DIR, { recursive: true })
14 |
15 | http.createServer((req, res) => {
16 | const headers = {
17 | 'Content-Type': 'application/json',
18 | 'Access-Control-Allow-Origin': '*',
19 | 'Access-Control-Allow-Methods': 'OPTIONS, POST, GET',
20 | 'Access-Control-Max-Age': 2592000, // 30 days
21 | /** add other headers as per requirement */
22 | }
23 |
24 | if (req.method === 'OPTIONS') {
25 | res.writeHead(204, headers)
26 | res.end()
27 | return
28 | }
29 | if (req.url === '/upload' && req.method.toLowerCase() === 'post') {
30 | // parse a file upload
31 | const form = formidable({
32 | keepExtensions: true,
33 | uploadDir: fileURLToPath(UPLOAD_DIR),
34 | })
35 |
36 | form.parse(req, (err, fields, files) => {
37 | if (err) {
38 | console.log('some error', err)
39 | res.writeHead(200, headers)
40 | res.write(JSON.stringify(err))
41 | return res.end()
42 | }
43 | const { file:[{ filepath, originalFilename, mimetype, size }] } = files
44 | console.log('saved file', { filepath, originalFilename, mimetype, size })
45 | res.writeHead(200, headers)
46 | res.write(JSON.stringify({ fields, files }))
47 | return res.end()
48 | })
49 | }
50 | }).listen(3020, () => {
51 | console.log('server started')
52 | })
53 |
--------------------------------------------------------------------------------
/e2e/cypress/support/createFakeFile.ts:
--------------------------------------------------------------------------------
1 | declare global {
2 | namespace Cypress {
3 | interface Chainable {
4 | // eslint-disable-next-line no-use-before-define
5 | createFakeFile: typeof createFakeFile
6 | }
7 | }
8 | }
9 |
10 | interface File {
11 | source: string
12 | name: string
13 | type: string
14 | data: Blob
15 | }
16 |
17 | export function createFakeFile(
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 |
--------------------------------------------------------------------------------
/examples/aws-companion/server.cjs:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs')
2 | const path = require('node:path')
3 | const crypto = require('node:crypto')
4 | const companion = require('@ExpTestImg/companion')
5 |
6 | require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env') })
7 | const app = require('express')()
8 |
9 | const DATA_DIR = path.join(__dirname, 'tmp')
10 |
11 | app.use(require('cors')({
12 | origin: 'http://localhost:3000',
13 | methods: ['GET', 'POST', 'OPTIONS'],
14 | credentials: true,
15 | }))
16 | app.use(require('cookie-parser')())
17 | app.use(require('body-parser').json())
18 | app.use(require('express-session')({
19 | secret: 'hello planet',
20 | saveUninitialized: false,
21 | resave: false,
22 | }))
23 |
24 | const options = {
25 | providerOptions: {
26 | drive: {
27 | key: process.env.COMPANION_GOOGLE_KEY,
28 | secret: process.env.COMPANION_GOOGLE_SECRET,
29 | },
30 | },
31 | s3: {
32 | getKey: (req, filename) => `${crypto.randomUUID()}-${filename}`,
33 | key: process.env.COMPANION_AWS_KEY,
34 | secret: process.env.COMPANION_AWS_SECRET,
35 | bucket: process.env.COMPANION_AWS_BUCKET,
36 | region: process.env.COMPANION_AWS_REGION,
37 | endpoint: process.env.COMPANION_AWS_ENDPOINT,
38 | },
39 | server: { host: 'localhost:3020' },
40 | filePath: DATA_DIR,
41 | secret: 'blah blah',
42 | debug: true,
43 | }
44 |
45 | // Create the data directory here for the sake of the example.
46 | try {
47 | fs.accessSync(DATA_DIR)
48 | } catch (err) {
49 | fs.mkdirSync(DATA_DIR)
50 | }
51 | process.on('exit', () => {
52 | fs.rmSync(DATA_DIR, { recursive: true, force: true })
53 | })
54 |
55 | const { app: companionApp } = companion.app(options)
56 |
57 | app.use(companionApp)
58 |
59 | const server = app.listen(3020, () => {
60 | console.log('listening on port 3020')
61 | })
62 |
63 | companion.socket(server)
64 |
--------------------------------------------------------------------------------
/examples/custom-provider/client/MyCustomProvider.jsx:
--------------------------------------------------------------------------------
1 | /** @jsx h */
2 |
3 | import { UIPlugin } from '@ExpTestImg/core'
4 | import { Provider } from '@ExpTestImg/companion-client'
5 | import { ProviderViews } from '@ExpTestImg/provider-views'
6 | import { h } from 'preact'
7 |
8 | const defaultOptions = {}
9 |
10 | export default class MyCustomProvider extends UIPlugin {
11 | constructor (ExpTestImg, opts) {
12 | super(ExpTestImg, opts)
13 | this.type = 'acquirer'
14 | this.id = this.opts.id || 'MyCustomProvider'
15 | Provider.initPlugin(this, opts)
16 |
17 | this.icon = () => (
18 |
21 | )
22 |
23 | this.provider = new Provider(ExpTestImg, {
24 | companionUrl: this.opts.companionUrl,
25 | companionHeaders: this.opts.companionHeaders,
26 | provider: 'myunsplash',
27 | pluginId: this.id,
28 | })
29 |
30 | this.defaultLocale = {
31 | strings: {
32 | pluginNameMyUnsplash: 'MyUnsplash',
33 | },
34 | }
35 |
36 | // merge default options with the ones set by user
37 | this.opts = { ...defaultOptions, ...opts }
38 |
39 | this.i18nInit()
40 | this.title = this.i18n('pluginNameMyUnsplash')
41 |
42 | this.files = []
43 | }
44 |
45 | install () {
46 | this.view = new ProviderViews(this, {
47 | provider: this.provider,
48 | })
49 |
50 | const { target } = this.opts
51 | if (target) {
52 | this.mount(target, this)
53 | }
54 | }
55 |
56 | uninstall () {
57 | this.view.tearDown()
58 | this.unmount()
59 | }
60 |
61 | onFirstRender () {
62 | return this.view.getFolder()
63 | }
64 |
65 | render (state) {
66 | return this.view.render(state)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/dashboard-ui.spec.ts:
--------------------------------------------------------------------------------
1 | describe('dashboard-ui', () => {
2 | beforeEach(() => {
3 | cy.visit('/dashboard-ui')
4 | cy.get('.ExpTestImg-Dashboard-input:first').as('file-input')
5 | cy.get('.ExpTestImg-Dashboard-AddFiles').as('drop-target')
6 | })
7 |
8 | it('should not throw when calling ExpTestImg.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(({ ExpTestImg }) => {
18 | expect(ExpTestImg.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('.ExpTestImg-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('.ExpTestImg-Dashboard-Item').should('have.length', 4)
47 | cy.get('.ExpTestImg-Dashboard-Item-previewImg')
48 | .should('have.length', 3)
49 | .each((element) => expect(element).attr('src').to.include('blob:'))
50 | cy.window().then(({ ExpTestImg }) => {
51 | expect(
52 | JSON.stringify(ExpTestImg.getFiles().map((file) => file.meta.relativePath)),
53 | ).to.be.equal('[null,null,null,null]')
54 | })
55 | })
56 | })
57 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/dashboard-tus.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | runRemoteUrlImageUploadTest,
3 | runRemoteUnsplashUploadTest,
4 | } from './reusable-tests'
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('.ExpTestImg-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('.ExpTestImg-StatusBar-actionBtn--upload').click()
24 | cy.wait(['@post', '@patch']).then(() => {
25 | cy.get('.ExpTestImg-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/1.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('.ExpTestImg-StatusBar-actionBtn--upload').click()
40 | cy.wait('@tus').then(() => {
41 | cy.get('.ExpTestImg-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 |
--------------------------------------------------------------------------------
/examples/bundled/sw.js:
--------------------------------------------------------------------------------
1 | // This service worker is needed for Golden Retriever plugin,
2 | // only include if you’ve enabled it
3 | // https://ExpTestImg.io/docs/golden-retriever/
4 |
5 | /* globals clients */
6 | /* eslint-disable no-restricted-globals */
7 |
8 | const fileCache = Object.create(null)
9 |
10 | function getCache (name) {
11 | if (!fileCache[name]) {
12 | fileCache[name] = Object.create(null)
13 | }
14 | return fileCache[name]
15 | }
16 |
17 | self.addEventListener('install', (event) => {
18 | console.log('Installing ExpTestImg Service Worker...')
19 |
20 | event.waitUntil(Promise.resolve()
21 | .then(() => self.skipWaiting()))
22 | })
23 |
24 | self.addEventListener('activate', (event) => {
25 | event.waitUntil(self.clients.claim())
26 | })
27 |
28 | function sendMessageToAllClients (msg) {
29 | clients.matchAll().then((clients) => {
30 | clients.forEach((client) => {
31 | client.postMessage(msg)
32 | })
33 | })
34 | }
35 |
36 | function addFile (store, file) {
37 | getCache(store)[file.id] = file.data
38 | console.log('Added file blob to service worker cache:', file.data)
39 | }
40 |
41 | function removeFile (store, fileID) {
42 | delete getCache(store)[fileID]
43 | console.log('Removed file blob from service worker cache:', fileID)
44 | }
45 |
46 | function getFiles (store) {
47 | sendMessageToAllClients({
48 | type: 'ExpTestImg/ALL_FILES',
49 | store,
50 | files: getCache(store),
51 | })
52 | }
53 |
54 | self.addEventListener('message', (event) => {
55 | switch (event.data.type) {
56 | case 'ExpTestImg/ADD_FILE':
57 | addFile(event.data.store, event.data.file)
58 | break
59 | case 'ExpTestImg/REMOVE_FILE':
60 | removeFile(event.data.store, event.data.fileID)
61 | break
62 | case 'ExpTestImg/GET_FILES':
63 | getFiles(event.data.store)
64 | break
65 |
66 | default: throw new Error('unreachable')
67 | }
68 | })
69 |
--------------------------------------------------------------------------------
/examples/angular-example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ExpTestImg-example/angular",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "watch": "ng build --watch --configuration development",
9 | "test": "ng test",
10 | "lint": "ng lint"
11 | },
12 | "private": true,
13 | "dependencies": {
14 | "@angular/animations": "^16.2.0",
15 | "@angular/common": "^16.2.0",
16 | "@angular/compiler": "^16.2.0",
17 | "@angular/core": "^16.2.0",
18 | "@angular/forms": "^16.2.0",
19 | "@angular/platform-browser": "^16.2.0",
20 | "@angular/platform-browser-dynamic": "^16.2.0",
21 | "@angular/router": "^16.2.0",
22 | "@ExpTestImg/angular": "workspace:*",
23 | "@ExpTestImg/core": "workspace:*",
24 | "@ExpTestImg/drag-drop": "workspace:*",
25 | "@ExpTestImg/google-drive": "workspace:*",
26 | "@ExpTestImg/progress-bar": "workspace:*",
27 | "@ExpTestImg/tus": "workspace:*",
28 | "@ExpTestImg/webcam": "workspace:*",
29 | "rxjs": "~7.8.0",
30 | "tslib": "^2.3.0",
31 | "zone.js": "~0.13.0"
32 | },
33 | "devDependencies": {
34 | "@angular-devkit/build-angular": "^16.2.0",
35 | "@angular-eslint/builder": "16.1.1",
36 | "@angular-eslint/eslint-plugin": "16.1.1",
37 | "@angular-eslint/eslint-plugin-template": "16.1.1",
38 | "@angular-eslint/schematics": "16.1.1",
39 | "@angular-eslint/template-parser": "16.1.1",
40 | "@angular/cli": "~16.2.0",
41 | "@angular/compiler-cli": "^16.2.0",
42 | "@types/jasmine": "~4.3.0",
43 | "@typescript-eslint/eslint-plugin": "5.62.0",
44 | "@typescript-eslint/parser": "5.62.0",
45 | "eslint": "^8.0.0",
46 | "jasmine-core": "~4.6.0",
47 | "karma": "~6.4.0",
48 | "karma-chrome-launcher": "~3.2.0",
49 | "karma-coverage": "~2.2.0",
50 | "karma-jasmine": "~5.1.0",
51 | "karma-jasmine-html-reporter": "~2.1.0",
52 | "typescript": "~5.1"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/examples/cdn-example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
14 |
15 |
16 |
33 |
34 |
35 |
39 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/examples/ExpTestImg-with-companion/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const bodyParser = require('body-parser')
3 | const session = require('express-session')
4 | const companion = require('@ExpTestImg/companion')
5 |
6 | const app = express()
7 |
8 | app.use(bodyParser.json())
9 | app.use(session({
10 | secret: 'some-secret',
11 | resave: true,
12 | saveUninitialized: true,
13 | }))
14 |
15 | app.use((req, res, next) => {
16 | res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')
17 | next()
18 | })
19 |
20 | // Routes
21 | app.get('/', (req, res) => {
22 | res.setHeader('Content-Type', 'text/plain')
23 | res.send('Welcome to Companion')
24 | })
25 |
26 | // initialize ExpTestImg
27 | const companionOptions = {
28 | providerOptions: {
29 | drive: {
30 | key: 'your google key',
31 | secret: 'your google secret',
32 | },
33 | instagram: {
34 | key: 'your instagram key',
35 | secret: 'your instagram secret',
36 | },
37 | dropbox: {
38 | key: 'your dropbox key',
39 | secret: 'your dropbox secret',
40 | },
41 | box: {
42 | key: 'your box key',
43 | secret: 'your box secret',
44 | },
45 | // you can also add options for additional providers here
46 | },
47 | server: {
48 | host: 'localhost:3020',
49 | protocol: 'http',
50 | },
51 | filePath: './output',
52 | secret: 'some-secret',
53 | debug: true,
54 | }
55 |
56 | const { app: companionApp } = companion.app(companionOptions)
57 | app.use(companionApp)
58 |
59 | // handle 404
60 | app.use((req, res) => {
61 | return res.status(404).json({ message: 'Not Found' })
62 | })
63 |
64 | // handle server errors
65 | app.use((err, req, res) => {
66 | console.error('\x1b[31m', err.stack, '\x1b[0m')
67 | res.status(err.status || 500).json({ message: err.message, error: err })
68 | })
69 |
70 | companion.socket(app.listen(3020))
71 |
72 | console.log('Welcome to Companion!')
73 | console.log(`Listening on http://0.0.0.0:${3020}`)
74 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/reusable-tests.ts:
--------------------------------------------------------------------------------
1 | /* global cy */
2 |
3 | const interceptCompanionUrlRequest = () =>
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('.ExpTestImg-Url-input').type(
15 | 'https://raw.githubusercontent.com/transloadit/ExpTestImg/main/e2e/cypress/fixtures/images/cat.jpg',
16 | )
17 | cy.get('.ExpTestImg-Url-importButton').click()
18 | interceptCompanionUrlRequest()
19 | cy.get('.ExpTestImg-StatusBar-actionBtn--upload').click()
20 | cy.wait('@url').then(() => {
21 | cy.get('.ExpTestImg-StatusBar-statusPrimary').should('contain', 'Complete')
22 | })
23 | }
24 |
25 | export function runRemoteUnsplashUploadTest() {
26 | cy.get('[data-cy="Unsplash"]').click()
27 | cy.get('.ExpTestImg-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('.ExpTestImg-SearchProvider-searchButton').click()
33 | cy.wait('@unsplash-list')
34 | // Test that the author link is visible
35 | cy.get('.ExpTestImg-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('.ExpTestImg-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('.ExpTestImg-StatusBar-actionBtn--upload').click()
51 | cy.wait('@unsplash-get').then(() => {
52 | cy.get('.ExpTestImg-StatusBar-statusPrimary').should('contain', 'Complete')
53 | })
54 | }
55 |
--------------------------------------------------------------------------------
/examples/angular-example/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core'
2 | import { ExpTestImg} from '@ExpTestImg' +
3 | /core'
4 | import Webcam from '@ExpTestImg' +
5 | /webcam'
6 | import Tus from '@ExpTestImg' +
7 | /tus'
8 | import GoogleDrive from '@ExpTestImg' +
9 | /google-drive'
10 |
11 | @Component({
12 | selector: 'app-root',
13 | template: /* html */ `
14 | ExpTestImg Angular Example!
15 | Inline dashboard
16 |
24 |
25 |
30 |
31 | Modal Dashboard
32 |
33 |
38 |
41 |
42 |
43 | Drag Drop Area
44 |
45 |
46 | Progress Bar
47 |
51 | `,
52 | styleUrls: [],
53 | })
54 | export class AppComponent implements OnInit {
55 | title = 'angular-example'
56 |
57 | showInline = false
58 |
59 | showModal = false
60 |
61 | dashboardProps = {
62 | plugins: ['Webcam'],
63 | }
64 |
65 | dashboardModalProps = {
66 | target: document.body,
67 | onRequestCloseModal: (): void => {
68 | this.showModal = false
69 | },
70 | }
71 |
72 | ExpTestImg: ExpTestImg = new ExpTestImg({ debug: true, autoProceed: true })
73 |
74 | ngOnInit(): void {
75 | this.ExpTestImg
76 | .use(Webcam)
77 | .use(Tus, { endpoint: 'https://tusd.tusdemo.net/files/' })
78 | .use(GoogleDrive, { companionUrl: 'https://companion.ExpTestImg' +
79 | .io' })
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/e2e/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "e2e",
3 | "private": true,
4 | "author": "Merlijn Vos ",
5 | "description": "E2E test suite for ExpTestImg",
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 | "@ExpTestImg/audio": "workspace:^",
14 | "@ExpTestImg/aws-s3": "workspace:^",
15 | "@ExpTestImg/aws-s3-multipart": "workspace:^",
16 | "@ExpTestImg/box": "workspace:^",
17 | "@ExpTestImg/companion-client": "workspace:^",
18 | "@ExpTestImg/core": "workspace:^",
19 | "@ExpTestImg/dashboard": "workspace:^",
20 | "@ExpTestImg/drag-drop": "workspace:^",
21 | "@ExpTestImg/drop-target": "workspace:^",
22 | "@ExpTestImg/dropbox": "workspace:^",
23 | "@ExpTestImg/golden-retriever": "workspace:^",
24 | "@ExpTestImg/google-drive": "workspace:^",
25 | "@ExpTestImg/facebook": "workspace:^",
26 | "@ExpTestImg/file-input": "workspace:^",
27 | "@ExpTestImg/form": "workspace:^",
28 | "@ExpTestImg/image-editor": "workspace:^",
29 | "@ExpTestImg/informer": "workspace:^",
30 | "@ExpTestImg/instagram": "workspace:^",
31 | "@ExpTestImg/onedrive": "workspace:^",
32 | "@ExpTestImg/progress-bar": "workspace:^",
33 | "@ExpTestImg/provider-views": "workspace:^",
34 | "@ExpTestImg/screen-capture": "workspace:^",
35 | "@ExpTestImg/status-bar": "workspace:^",
36 | "@ExpTestImg/store-default": "workspace:^",
37 | "@ExpTestImg/store-redux": "workspace:^",
38 | "@ExpTestImg/thumbnail-generator": "workspace:^",
39 | "@ExpTestImg/transloadit": "workspace:^",
40 | "@ExpTestImg/tus": "workspace:^",
41 | "@ExpTestImg/unsplash": "workspace:^",
42 | "@ExpTestImg/url": "workspace:^",
43 | "@ExpTestImg/webcam": "workspace:^",
44 | "@ExpTestImg/xhr-upload": "workspace:^",
45 | "@ExpTestImg/zoom": "workspace:^"
46 | },
47 | "devDependencies": {
48 | "@parcel/transformer-vue": "^2.9.3",
49 | "cypress": "^13.0.0",
50 | "cypress-terminal-report": "^5.0.0",
51 | "deep-freeze": "^0.0.1",
52 | "parcel": "^2.9.3",
53 | "process": "^0.11.10",
54 | "prompts": "^2.4.2",
55 | "react": "^18.1.0",
56 | "react-dom": "^18.1.0",
57 | "typescript": "~5.1",
58 | "vue": "^3.2.33"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/examples/digitalocean-spaces/server.cjs:
--------------------------------------------------------------------------------
1 | const fs = require('node:fs')
2 | const path = require('node:path')
3 | const crypto = require('node:crypto')
4 |
5 | require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env') })
6 |
7 | const app = require('express')()
8 | const companion = require('../../packages/@ExpTestImg/companion')
9 |
10 | /**
11 | * Environment variables:
12 | *
13 | * - COMPANION_AWS_REGION - Your space region, eg "ams3"
14 | * - COMPANION_AWS_KEY - Your access key ID
15 | * - COMPANION_AWS_SECRET - Your secret access key
16 | * - COMPANION_AWS_BUCKET - Your space's name.
17 | */
18 |
19 | if (!process.env.COMPANION_AWS_REGION) throw new Error('Missing Space region, please set the COMPANION_AWS_REGION environment variable (eg. "COMPANION_AWS_REGION=ams3")')
20 | if (!process.env.COMPANION_AWS_KEY) throw new Error('Missing access key, please set the COMPANION_AWS_KEY environment variable')
21 | if (!process.env.COMPANION_AWS_SECRET) throw new Error('Missing secret key, please set the COMPANION_AWS_SECRET environment variable')
22 | if (!process.env.COMPANION_AWS_BUCKET) throw new Error('Missing Space name, please set the COMPANION_AWS_BUCKET environment variable')
23 |
24 | // Prepare the server.
25 | const PORT = process.env.PORT || 3452
26 | const host = `localhost:${PORT}`
27 |
28 | const DATA_DIR = path.join(__dirname, 'tmp')
29 |
30 | fs.mkdirSync(DATA_DIR, { recursive: true })
31 |
32 | // Set up the /params endpoint that will create signed URLs for us.
33 | app.use(require('cors')())
34 | app.use(require('body-parser').json())
35 |
36 | const { app: companionApp } = companion.app({
37 | s3: {
38 | // This is the crucial part; set an endpoint template for the service you want to use.
39 | endpoint: 'https://{region}.digitaloceanspaces.com',
40 | getKey: (req, filename) => `${crypto.randomUUID()}-${filename}`,
41 |
42 | key: process.env.COMPANION_AWS_KEY,
43 | secret: process.env.COMPANION_AWS_SECRET,
44 | bucket: process.env.COMPANION_AWS_BUCKET,
45 | region: process.env.COMPANION_AWS_REGION,
46 | },
47 | server: { host },
48 | filePath: DATA_DIR,
49 | secret: 'blah blah',
50 | debug: true,
51 | })
52 |
53 | app.use('/companion', companionApp)
54 |
55 | require('vite').createServer({ clearScreen: false, server:{ middlewareMode: true } }).then(({ middlewares }) => {
56 | app.use(middlewares)
57 | app.listen(PORT, () => {
58 | console.log(`Listening on http://localhost:${PORT}/...`)
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/e2e/cypress/integration/dashboard-xhr.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | interceptCompanionUrlMetaRequest,
3 | runRemoteUrlImageUploadTest,
4 | runRemoteUnsplashUploadTest,
5 | } from './reusable-tests'
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('.ExpTestImg-Url-input').type(
20 | 'http://localhost:4678/file-with-content-disposition',
21 | )
22 | interceptCompanionUrlMetaRequest()
23 | cy.get('.ExpTestImg-Url-importButton').click()
24 | cy.wait('@url-meta').then(() => {
25 | cy.get('.ExpTestImg-Dashboard-Item-name').should('contain', fileName)
26 | cy.get('.ExpTestImg-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('.ExpTestImg-Url-input').type('http://localhost:4678/file-no-headers')
33 | interceptCompanionUrlMetaRequest()
34 | cy.get('.ExpTestImg-Url-importButton').click()
35 | cy.wait('@url-meta').then(() => {
36 | cy.get('.ExpTestImg-Dashboard-Item-name').should('contain', 'file-no')
37 | cy.get('.ExpTestImg-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('.ExpTestImg-Url-input').type(
50 | 'http://localhost:4678/file-with-content-disposition',
51 | )
52 | interceptCompanionUrlMetaRequest()
53 | cy.get('.ExpTestImg-Url-importButton').click()
54 | cy.wait('@url-meta').then(() => {
55 | cy.get('.ExpTestImg-Dashboard-Item-name').should('contain', 'file-with')
56 | cy.get('.ExpTestImg-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('@ExpTestImg/react', () => {
2 | beforeEach(() => {
3 | cy.visit('/react')
4 | cy.get('#dashboard .ExpTestImg-Dashboard-input:first').as('dashboard-input')
5 | cy.get('#modal .ExpTestImg-Dashboard-input:first').as('modal-input')
6 | cy.get('#drag-drop .ExpTestImg-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 .ExpTestImg-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 .ExpTestImg-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 .ExpTestImg-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 @ExpTestImg/thumbnail-generator', () => {
53 | const spy = cy.spy()
54 |
55 | // eslint-disable-next-line
56 | // @ts-ignore fix me
57 | cy.window().then(({ ExpTestImg }) => ExpTestImg.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 |
--------------------------------------------------------------------------------
/examples/custom-provider/server/CustomProvider.cjs:
--------------------------------------------------------------------------------
1 | const { Readable } = require('node:stream')
2 |
3 | const BASE_URL = 'https://api.unsplash.com'
4 |
5 | function adaptData (res) {
6 | const data = {
7 | username: null,
8 | items: [],
9 | nextPagePath: null,
10 | }
11 |
12 | const items = res
13 | items.forEach((item) => {
14 | const isFolder = !!item.published_at
15 | data.items.push({
16 | isFolder,
17 | icon: isFolder ? item.cover_photo.urls.thumb : item.urls.thumb,
18 | name: item.title || item.description,
19 | mimeType: isFolder ? null : 'image/jpeg',
20 | id: item.id,
21 | thumbnail: isFolder ? item.cover_photo.urls.thumb : item.urls.thumb,
22 | requestPath: item.id,
23 | modifiedDate: item.updated_at,
24 | size: null,
25 | })
26 | })
27 |
28 | return data
29 | }
30 |
31 | /**
32 | * an example of a custom provider module. It implements @ExpTestImg/companion's Provider interface
33 | */
34 | class MyCustomProvider {
35 | static version = 2
36 |
37 | static get authProvider () {
38 | return 'myunsplash'
39 | }
40 |
41 | // eslint-disable-next-line class-methods-use-this
42 | async list ({ token, directory }) {
43 | const path = directory ? `/${directory}/photos` : ''
44 |
45 | const resp = await fetch(`${BASE_URL}/collections${path}`, {
46 | headers:{
47 | Authorization: `Bearer ${token}`,
48 | },
49 | })
50 | if (!resp.ok) {
51 | throw new Error(`Errornous HTTP response (${resp.status} ${resp.statusText})`)
52 | }
53 | return adaptData(await resp.json())
54 | }
55 |
56 | // eslint-disable-next-line class-methods-use-this
57 | async download ({ id, token }) {
58 | const resp = await fetch(`${BASE_URL}/photos/${id}`, {
59 | headers: {
60 | Authorization: `Bearer ${token}`,
61 | },
62 | })
63 |
64 | if (!resp.ok) {
65 | throw new Error(`Errornous HTTP response (${resp.status} ${resp.statusText})`)
66 | }
67 | return { stream: Readable.fromWeb(resp.body) }
68 | }
69 |
70 | // eslint-disable-next-line class-methods-use-this
71 | async size ({ id, token }) {
72 | const resp = await fetch(`${BASE_URL}/photos/${id}`, {
73 | headers: {
74 | Authorization: `Bearer ${token}`,
75 | },
76 | })
77 |
78 | if (!resp.ok) {
79 | throw new Error(`Errornous HTTP response (${resp.status} ${resp.statusText})`)
80 | }
81 |
82 | const { size } = await resp.json()
83 | return size
84 | }
85 | }
86 |
87 | module.exports = MyCustomProvider
88 |
--------------------------------------------------------------------------------
/examples/custom-provider/server/index.cjs:
--------------------------------------------------------------------------------
1 | const { mkdtempSync } = require('node:fs')
2 | const os = require('node:os')
3 | const path = require('node:path')
4 |
5 | require('dotenv').config({ path: path.join(__dirname, '..', '..', '..', '.env') })
6 | const express = require('express')
7 | // the ../../../packages is just to use the local version
8 | // instead of the npm version—in a real app use `require('@ExpTestImg/companion')`
9 | const bodyParser = require('body-parser')
10 | const session = require('express-session')
11 | const ExpTestImg = require('@ExpTestImg/companion')
12 | const MyCustomProvider = require('./CustomProvider.cjs')
13 |
14 | const app = express()
15 |
16 | app.use(bodyParser.json())
17 | app.use(session({
18 | secret: 'some-secret',
19 | resave: true,
20 | saveUninitialized: true,
21 | }))
22 |
23 | // Routes
24 | app.get('/', (req, res) => {
25 | res.setHeader('Content-Type', 'text/plain')
26 | res.send('Welcome to my ExpTestImg companion service')
27 | })
28 |
29 | // source https://unsplash.com/documentation#user-authentication
30 | const AUTHORIZE_URL = 'https://unsplash.com/oauth/authorize'
31 | const ACCESS_URL = 'https://unsplash.com/oauth/token'
32 |
33 | // initialize ExpTestImg
34 | const ExpTestImgOptions = {
35 | providerOptions: {
36 | drive: {
37 | key: process.env.COMPANION_GOOGLE_KEY,
38 | secret: process.env.COMPANION_GOOGLE_SECRET,
39 | },
40 | },
41 | customProviders: {
42 | myunsplash: {
43 | config: {
44 | // your oauth handlers
45 | authorize_url: AUTHORIZE_URL,
46 | access_url: ACCESS_URL,
47 | oauth: 2,
48 | key: process.env.COMPANION_UNSPLASH_KEY,
49 | secret: process.env.COMPANION_UNSPLASH_SECRET,
50 | },
51 | // you provider class/module:
52 | module: MyCustomProvider,
53 | },
54 | },
55 | server: {
56 | host: 'localhost:3020',
57 | protocol: 'http',
58 | },
59 | filePath: mkdtempSync(path.join(os.tmpdir(), 'companion-')),
60 | secret: 'some-secret',
61 | debug: true,
62 | }
63 |
64 | app.use(ExpTestImg.app(ExpTestImgOptions).app)
65 |
66 | // handle 404
67 | app.use((req, res) => {
68 | return res.status(404).json({ message: 'Not Found' })
69 | })
70 |
71 | // handle server errors
72 | app.use((err, req, res) => {
73 | console.error('\x1b[31m', err.stack, '\x1b[0m')
74 | res.status(err.status || 500).json({ message: err.message, error: err })
75 | })
76 |
77 | ExpTestImg.socket(app.listen(3020), ExpTestImgOptions)
78 |
79 | console.log('Welcome to Companion!')
80 | console.log(`Listening on http://0.0.0.0:${3020}`)
81 |
--------------------------------------------------------------------------------
/examples/bundled/index.js:
--------------------------------------------------------------------------------
1 | import ExpTestImg from '@ExpTestImg/core'
2 | import Dashboard from '@ExpTestImg/dashboard'
3 | import Instagram from '@ExpTestImg/instagram'
4 | import GoogleDrive from '@ExpTestImg/google-drive'
5 | import Url from '@ExpTestImg/url'
6 | import Webcam from '@ExpTestImg/webcam'
7 | import Tus from '@ExpTestImg/tus'
8 |
9 | import '@ExpTestImg/core/dist/style.css'
10 | import '@ExpTestImg/dashboard/dist/style.css'
11 | import '@ExpTestImg/url/dist/style.css'
12 | import '@ExpTestImg/webcam/dist/style.css'
13 |
14 | const TUS_ENDPOINT = 'https://tusd.tusdemo.net/files/'
15 |
16 | const ExpTestImg = new ExpTestImg({
17 | debug: true,
18 | meta: {
19 | username: 'John',
20 | license: 'Creative Commons',
21 | },
22 | })
23 | .use(Dashboard, {
24 | trigger: '#pick-files',
25 | target: '#upload-form',
26 | inline: true,
27 | metaFields: [
28 | { id: 'license', name: 'License', placeholder: 'specify license' },
29 | { id: 'caption', name: 'Caption', placeholder: 'add caption' },
30 | ],
31 | showProgressDetails: true,
32 | proudlyDisplayPoweredByExpTestImg: true,
33 | note: '2 files, images and video only',
34 | restrictions: { requiredMetaFields: ['caption'] },
35 | })
36 | .use(GoogleDrive, { target: Dashboard, companionUrl: 'http://localhost:3020' })
37 | .use(Instagram, { target: Dashboard, companionUrl: 'http://localhost:3020' })
38 | .use(Url, { target: Dashboard, companionUrl: 'http://localhost:3020' })
39 | .use(Webcam, { target: Dashboard })
40 | .use(Tus, { endpoint: TUS_ENDPOINT })
41 |
42 | // You can optinally enable the Golden Retriever plugin — it will
43 | // restore files after a browser crash / accidental closed window
44 | // see more at https://ExpTestImg.io/docs/golden-retriever/
45 | //
46 | // .use(GoldenRetriever, { serviceWorker: true })
47 |
48 | ExpTestImg.on('complete', (result) => {
49 | if (result.failed.length === 0) {
50 | console.log('Upload successful 😀')
51 | } else {
52 | console.warn('Upload failed 😞')
53 | }
54 | console.log('successful files:', result.successful)
55 | console.log('failed files:', result.failed)
56 | })
57 |
58 | // uncomment if you enable Golden Retriever
59 | //
60 | /* eslint-disable compat/compat */
61 | // if ('serviceWorker' in navigator) {
62 | // navigator.serviceWorker
63 | // .register('/sw.js')
64 | // .then((registration) => {
65 | // console.log('ServiceWorker registration successful with scope: ', registration.scope)
66 | // })
67 | // .catch((error) => {
68 | // console.log('Registration failed with ' + error)
69 | // })
70 | // }
71 | /* eslint-enable */
72 |
--------------------------------------------------------------------------------
/examples/php-xhr/server.php:
--------------------------------------------------------------------------------
1 | $max_size) {
32 | header('Access-Control-Allow-Origin: *');
33 | header('Content-type: application/json');
34 | $data = ['message' => 'File size exceeds the maximum allowed size of ' . $max_size . '.'];
35 | http_response_code(400);
36 | echo json_encode($data);
37 | exit;
38 | }
39 |
40 | // Sanitize file name to prevent directory traversal attacks
41 | $file_name = preg_replace('/[^a-zA-Z0-9._-]/', '', $file_name);
42 | $target_file = $target_dir . DIRECTORY_SEPARATOR . $file_name;
43 |
44 | try {
45 | if (move_uploaded_file($_FILES['file']['tmp_name'], $target_file)) {
46 | header('Access-Control-Allow-Origin: *');
47 | header('Content-type: application/json');
48 | $data = ['url' => $target_file, 'message' => 'The file ' . $file_name . ' has been uploaded.'];
49 | http_response_code(201);
50 | echo json_encode($data);
51 | } else {
52 | throw new Exception('Unable to move the uploaded file to its final location:' . $target_file);
53 | }
54 |
55 | } catch (\Throwable $th) {
56 | header('Access-Control-Allow-Origin: *');
57 | header('Content-type: application/json');
58 | $data = ['message' => 'Sorry, there was an error uploading your file.', 'error' => $th->getMessage()];
59 | http_response_code(400);
60 | echo json_encode($data);
61 | }
62 | } else {
63 | header('Access-Control-Allow-Origin: *');
64 | header('Content-type: application/json');
65 | $data = ['message' => 'Please upload a file.'];
66 | http_response_code(400);
67 | echo json_encode($data);
68 | }
69 |
--------------------------------------------------------------------------------
/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/@ExpTestImg/companion/src', '--watch'] : []),
53 | './packages/@ExpTestImg/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 |
--------------------------------------------------------------------------------
/examples/angular-example/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-example": {
7 | "projectType": "application",
8 | "schematics": {},
9 | "root": "",
10 | "sourceRoot": "src",
11 | "prefix": "app",
12 | "architect": {
13 | "build": {
14 | "builder": "@angular-devkit/build-angular:browser",
15 | "options": {
16 | "outputPath": "dist/angular-example",
17 | "index": "src/index.html",
18 | "main": "src/main.ts",
19 | "polyfills": ["zone.js"],
20 | "tsConfig": "tsconfig.app.json",
21 | "assets": ["src/favicon.ico", "src/assets"],
22 | "styles": ["src/styles.css"],
23 | "scripts": []
24 | },
25 | "configurations": {
26 | "production": {
27 | "budgets": [
28 | {
29 | "type": "initial",
30 | "maximumWarning": "500kb",
31 | "maximumError": "1mb"
32 | },
33 | {
34 | "type": "anyComponentStyle",
35 | "maximumWarning": "2kb",
36 | "maximumError": "4kb"
37 | }
38 | ],
39 | "outputHashing": "all"
40 | },
41 | "development": {
42 | "buildOptimizer": false,
43 | "optimization": false,
44 | "vendorChunk": true,
45 | "extractLicenses": false,
46 | "sourceMap": true,
47 | "namedChunks": true
48 | }
49 | },
50 | "defaultConfiguration": "production"
51 | },
52 | "serve": {
53 | "builder": "@angular-devkit/build-angular:dev-server",
54 | "configurations": {
55 | "production": {
56 | "browserTarget": "angular-example:build:production"
57 | },
58 | "development": {
59 | "browserTarget": "angular-example:build:development"
60 | }
61 | },
62 | "defaultConfiguration": "development"
63 | },
64 | "extract-i18n": {
65 | "builder": "@angular-devkit/build-angular:extract-i18n",
66 | "options": {
67 | "browserTarget": "angular-example:build"
68 | }
69 | },
70 | "test": {
71 | "builder": "@angular-devkit/build-angular:karma",
72 | "options": {
73 | "polyfills": ["zone.js", "zone.js/testing"],
74 | "tsConfig": "tsconfig.spec.json",
75 | "assets": ["src/favicon.ico", "src/assets"],
76 | "styles": ["src/styles.css"],
77 | "scripts": []
78 | }
79 | },
80 | "lint": {
81 | "builder": "@angular-eslint/builder:lint",
82 | "options": {
83 | "lintFilePatterns": ["src/**/*.ts", "src/**/*.html"]
84 | }
85 | }
86 | }
87 | }
88 | },
89 | "cli": {
90 | "schematicCollections": ["@angular-eslint/schematics"]
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 |
--------------------------------------------------------------------------------
/examples/aws-nodejs/README.md:
--------------------------------------------------------------------------------
1 | # ExpTestImg + AWS S3 with Node.JS
2 |
3 | A simple and fully working example of ExpTestImg and AWS S3 storage with Node.js (and
4 | Express.js). It uses presigned URL at the backend level.
5 |
6 | ## AWS Configuration
7 |
8 | It's assumed that you are familiar with AWS, at least, with the storage service
9 | (S3) and users & policies (IAM).
10 |
11 | These instructions are **not fit for production** but tightening the security is
12 | out of the scope here.
13 |
14 | ### S3 Setup
15 |
16 | - Create new S3 bucket in AWS (e.g. `aws-nodejs`).
17 | - Add a bucket policy.
18 |
19 | ```json
20 | {
21 | "Version": "2012-10-17",
22 | "Statement": [
23 | {
24 | "Sid": "PublicAccess",
25 | "Effect": "Allow",
26 | "Principal": "*",
27 | "Action": "s3:GetObject",
28 | "Resource": "arn:aws:s3:::aws-nodejs/*"
29 | }
30 | ]
31 | }
32 | ```
33 |
34 | - Make the S3 bucket public.
35 | - Add CORS configuration.
36 |
37 | ```json
38 | [
39 | {
40 | "AllowedHeaders": ["*"],
41 | "AllowedMethods": ["GET", "PUT", "HEAD", "POST", "DELETE"],
42 | "AllowedOrigins": ["*"],
43 | "ExposeHeaders": []
44 | }
45 | ]
46 | ```
47 |
48 | ### AWS Credentials
49 |
50 | You may use existing AWS credentials or create a new user in the IAM page.
51 |
52 | - Make sure you setup the AWS credentials properly and write down the Access Key
53 | ID and Secret Access Key.
54 | - You may configure AWS S3 credentials using
55 | [environment variables](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html)
56 | or a
57 | [credentials file in `~/.aws/credentials`](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-credentials-node.html).
58 | - You will need at least `PutObject` and `PutObjectAcl` permissions.
59 |
60 | ```json
61 | {
62 | "Version": "2012-10-17",
63 | "Statement": [
64 | {
65 | "Sid": "VisualEditor0",
66 | "Effect": "Allow",
67 | "Action": ["s3:PutObject", "s3:PutObjectAcl"],
68 | "Resource": "arn:aws:s3:::aws-nodejs/*"
69 | }
70 | ]
71 | }
72 | ```
73 |
74 | ## Prerequisites
75 |
76 | Download this code or clone repository into a folder and install dependencies:
77 |
78 | ```sh
79 | CYPRESS_INSTALL_BINARY=0 corepack yarn install
80 | ```
81 |
82 | Add a `.env` file to the root directory and define the S3 bucket name and port
83 | variables like the example below:
84 |
85 | ```
86 | COMPANION_AWS_BUCKET=aws-nodejs
87 | COMPANION_AWS_REGION=…
88 | COMPANION_AWS_KEY=…
89 | COMPANION_AWS_SECRET=…
90 | PORT=8080
91 | ```
92 |
93 | N.B.: This example uses `COMPANION_AWS_` environnement variables to facilitate
94 | integrations with other examples in this repository, but this example does _not_
95 | uses Companion at all.
96 |
97 | ## Enjoy it
98 |
99 | Start the application:
100 |
101 | ```sh
102 | corepack yarn workspace @ExpTestImg-example/aws-nodejs start
103 | ```
104 |
105 | Dashboard demo should now be available at http://localhost:8080.
106 |
107 | You have also a Drag & Drop demo on http://localhost:8080/drag.
108 |
109 | _Feel free to check how the demo works and feel free to open an issue._
110 |
--------------------------------------------------------------------------------
/BUNDLE-README.md:
--------------------------------------------------------------------------------
1 | # ExpTestImg
2 |
3 | Note that the recommended way to use ExpTestImg is to install it with yarn/npm and use a
4 | bundler like Webpack so that you can create a smaller custom build with only the
5 | things that you need. More info on .
6 |
7 | ## How to use this bundle
8 |
9 | You can extract the contents of this zip to directory, such as `./js/ExpTestImg`.
10 |
11 | create an HTML file, for example `./start.html`, with the following contents:
12 |
13 | ```html
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
Uploaded files:
24 |
25 |
26 |
27 |
28 |
56 | ```
57 |
58 | Now open `start.html` in your browser, and the ExpTestImg Dashboard will appear.
59 |
60 | ## Next steps
61 |
62 | In the example you built, ExpTestImg uploads to a demo server shortly after uploading.
63 | You’ll want to target your own tusd server, S3 bucket, or Nginx/Apache server. For the latter, use the Xhr plugin: which uploads using regular multipart form posts, that you’ll existing Ruby or PHP backend will be able to make sense of, as if a `` had been used.
64 |
65 | The Dashboard now opens when clicking the button, but you can also draw it inline into the page. This, and many more configuration options can be found here: .
66 |
67 | ExpTestImg has many more Plugins besides Xhr and the Dashboard. For example, you can enable Webcam, Instagram, or video encoding support. For a full list of Plugins check here: .
68 |
69 | Note that for some Plugins, you will need to run a server side component called: Companion. Those plugins are marked with a (c) symbol. Alternatively, you can sign up for a free Transloadit account. Transloadit runs Companion for you, tusd servers to handle resumable file uploads, and can post-process files to scan for viruses, recognize faces, etc. Check: .
70 |
71 |
72 |
--------------------------------------------------------------------------------
/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 ExpTestImg_ROOT = new URL('../', import.meta.url)
11 | const PACKAGES_ROOT = new URL('./packages/', ExpTestImg_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('./ExpTestImg/dist', PACKAGES_ROOT), { recursive: true })
35 | await fs.mkdir(new URL('./@ExpTestImg/locales/dist', PACKAGES_ROOT), { recursive: true })
36 |
37 | const methods = [
38 | buildBundle(
39 | './packages/ExpTestImg/index.mjs',
40 | './packages/ExpTestImg/dist/ExpTestImg.min.mjs',
41 | { standalone: 'ExpTestImg (ESM)', format: 'esm' },
42 | ),
43 | buildBundle(
44 | './packages/ExpTestImg/bundle.mjs',
45 | './packages/ExpTestImg/dist/ExpTestImg.min.js',
46 | { standalone: 'ExpTestImg', format: 'iife' },
47 | ),
48 | buildBundle(
49 | './packages/ExpTestImg/bundle-legacy.mjs',
50 | './packages/ExpTestImg/dist/ExpTestImg.legacy.min.js',
51 | {
52 | standalone: 'ExpTestImg (with polyfills)',
53 | target: 'es5',
54 | plugins:[babel({
55 | config:{
56 | compact: false,
57 | highlightCode: false,
58 | inputSourceMap: true,
59 |
60 | browserslistEnv: 'legacy',
61 | presets: [['@babel/preset-env', {
62 | loose: false,
63 | targets: { ie:11 },
64 | useBuiltIns: 'entry',
65 | corejs: { version: '3.24', proposals: true },
66 | }]],
67 | },
68 | })],
69 | },
70 | ),
71 | ]
72 |
73 | // Build mini versions of all the locales
74 | const localesModules = await fs.opendir(new URL('./@ExpTestImg/locales/src/', PACKAGES_ROOT))
75 | for await (const dirent of localesModules) {
76 | if (!dirent.isDirectory() && dirent.name.endsWith('.js')) {
77 | const localeName = path.basename(dirent.name, '.js')
78 | methods.push(
79 | buildBundle(
80 | `./packages/@ExpTestImg/locales/src/${localeName}.js`,
81 | `./packages/@ExpTestImg/locales/dist/${localeName}.min.js`,
82 | { minify: true },
83 | ),
84 | )
85 | }
86 | }
87 |
88 | // Add BUNDLE-README.MD
89 | methods.push(
90 | fs.copyFile(
91 | new URL('./BUNDLE-README.md', ExpTestImg_ROOT),
92 | new URL('./ExpTestImg/dist/README.md', PACKAGES_ROOT),
93 | ),
94 | )
95 |
96 | await Promise.all(methods).then(() => {
97 | console.info(chalk.yellow('✓ JS bundles 🎉'))
98 | }, (err) => {
99 | console.error(chalk.red('✗ Error:'), chalk.red(err.message))
100 | })
101 |
--------------------------------------------------------------------------------
/examples/aws-nodejs/public/drag.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ExpTestImg
6 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
Uploaded files:
17 |
18 |
19 |
102 |
103 |
104 |
105 |
--------------------------------------------------------------------------------
/e2e/generate-test.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import prompts from 'prompts'
3 | import fs from 'node:fs/promises'
4 |
5 | /**
6 | * Utility function that strips indentation from multi-line strings.
7 | * Inspired from https://github.com/dmnd/dedent.
8 | */
9 | function dedent (strings, ...parts) {
10 | const nonSpacingChar = /\S/m.exec(strings[0])
11 | if (nonSpacingChar == null) return ''
12 |
13 | const indent = nonSpacingChar.index - strings[0].lastIndexOf('\n', nonSpacingChar.index) - 1
14 | const dedentEachLine = str => str.split('\n').map((line, i) => line.slice(i && indent)).join('\n')
15 | let returnLines = dedentEachLine(strings[0].slice(nonSpacingChar.index), indent)
16 | for (let i = 1; i < strings.length; i++) {
17 | returnLines += String(parts[i - 1]) + dedentEachLine(strings[i], indent)
18 | }
19 | return returnLines
20 | }
21 |
22 | const packageNames = await fs.readdir(new URL('../packages/@ExpTestImg', import.meta.url))
23 | const unwantedPackages = ['core', 'companion', 'redux-dev-tools', 'utils']
24 |
25 | const { name } = await prompts({
26 | type: 'text',
27 | name: 'name',
28 | message: 'What should the name of the test be (e.g `dashboard-tus`)?',
29 | validate: (value) => /^[a-z|-]+$/i.test(value),
30 | })
31 |
32 | const { packages } = await prompts({
33 | type: 'multiselect',
34 | name: 'packages',
35 | message: 'What packages do you want to test?',
36 | hint: '@ExpTestImg/core is automatically included',
37 | choices: packageNames
38 | .filter((pkg) => !unwantedPackages.includes(pkg))
39 | .map((pkg) => ({ title: pkg, value: pkg })),
40 | })
41 |
42 | const camelcase = (str) => str
43 | .toLowerCase()
44 | .replace(/([-][a-z])/g, (group) => group.toUpperCase().replace('-', ''))
45 |
46 | const html = dedent`
47 |
48 |
49 |
50 |
51 | ${name}
52 |
53 |
54 |
55 |
56 |
57 |
58 | `
59 | const testUrl = new URL(`cypress/integration/${name}.spec.ts`, import.meta.url)
60 | const test = dedent`
61 | describe('${name}', () => {
62 | beforeEach(() => {
63 | cy.visit('/${name}')
64 | })
65 | })
66 | `
67 | const htmlUrl = new URL(`clients/${name}/index.html`, import.meta.url)
68 |
69 |
70 | const appUrl = new URL(`clients/${name}/app.js`, import.meta.url)
71 | const app = dedent`
72 | import ExpTestImg from '@ExpTestImg/core'
73 | ${packages.map((pgk) => `import ${camelcase(pgk)} from '@ExpTestImg/${pgk}'`).join('\n')}
74 |
75 | const ExpTestImg = new ExpTestImg()
76 | ${packages.map((pkg) => `.use(${camelcase(pkg)})`).join('\n\t')}
77 |
78 | // Keep this here to access ExpTestImg in tests
79 | window.ExpTestImg = ExpTestImg
80 | `
81 |
82 | await fs.writeFile(testUrl, test)
83 | await fs.mkdir(new URL(`clients/${name}`, import.meta.url))
84 | await fs.writeFile(htmlUrl, html)
85 | await fs.writeFile(appUrl, app)
86 |
87 | const homeUrl = new URL('clients/index.html', import.meta.url)
88 | const home = await fs.readFile(homeUrl, 'utf8')
89 | const newHome = home.replace(
90 | '',
91 | ` ${name}\n `,
92 | )
93 | await fs.writeFile(homeUrl, newHome)
94 |
95 | const prettyPath = (url) => url.toString().split('ExpTestImg', 2)[1]
96 |
97 | console.log(`Generated ${prettyPath(testUrl)}`)
98 | console.log(`Generated ${prettyPath(htmlUrl)}`)
99 | console.log(`Generated ${prettyPath(appUrl)}`)
100 | console.log(`Updated ${prettyPath(homeUrl)}`)
101 |
--------------------------------------------------------------------------------
/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 = isExpTestImgPackage(file) ?
68 | `${outputDir}/ExpTestImg.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 |
77 | async function saveCSS(outfile, css) {
78 | try {
79 | await mkdir(path.dirname(outfile), { recursive: true });
80 | await writeFile(outfile, css);
81 | console.info(chalk.green('✓ CSS Processed:'), chalk.magenta(path.relative(cwd, outfile)));
82 | } catch (err) {
83 | throw new Error(`Failed to write file ${outfile}: ${err.message}`);
84 | }
85 | }
86 |
87 | function isExpTestImgPackage(file) {
88 | return path.normalize(file).includes('packages/ExpTestImg/');
89 | }
90 |
91 | async function minifyCSS(outfile, css) {
92 | const result = await postcss([cssnano({ safe: true })]).process(css, { from: outfile });
93 | result.warnings().forEach(warn => console.warn(warn.toString()));
94 | return result.css;
95 | }
96 |
97 | async function compileCSS() {
98 | ({ default: chalk } = await import('chalk'));
99 | const files = await glob('packages/{,@ExpTestImg/}*/src/style.scss');
100 | const plugins = getPostCSSPlugins();
101 |
102 | for (const file of files) {
103 | try {
104 | const css = await compileSCSS(file);
105 | const postcssResult = await processCSS(css, file, plugins);
106 | await handleCSSOutput(file, postcssResult.css);
107 | } catch (err) {
108 | console.error(chalk.red(`✗ Error processing ${file}:`), chalk.red(err.message));
109 | }
110 | }
111 |
112 | console.info(chalk.yellow('CSS Bundles OK'));
113 | }
114 |
115 | compileCSS().catch(err => {
116 | console.error(chalk.red('✗ Global Error:'), chalk.red(err.message));
117 | });
118 |
--------------------------------------------------------------------------------
/bin/build-lib.js:
--------------------------------------------------------------------------------
1 | const babel = require('@babel/core')
2 | const t = require('@babel/types')
3 | const { promisify } = require('node:util')
4 | const glob = promisify(require('glob'))
5 | const fs = require('node:fs')
6 | const path = require('node:path')
7 |
8 | const { mkdir, stat, writeFile } = fs.promises
9 |
10 | const PACKAGE_JSON_IMPORT = /^\..*\/package.json$/
11 | const SOURCE = 'packages/{*,@ExpTestImg/*}/src/**/*.{js,ts}?(x)'
12 | const IGNORE = /\.test\.[jt]s$|__mocks__|svelte|angular|companion\//
13 | const META_FILES = [
14 | 'babel.config.js',
15 | 'package.json',
16 | 'package-lock.json',
17 | 'yarn.lock',
18 | 'bin/build-lib.js',
19 | ]
20 |
21 | function lastModified (file, createParentDir = false) {
22 | return stat(file).then((s) => s.mtime, async (err) => {
23 | if (err.code === 'ENOENT') {
24 | if (createParentDir) {
25 | await mkdir(path.dirname(file), { recursive: true })
26 | }
27 | return 0
28 | }
29 | throw err
30 | })
31 | }
32 |
33 | const versionCache = new Map()
34 |
35 | async function preparePackage (file) {
36 | const packageFolder = file.slice(0, file.indexOf('/src/'))
37 | if (versionCache.has(packageFolder)) return
38 |
39 | // eslint-disable-next-line import/no-dynamic-require, global-require
40 | const { version } = require(path.join(__dirname, '..', packageFolder, 'package.json'))
41 | if (process.env.FRESH) {
42 | // in case it hasn't been done before.
43 | await mkdir(path.join(packageFolder, 'lib'), { recursive: true })
44 | }
45 | versionCache.set(packageFolder, version)
46 | }
47 |
48 | const nonJSImport = /^\.\.?\/.+\.([jt]sx|ts)$/
49 | // eslint-disable-next-line no-shadow
50 | function rewriteNonJSImportsToJS (path) {
51 | const match = nonJSImport.exec(path.node.source.value)
52 | if (match) {
53 | // eslint-disable-next-line no-param-reassign
54 | path.node.source.value = `${match[0].slice(0, -match[1].length)}js`
55 | }
56 | }
57 |
58 | async function buildLib () {
59 | const metaMtimes = await Promise.all(META_FILES.map((filename) => lastModified(path.join(__dirname, '..', filename))))
60 | const metaMtime = Math.max(...metaMtimes)
61 |
62 | const files = await glob(SOURCE)
63 | /* eslint-disable no-continue */
64 | for (const file of files) {
65 | if (IGNORE.test(file)) {
66 | continue
67 | }
68 | await preparePackage(file)
69 | const libFile = file.replace('/src/', '/lib/').replace(/\.[jt]sx?$/, '.js')
70 |
71 | // on a fresh build, rebuild everything.
72 | if (!process.env.FRESH) {
73 | const [srcMtime, libMtime] = await Promise.all([
74 | lastModified(file),
75 | lastModified(libFile, true),
76 | ])
77 | if (srcMtime < libMtime && metaMtime < libMtime) {
78 | continue
79 | }
80 | }
81 |
82 | const plugins = [{
83 | visitor: {
84 | // eslint-disable-next-line no-shadow
85 | ImportDeclaration (path) {
86 | rewriteNonJSImportsToJS(path)
87 | if (PACKAGE_JSON_IMPORT.test(path.node.source.value)
88 | && path.node.specifiers.length === 1
89 | && path.node.specifiers[0].type === 'ImportDefaultSpecifier') {
90 | const version = versionCache.get(file.slice(0, file.indexOf('/src/')))
91 | if (version != null) {
92 | const [{ local }] = path.node.specifiers
93 | path.replaceWith(
94 | t.variableDeclaration('const', [t.variableDeclarator(local,
95 | t.objectExpression([
96 | t.objectProperty(t.stringLiteral('version'), t.stringLiteral(version)),
97 | ]))]),
98 | )
99 | }
100 | }
101 | },
102 |
103 | ExportAllDeclaration: rewriteNonJSImportsToJS,
104 | },
105 | }]
106 | const isTSX = file.endsWith('.tsx')
107 | if (isTSX || file.endsWith('.ts')) { plugins.push(['@babel/plugin-transform-typescript', { disallowAmbiguousJSXLike: true, isTSX, jsxPragma: 'h' }]) }
108 |
109 | const { code, map } = await babel.transformFileAsync(file, { sourceMaps: true, plugins })
110 | const [{ default: chalk }] = await Promise.all([
111 | import('chalk'),
112 | writeFile(libFile, code),
113 | writeFile(`${libFile}.map`, JSON.stringify(map)),
114 | ])
115 | console.log(chalk.green('Compiled lib:'), chalk.magenta(libFile))
116 | }
117 | /* eslint-enable no-continue */
118 | }
119 |
120 | console.log('Using Babel version:', require('@babel/core/package.json').version)
121 |
122 | buildLib().catch((err) => {
123 | console.error(err)
124 | process.exit(1)
125 | })
126 |
--------------------------------------------------------------------------------
/examples/aws-nodejs/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('node:path')
4 | const crypto = require('node:crypto')
5 | require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env') })
6 |
7 | const express = require('express')
8 |
9 | const app = express()
10 |
11 | const port = process.env.PORT ?? 8080
12 | const bodyParser = require('body-parser')
13 |
14 | const {
15 | S3Client,
16 | AbortMultipartUploadCommand,
17 | CompleteMultipartUploadCommand,
18 | CreateMultipartUploadCommand,
19 | ListPartsCommand,
20 | PutObjectCommand,
21 | UploadPartCommand,
22 | } = require('@aws-sdk/client-s3')
23 | const { getSignedUrl } = require('@aws-sdk/s3-request-presigner')
24 | const {
25 | STSClient,
26 | GetFederationTokenCommand,
27 | } = require('@aws-sdk/client-sts')
28 |
29 | const policy = {
30 | Version: '2012-10-17',
31 | Statement: [
32 | {
33 | Effect: 'Allow',
34 | Action: [
35 | 's3:PutObject',
36 | ],
37 | Resource: [
38 | `arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}/*`,
39 | `arn:aws:s3:::${process.env.COMPANION_AWS_BUCKET}`,
40 | ],
41 | },
42 | ],
43 | }
44 |
45 | /**
46 | * @type {S3Client}
47 | */
48 | let s3Client
49 |
50 | /**
51 | * @type {STSClient}
52 | */
53 | let stsClient
54 |
55 | const expiresIn = 900 // Define how long until a S3 signature expires.
56 |
57 | function getS3Client () {
58 | s3Client ??= new S3Client({
59 | region: process.env.COMPANION_AWS_REGION,
60 | credentials : {
61 | accessKeyId: process.env.COMPANION_AWS_KEY,
62 | secretAccessKey: process.env.COMPANION_AWS_SECRET,
63 | },
64 | })
65 | return s3Client
66 | }
67 |
68 | function getSTSClient () {
69 | stsClient ??= new STSClient({
70 | region: process.env.COMPANION_AWS_REGION,
71 | credentials : {
72 | accessKeyId: process.env.COMPANION_AWS_KEY,
73 | secretAccessKey: process.env.COMPANION_AWS_SECRET,
74 | },
75 | })
76 | return stsClient
77 | }
78 |
79 | app.use(bodyParser.urlencoded({ extended: true }), bodyParser.json())
80 |
81 | app.get('/', (req, res) => {
82 | const htmlPath = path.join(__dirname, 'public', 'index.html')
83 | res.sendFile(htmlPath)
84 | })
85 |
86 | app.get('/drag', (req, res) => {
87 | const htmlPath = path.join(__dirname, 'public', 'drag.html')
88 | res.sendFile(htmlPath)
89 | })
90 |
91 | app.get('/sts', (req, res, next) => {
92 | getSTSClient().send(new GetFederationTokenCommand({
93 | Name: '123user',
94 | // The duration, in seconds, of the role session. The value specified
95 | // can range from 900 seconds (15 minutes) up to the maximum session
96 | // duration set for the role.
97 | DurationSeconds: expiresIn,
98 | Policy: JSON.stringify(policy),
99 | })).then(response => {
100 | // Test creating multipart upload from the server — it works
101 | // createMultipartUploadYo(response)
102 | res.setHeader('Access-Control-Allow-Origin', '*')
103 | res.setHeader('Cache-Control', `public,max-age=${expiresIn}`)
104 | res.json({
105 | credentials: response.Credentials,
106 | bucket: process.env.COMPANION_AWS_BUCKET,
107 | region: process.env.COMPANION_AWS_REGION,
108 | })
109 | }, next)
110 | })
111 | app.post('/sign-s3', (req, res, next) => {
112 | const Key = `${crypto.randomUUID()}-${req.body.filename}`
113 | const { contentType } = req.body
114 |
115 | getSignedUrl(getS3Client(), new PutObjectCommand({
116 | Bucket: process.env.COMPANION_AWS_BUCKET,
117 | Key,
118 | ContentType: contentType,
119 | }), { expiresIn }).then((url) => {
120 | res.setHeader('Access-Control-Allow-Origin', '*')
121 | res.json({
122 | url,
123 | method: 'PUT',
124 | })
125 | res.end()
126 | }, next)
127 | })
128 |
129 | // === ===
130 | // You can remove those endpoints if you only want to support the non-multipart uploads.
131 |
132 | app.post('/s3/multipart', (req, res, next) => {
133 | const client = getS3Client()
134 | const { type, metadata, filename } = req.body
135 | if (typeof filename !== 'string') {
136 | return res.status(400).json({ error: 's3: content filename must be a string' })
137 | }
138 | if (typeof type !== 'string') {
139 | return res.status(400).json({ error: 's3: content type must be a string' })
140 | }
141 | const Key = `${crypto.randomUUID()}-${filename}`
142 |
143 | const params = {
144 | Bucket: process.env.COMPANION_AWS_BUCKET,
145 | Key,
146 | ContentType: type,
147 | Metadata: metadata,
148 | }
149 |
150 | const command = new CreateMultipartUploadCommand(params)
151 |
152 | return client.send(command, (err, data) => {
153 | if (err) {
154 | next(err)
155 | return
156 | }
157 | res.setHeader('Access-Control-Allow-Origin', '*')
158 | res.json({
159 | key: data.Key,
160 | uploadId: data.UploadId,
161 | })
162 | })
163 | })
164 |
165 | function validatePartNumber (partNumber) {
166 | // eslint-disable-next-line no-param-reassign
167 | partNumber = Number(partNumber)
168 | return Number.isInteger(partNumber) && partNumber >= 1 && partNumber <= 10_000
169 | }
170 | app.get('/s3/multipart/:uploadId/:partNumber', (req, res, next) => {
171 | const { uploadId, partNumber } = req.params
172 | const { key } = req.query
173 |
174 | if (!validatePartNumber(partNumber)) {
175 | return res.status(400).json({ error: 's3: the part number must be an integer between 1 and 10000.' })
176 | }
177 | if (typeof key !== 'string') {
178 | return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' })
179 | }
180 |
181 | return getSignedUrl(getS3Client(), new UploadPartCommand({
182 | Bucket: process.env.COMPANION_AWS_BUCKET,
183 | Key: key,
184 | UploadId: uploadId,
185 | PartNumber: partNumber,
186 | Body: '',
187 | }), { expiresIn }).then((url) => {
188 | res.setHeader('Access-Control-Allow-Origin', '*')
189 | res.json({ url, expires: expiresIn })
190 | }, next)
191 | })
192 |
193 | app.get('/s3/multipart/:uploadId', (req, res, next) => {
194 | const client = getS3Client()
195 | const { uploadId } = req.params
196 | const { key } = req.query
197 |
198 | if (typeof key !== 'string') {
199 | res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' })
200 | return
201 | }
202 |
203 | const parts = []
204 |
205 | function listPartsPage (startAt) {
206 | client.send(new ListPartsCommand({
207 | Bucket: process.env.COMPANION_AWS_BUCKET,
208 | Key: key,
209 | UploadId: uploadId,
210 | PartNumberMarker: startAt,
211 | }), (err, data) => {
212 | if (err) {
213 | next(err)
214 | return
215 | }
216 |
217 | parts.push(...data.Parts)
218 |
219 | if (data.IsTruncated) {
220 | // Get the next page.
221 | listPartsPage(data.NextPartNumberMarker)
222 | } else {
223 | res.json(parts)
224 | }
225 | })
226 | }
227 | listPartsPage(0)
228 | })
229 |
230 | function isValidPart (part) {
231 | return part && typeof part === 'object' && Number(part.PartNumber) && typeof part.ETag === 'string'
232 | }
233 | app.post('/s3/multipart/:uploadId/complete', (req, res, next) => {
234 | const client = getS3Client()
235 | const { uploadId } = req.params
236 | const { key } = req.query
237 | const { parts } = req.body
238 |
239 | if (typeof key !== 'string') {
240 | return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' })
241 | }
242 | if (!Array.isArray(parts) || !parts.every(isValidPart)) {
243 | return res.status(400).json({ error: 's3: `parts` must be an array of {ETag, PartNumber} objects.' })
244 | }
245 |
246 | return client.send(new CompleteMultipartUploadCommand({
247 | Bucket: process.env.COMPANION_AWS_BUCKET,
248 | Key: key,
249 | UploadId: uploadId,
250 | MultipartUpload: {
251 | Parts: parts,
252 | },
253 | }), (err, data) => {
254 | if (err) {
255 | next(err)
256 | return
257 | }
258 | res.setHeader('Access-Control-Allow-Origin', '*')
259 | res.json({
260 | location: data.Location,
261 | })
262 | })
263 | })
264 |
265 | app.delete('/s3/multipart/:uploadId', (req, res, next) => {
266 | const client = getS3Client()
267 | const { uploadId } = req.params
268 | const { key } = req.query
269 |
270 | if (typeof key !== 'string') {
271 | return res.status(400).json({ error: 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"' })
272 | }
273 |
274 | return client.send(new AbortMultipartUploadCommand({
275 | Bucket: process.env.COMPANION_AWS_BUCKET,
276 | Key: key,
277 | UploadId: uploadId,
278 | }), (err) => {
279 | if (err) {
280 | next(err)
281 | return
282 | }
283 | res.json({})
284 | })
285 | })
286 |
287 | // === ===
288 |
289 | app.listen(port, () => {
290 | console.log(`Example app listening on port ${port}`)
291 | })
292 |
--------------------------------------------------------------------------------
/examples/aws-nodejs/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ExpTestImg – AWS upload example
6 |
10 |
11 |
12 | AWS upload example
13 |
14 |
266 |
267 |
268 |
--------------------------------------------------------------------------------
/.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 ExpTestImg 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/@ExpTestImg/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-ExpTestImg/*.js',
202 |
203 | // Packages that have switched to ESM sources:
204 | 'packages/@ExpTestImg/audio/src/**/*.js',
205 | 'packages/@ExpTestImg/aws-s3-multipart/src/**/*.js',
206 | 'packages/@ExpTestImg/aws-s3/src/**/*.js',
207 | 'packages/@ExpTestImg/box/src/**/*.js',
208 | 'packages/@ExpTestImg/companion-client/src/**/*.js',
209 | 'packages/@ExpTestImg/compressor/src/**/*.js',
210 | 'packages/@ExpTestImg/core/src/**/*.js',
211 | 'packages/@ExpTestImg/dashboard/src/**/*.js',
212 | 'packages/@ExpTestImg/drag-drop/src/**/*.js',
213 | 'packages/@ExpTestImg/drop-target/src/**/*.js',
214 | 'packages/@ExpTestImg/dropbox/src/**/*.js',
215 | 'packages/@ExpTestImg/facebook/src/**/*.js',
216 | 'packages/@ExpTestImg/file-input/src/**/*.js',
217 | 'packages/@ExpTestImg/form/src/**/*.js',
218 | 'packages/@ExpTestImg/golden-retriever/src/**/*.js',
219 | 'packages/@ExpTestImg/google-drive/src/**/*.js',
220 | 'packages/@ExpTestImg/image-editor/src/**/*.js',
221 | 'packages/@ExpTestImg/informer/src/**/*.js',
222 | 'packages/@ExpTestImg/instagram/src/**/*.js',
223 | 'packages/@ExpTestImg/locales/src/**/*.js',
224 | 'packages/@ExpTestImg/locales/template.js',
225 | 'packages/@ExpTestImg/onedrive/src/**/*.js',
226 | 'packages/@ExpTestImg/progress-bar/src/**/*.js',
227 | 'packages/@ExpTestImg/provider-views/src/**/*.js',
228 | 'packages/@ExpTestImg/react/src/**/*.js',
229 | 'packages/@ExpTestImg/redux-dev-tools/src/**/*.js',
230 | 'packages/@ExpTestImg/remote-sources/src/**/*.js',
231 | 'packages/@ExpTestImg/screen-capture/src/**/*.js',
232 | 'packages/@ExpTestImg/status-bar/src/**/*.js',
233 | 'packages/@ExpTestImg/store-default/src/**/*.js',
234 | 'packages/@ExpTestImg/store-redux/src/**/*.js',
235 | 'packages/@ExpTestImg/svelte/rollup.config.js',
236 | 'packages/@ExpTestImg/svelte/src/**/*.js',
237 | 'packages/@ExpTestImg/thumbnail-generator/src/**/*.js',
238 | 'packages/@ExpTestImg/transloadit/src/**/*.js',
239 | 'packages/@ExpTestImg/tus/src/**/*.js',
240 | 'packages/@ExpTestImg/unsplash/src/**/*.js',
241 | 'packages/@ExpTestImg/url/src/**/*.js',
242 | 'packages/@ExpTestImg/utils/src/**/*.js',
243 | 'packages/@ExpTestImg/vue/src/**/*.js',
244 | 'packages/@ExpTestImg/webcam/src/**/*.js',
245 | 'packages/@ExpTestImg/xhr-upload/src/**/*.js',
246 | 'packages/@ExpTestImg/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/ExpTestImg/*.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/@ExpTestImg/*/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/@ExpTestImg/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/@ExpTestImg/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/@ExpTestImg/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/@ExpTestImg/locales/src/*.js',
379 | 'packages/@ExpTestImg/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 @ExpTestImg 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/@ExpTestImg/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/@ExpTestImg/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/@ExpTestImg/*/src/**/*.ts', 'packages/@ExpTestImg/*/src/**/*.tsx'],
472 | excludedFiles: ['packages/@ExpTestImg/**/*.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 |
--------------------------------------------------------------------------------