Migrate from Eslint/Prettier/Stylelint to Biome (#5794)

This commit is contained in:
Merlijn Vos 2025-07-01 14:55:41 +02:00 committed by GitHub
parent d408570373
commit 78299475ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
544 changed files with 7527 additions and 8168 deletions

View file

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

View file

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

View file

@ -38,6 +38,4 @@ jobs:
corepack yarn workspaces focus @uppy/angular @uppy-example/angular corepack yarn workspaces focus @uppy/angular @uppy-example/angular
@uppy-example/react-native-expo @uppy/react-native @uppy-dev/build @uppy-example/react-native-expo @uppy/react-native @uppy-dev/build
- name: Run linter - name: Run linter
run: corepack yarn run lint run: corepack yarn run check:ci
- name: Run Prettier
run: corepack yarn run format:check-diff

View file

@ -1,7 +1,8 @@
name: Release candidate name: Release candidate
on: on:
push: push:
branches: release branches:
- release
env: env:
YARN_ENABLE_GLOBAL_CACHE: false YARN_ENABLE_GLOBAL_CACHE: false
@ -50,7 +51,7 @@ jobs:
releases.json | xargs git add releases.json | xargs git add
- name: Update contributors table - name: Update contributors table
run: run:
corepack yarn contributors:save && corepack yarn prettier -w README.md corepack yarn contributors:save
&& git add README.md && git add README.md
- name: Update CDN URLs - name: Update CDN URLs
run: run:

View file

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

View file

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

View file

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

View file

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

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

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

View file

@ -1,29 +1,23 @@
{ {
"folders": [ "folders": [
{ {
"path": "..", "path": ".."
}, }
], ],
"settings": { "settings": {
"editor.defaultFormatter": "biomejs.biome",
"workbench.colorCustomizations": { "workbench.colorCustomizations": {
"titleBar.activeForeground": "#ffffff", "titleBar.activeForeground": "#ffffff",
"titleBar.activeBackground": "#ff009d", "titleBar.activeBackground": "#ff009d"
}, },
"search.exclude": { "search.exclude": {
"website/public/": true,
"node_modules/": true, "node_modules/": true,
"website/node_modules/": true,
"dist/": true, "dist/": true,
"lib/": true, "lib/": true,
"package-lock.json": true,
"website/package-lock.json": true,
"yarn-error.log": true, "yarn-error.log": true,
"website/.deploy_git": true,
"npm-debug.log": true, "npm-debug.log": true,
"website/npm-debug.log": true,
"website/debug.log": true,
"nohup.out": true, "nohup.out": true,
"yarn.lock": true, "yarn.lock": true
}, }
}, }
} }

View file

@ -1,29 +0,0 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"workbench.colorCustomizations": {
"titleBar.activeForeground": "#ffffff",
"titleBar.activeBackground": "#ff009d",
},
"search.exclude": {
"website/public/": true,
"node_modules/": true,
"website/node_modules/": true,
"dist/": true,
"lib/": true,
"package-lock.json": true,
"website/package-lock.json": true,
"yarn-error.log": true,
"website/.deploy_git": true,
"npm-debug.log": true,
"website/npm-debug.log": true,
"website/debug.log": true,
"nohup.out": true,
"yarn.lock": true
}
}
}

View file

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

View file

@ -1,7 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import fs from 'node:fs/promises'
import { existsSync } from 'node:fs' import { existsSync } from 'node:fs'
import fs from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
@ -13,7 +13,10 @@ const rootDir = path.resolve(scriptDir, '..')
// source: // source:
const COMPONENTS_DIR = path.join(rootDir, 'packages/@uppy/components/src') const COMPONENTS_DIR = path.join(rootDir, 'packages/@uppy/components/src')
// destinations: // destinations:
const REACT_DIR = path.join(rootDir, 'packages/@uppy/react/src/headless/generated') const REACT_DIR = path.join(
rootDir,
'packages/@uppy/react/src/headless/generated',
)
const VUE_DIR = path.join(rootDir, 'packages/@uppy/vue/src/headless/generated') const VUE_DIR = path.join(rootDir, 'packages/@uppy/vue/src/headless/generated')
const SVELTE_DIR = path.join( const SVELTE_DIR = path.join(
rootDir, rootDir,
@ -222,7 +225,10 @@ try {
const reactIndexContent = reactComponents const reactIndexContent = reactComponents
.map((name) => `export { default as ${name} } from './${name}.js'`) .map((name) => `export { default as ${name} } from './${name}.js'`)
.join('\n') .join('\n')
await fs.writeFile(path.join(REACT_DIR, 'index.ts'), `${reactIndexContent}\n`) await fs.writeFile(
path.join(REACT_DIR, 'index.ts'),
`${reactIndexContent}\n`,
)
console.log(`\nExporting React components from ${REACT_DIR}`) console.log(`\nExporting React components from ${REACT_DIR}`)
} }
@ -238,7 +244,10 @@ try {
const svelteIndexContent = svelteComponents const svelteIndexContent = svelteComponents
.map((name) => `export { default as ${name} } from './${name}.svelte'`) .map((name) => `export { default as ${name} } from './${name}.svelte'`)
.join('\n') .join('\n')
await fs.writeFile(path.join(SVELTE_DIR, 'index.ts'), `${svelteIndexContent}\n`) await fs.writeFile(
path.join(SVELTE_DIR, 'index.ts'),
`${svelteIndexContent}\n`,
)
console.log(`Exporting Svelte components from ${SVELTE_DIR}`) console.log(`Exporting Svelte components from ${SVELTE_DIR}`)
} }

View file

