Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@uppy/companion: initiate refactor to TS #4696

Draft
wants to merge 6 commits into
base: 4.x
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
42 changes: 40 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,24 @@ module.exports = {
},
},
{
files: ['./packages/@uppy/companion/**/*.js'],
files: [
'./packages/@uppy/companion/**/*.js',
'./packages/@uppy/companion/**/*.ts',
],
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.ts'],
},
typescript: true,
},
'import/parsers': {
'@typescript-eslint/parser': ['.ts'],
},
},
parserOptions: {
sourceType: 'script',
},
rules: {
'no-underscore-dangle': 'off',
},
Expand Down Expand Up @@ -457,7 +474,11 @@ module.exports = {
},
{
files: ['**/*.ts', '**/*.md/*.ts', '**/*.md/*.typescript', '**/*.tsx', '**/*.md/*.tsx'],
excludedFiles: ['examples/angular-example/**/*.ts', 'packages/@uppy/angular/**/*.ts'],
excludedFiles: [
'examples/angular-example/**/*.ts',
'packages/@uppy/angular/**/*.ts',
'packages/@uppy/companion/**/*.ts',
],
parser: '@typescript-eslint/parser',
settings: {
'import/resolver': {
Expand Down Expand Up @@ -493,6 +514,23 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'error',
},
},
{
files: [
'./packages/@uppy/companion/**/*.ts',
],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
],
rules: {
'@typescript-eslint/no-var-requires': 'off',
'import/prefer-default-export': 'off',
'global-require': 'off',
},
},
{
files: ['**/*.md/*.*'],
rules: {
Expand Down
1 change: 1 addition & 0 deletions e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
},
"devDependencies": {
"@parcel/transformer-vue": "^2.9.3",
"@transloadit/ts-fly": "^0.1.4",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for Companion we should use tsx.
Better maintained, and less quirks

"cypress": "^13.0.0",
"cypress-terminal-report": "^6.0.0",
"deep-freeze": "^0.0.1",
Expand Down
3 changes: 2 additions & 1 deletion e2e/start-companion-with-load-balancer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@ const isOSX = process.platform === 'darwin'

const startCompanion = ({ name, port }) => {
const cp = spawn(process.execPath, [
'-r', '@transloadit/ts-fly',
'-r', 'dotenv/config',
// Watch mode support is limited to Windows and macOS at the time of writing.
...(isWindows || isOSX ? ['--watch-path', 'packages/@uppy/companion/src', '--watch'] : []),
'./packages/@uppy/companion/src/standalone/start-server.js',
'./packages/@uppy/companion/src/standalone/start-server.ts',
], {
cwd: new URL('../', import.meta.url),
stdio: 'inherit',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"eslint": "^8.0.0",
"eslint-config-prettier": "^9.0.0",
"eslint-config-transloadit": "^2.0.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-compat": "^4.0.0",
"eslint-plugin-cypress": "^3.0.0",
"eslint-plugin-import": "^2.25.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,12 @@
const crypto = require('node:crypto')
import crypto = require('node:crypto')

/**
*
* @param {string} value
* @param {string[]} criteria
* @returns {boolean}
*/
exports.hasMatch = (value, criteria) => {
export const hasMatch = (value: string, criteria: string[]) => {
return criteria.some((i) => {
return value === i || (new RegExp(i)).test(value)
})
}

/**
*
* @param {object} data
* @returns {string}
*/
exports.jsonStringify = (data) => {
export const jsonStringify = (data: object) => {
const cache = []
return JSON.stringify(data, (key, value) => {
if (typeof value === 'object' && value !== null) {
Expand All @@ -34,26 +23,23 @@ exports.jsonStringify = (data) => {
// all paths are assumed to be '/' prepended
/**
* Returns a url builder
*
* @param {object} options companion options
*/
module.exports.getURLBuilder = (options) => {
export const getURLBuilder = (options: Record<string, unknown>) => {
/**
* Builds companion targeted url
*
* @param {string} subPath the tail path of the url
* @param {boolean} isExternal if the url is for the external world
* @param {boolean} [excludeHost] if the server domain and protocol should be included
*/
const buildURL = (subPath, isExternal, excludeHost) => {
const buildURL = (subPath: string, isExternal: boolean, excludeHost?: boolean) => {
const server = options.server as Record<string, unknown>
const { implicitPath, path: serverPath } = server

let path = ''

if (isExternal && options.server.implicitPath) {
path += options.server.implicitPath
if (isExternal && implicitPath) {
path += implicitPath
}

if (options.server.path) {
path += options.server.path
if (serverPath) {
path += serverPath
}

path += subPath
Expand All @@ -62,7 +48,7 @@ module.exports.getURLBuilder = (options) => {
return path
}

return `${options.server.protocol}://${options.server.host}${path}`
return `${server.protocol}://${server.host}${path}`
}

return buildURL
Expand All @@ -82,29 +68,25 @@ function createSecret(secret) {

/**
* Create an initialization vector for AES256.
*
* @returns {Buffer}
*/
function createIv() {
return crypto.randomBytes(16)
}

function urlEncode(unencoded) {
function urlEncode(unencoded: string) {
return unencoded.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '~')
}

function urlDecode(encoded) {
function urlDecode(encoded: string) {
return encoded.replace(/-/g, '+').replace(/_/g, '/').replace(/~/g, '=')
}

/**
* Encrypt a buffer or string with AES256 and a random iv.
*
* @param {string} input
* @param {string|Buffer} secret
* @returns {string} Ciphertext as a hex string, prefixed with 32 hex characters containing the iv.
* @returns Ciphertext as a hex string, prefixed with 32 hex characters containing the iv.
*/
module.exports.encrypt = (input, secret) => {
export const encrypt = (input: string, secret: string | Buffer) => {
const iv = createIv()
const cipher = crypto.createCipheriv('aes256', createSecret(secret), iv)
let encrypted = cipher.update(input, 'utf8', 'base64')
Expand All @@ -116,11 +98,9 @@ module.exports.encrypt = (input, secret) => {
/**
* Decrypt an iv-prefixed or string with AES256. The iv should be in the first 32 hex characters.
*
* @param {string} encrypted
* @param {string|Buffer} secret
* @returns {string} Decrypted value.
* @returns Decrypted value.
*/
module.exports.decrypt = (encrypted, secret) => {
export const decrypt = (encrypted: string, secret: string | Buffer) => {
// Need at least 32 chars for the iv
if (encrypted.length < 32) {
throw new Error('Invalid encrypted value. Maybe it was generated with an old Companion version?')
Expand All @@ -130,7 +110,7 @@ module.exports.decrypt = (encrypted, secret) => {
const iv = Buffer.from(encrypted.slice(0, 32), 'hex')
const encryptionWithoutIv = encrypted.slice(32)

let decipher
let decipher: crypto.Decipher
try {
decipher = crypto.createDecipheriv('aes256', createSecret(secret), iv)
} catch (err) {
Expand All @@ -146,7 +126,7 @@ module.exports.decrypt = (encrypted, secret) => {
return decrypted
}

module.exports.defaultGetKey = (req, filename) => `${crypto.randomUUID()}-${filename}`
export const defaultGetKey = (req: never, filename: string) => `${crypto.randomUUID()}-${filename}`

class StreamHttpJsonError extends Error {
statusCode
Expand All @@ -163,7 +143,7 @@ class StreamHttpJsonError extends Error {

module.exports.StreamHttpJsonError = StreamHttpJsonError

module.exports.prepareStream = async (stream) => new Promise((resolve, reject) => {
export const prepareStream = async (stream) => new Promise<void>((resolve, reject) => (
stream
.on('response', () => {
// Don't allow any more data to flow yet.
Expand All @@ -189,21 +169,21 @@ module.exports.prepareStream = async (stream) => new Promise((resolve, reject) =

reject(err)
})
})
))

module.exports.getBasicAuthHeader = (key, secret) => {
export const getBasicAuthHeader = (key: string, secret: string) => {
const base64 = Buffer.from(`${key}:${secret}`, 'binary').toString('base64')
return `Basic ${base64}`
}

const rfc2047Encode = (dataIn) => {
const rfc2047Encode = (dataIn: string) => {
const data = `${dataIn}`
// eslint-disable-next-line no-control-regex
if (/^[\x00-\x7F]*$/.test(data)) return data // we return ASCII as is
return `=?UTF-8?B?${Buffer.from(data).toString('base64')}?=` // We encode non-ASCII strings
}

module.exports.rfc2047EncodeMetadata = (metadata) => (
export const rfc2047EncodeMetadata = (metadata: Record<string, string>) => (
Object.fromEntries(Object.entries(metadata).map((entry) => entry.map(rfc2047Encode)))
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,43 +1,35 @@
const fs = require('node:fs')
const merge = require('lodash/merge')
const stripIndent = require('common-tags/lib/stripIndent')
const crypto = require('node:crypto')
import fs = require('node:fs')
import merge = require('lodash/merge')
import stripIndent = require('common-tags/lib/stripIndent')
import crypto = require('node:crypto')

const utils = require('../server/helpers/utils')
const logger = require('../server/logger')
// @ts-ignore
const { version } = require('../../package.json')
import utils = require('../server/helpers/utils')
import logger = require('../server/logger')

// We don't want TS to resolve the following require, otherwise it wraps the
// output files in `lib/src/` instead of `lib/`.
const { version } = require('../../package.json') as { version: string }

/**
* Tries to read the secret from a file if the according environment variable is set.
* Otherwise it falls back to the standard secret environment variable.
*
* @param {string} baseEnvVar
*
* @returns {string}
*/
const getSecret = (baseEnvVar) => {
const getSecret = (baseEnvVar: string) => {
const secretFile = process.env[`${baseEnvVar}_FILE`]
return secretFile
? fs.readFileSync(secretFile).toString()
? fs.readFileSync(secretFile, 'utf-8')
: process.env[baseEnvVar]
}

/**
* Auto-generates server secret
*
* @returns {string}
*/
exports.generateSecret = () => {
export const generateSecret = () => {
logger.warn('auto-generating server secret because none was specified', 'startup.secret')
return crypto.randomBytes(64).toString('hex')
}

/**
*
* @param {string} url
*/
const hasProtocol = (url) => {
const hasProtocol = (url: string) => {
return url.startsWith('https://') || url.startsWith('http://')
}

Expand All @@ -59,15 +51,11 @@ const s3Prefix = process.env.COMPANION_AWS_PREFIX || ''

/**
* Default getKey for Companion standalone variant
*
* @returns {string}
*/
const defaultStandaloneGetKey = (...args) => `${s3Prefix}${utils.defaultGetKey(...args)}`
const defaultStandaloneGetKey = (...args: Parameters<typeof utils.defaultGetKey>) => `${s3Prefix}${utils.defaultGetKey(...args)}`

/**
* Loads the config from environment variables
*
* @returns {object}
*/
const getConfigFromEnv = () => {
const uploadUrls = process.env.COMPANION_UPLOAD_URLS
Expand Down Expand Up @@ -182,11 +170,9 @@ const getConfigFromEnv = () => {

/**
* Returns the config path specified via cli arguments
*
* @returns {string}
*/
const getConfigPath = () => {
let configPath
let configPath: string

for (let i = process.argv.length - 1; i >= 0; i--) {
const isConfigFlag = process.argv[i] === '-c' || process.argv[i] === '--config'
Expand All @@ -202,29 +188,24 @@ const getConfigPath = () => {

/**
* Loads the config from a file and returns it as an object
*
* @returns {object}
*/
const getConfigFromFile = () => {
const path = getConfigPath()
if (!path) return {}

const rawdata = fs.readFileSync(getConfigPath())
// @ts-ignore
const rawdata = fs.readFileSync(getConfigPath(), 'utf-8')
return JSON.parse(rawdata)
}

/**
* Reads all companion configuration set via environment variables
* and via the config file path
*
* @returns {object}
*/
exports.getCompanionOptions = (options = {}) => {
export const getCompanionOptions = (options = {}) => {
return merge({}, getConfigFromEnv(), getConfigFromFile(), options)
}

exports.buildHelpfulStartupMessage = (companionOptions) => {
export const buildHelpfulStartupMessage = (companionOptions: Record<string, unknown>) => {
const buildURL = utils.getURLBuilder(companionOptions)
const callbackURLs = []
Object.keys(companionOptions.providerOptions).forEach((providerName) => {
Expand Down