@ -16,49 +16,50 @@ const { mkdir, writeFile } = fs.promises
const cwd = process.cwd() const cwd = process.cwd()
let chalk let chalk
function handleErr (err) { function handleErr(err) {
console.error(chalk.red('✗ Error:'), chalk.red(err.message)) console.error(chalk.red('✗ Error:'), chalk.red(err.message))
} }
async function compileCSS () { async function compileCSS() {
({ default:chalk } = await import('chalk')) ;({ default: chalk } = await import('chalk'))
const files = await glob('packages/{,@uppy/}*/src/style.scss') const files = await glob('packages/{,@uppy/}*/src/style.scss')
for (const file of files) { for (const file of files) {
const importedFiles = new Set() const importedFiles = new Set()
const scssResult = await renderScss({ const scssResult = await renderScss({
file, file,
importer (url, from, done) { importer(url, from, done) {
resolve(url, { resolve(
basedir: path.dirname(from), url,
filename: from, {
extensions: ['.scss'], basedir: path.dirname(from),
}, (err, resolved) => { filename: from,
if (err) { extensions: ['.scss'],
done(err) },
return (err, resolved) => {
} if (err) {
done(err)
return
}
const realpath = fs.realpathSync(resolved) const realpath = fs.realpathSync(resolved)
if (importedFiles.has(realpath)) { if (importedFiles.has(realpath)) {
done({ contents: '' }) done({ contents: '' })
return return
} }
importedFiles.add(realpath) importedFiles.add(realpath)
done({ file: realpath }) done({ file: realpath })
}) },
)
}, },
}) })
const plugins = [ const plugins = [autoprefixer, postcssLogical(), postcssDirPseudoClass()]
autoprefixer, const postcssResult = await postcss(plugins).process(scssResult.css, {
postcssLogical(), from: file,
postcssDirPseudoClass(), })
]
const postcssResult = await postcss(plugins)
.process(scssResult.css, { from: file })
postcssResult.warnings().forEach((warn) => { postcssResult.warnings().forEach((warn) => {
console.warn(warn.toString()) console.warn(warn.toString())
}) })
@ -79,9 +80,10 @@ async function compileCSS () {
chalk.magenta(path.relative(cwd, outfile)), chalk.magenta(path.relative(cwd, outfile)),
) )
const minifiedResult = await postcss([ const minifiedResult = await postcss([cssnano({ safe: true })]).process(
cssnano({ safe: true }), postcssResult.css,
]).process(postcssResult.css, { from: outfile }) { from: outfile },
)
minifiedResult.warnings().forEach((warn) => { minifiedResult.warnings().forEach((warn) => {
console.warn(warn.toString()) console.warn(warn.toString())
}) })

View file

@ -1,12 +1,12 @@
#!/usr/bin/env node #!/usr/bin/env node
import fetchContrib from 'github-contributors-list/lib/contributors.js'
import layoutStrategy from 'github-contributors-list/lib/strategies/layout_strategies/json.js'
import sortStrategy from 'github-contributors-list/lib/strategies/sort_strategies/sort_desc.js'
import filterStrategy from 'github-contributors-list/lib/strategies/filter_strategies/login.js'
import { Buffer } from 'node:buffer' import { Buffer } from 'node:buffer'
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import process from 'node:process' import process from 'node:process'
import fetchContrib from 'github-contributors-list/lib/contributors.js'
import filterStrategy from 'github-contributors-list/lib/strategies/filter_strategies/login.js'
import layoutStrategy from 'github-contributors-list/lib/strategies/layout_strategies/json.js'
import sortStrategy from 'github-contributors-list/lib/strategies/sort_strategies/sort_desc.js'
const README_FILE_NAME = new URL('../README.md', import.meta.url) const README_FILE_NAME = new URL('../README.md', import.meta.url)

67
biome.json Normal file
View file

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

View file

@ -1,6 +1,6 @@
import AwsS3Multipart from '@uppy/aws-s3'
import { Uppy } from '@uppy/core' import { Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import AwsS3Multipart from '@uppy/aws-s3'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'

View file

@ -1,6 +1,6 @@
import AwsS3 from '@uppy/aws-s3'
import { Uppy } from '@uppy/core' import { Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import AwsS3 from '@uppy/aws-s3'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'

View file

@ -1,6 +1,6 @@
import Compressor from '@uppy/compressor'
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import Compressor from '@uppy/compressor'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'

View file

@ -1,12 +1,22 @@
const enc = new TextEncoder('utf-8') const enc = new TextEncoder('utf-8')
async function sign (secret, body) { async function sign(secret, body) {
const algorithm = { name: 'HMAC', hash: 'SHA-384' } const algorithm = { name: 'HMAC', hash: 'SHA-384' }
const key = await crypto.subtle.importKey('raw', enc.encode(secret), algorithm, false, ['sign', 'verify']) const key = await crypto.subtle.importKey(
const signature = await crypto.subtle.sign(algorithm.name, key, enc.encode(body)) 'raw',
return `sha384:${Array.from(new Uint8Array(signature), x => x.toString(16).padStart(2, '0')).join('')}` enc.encode(secret),
algorithm,
false,
['sign', 'verify'],
)
const signature = await crypto.subtle.sign(
algorithm.name,
key,
enc.encode(body),
)
return `sha384:${Array.from(new Uint8Array(signature), (x) => x.toString(16).padStart(2, '0')).join('')}`
} }
function getExpiration (future) { function getExpiration(future) {
return new Date(Date.now() + future) return new Date(Date.now() + future)
.toISOString() .toISOString()
.replace('T', ' ') .replace('T', ' ')
@ -20,12 +30,10 @@ function getExpiration (future) {
* @param {object} params * @param {object} params
* @returns {{ params: string, signature?: string }} * @returns {{ params: string, signature?: string }}
*/ */
export default async function generateSignatureIfSecret (secret, params) { export default async function generateSignatureIfSecret(secret, params) {
let signature let signature
if (secret) { if (secret) {
// eslint-disable-next-line no-param-reassign
params.auth.expires = getExpiration(5 * 60 * 1000) params.auth.expires = getExpiration(5 * 60 * 1000)
// eslint-disable-next-line no-param-reassign
params = JSON.stringify(params) params = JSON.stringify(params)
signature = await sign(secret, params) signature = await sign(secret, params)
} }

View file

@ -7,7 +7,7 @@ import Url from '@uppy/url'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'
function onShouldRetry (err, retryAttempt, options, next) { function onShouldRetry(err, retryAttempt, options, next) {
if (err?.originalResponse?.getStatus() === 418) { if (err?.originalResponse?.getStatus() === 418) {
return true return true
} }

View file

@ -1,13 +1,13 @@
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import RemoteSources from '@uppy/remote-sources'
import Webcam from '@uppy/webcam'
import ScreenCapture from '@uppy/screen-capture'
import GoldenRetriever from '@uppy/golden-retriever'
import ImageEditor from '@uppy/image-editor'
import DropTarget from '@uppy/drop-target'
import Audio from '@uppy/audio' import Audio from '@uppy/audio'
import Compressor from '@uppy/compressor' import Compressor from '@uppy/compressor'
import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import DropTarget from '@uppy/drop-target'
import GoldenRetriever from '@uppy/golden-retriever'
import ImageEditor from '@uppy/image-editor'
import RemoteSources from '@uppy/remote-sources'
import ScreenCapture from '@uppy/screen-capture'
import Webcam from '@uppy/webcam'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'

View file

@ -21,16 +21,16 @@
</template> </template>
<script> <script>
import Vue from 'vue'
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Tus from '@uppy/tus' import Tus from '@uppy/tus'
import { import {
UppyContextProvider,
Dropzone, Dropzone,
FilesList,
FilesGrid, FilesGrid,
FilesList,
UploadButton, UploadButton,
UppyContextProvider,
} from '@uppy/vue' } from '@uppy/vue'
import Vue from 'vue'
export default { export default {
name: 'App', name: 'App',

View file

@ -1,8 +1,8 @@
import { Uppy } from '@uppy/core' import { Uppy } from '@uppy/core'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import XHRUpload from '@uppy/xhr-upload'
import Unsplash from '@uppy/unsplash' import Unsplash from '@uppy/unsplash'
import Url from '@uppy/url' import Url from '@uppy/url'
import XHRUpload from '@uppy/xhr-upload'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'
@ -10,7 +10,10 @@ import '@uppy/dashboard/dist/style.css'
const companionUrl = 'http://localhost:3020' const companionUrl = 'http://localhost:3020'
const uppy = new Uppy() const uppy = new Uppy()
.use(Dashboard, { target: '#app', inline: true }) .use(Dashboard, { target: '#app', inline: true })
.use(XHRUpload, { endpoint: 'https://xhr-server.herokuapp.com/upload', limit: 6 }) .use(XHRUpload, {
endpoint: 'https://xhr-server.herokuapp.com/upload',
limit: 6,
})
.use(Url, { target: Dashboard, companionUrl }) .use(Url, { target: Dashboard, companionUrl })
.use(Unsplash, { target: Dashboard, companionUrl }) .use(Unsplash, { target: Dashboard, companionUrl })

View file

@ -1,10 +1,9 @@
/* eslint-disable react/react-in-jsx-scope */ /** biome-ignore-all lint/nursery/useUniqueElementIds: it's fine */
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
/* eslint-disable-next-line no-unused-vars */
import React, { useState } from 'react'
import { Dashboard, DashboardModal, DragDrop } from '@uppy/react' import { Dashboard, DashboardModal, DragDrop } from '@uppy/react'
import ThumbnailGenerator from '@uppy/thumbnail-generator'
import RemoteSources from '@uppy/remote-sources' import RemoteSources from '@uppy/remote-sources'
import ThumbnailGenerator from '@uppy/thumbnail-generator'
import React, { useState } from 'react'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'

View file

@ -1,4 +1,3 @@
/* eslint-disable react/react-in-jsx-scope */
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'
import App from './App.jsx' import App from './App.jsx'

View file

@ -10,7 +10,7 @@ export default defineConfig({
baseUrl: 'http://localhost:1234', baseUrl: 'http://localhost:1234',
specPattern: 'cypress/integration/*.spec.ts', specPattern: 'cypress/integration/*.spec.ts',
setupNodeEvents (on) { setupNodeEvents(on) {
// implement node event listeners here // implement node event listeners here
installLogsPrinter(on) installLogsPrinter(on)

View file

@ -7,16 +7,16 @@ import deepFreeze from 'deep-freeze'
* Default store + deepFreeze on setState to make sure nothing is mutated accidentally * Default store + deepFreeze on setState to make sure nothing is mutated accidentally
*/ */
class DeepFrozenStore { class DeepFrozenStore {
constructor () { constructor() {
this.state = {} this.state = {}
this.callbacks = [] this.callbacks = []
} }
getState () { getState() {
return this.state return this.state
} }
setState (patch) { setState(patch) {
const prevState = { ...this.state } const prevState = { ...this.state }
const nextState = deepFreeze({ ...this.state, ...patch }) const nextState = deepFreeze({ ...this.state, ...patch })
@ -24,24 +24,21 @@ class DeepFrozenStore {
this._publish(prevState, nextState, patch) this._publish(prevState, nextState, patch)
} }
subscribe (listener) { subscribe(listener) {
this.callbacks.push(listener) this.callbacks.push(listener)
return () => { return () => {
// Remove the listener. // Remove the listener.
this.callbacks.splice( this.callbacks.splice(this.callbacks.indexOf(listener), 1)
this.callbacks.indexOf(listener),
1,
)
} }
} }
_publish (...args) { _publish(...args) {
this.callbacks.forEach((listener) => { this.callbacks.forEach((listener) => {
listener(...args) listener(...args)
}) })
} }
} }
export default function defaultStore () { export default function defaultStore() {
return new DeepFrozenStore() return new DeepFrozenStore()
} }

View file

@ -201,14 +201,12 @@ describe('Dashboard with @uppy/aws-s3-multipart', () => {
cy.wait('@post') cy.wait('@post')
cy.get('button[data-cy=togglePauseResume]').click() cy.get('button[data-cy=togglePauseResume]').click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(300) // Wait an arbitrary amount of time as a user would do. cy.wait(300) // Wait an arbitrary amount of time as a user would do.
cy.get('button[data-cy=togglePauseResume]').click() cy.get('button[data-cy=togglePauseResume]').click()
cy.wait('@get') cy.wait('@get')
cy.get('button[data-cy=togglePauseResume]').click() cy.get('button[data-cy=togglePauseResume]').click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(300) // Wait an arbitrary amount of time as a user would do. cy.wait(300) // Wait an arbitrary amount of time as a user would do.
cy.get('button[data-cy=togglePauseResume]').click() cy.get('button[data-cy=togglePauseResume]').click()

View file

@ -1,5 +1,5 @@
import Uppy from '@uppy/core' import type Uppy from '@uppy/core'
import Transloadit from '@uppy/transloadit' import type Transloadit from '@uppy/transloadit'
function getPlugin<M = any, B = any>(uppy: Uppy<M, B>) { function getPlugin<M = any, B = any>(uppy: Uppy<M, B>) {
return uppy.getPlugin<Transloadit<M, B>>('Transloadit')! return uppy.getPlugin<Transloadit<M, B>>('Transloadit')!
@ -367,7 +367,6 @@ describe('Dashboard with Transloadit', () => {
cy.wait('@firstUpload') cy.wait('@firstUpload')
cy.get('button[data-cy=togglePauseResume]').click() cy.get('button[data-cy=togglePauseResume]').click()
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(300) // Wait an arbitrary amount of time as a user would do. cy.wait(300) // Wait an arbitrary amount of time as a user would do.
cy.get('button[data-cy=togglePauseResume]').click() cy.get('button[data-cy=togglePauseResume]').click()

View file

@ -1,6 +1,6 @@
import { import {
runRemoteUrlImageUploadTest,
runRemoteUnsplashUploadTest, runRemoteUnsplashUploadTest,
runRemoteUrlImageUploadTest,
} from './reusable-tests.ts' } from './reusable-tests.ts'
// NOTE: we have to use different files to upload per test // NOTE: we have to use different files to upload per test

View file

@ -1,8 +1,8 @@
import { import {
interceptCompanionUrlMetaRequest, interceptCompanionUrlMetaRequest,
interceptCompanionUrlRequest, interceptCompanionUrlRequest,
runRemoteUrlImageUploadTest,
runRemoteUnsplashUploadTest, runRemoteUnsplashUploadTest,
runRemoteUrlImageUploadTest,
} from './reusable-tests.ts' } from './reusable-tests.ts'
describe('Dashboard with XHR', () => { describe('Dashboard with XHR', () => {

View file

@ -52,7 +52,6 @@ describe('@uppy/react', () => {
it('should render Drag & Drop in React and create a thumbail with @uppy/thumbnail-generator', () => { it('should render Drag & Drop in React and create a thumbail with @uppy/thumbnail-generator', () => {
const spy = cy.spy() const spy = cy.spy()
// eslint-disable-next-line
// @ts-ignore fix me // @ts-ignore fix me
cy.window().then(({ uppy }) => uppy.on('thumbnail:generated', spy)) cy.window().then(({ uppy }) => uppy.on('thumbnail:generated', spy))
cy.get('@dragdrop-input').selectFile( cy.get('@dragdrop-input').selectFile(
@ -63,7 +62,6 @@ describe('@uppy/react', () => {
{ force: true }, { force: true },
) )
// not sure how I can accurately wait for the thumbnail // not sure how I can accurately wait for the thumbnail
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000).then(() => expect(spy).to.be.called) cy.wait(1000).then(() => expect(spy).to.be.called)
}) })
}) })

View file

@ -1,7 +1,6 @@
declare global { declare global {
namespace Cypress { namespace Cypress {
interface Chainable { interface Chainable {
// eslint-disable-next-line no-use-before-define
createFakeFile: typeof createFakeFile createFakeFile: typeof createFakeFile
} }
} }
@ -20,11 +19,9 @@ export function createFakeFile(
b64?: string, b64?: string,
): File { ): File {
if (!b64) { if (!b64) {
// eslint-disable-next-line no-param-reassign
b64 = b64 =
'PHN2ZyB2aWV3Qm94PSIwIDAgMTIwIDEyMCI+CiAgPGNpcmNsZSBjeD0iNjAiIGN5PSI2MCIgcj0iNTAiLz4KPC9zdmc+Cg==' 'PHN2ZyB2aWV3Qm94PSIwIDAgMTIwIDEyMCI+CiAgPGNpcmNsZSBjeD0iNjAiIGN5PSI2MCIgcj0iNTAiLz4KPC9zdmc+Cg=='
} }
// eslint-disable-next-line no-param-reassign
if (!type) type = 'image/svg+xml' if (!type) type = 'image/svg+xml'
// https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript // https://stackoverflow.com/questions/16245767/creating-a-blob-from-a-base64-string-in-javascript

View file

@ -19,7 +19,6 @@ import './commands.ts'
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')
// eslint-disable-next-line
// @ts-ignore // @ts-ignore
import installLogsCollector from 'cypress-terminal-report/src/installLogsCollector.js' import installLogsCollector from 'cypress-terminal-report/src/installLogsCollector.js'

View file

@ -1,25 +1,37 @@
#!/usr/bin/env node #!/usr/bin/env node
import prompts from 'prompts'
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import prompts from 'prompts'
/** /**
* Utility function that strips indentation from multi-line strings. * Utility function that strips indentation from multi-line strings.
* Inspired from https://github.com/dmnd/dedent. * Inspired from https://github.com/dmnd/dedent.
*/ */
function dedent (strings, ...parts) { function dedent(strings, ...parts) {
const nonSpacingChar = /\S/m.exec(strings[0]) const nonSpacingChar = /\S/m.exec(strings[0])
if (nonSpacingChar == null) return '' if (nonSpacingChar == null) return ''
const indent = nonSpacingChar.index - strings[0].lastIndexOf('\n', nonSpacingChar.index) - 1 const indent =
const dedentEachLine = str => str.split('\n').map((line, i) => line.slice(i && indent)).join('\n') nonSpacingChar.index -
let returnLines = dedentEachLine(strings[0].slice(nonSpacingChar.index), indent) strings[0].lastIndexOf('\n', nonSpacingChar.index) -
1
const dedentEachLine = (str) =>
str
.split('\n')
.map((line, i) => line.slice(i && indent))
.join('\n')
let returnLines = dedentEachLine(
strings[0].slice(nonSpacingChar.index),
indent,
)
for (let i = 1; i < strings.length; i++) { for (let i = 1; i < strings.length; i++) {
returnLines += String(parts[i - 1]) + dedentEachLine(strings[i], indent) returnLines += String(parts[i - 1]) + dedentEachLine(strings[i], indent)
} }
return returnLines return returnLines
} }
const packageNames = await fs.readdir(new URL('../packages/@uppy', import.meta.url)) const packageNames = await fs.readdir(
new URL('../packages/@uppy', import.meta.url),
)
const unwantedPackages = ['core', 'companion', 'redux-dev-tools', 'utils'] const unwantedPackages = ['core', 'companion', 'redux-dev-tools', 'utils']
const { name } = await prompts({ const { name } = await prompts({
@ -39,9 +51,10 @@ const { packages } = await prompts({
.map((pkg) => ({ title: pkg, value: pkg })), .map((pkg) => ({ title: pkg, value: pkg })),
}) })
const camelcase = (str) => str const camelcase = (str) =>
.toLowerCase() str
.replace(/([-][a-z])/g, (group) => group.toUpperCase().replace('-', '')) .toLowerCase()
.replace(/([-][a-z])/g, (group) => group.toUpperCase().replace('-', ''))
const testUrl = new URL(`cypress/integration/${name}.spec.ts`, import.meta.url) const testUrl = new URL(`cypress/integration/${name}.spec.ts`, import.meta.url)
const test = dedent` const test = dedent`

View file

@ -6,38 +6,47 @@ const requestListener = (req, res) => {
switch (endpoint) { switch (endpoint) {
case '/file-with-content-disposition': { case '/file-with-content-disposition': {
const fileName = `DALL·E IMG_9078 - 学中文 🤑` const fileName = `DALL·E IMG_9078 - 学中文 🤑`
res.setHeader('Content-Disposition', `attachment; filename="ASCII-name.zip"; filename*=UTF-8''${encodeURIComponent(fileName)}`) res.setHeader(
'Content-Disposition',
`attachment; filename="ASCII-name.zip"; filename*=UTF-8''${encodeURIComponent(fileName)}`,
)
res.setHeader('Content-Type', 'image/jpeg') res.setHeader('Content-Type', 'image/jpeg')
res.setHeader('Content-Length', '86500') res.setHeader('Content-Length', '86500')
break break
} }
case '/file-no-headers': case '/file-no-headers':
break break
case '/unknown-size': { case '/unknown-size':
res.setHeader('Content-Type', 'text/html; charset=UTF-8'); {
res.setHeader('Transfer-Encoding', 'chunked'); res.setHeader('Content-Type', 'text/html; charset=UTF-8')
const chunkSize = 1e5; res.setHeader('Transfer-Encoding', 'chunked')
if (req.method === 'GET') { const chunkSize = 1e5
let i = 0; if (req.method === 'GET') {
const interval = setInterval(() => { let i = 0
if (i >= 10) { // 1MB const interval = setInterval(() => {
clearInterval(interval); if (i >= 10) {
res.end(); // 1MB
return; clearInterval(interval)
} res.end()
res.write(Buffer.from(Array.from({ length: chunkSize }, () => '1').join(''))); return
res.write('\n'); }
i++; res.write(
}, 10); Buffer.from(
} else if (req.method === 'HEAD') { Array.from({ length: chunkSize }, () => '1').join(''),
res.end(); ),
} else { )
throw new Error('Unhandled method') res.write('\n')
i++
}, 10)
} else if (req.method === 'HEAD') {
res.end()
} else {
throw new Error('Unhandled method')
}
} }
} break
break;
default: default:
res.writeHead(404).end('Unhandled request') res.writeHead(404).end('Unhandled request')
} }
@ -45,7 +54,7 @@ const requestListener = (req, res) => {
res.end() res.end()
} }
export default function startMockServer (host, port) { export default function startMockServer(host, port) {
const server = http.createServer(requestListener) const server = http.createServer(requestListener)
server.listen(port, host, () => { server.listen(port, host, () => {
console.log(`Mock server is running on http://${host}:${port}`) console.log(`Mock server is running on http://${host}:${port}`)

View file

@ -1,22 +1,21 @@
#!/usr/bin/env node #!/usr/bin/env node
import http from 'node:http' import http from 'node:http'
import httpProxy from 'http-proxy'
import process from 'node:process' import process from 'node:process'
import { execaNode } from 'execa'; import { execaNode } from 'execa'
import httpProxy from 'http-proxy'
const numInstances = 3 const numInstances = 3
const lbPort = 3020 const lbPort = 3020
const companionStartPort = 3021 const companionStartPort = 3021
// simple load balancer that will direct requests round robin between companion instances // simple load balancer that will direct requests round robin between companion instances
function createLoadBalancer (baseUrls) { function createLoadBalancer(baseUrls) {
const proxy = httpProxy.createProxyServer({ ws: true }) const proxy = httpProxy.createProxyServer({ ws: true })
let i = 0 let i = 0
function getTarget () { function getTarget() {
return baseUrls[i % baseUrls.length] return baseUrls[i % baseUrls.length]
} }
@ -50,40 +49,50 @@ function createLoadBalancer (baseUrls) {
const isWindows = process.platform === 'win32' const isWindows = process.platform === 'win32'
const isOSX = process.platform === 'darwin' const isOSX = process.platform === 'darwin'
const startCompanion = ({ name, port }) => execaNode('packages/@uppy/companion/src/standalone/start-server.js', { const startCompanion = ({ name, port }) =>
nodeOptions: [ execaNode('packages/@uppy/companion/src/standalone/start-server.js', {
'-r', 'dotenv/config', nodeOptions: [
// Watch mode support is limited to Windows and macOS at the time of writing. '-r',
...(isWindows || isOSX ? ['--watch-path', 'packages/@uppy/companion/src', '--watch'] : []), 'dotenv/config',
], // Watch mode support is limited to Windows and macOS at the time of writing.
cwd: new URL('../', import.meta.url), ...(isWindows || isOSX
stdio: 'inherit', ? ['--watch-path', 'packages/@uppy/companion/src', '--watch']
env: { : []),
// Note: these env variables will override anything set in .env ],
...process.env, cwd: new URL('../', import.meta.url),
COMPANION_PORT: port, stdio: 'inherit',
COMPANION_SECRET: 'development', // multi instance will not work without secret set env: {
COMPANION_PREAUTH_SECRET: 'development', // multi instance will not work without secret set // Note: these env variables will override anything set in .env
COMPANION_ALLOW_LOCAL_URLS: 'true', ...process.env,
COMPANION_ENABLE_URL_ENDPOINT: 'true', COMPANION_PORT: port,
COMPANION_LOGGER_PROCESS_NAME: name, COMPANION_SECRET: 'development', // multi instance will not work without secret set
COMPANION_CLIENT_ORIGINS: 'true', COMPANION_PREAUTH_SECRET: 'development', // multi instance will not work without secret set
}, COMPANION_ALLOW_LOCAL_URLS: 'true',
}) COMPANION_ENABLE_URL_ENDPOINT: 'true',
COMPANION_LOGGER_PROCESS_NAME: name,
COMPANION_CLIENT_ORIGINS: 'true',
},
})
const hosts = Array.from({ length: numInstances }, (_, index) => { const hosts = Array.from({ length: numInstances }, (_, index) => {
const port = companionStartPort + index const port = companionStartPort + index
return { index, port } return { index, port }
}) })
console.log('Starting companion instances on ports', hosts.map(({ port }) => port)) console.log(
'Starting companion instances on ports',
hosts.map(({ port }) => port),
)
const companions = hosts.map(({ index, port }) => startCompanion({ name: `companion${index}`, port })) const companions = hosts.map(({ index, port }) =>
startCompanion({ name: `companion${index}`, port }),
)
let loadBalancer let loadBalancer
try { try {
loadBalancer = createLoadBalancer(hosts.map(({ port }) => `http://localhost:${port}`)) loadBalancer = createLoadBalancer(
hosts.map(({ port }) => `http://localhost:${port}`),
)
await Promise.all(companions) await Promise.all(companions)
} finally { } finally {
loadBalancer?.close() loadBalancer?.close()

View file

@ -5,7 +5,7 @@
"noEmit": true, "noEmit": true,
"target": "es2020", "target": "es2020",
"lib": ["es2020", "dom"], "lib": ["es2020", "dom"],
"types": ["cypress"], "types": ["cypress"]
}, },
"include": ["cypress/**/*.ts"], "include": ["cypress/**/*.ts"]
} }

View file

@ -1,8 +1,8 @@
import { Component, OnInit } from '@angular/core' import { Component, type OnInit } from '@angular/core'
import { Uppy } from '@uppy/core' import { Uppy } from '@uppy/core'
import Webcam from '@uppy/webcam'
import Tus from '@uppy/tus'
import GoogleDrive from '@uppy/google-drive' import GoogleDrive from '@uppy/google-drive'
import Tus from '@uppy/tus'
import Webcam from '@uppy/webcam'
@Component({ @Component({
selector: 'app-root', selector: 'app-root',

View file

@ -2,11 +2,11 @@ import { NgModule } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser' import { BrowserModule } from '@angular/platform-browser'
import { import {
UppyAngularDashboardModalModule,
UppyAngularDashboardModule, UppyAngularDashboardModule,
UppyAngularStatusBarModule,
UppyAngularDragDropModule, UppyAngularDragDropModule,
UppyAngularProgressBarModule, UppyAngularProgressBarModule,
UppyAngularDashboardModalModule, UppyAngularStatusBarModule,
} from '@uppy/angular' } from '@uppy/angular'
import { AppComponent } from './app.component' import { AppComponent } from './app.component'

View file

@ -4,4 +4,4 @@ import { AppModule } from './app/app.module'
platformBrowserDynamic() platformBrowserDynamic()
.bootstrapModule(AppModule) .bootstrapModule(AppModule)
.catch((err) => console.error(err)) // eslint-disable-line no-console .catch((err) => console.error(err))

View file

@ -1,5 +1,5 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
@import '@uppy/core/dist/style.css'; @import "@uppy/core/dist/style.css";
@import '@uppy/dashboard/dist/style.css'; @import "@uppy/dashboard/dist/style.css";
@import '@uppy/drag-drop/dist/style.css'; @import "@uppy/drag-drop/dist/style.css";
@import '@uppy/progress-bar/dist/style.css'; @import "@uppy/progress-bar/dist/style.css";

View file

@ -19,12 +19,12 @@
"target": "ES2022", "target": "ES2022",
"module": "ES2022", "module": "ES2022",
"useDefineForClassFields": false, "useDefineForClassFields": false,
"lib": ["ES2022", "dom"], "lib": ["ES2022", "dom"]
}, },
"angularCompilerOptions": { "angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false, "enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true, "strictInjectionParameters": true,
"strictInputAccessModifiers": true, "strictInputAccessModifiers": true,
"strictTemplates": true, "strictTemplates": true
}, }
} }

View file

@ -8,18 +8,22 @@ const app = require('express')()
const DATA_DIR = path.join(__dirname, 'tmp') const DATA_DIR = path.join(__dirname, 'tmp')
app.use(require('cors')({ app.use(
origin: 'http://localhost:3000', require('cors')({
methods: ['GET', 'POST', 'OPTIONS'], origin: 'http://localhost:3000',
credentials: true, methods: ['GET', 'POST', 'OPTIONS'],
})) credentials: true,
}),
)
app.use(require('cookie-parser')()) app.use(require('cookie-parser')())
app.use(require('body-parser').json()) app.use(require('body-parser').json())
app.use(require('express-session')({ app.use(
secret: 'hello planet', require('express-session')({
saveUninitialized: false, secret: 'hello planet',
resave: false, saveUninitialized: false,
})) resave: false,
}),
)
const options = { const options = {
providerOptions: { providerOptions: {
@ -46,7 +50,7 @@ const options = {
// Create the data directory here for the sake of the example. // Create the data directory here for the sake of the example.
try { try {
fs.accessSync(DATA_DIR) fs.accessSync(DATA_DIR)
} catch (err) { } catch (_err) {
fs.mkdirSync(DATA_DIR) fs.mkdirSync(DATA_DIR)
} }
process.on('exit', () => { process.on('exit', () => {

View file

@ -1,5 +1,3 @@
'use strict'
const path = require('node:path') const path = require('node:path')
const crypto = require('node:crypto') const crypto = require('node:crypto')
const { existsSync } = require('node:fs') const { existsSync } = require('node:fs')
@ -84,14 +82,16 @@ const extractFileParameters = (req) => {
return { return {
filename: params.filename, filename: params.filename,
contentType: params.type contentType: params.type,
} }
} }
// Validate the file parameters // Validate the file parameters
const validateFileParameters = (filename, contentType) => { const validateFileParameters = (filename, contentType) => {
if (!filename || !contentType) { if (!filename || !contentType) {
throw new Error('Missing required parameters: filename and content type are required') throw new Error(
'Missing required parameters: filename and content type are required',
)
} }
} }
@ -195,7 +195,6 @@ app.post('/s3/multipart', (req, res, next) => {
}) })
function validatePartNumber(partNumber) { function validatePartNumber(partNumber) {
// eslint-disable-next-line no-param-reassign
partNumber = Number(partNumber) partNumber = Number(partNumber)
return Number.isInteger(partNumber) && partNumber >= 1 && partNumber <= 10_000 return Number.isInteger(partNumber) && partNumber >= 1 && partNumber <= 10_000
} }
@ -204,19 +203,15 @@ app.get('/s3/multipart/:uploadId/:partNumber', (req, res, next) => {
const { key } = req.query const { key } = req.query
if (!validatePartNumber(partNumber)) { if (!validatePartNumber(partNumber)) {
return res return res.status(400).json({
.status(400) error: 's3: the part number must be an integer between 1 and 10000.',
.json({ })
error: 's3: the part number must be an integer between 1 and 10000.',
})
} }
if (typeof key !== 'string') { if (typeof key !== 'string') {
return res return res.status(400).json({
.status(400) error:
.json({ 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"',
error: })
's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"',
})
} }
return getSignedUrl( return getSignedUrl(
@ -241,38 +236,39 @@ app.get('/s3/multipart/:uploadId', (req, res, next) => {
const { key } = req.query const { key } = req.query
if (typeof key !== 'string') { if (typeof key !== 'string') {
res res.status(400).json({
.status(400) error:
.json({ 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"',
error: })
's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"',
})
return return
} }
const parts = [] const parts = []
function listPartsPage(startsAt = undefined) { function listPartsPage(startsAt = undefined) {
client.send(new ListPartsCommand({ client.send(
Bucket: process.env.COMPANION_AWS_BUCKET, new ListPartsCommand({
Key: key, Bucket: process.env.COMPANION_AWS_BUCKET,
UploadId: uploadId, Key: key,
PartNumberMarker: startsAt, UploadId: uploadId,
}), (err, data) => { PartNumberMarker: startsAt,
if (err) { }),
next(err) (err, data) => {
return if (err) {
} next(err)
return
}
parts.push(...data.Parts) parts.push(...data.Parts)
// continue to get list of all uploaded parts until the IsTruncated flag is false // continue to get list of all uploaded parts until the IsTruncated flag is false
if (data.IsTruncated) { if (data.IsTruncated) {
listPartsPage(data.NextPartNumberMarker) listPartsPage(data.NextPartNumberMarker)
} else { } else {
res.json(parts) res.json(parts)
} }
}) },
)
} }
listPartsPage() listPartsPage()
}) })
@ -292,19 +288,15 @@ app.post('/s3/multipart/:uploadId/complete', (req, res, next) => {
const { parts } = req.body const { parts } = req.body
if (typeof key !== 'string') { if (typeof key !== 'string') {
return res return res.status(400).json({
.status(400) error:
.json({ 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"',
error: })
's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"',
})
} }
if (!Array.isArray(parts) || !parts.every(isValidPart)) { if (!Array.isArray(parts) || !parts.every(isValidPart)) {
return res return res.status(400).json({
.status(400) error: 's3: `parts` must be an array of {ETag, PartNumber} objects.',
.json({ })
error: 's3: `parts` must be an array of {ETag, PartNumber} objects.',
})
} }
return client.send( return client.send(
@ -335,12 +327,10 @@ app.delete('/s3/multipart/:uploadId', (req, res, next) => {
const { key } = req.query const { key } = req.query
if (typeof key !== 'string') { if (typeof key !== 'string') {
return res return res.status(400).json({
.status(400) error:
.json({ 's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"',
error: })
's3: the object key must be passed as a query parameter. For example: "?key=abc.jpg"',
})
} }
return client.send( return client.send(

View file

@ -1,6 +1,6 @@
import AwsS3 from '@uppy/aws-s3'
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import AwsS3 from '@uppy/aws-s3'
const uppy = new Uppy({ const uppy = new Uppy({
debug: true, debug: true,
@ -13,7 +13,7 @@ uppy.use(Dashboard, {
uppy.use(AwsS3, { uppy.use(AwsS3, {
shouldUseMultipart: false, // The PHP backend only supports non-multipart uploads shouldUseMultipart: false, // The PHP backend only supports non-multipart uploads
getUploadParameters (file) { getUploadParameters(file) {
// Send a request to our PHP signing endpoint. // Send a request to our PHP signing endpoint.
return fetch('/s3-sign.php', { return fetch('/s3-sign.php', {
method: 'post', method: 'post',
@ -26,17 +26,19 @@ uppy.use(AwsS3, {
filename: file.name, filename: file.name,
contentType: file.type, contentType: file.type,
}), }),
}).then((response) => {
// Parse the JSON response.
return response.json()
}).then((data) => {
// Return an object in the correct shape.
return {
method: data.method,
url: data.url,
fields: data.fields,
headers: data.headers,
}
}) })
.then((response) => {
// Parse the JSON response.
return response.json()
})
.then((data) => {
// Return an object in the correct shape.
return {
method: data.method,
url: data.url,
fields: data.fields,
headers: data.headers,
}
})
}, },
}) })

View file

@ -1,10 +1,10 @@
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import Instagram from '@uppy/instagram'
import GoogleDrive from '@uppy/google-drive' import GoogleDrive from '@uppy/google-drive'
import Instagram from '@uppy/instagram'
import Tus from '@uppy/tus'
import Url from '@uppy/url' import Url from '@uppy/url'
import Webcam from '@uppy/webcam' import Webcam from '@uppy/webcam'
import Tus from '@uppy/tus'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'
@ -33,7 +33,10 @@ const uppy = new Uppy({
note: '2 files, images and video only', note: '2 files, images and video only',
restrictions: { requiredMetaFields: ['caption'] }, restrictions: { requiredMetaFields: ['caption'] },
}) })
.use(GoogleDrive, { target: Dashboard, companionUrl: 'http://localhost:3020' }) .use(GoogleDrive, {
target: Dashboard,
companionUrl: 'http://localhost:3020',
})
.use(Instagram, { target: Dashboard, companionUrl: 'http://localhost:3020' }) .use(Instagram, { target: Dashboard, companionUrl: 'http://localhost:3020' })
.use(Url, { target: Dashboard, companionUrl: 'http://localhost:3020' }) .use(Url, { target: Dashboard, companionUrl: 'http://localhost:3020' })
.use(Webcam, { target: Dashboard }) .use(Webcam, { target: Dashboard })
@ -67,4 +70,3 @@ uppy.on('complete', (result) => {
// console.log('Registration failed with ' + error) // console.log('Registration failed with ' + error)
// }) // })
// } // }
/* eslint-enable */

View file

@ -3,11 +3,10 @@
// https://uppy.io/docs/golden-retriever/ // https://uppy.io/docs/golden-retriever/
/* globals clients */ /* globals clients */
/* eslint-disable no-restricted-globals */
const fileCache = Object.create(null) const fileCache = Object.create(null)
function getCache (name) { function getCache(name) {
if (!fileCache[name]) { if (!fileCache[name]) {
fileCache[name] = Object.create(null) fileCache[name] = Object.create(null)
} }
@ -17,15 +16,14 @@ function getCache (name) {
self.addEventListener('install', (event) => { self.addEventListener('install', (event) => {
console.log('Installing Uppy Service Worker...') console.log('Installing Uppy Service Worker...')
event.waitUntil(Promise.resolve() event.waitUntil(Promise.resolve().then(() => self.skipWaiting()))
.then(() => self.skipWaiting()))
}) })
self.addEventListener('activate', (event) => { self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim()) event.waitUntil(self.clients.claim())
}) })
function sendMessageToAllClients (msg) { function sendMessageToAllClients(msg) {
clients.matchAll().then((clients) => { clients.matchAll().then((clients) => {
clients.forEach((client) => { clients.forEach((client) => {
client.postMessage(msg) client.postMessage(msg)
@ -33,17 +31,17 @@ function sendMessageToAllClients (msg) {
}) })
} }
function addFile (store, file) { function addFile(store, file) {
getCache(store)[file.id] = file.data getCache(store)[file.id] = file.data
console.log('Added file blob to service worker cache:', file.data) console.log('Added file blob to service worker cache:', file.data)
} }
function removeFile (store, fileID) { function removeFile(store, fileID) {
delete getCache(store)[fileID] delete getCache(store)[fileID]
console.log('Removed file blob from service worker cache:', fileID) console.log('Removed file blob from service worker cache:', fileID)
} }
function getFiles (store) { function getFiles(store) {
sendMessageToAllClients({ sendMessageToAllClients({
type: 'uppy/ALL_FILES', type: 'uppy/ALL_FILES',
store, store,
@ -63,6 +61,7 @@ self.addEventListener('message', (event) => {
getFiles(event.data.store) getFiles(event.data.store)
break break
default: throw new Error('unreachable') default:
throw new Error('unreachable')
} }
}) })

View file

@ -1,7 +1,7 @@
/** @jsx h */ /** @jsx h */
import { getAllowedHosts, Provider, tokenStorage } from '@uppy/companion-client'
import { UIPlugin } from '@uppy/core' import { UIPlugin } from '@uppy/core'
import { Provider, getAllowedHosts, tokenStorage } from '@uppy/companion-client'
import { ProviderViews } from '@uppy/provider-views' import { ProviderViews } from '@uppy/provider-views'
import { h } from 'preact' import { h } from 'preact'

View file

@ -1,7 +1,7 @@
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard'
import GoogleDrive from '@uppy/google-drive' import GoogleDrive from '@uppy/google-drive'
import Tus from '@uppy/tus' import Tus from '@uppy/tus'
import Dashboard from '@uppy/dashboard'
import MyCustomProvider from './MyCustomProvider.jsx' import MyCustomProvider from './MyCustomProvider.jsx'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'

View file

@ -2,7 +2,7 @@ const { Readable } = require('node:stream')
const BASE_URL = 'https://api.unsplash.com' const BASE_URL = 'https://api.unsplash.com'
function adaptData (res) { function adaptData(res) {
const data = { const data = {
username: null, username: null,
items: [], items: [],
@ -34,27 +34,29 @@ function adaptData (res) {
class MyCustomProvider { class MyCustomProvider {
static version = 2 static version = 2
static get oauthProvider () { static get oauthProvider() {
return 'myunsplash' return 'myunsplash'
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
async list ({ token, directory }) { async list({ token, directory }) {
const path = directory ? `/${directory}/photos` : '' const path = directory ? `/${directory}/photos` : ''
const resp = await fetch(`${BASE_URL}/collections${path}`, { const resp = await fetch(`${BASE_URL}/collections${path}`, {
headers:{ headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
}, },
}) })
if (!resp.ok) { if (!resp.ok) {
throw new Error(`Errornous HTTP response (${resp.status} ${resp.statusText})`) throw new Error(
`Errornous HTTP response (${resp.status} ${resp.statusText})`,
)
} }
return adaptData(await resp.json()) return adaptData(await resp.json())
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
async download ({ id, token }) { async download({ id, token }) {
const resp = await fetch(`${BASE_URL}/photos/${id}`, { const resp = await fetch(`${BASE_URL}/photos/${id}`, {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,
@ -62,11 +64,16 @@ class MyCustomProvider {
}) })
const contentLengthStr = resp.headers['content-length'] const contentLengthStr = resp.headers['content-length']
const contentLength = parseInt(contentLengthStr, 10); const contentLength = parseInt(contentLengthStr, 10)
const size = !Number.isNaN(contentLength) && contentLength >= 0 ? contentLength : undefined; const size =
!Number.isNaN(contentLength) && contentLength >= 0
? contentLength
: undefined
if (!resp.ok) { if (!resp.ok) {
throw new Error(`Errornous HTTP response (${resp.status} ${resp.statusText})`) throw new Error(
`Errornous HTTP response (${resp.status} ${resp.statusText})`,
)
} }
return { stream: Readable.fromWeb(resp.body), size } return { stream: Readable.fromWeb(resp.body), size }
} }

View file

@ -2,7 +2,9 @@ const { mkdtempSync } = require('node:fs')
const os = require('node:os') const os = require('node:os')
const path = require('node:path') const path = require('node:path')
require('dotenv').config({ path: path.join(__dirname, '..', '..', '..', '.env') }) require('dotenv').config({
path: path.join(__dirname, '..', '..', '..', '.env'),
})
const express = require('express') const express = require('express')
// the ../../../packages is just to use the local version // the ../../../packages is just to use the local version
// instead of the npm version—in a real app use `require('@uppy/companion')` // instead of the npm version—in a real app use `require('@uppy/companion')`
@ -14,11 +16,13 @@ const MyCustomProvider = require('./CustomProvider.cjs')
const app = express() const app = express()
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use(session({ app.use(
secret: 'some-secret', session({
resave: true, secret: 'some-secret',
saveUninitialized: true, resave: true,
})) saveUninitialized: true,
}),
)
// Routes // Routes
app.get('/', (req, res) => { app.get('/', (req, res) => {

View file

@ -1,6 +1,6 @@
import AwsS3 from '@uppy/aws-s3'
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import AwsS3 from '@uppy/aws-s3'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'

View file

@ -17,10 +17,22 @@ const companion = require('../../packages/@uppy/companion')
* - COMPANION_AWS_FORCE_PATH_STYLE - Indicates if s3ForcePathStyle should be used rather than subdomain for S3 buckets. * - COMPANION_AWS_FORCE_PATH_STYLE - Indicates if s3ForcePathStyle should be used rather than subdomain for S3 buckets.
*/ */
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")') if (!process.env.COMPANION_AWS_REGION)
if (!process.env.COMPANION_AWS_KEY) throw new Error('Missing access key, please set the COMPANION_AWS_KEY environment variable') throw new Error(
if (!process.env.COMPANION_AWS_SECRET) throw new Error('Missing secret key, please set the COMPANION_AWS_SECRET environment variable') 'Missing Space region, please set the COMPANION_AWS_REGION environment variable (eg. "COMPANION_AWS_REGION=ams3")',
if (!process.env.COMPANION_AWS_BUCKET) throw new Error('Missing Space name, please set the COMPANION_AWS_BUCKET environment variable') )
if (!process.env.COMPANION_AWS_KEY)
throw new Error(
'Missing access key, please set the COMPANION_AWS_KEY environment variable',
)
if (!process.env.COMPANION_AWS_SECRET)
throw new Error(
'Missing secret key, please set the COMPANION_AWS_SECRET environment variable',
)
if (!process.env.COMPANION_AWS_BUCKET)
throw new Error(
'Missing Space name, please set the COMPANION_AWS_BUCKET environment variable',
)
// Prepare the server. // Prepare the server.
const PORT = process.env.PORT || 3452 const PORT = process.env.PORT || 3452
@ -54,9 +66,11 @@ const { app: companionApp } = companion.app({
app.use('/companion', companionApp) app.use('/companion', companionApp)
require('vite').createServer({ clearScreen: false, server:{ middlewareMode: true } }).then(({ middlewares }) => { require('vite')
app.use(middlewares) .createServer({ clearScreen: false, server: { middlewareMode: true } })
app.listen(PORT, () => { .then(({ middlewares }) => {
console.log(`Listening on http://localhost:${PORT}/...`) app.use(middlewares)
app.listen(PORT, () => {
console.log(`Listening on http://localhost:${PORT}/...`)
})
}) })
})

View file

@ -1,7 +1,7 @@
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import XHRUpload from '@uppy/xhr-upload'
import Webcam from '@uppy/webcam' import Webcam from '@uppy/webcam'
import XHRUpload from '@uppy/xhr-upload'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'

View file

@ -1,10 +1,8 @@
#!/usr/bin/env node #!/usr/bin/env node
/* eslint-disable no-console */ import { mkdir } from 'node:fs/promises'
import http from 'node:http' import http from 'node:http'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { mkdir } from 'node:fs/promises'
import formidable from 'formidable' import formidable from 'formidable'
@ -12,41 +10,50 @@ const UPLOAD_DIR = new URL('./uploads/', import.meta.url)
await mkdir(UPLOAD_DIR, { recursive: true }) await mkdir(UPLOAD_DIR, { recursive: true })
http.createServer((req, res) => { http
const headers = { .createServer((req, res) => {
'Content-Type': 'application/json', const headers = {
'Access-Control-Allow-Origin': '*', 'Content-Type': 'application/json',
'Access-Control-Allow-Methods': 'OPTIONS, POST, GET', 'Access-Control-Allow-Origin': '*',
'Access-Control-Max-Age': 2592000, // 30 days 'Access-Control-Allow-Methods': 'OPTIONS, POST, GET',
/** add other headers as per requirement */ 'Access-Control-Max-Age': 2592000, // 30 days
} /** add other headers as per requirement */
}
if (req.method === 'OPTIONS') { if (req.method === 'OPTIONS') {
res.writeHead(204, headers) res.writeHead(204, headers)
res.end() res.end()
return return
} }
if (req.url === '/upload' && req.method.toLowerCase() === 'post') { if (req.url === '/upload' && req.method.toLowerCase() === 'post') {
// parse a file upload // parse a file upload
const form = formidable({ const form = formidable({
keepExtensions: true, keepExtensions: true,
uploadDir: fileURLToPath(UPLOAD_DIR), uploadDir: fileURLToPath(UPLOAD_DIR),
}) })
form.parse(req, (err, fields, files) => { form.parse(req, (err, fields, files) => {
if (err) { if (err) {
console.log('some error', err) console.log('some error', err)
res.writeHead(200, headers)
res.write(JSON.stringify(err))
return res.end()
}
const {
file: [{ filepath, originalFilename, mimetype, size }],
} = files
console.log('saved file', {
filepath,
originalFilename,
mimetype,
size,
})
res.writeHead(200, headers) res.writeHead(200, headers)
res.write(JSON.stringify(err)) res.write(JSON.stringify({ fields, files }))
return res.end() return res.end()
} })
const { file:[{ filepath, originalFilename, mimetype, size }] } = files }
console.log('saved file', { filepath, originalFilename, mimetype, size }) })
res.writeHead(200, headers) .listen(3020, () => {
res.write(JSON.stringify({ fields, files })) console.log('server started')
return res.end() })
})
}
}).listen(3020, () => {
console.log('server started')
})

View file

@ -1,6 +1,6 @@
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Webcam from '@uppy/webcam'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import Webcam from '@uppy/webcam'
import XHRUpload from '@uppy/xhr-upload' import XHRUpload from '@uppy/xhr-upload'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'

View file

@ -1,6 +1,6 @@
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Webcam from '@uppy/webcam'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import Webcam from '@uppy/webcam'
import XHRUpload from '@uppy/xhr-upload' import XHRUpload from '@uppy/xhr-upload'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'

View file

@ -1,16 +1,16 @@
import React, { useEffect, useState, useCallback } from 'react'
import { Text, View, Image, StyleSheet } from 'react-native'
import AsyncStorage from '@react-native-async-storage/async-storage' import AsyncStorage from '@react-native-async-storage/async-storage'
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import FilePicker from '@uppy/react-native'
import Tus from '@uppy/tus' import Tus from '@uppy/tus'
import FilePicker from '@uppy/react-native' import React, { useCallback, useEffect, useState } from 'react'
import { Image, StyleSheet, Text, View } from 'react-native'
import FileList from './FileList' import FileList from './FileList'
import PauseResumeButton from './PauseResumeButton' import PauseResumeButton from './PauseResumeButton'
import ProgressBar from './ProgressBar' import ProgressBar from './ProgressBar'
import SelectFiles from './SelectFilesButton' import SelectFiles from './SelectFilesButton'
import getTusFileReader from './tusFileReader' import getTusFileReader from './tusFileReader'
export default function App () { export default function App() {
const [state, _setState] = useState({ const [state, _setState] = useState({
progress: 0, progress: 0,
total: 0, total: 0,
@ -24,15 +24,19 @@ export default function App () {
totalProgress: 0, totalProgress: 0,
}) })
const setState = useCallback((newState) => _setState((oldState) => ({ ...oldState, ...newState })), []) const setState = useCallback(
(newState) => _setState((oldState) => ({ ...oldState, ...newState })),
[],
)
const [uppy] = useState(() => new Uppy({ autoProceed: true, debug: true }) const [uppy] = useState(() =>
.use(Tus, { new Uppy({ autoProceed: true, debug: true }).use(Tus, {
endpoint: 'https://tusd.tusdemo.net/files/', endpoint: 'https://tusd.tusdemo.net/files/',
urlStorage: AsyncStorage, urlStorage: AsyncStorage,
fileReader: getTusFileReader, fileReader: getTusFileReader,
chunkSize: 10 * 1024 * 1024, // keep the chunk size small to avoid memory exhaustion chunkSize: 10 * 1024 * 1024, // keep the chunk size small to avoid memory exhaustion
})); }),
)
useEffect(() => { useEffect(() => {
uppy.on('upload-progress', (file, progress) => { uppy.on('upload-progress', (file, progress) => {
@ -48,7 +52,9 @@ export default function App () {
}) })
uppy.on('complete', (result) => { uppy.on('complete', (result) => {
setState({ setState({
status: result.successful[0] ? 'Upload complete ✅' : 'Upload errored ❌', status: result.successful[0]
? 'Upload complete ✅'
: 'Upload errored ❌',
uploadURL: result.successful[0] ? result.successful[0].uploadURL : null, uploadURL: result.successful[0] ? result.successful[0].uploadURL : null,
uploadComplete: true, uploadComplete: true,
uploadStarted: false, uploadStarted: false,
@ -98,34 +104,24 @@ export default function App () {
} }
return ( return (
<View <View style={styles.root}>
style={styles.root} <Text style={styles.title}>Uppy in React Native</Text>
>
<Text
style={styles.title}
>
Uppy in React Native
</Text>
<View style={{ alignItems: 'center' }}> <View style={{ alignItems: 'center' }}>
<Image <Image style={styles.logo} source={require('./assets/uppy-logo.png')} />
style={styles.logo}
// eslint-disable-next-line global-require
source={require('./assets/uppy-logo.png')}
/>
</View> </View>
<SelectFiles showFilePicker={showFilePicker} /> <SelectFiles showFilePicker={showFilePicker} />
{state.info ? ( {state.info
<Text ? <Text
style={{ style={{
marginBottom: 10, marginBottom: 10,
marginTop: 10, marginTop: 10,
color: '#b8006b', color: '#b8006b',
}} }}
> >
{state.info.message} {state.info.message}
</Text> </Text>
) : null} : null}
<ProgressBar progress={state.totalProgress} /> <ProgressBar progress={state.totalProgress} />
@ -148,7 +144,9 @@ export default function App () {
{uppy && <FileList uppy={uppy} />} {uppy && <FileList uppy={uppy} />}
{state.status && <Text>Status: {state.status}</Text>} {state.status && <Text>Status: {state.status}</Text>}
<Text>{state.progress} of {state.total}</Text> <Text>
{state.progress} of {state.total}
</Text>
</View> </View>
) )
} }

View file

@ -1,8 +1,7 @@
import React from 'react'
import { StyleSheet, View, FlatList, Text, Image } from 'react-native'
import getFileTypeIcon from '@uppy/dashboard/lib/utils/getFileTypeIcon.js' import getFileTypeIcon from '@uppy/dashboard/lib/utils/getFileTypeIcon.js'
import renderStringFromJSX from 'preact-render-to-string' import renderStringFromJSX from 'preact-render-to-string'
import React from 'react'
import { FlatList, Image, StyleSheet, Text, View } from 'react-native'
const fileIcon = require('./assets/file-icon.png') const fileIcon = require('./assets/file-icon.png')
@ -15,18 +14,15 @@ const truncateString = (str) => {
return str return str
} }
function FileIcon () { function FileIcon() {
return ( return (
<View style={styles.itemIconContainer}> <View style={styles.itemIconContainer}>
<Image <Image style={styles.itemIcon} source={fileIcon} />
style={styles.itemIcon}
source={fileIcon}
/>
</View> </View>
) )
} }
function UppyDashboardFileIcon ({ type }) { function UppyDashboardFileIcon({ type }) {
const icon = renderStringFromJSX(getFileTypeIcon(type).icon) const icon = renderStringFromJSX(getFileTypeIcon(type).icon)
if (!icon) { if (!icon) {
return <FileIcon /> return <FileIcon />
@ -44,7 +40,7 @@ function UppyDashboardFileIcon ({ type }) {
) )
} }
export default function FileList ({ uppy }) { export default function FileList({ uppy }) {
const uppyFiles = uppy.store.state.files const uppyFiles = uppy.store.state.files
const uppyFilesArray = Object.keys(uppyFiles).map((id) => uppyFiles[id]) const uppyFilesArray = Object.keys(uppyFiles).map((id) => uppyFiles[id])
@ -57,15 +53,15 @@ export default function FileList ({ uppy }) {
renderItem={({ item }) => { renderItem={({ item }) => {
return ( return (
<View style={styles.item}> <View style={styles.item}>
{item.type === 'image' ? ( {item.type === 'image'
<Image ? <Image
style={styles.itemImage} style={styles.itemImage}
source={{ uri: item.data.uri }} source={{ uri: item.data.uri }}
/> />
) : ( : <UppyDashboardFileIcon type={item.type} />}
<UppyDashboardFileIcon type={item.type} /> <Text style={styles.itemName}>
)} {truncateString(item.name, 20)}
<Text style={styles.itemName}>{truncateString(item.name, 20)}</Text> </Text>
<Text style={styles.itemType}>{item.type}</Text> <Text style={styles.itemType}>{item.type}</Text>
</View> </View>
) )
@ -81,7 +77,7 @@ const styles = StyleSheet.create({
marginBottom: 20, marginBottom: 20,
flex: 1, flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems:'center', alignItems: 'center',
marginRight: -25, marginRight: -25,
}, },
item: { item: {

View file

@ -1,21 +1,19 @@
import React from 'react' import React from 'react'
import { StyleSheet, Text, TouchableHighlight } from 'react-native' import { StyleSheet, Text, TouchableHighlight } from 'react-native'
export default function PauseResumeButton ({ uploadStarted, uploadComplete, isPaused, onPress }) { export default function PauseResumeButton({
uploadStarted,
uploadComplete,
isPaused,
onPress,
}) {
if (!uploadStarted || uploadComplete) { if (!uploadStarted || uploadComplete) {
return null return null
} }
return ( return (
<TouchableHighlight <TouchableHighlight onPress={onPress} style={styles.button}>
onPress={onPress} <Text style={styles.text}>{isPaused ? 'Resume' : 'Pause'}</Text>
style={styles.button}
>
<Text
style={styles.text}
>
{isPaused ? 'Resume' : 'Pause'}
</Text>
</TouchableHighlight> </TouchableHighlight>
) )
} }

View file

@ -1,19 +1,21 @@
import React from 'react' import React from 'react'
import { View, Text, StyleSheet } from 'react-native' import { StyleSheet, Text, View } from 'react-native'
const colorGreen = '#0b8600' const colorGreen = '#0b8600'
const colorBlue = '#006bb7' const colorBlue = '#006bb7'
export default function ProgressBar ({ progress }) { export default function ProgressBar({ progress }) {
return ( return (
<View style={styles.root}> <View style={styles.root}>
<View <View style={styles.wrapper}>
style={styles.wrapper} <View
> style={[
<View style={[styles.bar, { styles.bar,
backgroundColor: progress === 100 ? colorGreen : colorBlue, {
width: `${progress}%`, backgroundColor: progress === 100 ? colorGreen : colorBlue,
}]} width: `${progress}%`,
},
]}
/> />
</View> </View>
<Text>{progress ? `${progress}%` : null}</Text> <Text>{progress ? `${progress}%` : null}</Text>
@ -26,7 +28,7 @@ const styles = StyleSheet.create({
marginTop: 15, marginTop: 15,
marginBottom: 15, marginBottom: 15,
}, },
wrapper:{ wrapper: {
height: 5, height: 5,
overflow: 'hidden', overflow: 'hidden',
backgroundColor: '#dee1e3', backgroundColor: '#dee1e3',

View file

@ -1,12 +1,9 @@
import React from 'react' import React from 'react'
import { Text, TouchableHighlight, StyleSheet } from 'react-native' import { StyleSheet, Text, TouchableHighlight } from 'react-native'
export default function SelectFiles ({ showFilePicker }) { export default function SelectFiles({ showFilePicker }) {
return ( return (
<TouchableHighlight <TouchableHighlight onPress={showFilePicker} style={styles.button}>
onPress={showFilePicker}
style={styles.button}
>
<Text style={styles.text}>Select files</Text> <Text style={styles.text}>Select files</Text>
</TouchableHighlight> </TouchableHighlight>
) )

View file

@ -1,26 +1,30 @@
import * as FileSystem from 'expo-file-system'
import base64 from 'base64-js' import base64 from 'base64-js'
import * as FileSystem from 'expo-file-system'
export default function getTusFileReader (file, chunkSize, cb) { export default function getTusFileReader(file, chunkSize, cb) {
FileSystem.getInfoAsync(file.uri, { size: true }).then((info) => { FileSystem.getInfoAsync(file.uri, { size: true })
cb(null, new TusFileReader(file, info.size)) .then((info) => {
}).catch(cb) cb(null, new TusFileReader(file, info.size))
})
.catch(cb)
} }
class TusFileReader { class TusFileReader {
constructor (file, size) { constructor(file, size) {
this.file = file this.file = file
this.size = size this.size = size
} }
slice (start, end, cb) { slice(start, end, cb) {
const options = { const options = {
encoding: FileSystem.EncodingType.Base64, encoding: FileSystem.EncodingType.Base64,
length: Math.min(end, this.size) - start, length: Math.min(end, this.size) - start,
position: start, position: start,
} }
FileSystem.readAsStringAsync(this.file.uri, options).then((data) => { FileSystem.readAsStringAsync(this.file.uri, options)
cb(null, base64.toByteArray(data)) .then((data) => {
}).catch(cb) cb(null, base64.toByteArray(data))
})
.catch(cb)
} }
} }

View file

@ -1,9 +1,5 @@
/* eslint-disable no-shadow */ /** biome-ignore-all lint/nursery/useUniqueElementIds: it's fine */
/* eslint-disable jsx-a11y/media-has-caption */ import Uppy from '@uppy/core'
/* eslint-disable react/button-has-type */
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable react/react-in-jsx-scope */
import React, { useState, useRef } from 'react'
import { import {
Dropzone, Dropzone,
FilesGrid, FilesGrid,
@ -11,16 +7,15 @@ import {
UploadButton, UploadButton,
UppyContextProvider, UppyContextProvider,
} from '@uppy/react' } from '@uppy/react'
import Uppy from '@uppy/core' import UppyRemoteSources from '@uppy/remote-sources'
import UppyScreenCapture from '@uppy/screen-capture'
import Tus from '@uppy/tus' import Tus from '@uppy/tus'
import UppyWebcam from '@uppy/webcam' import UppyWebcam from '@uppy/webcam'
import UppyRemoteSources from '@uppy/remote-sources' import React, { useRef, useState } from 'react'
import UppyScreenCapture from '@uppy/screen-capture'
import { RemoteSource } from './RemoteSource.js'
import Webcam from './Webcam.tsx'
import ScreenCapture from './ScreenCapture.tsx'
import CustomDropzone from './CustomDropzone.tsx' import CustomDropzone from './CustomDropzone.tsx'
import { RemoteSource } from './RemoteSource.js'
import ScreenCapture from './ScreenCapture.tsx'
import Webcam from './Webcam.tsx'
import './app.css' import './app.css'
import '@uppy/react/dist/styles.css' import '@uppy/react/dist/styles.css'

View file

@ -1,7 +1,4 @@
/* eslint-disable react/jsx-props-no-spreading */ import { ProviderIcon, useDropzone, useFileInput } from '@uppy/react'
/* eslint-disable react/react-in-jsx-scope */
/* eslint-disable react/button-has-type */
import { useDropzone, useFileInput, ProviderIcon } from '@uppy/react'
export interface CustomDropzoneProps { export interface CustomDropzoneProps {
openModal: (plugin: 'webcam' | 'dropbox' | 'screen-capture') => void openModal: (plugin: 'webcam' | 'dropbox' | 'screen-capture') => void
@ -16,7 +13,6 @@ export function CustomDropzone({ openModal }: CustomDropzoneProps) {
<input {...getInputProps()} className="hidden" /> <input {...getInputProps()} className="hidden" />
<div <div
{...getRootProps()} {...getRootProps()}
role="button"
className="border-2 border-dashed border-gray-300 rounded-lg p-6 bg-gray-50 transition-colors duration-200" className="border-2 border-dashed border-gray-300 rounded-lg p-6 bg-gray-50 transition-colors duration-200"
> >
<div className="flex items-center justify-center gap-4"> <div className="flex items-center justify-center gap-4">

View file

@ -1,5 +1,3 @@
/* eslint-disable react/jsx-props-no-spreading, react/button-has-type */
/* eslint-disable react/react-in-jsx-scope */
import React from 'react' import React from 'react'
type ButtonProps = { type ButtonProps = {

View file

@ -1,6 +1,4 @@
/* eslint-disable no-shadow */ import type { PartialTreeFile, PartialTreeFolderNode } from '@uppy/core'
/* eslint-disable react/react-in-jsx-scope */
import { type PartialTreeFile, PartialTreeFolderNode } from '@uppy/core'
import { useRemoteSource } from '@uppy/react' import { useRemoteSource } from '@uppy/react'
import type { AvailablePluginsKeys } from '@uppy/remote-sources' import type { AvailablePluginsKeys } from '@uppy/remote-sources'
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
@ -104,11 +102,12 @@ export function RemoteSource({
{state.breadcrumbs.map((breadcrumb, index) => ( {state.breadcrumbs.map((breadcrumb, index) => (
<> <>
{index > 0 && <span className="text-gray-500">&gt;</span>}{' '} {index > 0 && <span className="text-gray-500">&gt;</span>}{' '}
{index === state.breadcrumbs.length - 1 ? {index === state.breadcrumbs.length - 1 ? (
<span> <span>
{breadcrumb.type === 'root' ? 'Dropbox' : breadcrumb.data.name} {breadcrumb.type === 'root' ? 'Dropbox' : breadcrumb.data.name}
</span> </span>
: <button ) : (
<button
type="button" type="button"
className="text-blue-500" className="text-blue-500"
key={breadcrumb.id} key={breadcrumb.id}
@ -116,7 +115,7 @@ export function RemoteSource({
> >
{breadcrumb.type === 'root' ? 'Dropbox' : breadcrumb.data.name} {breadcrumb.type === 'root' ? 'Dropbox' : breadcrumb.data.name}
</button> </button>
} )}
</> </>
))} ))}
<div className="flex items-center gap-2 ml-auto"> <div className="flex items-center gap-2 ml-auto">
@ -134,9 +133,10 @@ export function RemoteSource({
</div> </div>
<ul className="p-4 flex-1 overflow-y-auto"> <ul className="p-4 flex-1 overflow-y-auto">
{state.loading ? {state.loading ? (
<p>loading...</p> <p>loading...</p>
: state.partialTree.map((item) => { ) : (
state.partialTree.map((item) => {
if (item.type === 'file') { if (item.type === 'file') {
return <File key={item.id} item={item} checkbox={checkbox} /> return <File key={item.id} item={item} checkbox={checkbox} />
} }
@ -152,7 +152,7 @@ export function RemoteSource({
} }
return null return null
}) })
} )}
</ul> </ul>
{state.selectedAmount > 0 && ( {state.selectedAmount > 0 && (

View file

@ -1,6 +1,5 @@
/* eslint-disable react/react-in-jsx-scope */
import React, { useEffect } from 'react'
import { useScreenCapture } from '@uppy/react' import { useScreenCapture } from '@uppy/react'
import React, { useEffect } from 'react'
import MediaCapture from './MediaCapture.tsx' import MediaCapture from './MediaCapture.tsx'
export interface ScreenCaptureProps { export interface ScreenCaptureProps {

View file

@ -1,7 +1,5 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable react/react-in-jsx-scope */
import React, { useEffect } from 'react'
import { useWebcam } from '@uppy/react' import { useWebcam } from '@uppy/react'
import React, { useEffect } from 'react'
import MediaCapture from './MediaCapture.tsx' import MediaCapture from './MediaCapture.tsx'
export interface WebcamProps { export interface WebcamProps {

View file

@ -1,4 +1,4 @@
@import 'tailwindcss'; @import "tailwindcss";
/* example how to use the data attributes to apply custom styles */ /* example how to use the data attributes to apply custom styles */
/* button[data-uppy-element="upload-button"][data-state="uploading"] { /* button[data-uppy-element="upload-button"][data-state="uploading"] {

View file

@ -1,4 +1,3 @@
/* eslint-disable react/react-in-jsx-scope */
import React from 'react' import React from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import App from './App.js' import App from './App.js'

View file

@ -15,8 +15,8 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"isolatedModules": true, "isolatedModules": true,
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx"
}, },
"include": ["src", "src/**/*.tsx"], "include": ["src", "src/**/*.tsx"],
"references": [{ "path": "./tsconfig.node.json" }], "references": [{ "path": "./tsconfig.node.json" }]
} }

View file

@ -1,8 +1,7 @@
/* eslint-disable import/no-extraneous-dependencies */
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// @ts-expect-error untyped for some reason but fine // @ts-expect-error untyped for some reason but fine
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import { defineConfig } from 'vite'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({

View file

@ -1,16 +1,15 @@
import { compose, combineReducers, applyMiddleware } from 'redux'
import { configureStore } from '@reduxjs/toolkit' import { configureStore } from '@reduxjs/toolkit'
import logger from 'redux-logger'
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import ReduxStore from '@uppy/store-redux'
import * as uppyReduxStore from '@uppy/store-redux'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import ReduxStore, * as uppyReduxStore from '@uppy/store-redux'
import Tus from '@uppy/tus' import Tus from '@uppy/tus'
import { applyMiddleware, combineReducers, compose } from 'redux'
import logger from 'redux-logger'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'
function counter (state = 0, action) { function counter(state = 0, action) {
switch (action.type) { switch (action.type) {
case 'INCREMENT': case 'INCREMENT':
return state + 1 return state + 1
@ -28,31 +27,30 @@ const reducer = combineReducers({
uppy: uppyReduxStore.reducer, uppy: uppyReduxStore.reducer,
}) })
let enhancer = applyMiddleware( let enhancer = applyMiddleware(uppyReduxStore.middleware(), logger)
uppyReduxStore.middleware(),
logger,
)
if (typeof __REDUX_DEVTOOLS_EXTENSION__ !== 'undefined') { if (typeof __REDUX_DEVTOOLS_EXTENSION__ !== 'undefined') {
// eslint-disable-next-line no-undef
enhancer = compose(enhancer, __REDUX_DEVTOOLS_EXTENSION__()) enhancer = compose(enhancer, __REDUX_DEVTOOLS_EXTENSION__())
} }
const store = configureStore({ const store = configureStore({
reducer, reducer,
enhancers: [enhancer], enhancers: [enhancer],
middleware: (getDefaultMiddleware) => getDefaultMiddleware({ middleware: (getDefaultMiddleware) =>
serializableCheck: { getDefaultMiddleware({
ignoredActions: [uppyReduxStore.STATE_UPDATE], serializableCheck: {
ignoreState: true, ignoredActions: [uppyReduxStore.STATE_UPDATE],
}, ignoreState: true,
}), },
}),
}) })
// Counter example from https://github.com/reactjs/redux/blob/master/examples/counter-vanilla/index.html // Counter example from https://github.com/reactjs/redux/blob/master/examples/counter-vanilla/index.html
const valueEl = document.querySelector('#value') const valueEl = document.querySelector('#value')
function getCounter () { return store.getState().counter } function getCounter() {
function render () { return store.getState().counter
}
function render() {
valueEl.innerHTML = getCounter().toString() valueEl.innerHTML = getCounter().toString()
} }
render() render()

View file

@ -1 +1 @@
@import 'tailwindcss'; @import "tailwindcss";

View file

@ -1,14 +1,14 @@
<script lang="ts"> <script lang="ts">
import { useDropzone, useFileInput, ProviderIcon } from '@uppy/svelte' import { ProviderIcon, useDropzone, useFileInput } from '@uppy/svelte'
interface Props { interface Props {
openModal: (plugin: 'webcam' | 'dropbox' | 'screen-capture') => void openModal: (plugin: 'webcam' | 'dropbox' | 'screen-capture') => void
} }
const { openModal }: Props = $props() const { openModal }: Props = $props()
const { getRootProps, getInputProps } = useDropzone({ noClick: true }) const { getRootProps, getInputProps } = useDropzone({ noClick: true })
const { getButtonProps, getInputProps: getFileInputProps } = useFileInput() const { getButtonProps, getInputProps: getFileInputProps } = useFileInput()
</script> </script>
<div> <div>

View file

@ -1,30 +1,30 @@
<script lang="ts"> <script lang="ts">
type ButtonProps = Record<string, unknown> type ButtonProps = Record<string, unknown>
type VideoProps = Record<string, unknown> type VideoProps = Record<string, unknown>
interface Props { interface Props {
title: string title: string
close: () => void close: () => void
videoProps: VideoProps videoProps: VideoProps
primaryActionButtonProps: ButtonProps primaryActionButtonProps: ButtonProps
primaryActionButtonLabel: string primaryActionButtonLabel: string
recordButtonProps: ButtonProps recordButtonProps: ButtonProps
stopRecordingButtonProps: ButtonProps stopRecordingButtonProps: ButtonProps
submitButtonProps: ButtonProps submitButtonProps: ButtonProps
discardButtonProps: ButtonProps discardButtonProps: ButtonProps
} }
const { const {
title, title,
close, close,
videoProps, videoProps,
primaryActionButtonProps, primaryActionButtonProps,
primaryActionButtonLabel, primaryActionButtonLabel,
recordButtonProps, recordButtonProps,
stopRecordingButtonProps, stopRecordingButtonProps,
submitButtonProps, submitButtonProps,
discardButtonProps, discardButtonProps,
}: Props = $props() }: Props = $props()
</script> </script>
<div class="p-4 max-w-lg w-full"> <div class="p-4 max-w-lg w-full">

View file

@ -1,32 +1,32 @@
<script lang="ts"> <script lang="ts">
import { useRemoteSource } from '@uppy/svelte' import type { PartialTreeFolderNode } from '@uppy/core'
import type { AvailablePluginsKeys } from '@uppy/remote-sources' import type { AvailablePluginsKeys } from '@uppy/remote-sources'
import type { PartialTreeFolderNode } from '@uppy/core' import { useRemoteSource } from '@uppy/svelte'
interface Props { interface Props {
close: () => void close: () => void
id: AvailablePluginsKeys id: AvailablePluginsKeys
} }
const { close, id }: Props = $props() const { close, id }: Props = $props()
// Use the value directly, destructuring looses reactivity // Use the value directly, destructuring looses reactivity
const remoteSource = useRemoteSource(id) const remoteSource = useRemoteSource(id)
const dtf = new Intl.DateTimeFormat('en-US', { const dtf = new Intl.DateTimeFormat('en-US', {
dateStyle: 'short', dateStyle: 'short',
timeStyle: 'short', timeStyle: 'short',
}) })
function setFolderCheckboxIndeterminate( function setFolderCheckboxIndeterminate(
node: HTMLInputElement, node: HTMLInputElement,
item: PartialTreeFolderNode, item: PartialTreeFolderNode,
) { ) {
if (item.status === 'partial') { if (item.status === 'partial') {
// Can only be set via JS // Can only be set via JS
node.indeterminate = true node.indeterminate = true
}
} }
}
</script> </script>
{#if !remoteSource.state.authenticated} {#if !remoteSource.state.authenticated}

View file

@ -1,21 +1,21 @@
<script lang="ts"> <script lang="ts">
import MediaCapture from './MediaCapture.svelte' import { useScreenCapture } from '@uppy/svelte'
import { untrack } from 'svelte' import { untrack } from 'svelte'
import { useScreenCapture } from '@uppy/svelte' import MediaCapture from './MediaCapture.svelte'
interface Props { interface Props {
close: () => void close: () => void
}
const { close }: Props = $props()
const screenCaptureStore = useScreenCapture({ onSubmit: close })
$effect(() => {
untrack(() => screenCaptureStore.start())
return () => {
untrack(() => screenCaptureStore.stop())
} }
})
const { close }: Props = $props()
const screenCaptureStore = useScreenCapture({ onSubmit: close })
$effect(() => {
untrack(() => screenCaptureStore.start())
return () => {
untrack(() => screenCaptureStore.stop())
}
})
</script> </script>
<MediaCapture <MediaCapture

View file

@ -1,21 +1,21 @@
<script lang="ts"> <script lang="ts">
import MediaCapture from './MediaCapture.svelte' import { useWebcam } from '@uppy/svelte'
import { untrack } from 'svelte' import { untrack } from 'svelte'
import { useWebcam } from '@uppy/svelte' import MediaCapture from './MediaCapture.svelte'
interface Props { interface Props {
close: () => void close: () => void
}
const { close }: Props = $props()
const webcamStore = useWebcam({ onSubmit: close })
$effect(() => {
untrack(() => webcamStore.start())
return () => {
untrack(() => webcamStore.stop())
} }
})
const { close }: Props = $props()
const webcamStore = useWebcam({ onSubmit: close })
$effect(() => {
untrack(() => webcamStore.start())
return () => {
untrack(() => webcamStore.stop())
}
})
</script> </script>
<MediaCapture <MediaCapture

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import '../app.css'; import '../app.css'
let { children } = $props(); const { children } = $props()
</script> </script>
{@render children()} {@render children()}

View file

@ -1,43 +1,43 @@
<script lang="ts"> <script lang="ts">
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Tus from '@uppy/tus' import UppyRemoteSources from '@uppy/remote-sources'
import UppyWebcam from '@uppy/webcam' import UppyScreenCapture from '@uppy/screen-capture'
import UppyRemoteSources from '@uppy/remote-sources' import {
import UppyScreenCapture from '@uppy/screen-capture' Dropzone,
import { FilesGrid,
UppyContextProvider, FilesList,
Dropzone, UploadButton,
FilesList, UppyContextProvider,
FilesGrid, } from '@uppy/svelte'
UploadButton, import Tus from '@uppy/tus'
} from '@uppy/svelte' import UppyWebcam from '@uppy/webcam'
import '@uppy/svelte/dist/styles.css' import '@uppy/svelte/dist/styles.css'
import CustomDropzone from '../components/CustomDropzone.svelte' import CustomDropzone from '../components/CustomDropzone.svelte'
import Webcam from '../components/Webcam.svelte' import RemoteSource from '../components/RemoteSource.svelte'
import RemoteSource from '../components/RemoteSource.svelte' import ScreenCapture from '../components/ScreenCapture.svelte'
import ScreenCapture from '../components/ScreenCapture.svelte' import Webcam from '../components/Webcam.svelte'
const uppy = new Uppy() const uppy = new Uppy()
.use(Tus, { .use(Tus, {
endpoint: 'https://tusd.tusdemo.net/files/', endpoint: 'https://tusd.tusdemo.net/files/',
}) })
.use(UppyWebcam) .use(UppyWebcam)
.use(UppyScreenCapture) .use(UppyScreenCapture)
.use(UppyRemoteSources, { companionUrl: 'http://localhost:3020' }) .use(UppyRemoteSources, { companionUrl: 'http://localhost:3020' })
let dialogRef: HTMLDialogElement let dialogRef: HTMLDialogElement
let modalPlugin = $state<'webcam' | 'dropbox' | 'screen-capture' | null>(null) let modalPlugin = $state<'webcam' | 'dropbox' | 'screen-capture' | null>(null)
function openModal(plugin: 'webcam' | 'dropbox' | 'screen-capture') { function openModal(plugin: 'webcam' | 'dropbox' | 'screen-capture') {
modalPlugin = plugin modalPlugin = plugin
dialogRef?.showModal() dialogRef?.showModal()
} }
function closeModal() { function closeModal() {
modalPlugin = null modalPlugin = null
dialogRef?.close() dialogRef?.close()
} }
</script> </script>
<UppyContextProvider {uppy}> <UppyContextProvider {uppy}>

View file

@ -1,18 +1,18 @@
import adapter from '@sveltejs/adapter-auto'; import adapter from '@sveltejs/adapter-auto'
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://svelte.dev/docs/kit/integrations // Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors // for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
// adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list. // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter. // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters. // See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter() adapter: adapter(),
} },
}; }
export default config; export default config

View file

@ -10,8 +10,8 @@
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"module": "preserve", "module": "preserve",
"moduleResolution": "bundler", "moduleResolution": "bundler"
}, }
// Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
// except $lib which is handled by https://svelte.dev/docs/kit/configuration#files // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
// //

View file

@ -1,6 +1,5 @@
import tailwindcss from '@tailwindcss/vite'
import { sveltekit } from '@sveltejs/kit/vite' import { sveltekit } from '@sveltejs/kit/vite'
// eslint-disable-next-line import/no-extraneous-dependencies import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
export default defineConfig({ export default defineConfig({

View file

@ -1,11 +1,11 @@
import { marked } from 'marked'
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import DropTarget from '@uppy/drop-target'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import Transloadit from '@uppy/transloadit' import DropTarget from '@uppy/drop-target'
import RemoteSources from '@uppy/remote-sources'
import Webcam from '@uppy/webcam'
import ImageEditor from '@uppy/image-editor' import ImageEditor from '@uppy/image-editor'
import RemoteSources from '@uppy/remote-sources'
import Transloadit from '@uppy/transloadit'
import Webcam from '@uppy/webcam'
import { marked } from 'marked'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'
@ -14,13 +14,12 @@ import '@uppy/image-editor/dist/style.css'
const TRANSLOADIT_EXAMPLE_KEY = '35c1aed03f5011e982b6afe82599b6a0' const TRANSLOADIT_EXAMPLE_KEY = '35c1aed03f5011e982b6afe82599b6a0'
const TRANSLOADIT_EXAMPLE_TEMPLATE = '0b2ee2bc25dc43619700c2ce0a75164a' const TRANSLOADIT_EXAMPLE_TEMPLATE = '0b2ee2bc25dc43619700c2ce0a75164a'
function matchFilesAndThumbs (results) { function matchFilesAndThumbs(results) {
const filesById = {} const filesById = {}
const thumbsById = {} const thumbsById = {}
for (const [stepName, result] of Object.entries(results)) { for (const [stepName, result] of Object.entries(results)) {
// eslint-disable-next-line no-shadow result.forEach((result) => {
result.forEach(result => {
if (stepName === 'thumbnails') { if (stepName === 'thumbnails') {
thumbsById[result.original_id] = result thumbsById[result.original_id] = result
} else { } else {
@ -39,7 +38,7 @@ function matchFilesAndThumbs (results) {
* A textarea for markdown text, with support for file attachments. * A textarea for markdown text, with support for file attachments.
*/ */
class MarkdownTextarea { class MarkdownTextarea {
constructor (element) { constructor(element) {
this.element = element this.element = element
this.controls = document.createElement('div') this.controls = document.createElement('div')
this.controls.classList.add('mdtxt-controls') this.controls.classList.add('mdtxt-controls')
@ -52,7 +51,7 @@ class MarkdownTextarea {
) )
} }
install () { install() {
const { element } = this const { element } = this
const wrapper = document.createElement('div') const wrapper = document.createElement('div')
wrapper.classList.add('mdtxt') wrapper.classList.add('mdtxt')
@ -82,11 +81,9 @@ class MarkdownTextarea {
this.uppy.on('complete', (result) => { this.uppy.on('complete', (result) => {
const { successful, failed, transloadit } = result const { successful, failed, transloadit } = result
if (successful.length !== 0) { if (successful.length !== 0) {
this.insertAttachments( this.insertAttachments(matchFilesAndThumbs(transloadit[0].results))
matchFilesAndThumbs(transloadit[0].results),
)
} else { } else {
failed.forEach(error => { failed.forEach((error) => {
console.error(error) console.error(error)
this.reportUploadError(error) this.reportUploadError(error)
}) })
@ -95,14 +92,14 @@ class MarkdownTextarea {
}) })
} }
reportUploadError (err) { reportUploadError(err) {
this.uploadLine.classList.add('error') this.uploadLine.classList.add('error')
const message = document.createElement('span') const message = document.createElement('span')
message.appendChild(document.createTextNode(err.message)) message.appendChild(document.createTextNode(err.message))
this.uploadLine.insertChild(message, this.uploadLine.firstChild) this.uploadLine.insertChild(message, this.uploadLine.firstChild)
} }
unreportUploadError () { unreportUploadError() {
this.uploadLine.classList.remove('error') this.uploadLine.classList.remove('error')
const message = this.uploadLine.querySelector('message') const message = this.uploadLine.querySelector('message')
if (message) { if (message) {
@ -110,13 +107,16 @@ class MarkdownTextarea {
} }
} }
insertAttachments (attachments) { insertAttachments(attachments) {
attachments.forEach((attachment) => { attachments.forEach((attachment) => {
const { file, thumb } = attachment const { file, thumb } = attachment
const link = `\n[LABEL](${file.ssl_url})\n` const link = `\n[LABEL](${file.ssl_url})\n`
const labelText = `View File ${file.basename}` const labelText = `View File ${file.basename}`
if (thumb) { if (thumb) {
this.element.value += link.replace('LABEL', `![${labelText}](${thumb.ssl_url})`) this.element.value += link.replace(
'LABEL',
`![${labelText}](${thumb.ssl_url})`,
)
} else { } else {
this.element.value += link.replace('LABEL', labelText) this.element.value += link.replace('LABEL', labelText)
} }
@ -124,7 +124,7 @@ class MarkdownTextarea {
} }
uploadFiles = (files) => { uploadFiles = (files) => {
const filesForUppy = files.map(file => { const filesForUppy = files.map((file) => {
return { return {
data: file, data: file,
type: file.type, type: file.type,
@ -139,7 +139,7 @@ class MarkdownTextarea {
const textarea = new MarkdownTextarea(document.querySelector('#new textarea')) const textarea = new MarkdownTextarea(document.querySelector('#new textarea'))
textarea.install() textarea.install()
function renderSnippet (title, text) { function renderSnippet(title, text) {
const template = document.querySelector('#snippet') const template = document.querySelector('#snippet')
const newSnippet = document.importNode(template.content, true) const newSnippet = document.importNode(template.content, true)
const titleEl = newSnippet.querySelector('.snippet-title') const titleEl = newSnippet.querySelector('.snippet-title')
@ -152,13 +152,13 @@ function renderSnippet (title, text) {
list.insertBefore(newSnippet, list.firstChild) list.insertBefore(newSnippet, list.firstChild)
} }
function saveSnippet (title, text) { function saveSnippet(title, text) {
const id = parseInt(localStorage.numSnippets || 0, 10) const id = parseInt(localStorage.numSnippets || 0, 10)
localStorage[`snippet_${id}`] = JSON.stringify({ title, text }) localStorage[`snippet_${id}`] = JSON.stringify({ title, text })
localStorage.numSnippets = id + 1 localStorage.numSnippets = id + 1
} }
function loadSnippets () { function loadSnippets() {
for (let id = 0; localStorage[`snippet_${id}`] != null; id += 1) { for (let id = 0; localStorage[`snippet_${id}`] != null; id += 1) {
const { title, text } = JSON.parse(localStorage[`snippet_${id}`]) const { title, text } = JSON.parse(localStorage[`snippet_${id}`])
renderSnippet(title, text) renderSnippet(title, text)
@ -168,16 +168,13 @@ function loadSnippets () {
document.querySelector('#new').addEventListener('submit', (event) => { document.querySelector('#new').addEventListener('submit', (event) => {
event.preventDefault() event.preventDefault()
const title = event.target.elements['title'].value const title = event.target.elements.title.value || 'Unnamed Snippet'
|| 'Unnamed Snippet'
const text = textarea.element.value const text = textarea.element.value
saveSnippet(title, text) saveSnippet(title, text)
renderSnippet(title, text) renderSnippet(title, text)
// eslint-disable-next-line no-param-reassign
event.target.querySelector('input').value = '' event.target.querySelector('input').value = ''
// eslint-disable-next-line no-param-reassign
event.target.querySelector('textarea').value = '' event.target.querySelector('textarea').value = ''
}) })

View file

@ -1,11 +1,11 @@
import Transloadit, { COMPANION_URL } from '@uppy/transloadit'
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Form from '@uppy/form'
import Dashboard from '@uppy/dashboard' import Dashboard from '@uppy/dashboard'
import RemoteSources from '@uppy/remote-sources' import Form from '@uppy/form'
import ImageEditor from '@uppy/image-editor' import ImageEditor from '@uppy/image-editor'
import Webcam from '@uppy/webcam'
import ProgressBar from '@uppy/progress-bar' import ProgressBar from '@uppy/progress-bar'
import RemoteSources from '@uppy/remote-sources'
import Transloadit, { COMPANION_URL } from '@uppy/transloadit'
import Webcam from '@uppy/webcam'
import '@uppy/core/dist/style.css' import '@uppy/core/dist/style.css'
import '@uppy/dashboard/dist/style.css' import '@uppy/dashboard/dist/style.css'
@ -59,13 +59,11 @@ const formUppy = new Uppy({
}) })
formUppy.on('error', (err) => { formUppy.on('error', (err) => {
document.querySelector('#test-form .error') document.querySelector('#test-form .error').textContent = err.message
.textContent = err.message
}) })
formUppy.on('upload-error', (file, err) => { formUppy.on('upload-error', (file, err) => {
document.querySelector('#test-form .error') document.querySelector('#test-form .error').textContent = err.message
.textContent = err.message
}) })
formUppy.on('complete', ({ transloadit }) => { formUppy.on('complete', ({ transloadit }) => {
@ -163,15 +161,13 @@ const dashboardModal = new Uppy({
dashboardModal.on('complete', ({ transloadit, successful, failed }) => { dashboardModal.on('complete', ({ transloadit, successful, failed }) => {
if (failed?.length !== 0) { if (failed?.length !== 0) {
// eslint-disable-next-line no-console
console.error('it failed', failed) console.error('it failed', failed)
} else { } else {
// eslint-disable-next-line no-console
console.log('success', { transloadit, successful }) console.log('success', { transloadit, successful })
} }
}) })
function openModal () { function openModal() {
dashboardModal.getPlugin('Dashboard').openModal() dashboardModal.getPlugin('Dashboard').openModal()
} }
@ -208,7 +204,7 @@ window.doUpload = (event) => {
errorEl.classList.add('hidden') errorEl.classList.add('hidden')
resultEl.textContent = JSON.stringify(transloadit[0].results, null, 2) resultEl.textContent = JSON.stringify(transloadit[0].results, null, 2)
const resizedUrl = transloadit[0].results['resize'][0]['ssl_url'] const resizedUrl = transloadit[0].results.resize[0].ssl_url
const img = document.createElement('img') const img = document.createElement('img')
img.src = resizedUrl img.src = resizedUrl
document.getElementById('upload-result-image').appendChild(img) document.getElementById('upload-result-image').appendChild(img)

View file

@ -1,13 +1,12 @@
#!/usr/bin/env node #!/usr/bin/env node
import http from 'node:http' import http from 'node:http'
import qs from 'node:querystring' import qs from 'node:querystring'
import he from 'he' import he from 'he'
const e = he.encode const e = he.encode
function Header () { function Header() {
return ` return `
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
@ -28,7 +27,7 @@ function Header () {
` `
} }
function Footer () { function Footer() {
return ` return `
</main> </main>
</body> </body>
@ -36,31 +35,28 @@ function Footer () {
` `
} }
function FormFields (fields) { function FormFields(fields) {
function Field ([name, value]) { function Field([name, value]) {
if (name === 'transloadit') return '' if (name === 'transloadit') return ''
let isValueJSON = false let isValueJSON = false
if (value.startsWith('{') || value.startsWith('[')) { if (value.startsWith('{') || value.startsWith('[')) {
try { try {
// eslint-disable-next-line no-param-reassign value = JSON.stringify(JSON.parse(value), null, 2)
value = JSON.stringify(
JSON.parse(value),
null,
2,
)
isValueJSON = true isValueJSON = true
} catch { } catch {
// Nothing // Nothing
} }
} }
const prettyValue = isValueJSON ? ` const prettyValue = isValueJSON
? `
<details open> <details open>
<code> <code>
<pre style="max-width: 100%; max-height: 400px; white-space: pre-wrap; overflow: auto;">${e(value)}</pre> <pre style="max-width: 100%; max-height: 400px; white-space: pre-wrap; overflow: auto;">${e(value)}</pre>
</code> </code>
</details> </details>
` : e(value) `
: e(value)
return ` return `
<dt>${e(name)}</dt> <dt>${e(name)}</dt>
@ -78,8 +74,8 @@ function FormFields (fields) {
` `
} }
function UploadsList (uploads) { function UploadsList(uploads) {
function Upload (upload) { function Upload(upload) {
return `<li>${e(upload.name)}</li>` return `<li>${e(upload.name)}</li>`
} }
@ -90,12 +86,12 @@ function UploadsList (uploads) {
` `
} }
function ResultsList (results) { function ResultsList(results) {
function Result (result) { function Result(result) {
return `<li>${e(result.name)} <a href="${result.ssl_url}" target="_blank">View</a></li>` return `<li>${e(result.name)} <a href="${result.ssl_url}" target="_blank">View</a></li>`
} }
function ResultsSection (stepName) { function ResultsSection(stepName) {
return ` return `
<h2>${e(stepName)}</h2> <h2>${e(stepName)}</h2>
<ul> <ul>
@ -104,12 +100,10 @@ function ResultsList (results) {
` `
} }
return Object.keys(results) return Object.keys(results).map(ResultsSection).join('\n')
.map(ResultsSection)
.join('\n')
} }
function AssemblyResult (assembly) { function AssemblyResult(assembly) {
return ` return `
<h1>${e(assembly.assembly_id)} (${e(assembly.ok)})</h1> <h1>${e(assembly.assembly_id)} (${e(assembly.ok)})</h1>
${UploadsList(assembly.uploads)} ${UploadsList(assembly.uploads)}
@ -117,14 +111,14 @@ function AssemblyResult (assembly) {
` `
} }
function onrequest (req, res) { function onrequest(req, res) {
if (req.url !== '/test') { if (req.url !== '/test') {
res.writeHead(404, { 'content-type': 'text/html' }) res.writeHead(404, { 'content-type': 'text/html' })
res.end('404') res.end('404')
return return
} }
function onbody (body) { function onbody(body) {
const fields = qs.parse(body) const fields = qs.parse(body)
const result = JSON.parse(fields.uppyResult) const result = JSON.parse(fields.uppyResult)
const assemblies = result[0].transloadit const assemblies = result[0].transloadit
@ -140,7 +134,9 @@ function onrequest (req, res) {
{ {
let body = '' let body = ''
req.on('data', (chunk) => { body += chunk }) req.on('data', (chunk) => {
body += chunk
})
req.on('end', () => { req.on('end', () => {
onbody(body) onbody(body)
}) })

View file

@ -6,11 +6,13 @@ const companion = require('@uppy/companion')
const app = express() const app = express()
app.use(bodyParser.json()) app.use(bodyParser.json())
app.use(session({ app.use(
secret: 'some-secret', session({
resave: true, secret: 'some-secret',
saveUninitialized: true, resave: true,
})) saveUninitialized: true,
}),
)
app.use((req, res, next) => { app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*') res.setHeader('Access-Control-Allow-Origin', req.headers.origin || '*')

View file

@ -42,23 +42,23 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'
import Uppy from '@uppy/core' import Uppy from '@uppy/core'
import Tus from '@uppy/tus'
import {
UppyContextProvider,
Dropzone,
FilesList,
FilesGrid,
UploadButton,
} from '@uppy/vue'
import CustomDropzone from './Dropzone.vue'
import Webcam from './Webcam.vue'
import RemoteSource from './RemoteSource.vue'
import ScreenCapture from './ScreenCapture.vue'
import UppyWebcam from '@uppy/webcam'
import UppyRemoteSources from '@uppy/remote-sources' import UppyRemoteSources from '@uppy/remote-sources'
import UppyScreenCapture from '@uppy/screen-capture' import UppyScreenCapture from '@uppy/screen-capture'
import Tus from '@uppy/tus'
import {
Dropzone,
FilesGrid,
FilesList,
UploadButton,
UppyContextProvider,
} from '@uppy/vue'
import UppyWebcam from '@uppy/webcam'
import { computed, ref } from 'vue'
import CustomDropzone from './Dropzone.vue'
import RemoteSource from './RemoteSource.vue'
import ScreenCapture from './ScreenCapture.vue'
import Webcam from './Webcam.vue'
const dialogRef = ref<HTMLDialogElement | null>(null) const dialogRef = ref<HTMLDialogElement | null>(null)
const modalPlugin = ref<'webcam' | 'dropbox' | 'screen-capture' | null>(null) const modalPlugin = ref<'webcam' | 'dropbox' | 'screen-capture' | null>(null)

View file

@ -51,7 +51,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useDropzone, useFileInput, ProviderIcon } from '@uppy/vue' import { ProviderIcon, useDropzone, useFileInput } from '@uppy/vue'
const props = defineProps<{ const props = defineProps<{
openModal: (plugin: 'webcam' | 'dropbox' | 'screen-capture') => void openModal: (plugin: 'webcam' | 'dropbox' | 'screen-capture') => void

View file

@ -132,9 +132,9 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRemoteSource } from '@uppy/vue' import type { PartialTreeFolderNode } from '@uppy/core'
import type { AvailablePluginsKeys } from '@uppy/remote-sources' import type { AvailablePluginsKeys } from '@uppy/remote-sources'
import { PartialTreeFolderNode } from '@uppy/core' import { useRemoteSource } from '@uppy/vue'
const props = defineProps<{ const props = defineProps<{
close: () => void close: () => void

View file

@ -1 +1 @@
@import 'tailwindcss'; @import "tailwindcss";

View file

@ -1,7 +1,7 @@
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
import vue from '@vitejs/plugin-vue'
import { defineConfig } from 'vite'
const ROOT = new URL('../../', import.meta.url) const ROOT = new URL('../../', import.meta.url)

View file

@ -6,7 +6,7 @@ const upload = multer({
storage: multer.memoryStorage(), storage: multer.memoryStorage(),
}) })
function uploadRoute (req, res) { function uploadRoute(req, res) {
res.json({ res.json({
files: req.files.map((file) => { files: req.files.map((file) => {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign

View file

@ -24,8 +24,10 @@
"build:css": "yarn node ./bin/build-css.js && yarn workspace @uppy/components build:tailwind && yarn node ./bin/copy-tailwind-css.mjs", "build:css": "yarn node ./bin/build-css.js && yarn workspace @uppy/components build:tailwind && yarn node ./bin/copy-tailwind-css.mjs",
"build:js": "npm-run-all build:js:typeless build:locale-pack build:angular build:bundle", "build:js": "npm-run-all build:js:typeless build:locale-pack build:angular build:bundle",
"build:js:typeless": "npm-run-all build:companion build:svelte", "build:js:typeless": "npm-run-all build:companion build:svelte",
"build:locale-pack": "yarn workspace @uppy-dev/locale-pack build && eslint packages/@uppy/locales/src/en_US.ts --fix && yarn workspace @uppy-dev/locale-pack test unused", "build:locale-pack": "yarn workspace @uppy-dev/locale-pack build && yarn workspace @uppy-dev/locale-pack test unused",
"build:svelte": "yarn workspace @uppy/svelte build", "build:svelte": "yarn workspace @uppy/svelte build",
"check": "yarn exec biome check --write",
"check:ci": "yarn exec biome ci",
"contributors:save": "yarn node ./bin/update-contributors.mjs", "contributors:save": "yarn node ./bin/update-contributors.mjs",
"dev": "yarn workspace @uppy-dev/dev dev", "dev": "yarn workspace @uppy-dev/dev dev",
"dev:with-companion": "npm-run-all --parallel start:companion dev", "dev:with-companion": "npm-run-all --parallel start:companion dev",
@ -36,15 +38,6 @@
"e2e:generate": "yarn workspace e2e generate-test", "e2e:generate": "yarn workspace e2e generate-test",
"e2e:headless": "yarn workspace e2e cypress:headless", "e2e:headless": "yarn workspace e2e cypress:headless",
"e2e:skip-build": "npm-run-all --parallel watch:js:lib e2e:client start:companion:with-loadbalancer e2e:cypress", "e2e:skip-build": "npm-run-all --parallel watch:js:lib e2e:client start:companion:with-loadbalancer e2e:cypress",
"format": "prettier -w .",
"format:check": "prettier -c .",
"format:check-diff": "yarn format:check || (yarn format:show-diff && false)",
"format:show-diff": "git diff --quiet || (echo 'Unable to show a diff because there are unstaged changes'; false) && (prettier . -w --loglevel silent && git --no-pager diff; git restore .)",
"lint": "eslint . --cache",
"lint:css": "stylelint ./packages/**/*.scss",
"lint:css:fix": "stylelint ./packages/**/*.scss --fix",
"lint:fix": "yarn lint --fix",
"lint:staged": "lint-staged",
"release": "PACKAGES=$(yarn workspaces list --json) yarn workspace @uppy-dev/release interactive", "release": "PACKAGES=$(yarn workspaces list --json) yarn workspace @uppy-dev/release interactive",
"size": "echo 'JS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.js | gzip | wc -c && echo 'CSS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.css | gzip | wc -c", "size": "echo 'JS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.js | gzip | wc -c && echo 'CSS Bundle mingz:' && cat ./packages/uppy/dist/uppy.min.css | gzip | wc -c",
"start:companion": "bash bin/companion.sh", "start:companion": "bash bin/companion.sh",
@ -64,81 +57,36 @@
"watch:js:bundle": "onchange 'packages/{@uppy/,}*/src/**/*.{js,ts,jsx,tsx,svelte,scss,css}' --initial --verbose -- yarn run build:bundle", "watch:js:bundle": "onchange 'packages/{@uppy/,}*/src/**/*.{js,ts,jsx,tsx,svelte,scss,css}' --initial --verbose -- yarn run build:bundle",
"watch:js:lib": "onchange 'packages/{@uppy/,}*/src/**/*.{js,ts,jsx,tsx,svelte,scss,css}' --initial --verbose -- yarn run build" "watch:js:lib": "onchange 'packages/{@uppy/,}*/src/**/*.{js,ts,jsx,tsx,svelte,scss,css}' --initial --verbose -- yarn run build"
}, },
"pre-commit": "lint:staged",
"lint-staged": {
"*.{js,mjs,cjs,jsx}": "eslint --fix",
"*.{ts,mts,cts,tsx}": [
"eslint --fix",
"prettier -w",
"eslint"
],
"*.{css,html,json,scss,vue,yaml,yml}": "prettier -w",
"*.md": [
"eslint --fix",
"prettier -w",
"eslint"
]
},
"resolutions": { "resolutions": {
"@types/eslint@^7.2.13": "^8.2.0",
"@types/react": "^18", "@types/react": "^18",
"@types/webpack-dev-server": "^4", "@types/webpack-dev-server": "^4",
"@vitest/utils": "patch:@vitest/utils@npm%3A1.2.1#./.yarn/patches/@vitest-utils-npm-1.2.1-3028846845.patch", "@vitest/utils": "patch:@vitest/utils@npm%3A1.2.1#./.yarn/patches/@vitest-utils-npm-1.2.1-3028846845.patch",
"p-queue": "patch:p-queue@npm%3A8.0.1#~/.yarn/patches/p-queue-npm-8.0.1-fe1ddcd827.patch", "p-queue": "patch:p-queue@npm%3A8.0.1#~/.yarn/patches/p-queue-npm-8.0.1-fe1ddcd827.patch",
"pre-commit": "patch:pre-commit@npm:1.2.2#.yarn/patches/pre-commit-npm-1.2.2-f30af83877.patch",
"resize-observer-polyfill": "patch:resize-observer-polyfill@npm%3A1.5.1#./.yarn/patches/resize-observer-polyfill-npm-1.5.1-603120e8a0.patch", "resize-observer-polyfill": "patch:resize-observer-polyfill@npm%3A1.5.1#./.yarn/patches/resize-observer-polyfill-npm-1.5.1-603120e8a0.patch",
"start-server-and-test": "patch:start-server-and-test@npm:1.14.0#.yarn/patches/start-server-and-test-npm-1.14.0-841aa34fdf.patch", "start-server-and-test": "patch:start-server-and-test@npm:1.14.0#.yarn/patches/start-server-and-test-npm-1.14.0-841aa34fdf.patch",
"stylelint-order": "^6.0.0",
"stylelint@^9.10.1": "^16.0.0",
"uuid@^8.3.2": "patch:uuid@npm:8.3.2#.yarn/patches/uuid-npm-8.3.2-eca0baba53.patch" "uuid@^8.3.2": "patch:uuid@npm:8.3.2#.yarn/patches/uuid-npm-8.3.2-eca0baba53.patch"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-plugin": "^7.27.1", "@biomejs/biome": "2.0.5",
"@types/jasmine": "file:./private/@types/jasmine", "@types/jasmine": "file:./private/@types/jasmine",
"@types/jasminewd2": "file:./private/@types/jasmine", "@types/jasminewd2": "file:./private/@types/jasmine",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^8.35.0",
"autoprefixer": "^10.2.6", "autoprefixer": "^10.2.6",
"chalk": "^5.0.0", "chalk": "^5.0.0",
"cssnano": "^7.0.0", "cssnano": "^7.0.0",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"eslint": "^8.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-transloadit": "^2.0.0",
"eslint-plugin-compat": "^4.0.0",
"eslint-plugin-cypress": "^3.0.0",
"eslint-plugin-import": "^2.25.2",
"eslint-plugin-jest": "^28.0.0",
"eslint-plugin-jsdoc": "^48.0.0",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-markdown": "^5.0.0",
"eslint-plugin-no-only-tests": "^3.1.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prefer-import": "^0.0.1",
"eslint-plugin-promise": "^6.0.0",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"eslint-plugin-unicorn": "^53.0.0",
"execa": "^9.5.1", "execa": "^9.5.1",
"github-contributors-list": "^1.2.4", "github-contributors-list": "^1.2.4",
"glob": "^8.0.0", "glob": "^8.0.0",
"jsdom": "^24.0.0", "jsdom": "^24.0.0",
"lint-staged": "^15.0.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"onchange": "^7.1.0", "onchange": "^7.1.0",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"postcss-dir-pseudo-class": "^6.0.0", "postcss-dir-pseudo-class": "^6.0.0",
"postcss-logical": "^5.0.0", "postcss-logical": "^5.0.0",
"pre-commit": "^1.2.2",
"prettier": "^3.0.3",
"resolve": "^1.17.0", "resolve": "^1.17.0",
"sass": "^1.29.0", "sass": "^1.29.0",
"start-server-and-test": "^1.14.0", "start-server-and-test": "^1.14.0",
"stylelint": "^15.0.0",
"stylelint-config-rational-order": "^0.1.2",
"stylelint-config-standard": "^36.0.0",
"stylelint-config-standard-scss": "^13.0.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vitest": "^1.6.1", "vitest": "^1.6.1",
"vue-template-compiler": "workspace:*" "vue-template-compiler": "workspace:*"
@ -148,4 +96,4 @@
"node": "^16.15.0 || >=18.0.0", "node": "^16.15.0 || >=18.0.0",
"yarn": "3.6.1" "yarn": "3.6.1"
} }
} }

View file

@ -1,17 +1,17 @@
import { Component, ChangeDetectionStrategy } from '@angular/core'; import { ChangeDetectionStrategy, Component } from "@angular/core";
import * as Dashboard from '@uppy/dashboard'; import { Uppy } from "@uppy/core";
import { Uppy } from '@uppy/core'; import type * as Dashboard from "@uppy/dashboard";
import { Body, Meta } from '@uppy/utils/lib/UppyFile'; import type { Body, Meta } from "@uppy/utils/lib/UppyFile";
@Component({ @Component({
selector: 'uppy-dashboard-demo', selector: "uppy-dashboard-demo",
template: `<uppy-dashboard-modal template: `<uppy-dashboard-modal
[uppy]="uppy" [uppy]="uppy"
[props]="props" [props]="props"
></uppy-dashboard-modal>`, ></uppy-dashboard-modal>`,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class DashboardModalDemoComponent<M extends Meta, B extends Body> { export class DashboardModalDemoComponent<M extends Meta, B extends Body> {
uppy: Uppy<M, B> = new Uppy({ debug: true, autoProceed: true }); uppy: Uppy<M, B> = new Uppy({ debug: true, autoProceed: true });
props?: Dashboard.DashboardOptions<M, B>; props?: Dashboard.DashboardOptions<M, B>;
} }

View file

@ -1,25 +1,24 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, type ComponentFixture, TestBed } from "@angular/core/testing";
import { DashboardModalComponent } from './dashboard-modal.component'; import { DashboardModalComponent } from "./dashboard-modal.component";
describe('DashboardComponent', () => { describe("DashboardComponent", () => {
let component: DashboardModalComponent; let component: DashboardModalComponent;
let fixture: ComponentFixture<DashboardModalComponent>; let fixture: ComponentFixture<DashboardModalComponent>;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [ DashboardModalComponent ] declarations: [DashboardModalComponent],
}) }).compileComponents();
.compileComponents(); }));
}));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(DashboardModalComponent); fixture = TestBed.createComponent(DashboardModalComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); });
it('should create', () => { it("should create", () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
}); });

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