diff --git a/package-lock.json b/package-lock.json index 0c5966a1c..b255793b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2064,6 +2064,24 @@ "@types/node": "*" } }, + "node_modules/@types/koa__cors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/koa__cors/-/koa__cors-5.0.0.tgz", + "integrity": "sha512-LCk/n25Obq5qlernGOK/2LUwa/2YJb2lxHUkkvYFDOpLXlVI6tKcdfCHRBQnOY4LwH6el5WOLs6PD/a8Uzau6g==", + "dev": true, + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/koa__router": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/@types/koa__router/-/koa__router-12.0.4.tgz", + "integrity": "sha512-Y7YBbSmfXZpa/m5UGGzb7XadJIRBRnwNY9cdAojZGp65Cpe5MAP3mOZE7e3bImt8dfKS4UFcR16SLH8L/z7PBw==", + "dev": true, + "dependencies": { + "@types/koa": "*" + } + }, "node_modules/@types/koa-compose": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", @@ -12358,6 +12376,8 @@ "@koa/router": "10.1.1", "@types/express-serve-static-core": "^4.17.28", "@types/koa": "2.13.4", + "@types/koa__cors": "^5.0.0", + "@types/koa__router": "^12.0.4", "@types/ws": "8.5.3", "@uploadcare/api-client-utils": "^6.14.0", "chalk": "^4.1.2", @@ -14021,6 +14041,24 @@ "@types/node": "*" } }, + "@types/koa__cors": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/koa__cors/-/koa__cors-5.0.0.tgz", + "integrity": "sha512-LCk/n25Obq5qlernGOK/2LUwa/2YJb2lxHUkkvYFDOpLXlVI6tKcdfCHRBQnOY4LwH6el5WOLs6PD/a8Uzau6g==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, + "@types/koa__router": { + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/@types/koa__router/-/koa__router-12.0.4.tgz", + "integrity": "sha512-Y7YBbSmfXZpa/m5UGGzb7XadJIRBRnwNY9cdAojZGp65Cpe5MAP3mOZE7e3bImt8dfKS4UFcR16SLH8L/z7PBw==", + "dev": true, + "requires": { + "@types/koa": "*" + } + }, "@types/koa-compose": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.5.tgz", @@ -14312,6 +14350,8 @@ "@koa/router": "10.1.1", "@types/express-serve-static-core": "^4.17.28", "@types/koa": "2.13.4", + "@types/koa__cors": "^5.0.0", + "@types/koa__router": "^12.0.4", "@types/ws": "8.5.3", "@uploadcare/api-client-utils": "^6.14.0", "chalk": "^4.1.2", diff --git a/packages/api-client-utils/src/camelizeKeys.ts b/packages/api-client-utils/src/camelizeKeys.ts index 61600d5ec..497598402 100644 --- a/packages/api-client-utils/src/camelizeKeys.ts +++ b/packages/api-client-utils/src/camelizeKeys.ts @@ -37,7 +37,7 @@ export function camelizeKeys( if (!isObject(source)) { return source } - const result = {} + const result: Record = {} for (const key of Object.keys(source)) { let value = source[key] if (ignoreKeys.includes(key)) { diff --git a/packages/image-shrink/src/helper/memoize.ts b/packages/image-shrink/src/helper/memoize.ts index 54b0b925c..a774e0e79 100644 --- a/packages/image-shrink/src/helper/memoize.ts +++ b/packages/image-shrink/src/helper/memoize.ts @@ -1,6 +1,11 @@ -export const memoize = (fn, serializer) => { - const cache = {} - return (...args) => { +type FnArgs = [number, number] +type Cache = Record +type Serializer = (args: FnArgs, cache: Cache) => number +type Fn = (...args: FnArgs) => boolean + +export const memoize = (fn: Fn, serializer: Serializer) => { + const cache: Cache = {} + return (...args: FnArgs) => { const key = serializer(args, cache) return key in cache ? cache[key] : (cache[key] = fn(...args)) } @@ -13,7 +18,7 @@ export const memoize = (fn, serializer) => { * - Browser supports higher canvas size * - Browser doesn't support lower canvas size */ -export const memoKeySerializer = (args, cache) => { +export const memoKeySerializer: Serializer = (args, cache) => { const [w] = args const cachedWidths = Object.keys(cache) .map((val) => parseInt(val, 10)) diff --git a/packages/image-shrink/src/utils/canvas/canvasResize.ts b/packages/image-shrink/src/utils/canvas/canvasResize.ts index 4e61ba468..7d945df1b 100644 --- a/packages/image-shrink/src/utils/canvas/canvasResize.ts +++ b/packages/image-shrink/src/utils/canvas/canvasResize.ts @@ -1,7 +1,7 @@ import { createCanvas } from './createCanvas' -export const canvasResize = (img, w, h) => { - return new Promise((resolve, reject) => { +export const canvasResize = (img: CanvasImageSource, w: number, h: number) => { + return new Promise((resolve, reject) => { try { const { ctx, canvas } = createCanvas() @@ -11,8 +11,12 @@ export const canvasResize = (img, w, h) => { ctx.imageSmoothingQuality = 'high' ctx.drawImage(img, 0, 0, w, h) - img.src = '//:0' // for image - img.width = img.height = 1 // for canvas + if (img instanceof HTMLImageElement) { + img.src = '//:0' // free memory + } + if (img instanceof HTMLCanvasElement) { + img.width = img.height = 1 // free memory + } resolve(canvas) } catch (e) { diff --git a/packages/image-shrink/src/utils/canvas/canvasTest.ts b/packages/image-shrink/src/utils/canvas/canvasTest.ts index af845cef8..451ffa63a 100644 --- a/packages/image-shrink/src/utils/canvas/canvasTest.ts +++ b/packages/image-shrink/src/utils/canvas/canvasTest.ts @@ -13,7 +13,8 @@ const FILL_STYLE = `rgba(${TestPixel.R}, ${TestPixel.G}, ${TestPixel.B}, ${ })` type TFillRect = [number, number, number, number] -export const canvasTest = (width, height) => { + +export const canvasTest = (width: number, height: number) => { try { const fill: TFillRect = [width - 1, height - 1, 1, 1] // x, y, width, height diff --git a/packages/image-shrink/src/utils/canvas/canvasToBlob.ts b/packages/image-shrink/src/utils/canvas/canvasToBlob.ts index 8faea1800..5518b96f3 100644 --- a/packages/image-shrink/src/utils/canvas/canvasToBlob.ts +++ b/packages/image-shrink/src/utils/canvas/canvasToBlob.ts @@ -2,7 +2,7 @@ export const canvasToBlob = ( canvas: HTMLCanvasElement, type: string, quality: number | undefined, - callback + callback: BlobCallback ): void => { return canvas.toBlob(callback, type, quality) } diff --git a/packages/image-shrink/src/utils/canvas/hasTransparency.ts b/packages/image-shrink/src/utils/canvas/hasTransparency.ts index 562f935d2..0c988af00 100644 --- a/packages/image-shrink/src/utils/canvas/hasTransparency.ts +++ b/packages/image-shrink/src/utils/canvas/hasTransparency.ts @@ -1,6 +1,6 @@ import { createCanvas } from './createCanvas' -export const hasTransparency = (img) => { +export const hasTransparency = (img: CanvasImageSource) => { const canvasSize = 50 // Create a canvas element and get 2D rendering context diff --git a/packages/image-shrink/src/utils/canvas/testCanvasSize.ts b/packages/image-shrink/src/utils/canvas/testCanvasSize.ts index 735a503c4..5361196f4 100644 --- a/packages/image-shrink/src/utils/canvas/testCanvasSize.ts +++ b/packages/image-shrink/src/utils/canvas/testCanvasSize.ts @@ -2,8 +2,8 @@ import { sizes } from '../../constants' import { memoize, memoKeySerializer } from '../../helper/memoize' import { canvasTest } from './canvasTest' -function wrapAsync(fn) { - return (...args) => { +function wrapAsync(fn: (...args: A) => R) { + return (...args: A) => { return new Promise((resolve) => { setTimeout(() => { const result = fn(...args) @@ -16,7 +16,7 @@ function wrapAsync(fn) { const squareTest = wrapAsync(memoize(canvasTest, memoKeySerializer)) const dimensionTest = wrapAsync(memoize(canvasTest, memoKeySerializer)) -export const testCanvasSize = (w, h) => { +export const testCanvasSize = (w: number, h: number) => { return new Promise((resolve, reject) => { const testSquareSide = sizes.squareSide.find((side) => side * side >= w * h) const testDimension = sizes.dimension.find((side) => side >= w && side >= h) diff --git a/packages/image-shrink/src/utils/exif/findExifOrientation.ts b/packages/image-shrink/src/utils/exif/findExifOrientation.ts index b6a19d9a9..da47055ba 100644 --- a/packages/image-shrink/src/utils/exif/findExifOrientation.ts +++ b/packages/image-shrink/src/utils/exif/findExifOrientation.ts @@ -1,4 +1,7 @@ -export const findExifOrientation = (exif: DataView, exifCallback) => { +export const findExifOrientation = ( + exif: DataView, + exifCallback: (offset: number, littleEndian: boolean) => void +) => { let j, little, offset, ref if ( !exif || @@ -6,28 +9,27 @@ export const findExifOrientation = (exif: DataView, exifCallback) => { exif.getUint32(0) !== 0x45786966 || exif.getUint16(4) !== 0 ) { - return null + return } if (exif.getUint16(6) === 0x4949) { little = true } else if (exif.getUint16(6) === 0x4d4d) { little = false } else { - return null + return } if (exif.getUint16(8, little) !== 0x002a) { - return null + return } offset = 8 + exif.getUint32(10, little) const count = exif.getUint16(offset - 2, little) for (j = 0, ref = count; ref >= 0 ? j < ref : j > ref; ref >= 0 ? ++j : --j) { if (exif.byteLength < offset + 10) { - return null + return } if (exif.getUint16(offset, little) === 0x0112) { - return exifCallback(offset + 8, little) + exifCallback(offset + 8, little) } offset += 12 } - return null } diff --git a/packages/image-shrink/src/utils/exif/isBrowserApplyExif.ts b/packages/image-shrink/src/utils/exif/isBrowserApplyExif.ts index bb9a1c4cf..62308ab88 100644 --- a/packages/image-shrink/src/utils/exif/isBrowserApplyExif.ts +++ b/packages/image-shrink/src/utils/exif/isBrowserApplyExif.ts @@ -1,3 +1,4 @@ +// 2x1 pixel image 90CW rotated with orientation header const base64ImageSrc = 'data:image/jpg;base64,' + '/9j/4AAQSkZJRgABAQEASABIAAD/4QA6RXhpZgAATU0AKgAAAAgAAwESAAMAAAABAAYAAAEo' + @@ -5,7 +6,8 @@ const base64ImageSrc = '////////////////////////////////////////////////////////wAALCAABAAIBASIA' + '/8QAJgABAAAAAAAAAAAAAAAAAAAAAxABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAAPwBH/9k=' -let isApplied +let isApplied: boolean | undefined = undefined + export const isBrowserApplyExif = () => { return new Promise((resolve) => { if (isApplied !== undefined) { diff --git a/packages/image-shrink/src/utils/exif/replaceExif.ts b/packages/image-shrink/src/utils/exif/replaceExif.ts index e692ef422..bae6406d6 100644 --- a/packages/image-shrink/src/utils/exif/replaceExif.ts +++ b/packages/image-shrink/src/utils/exif/replaceExif.ts @@ -1,13 +1,13 @@ import { replaceJpegChunk } from '../image/JPEG/replaceJpegChunk' import { findExifOrientation } from './findExifOrientation' -export const setExifOrientation = (exif, orientation) => { - findExifOrientation(exif, (offset, little) => - exif.setUint16(offset, orientation, little) +export const setExifOrientation = (exif: DataView, orientation: number) => { + findExifOrientation(exif, (offset, littleEndian) => + exif.setUint16(offset, orientation, littleEndian) ) } export const replaceExif = async ( - file: File, + blob: Blob, exif: DataView, isExifApplied: boolean | unknown ) => { @@ -15,5 +15,5 @@ export const replaceExif = async ( setExifOrientation(exif, 1) } - return replaceJpegChunk(file, 0xe1, [exif.buffer]) + return replaceJpegChunk(blob, 0xe1, [exif.buffer]) } diff --git a/packages/image-shrink/src/utils/image/JPEG/readJpegChunks.ts b/packages/image-shrink/src/utils/image/JPEG/readJpegChunks.ts index 00e5a94d3..d1033e5d5 100644 --- a/packages/image-shrink/src/utils/image/JPEG/readJpegChunks.ts +++ b/packages/image-shrink/src/utils/image/JPEG/readJpegChunks.ts @@ -7,10 +7,10 @@ type TChunk = { export const readJpegChunks = () => { const stack: TChunk[] = [] - const promiseReadJpegChunks = (file) => + const promiseReadJpegChunks = (blob: Blob) => new Promise((resolve, reject) => { - let pos - const readToView = (file, cb) => { + let pos = 2 + const readToView = (blob: Blob, cb: (view: DataView) => void) => { const reader = new FileReader() reader.addEventListener('load', () => { @@ -21,11 +21,11 @@ export const readJpegChunks = () => { reject(`Reader error: ${e}`) }) - reader.readAsArrayBuffer(file) + reader.readAsArrayBuffer(blob) } const readNext = () => - readToView(file.slice(pos, pos + 128), (view) => { + readToView(blob.slice(pos, pos + 128), (view: DataView) => { let i, j, ref for ( i = j = 0, ref = view.byteLength; @@ -38,32 +38,38 @@ export const readJpegChunks = () => { } } - return readNextChunk() + readNextChunk() }) const readNextChunk = () => { const startPos = pos - return readToView(file.slice(pos, (pos += 4)), (view) => { + return readToView(blob.slice(pos, (pos += 4)), (view: DataView) => { if (view.byteLength !== 4 || view.getUint8(0) !== 0xff) { - return reject('Corrupted') + reject('Corrupted') + return } const marker = view?.getUint8(1) if (marker === 0xda) { - return resolve(true) + resolve(true) + return } const length = view.getUint16(2) - 2 - return readToView(file.slice(pos, (pos += length)), (view) => { - if (view.byteLength !== length) { - return reject('Corrupted') + return readToView( + blob.slice(pos, (pos += length)), + (view: DataView) => { + if (view.byteLength !== length) { + reject('Corrupted') + return + } + + stack.push({ startPos, length, marker, view }) + readNext() } - - stack.push({ startPos, length, marker, view }) - return readNext() - }) + ) }) } @@ -71,13 +77,12 @@ export const readJpegChunks = () => { reject('Not Support') } - pos = 2 - readToView(file.slice(0, 2), function (view) { + readToView(blob.slice(0, 2), (view: DataView) => { if (view.getUint16(0) !== 0xffd8) { reject('Not jpeg') } - return readNext() + readNext() }) }) diff --git a/packages/image-shrink/src/utils/image/JPEG/replaceJpegChunk.ts b/packages/image-shrink/src/utils/image/JPEG/replaceJpegChunk.ts index b8329bf27..001fa60e4 100644 --- a/packages/image-shrink/src/utils/image/JPEG/replaceJpegChunk.ts +++ b/packages/image-shrink/src/utils/image/JPEG/replaceJpegChunk.ts @@ -1,6 +1,10 @@ import { readJpegChunks } from './readJpegChunks' -export const replaceJpegChunk = (blob, marker, chunks) => { +export const replaceJpegChunk = ( + blob: Blob, + marker: number, + chunks: ArrayBuffer[] +) => { return new Promise((resolve, reject) => { const oldChunkPos: number[] = [] const oldChunkLength: number[] = [] @@ -17,7 +21,7 @@ export const replaceJpegChunk = (blob, marker, chunks) => { }) }) .then(() => { - const newChunks = [blob.slice(0, 2)] + const newChunks: (ArrayBuffer | Blob)[] = [blob.slice(0, 2)] for (const chunk of chunks) { const intro = new DataView(new ArrayBuffer(4)) diff --git a/packages/image-shrink/src/utils/render/fallback.ts b/packages/image-shrink/src/utils/render/fallback.ts index ccd05af74..e9cda6757 100644 --- a/packages/image-shrink/src/utils/render/fallback.ts +++ b/packages/image-shrink/src/utils/render/fallback.ts @@ -1,7 +1,20 @@ import { testCanvasSize } from '../canvas/testCanvasSize' import { canvasResize } from '../canvas/canvasResize' -const calcShrinkSteps = function (sourceW, targetW, targetH, step) { +/** + * Goes from target to source by step, the last incomplete step is dropped. + * Always returns at least one step - target. Source step is not included. + * Sorted descending. + * + * Example with step = 0.71, source = 2000, target = 400 400 (target) <- 563 <- + * 793 <- 1117 <- 1574 (dropped) <- [2000 (source)] + */ +const calcShrinkSteps = function ( + sourceW: number, + targetW: number, + targetH: number, + step: number +) { const steps: Array<[number, number]> = [] let sW: number = targetW let sH: number = targetH @@ -19,22 +32,38 @@ const calcShrinkSteps = function (sourceW, targetW, targetH, step) { return steps.reverse() } -export const fallback = ({ img, sourceW, targetW, targetH, step }) => { +/** + * Fallback resampling algorithm + * + * Reduces dimensions by step until reaches target dimensions, this gives a + * better output quality than one-step method + * + * Target dimensions expected to be supported by browser, unsupported steps will + * be dropped. + */ +export const fallback = ({ + img, + sourceW, + targetW, + targetH, + step +}: { + img: HTMLImageElement + sourceW: number + targetW: number + targetH: number + step: number +}): Promise => { const steps = calcShrinkSteps(sourceW, targetW, targetH, step) - return steps - .reduce((chain, [w, h]) => { - return chain - .then((canvas) => { - return testCanvasSize(w, h) - .then(() => canvas) - .catch(() => canvasResize(canvas, w, h)) - }) - .then((canvas) => { - const progress = (sourceW - w) / (sourceW - targetW) - return { canvas, progress } - }) - }, Promise.resolve(img)) - .then(({ canvas }) => canvas) - .catch((error) => Promise.reject(error)) + return steps.reduce((chain, [w, h]) => { + return chain.then((canvas) => { + return ( + testCanvasSize(w, h) + .then(() => canvasResize(canvas, w, h)) + // Here we assume that at least one step will be supported and HTMLImageElement will be converted to HTMLCanvasElement + .catch(() => canvas as unknown as HTMLCanvasElement) + ) + }) + }, Promise.resolve(img as HTMLCanvasElement | HTMLImageElement)) as Promise } diff --git a/packages/image-shrink/src/utils/render/native.ts b/packages/image-shrink/src/utils/render/native.ts index c778ed6f5..6a7d2dc32 100644 --- a/packages/image-shrink/src/utils/render/native.ts +++ b/packages/image-shrink/src/utils/render/native.ts @@ -1,4 +1,18 @@ import { canvasResize } from '../canvas/canvasResize' -export const native = ({ img, targetW, targetH }) => - canvasResize(img, targetW, targetH) +/** + * Native high-quality canvas resampling + * + * Browser support: + * https://caniuse.com/mdn-api_canvasrenderingcontext2d_imagesmoothingenabled + * Target dimensions expected to be supported by browser. + */ +export const native = ({ + img, + targetW, + targetH +}: { + img: HTMLImageElement + targetW: number + targetH: number +}) => canvasResize(img, targetW, targetH) diff --git a/packages/image-shrink/src/utils/shrinkFile.ts b/packages/image-shrink/src/utils/shrinkFile.ts index a097465e3..e4ffa7c08 100644 --- a/packages/image-shrink/src/utils/shrinkFile.ts +++ b/packages/image-shrink/src/utils/shrinkFile.ts @@ -15,7 +15,7 @@ export type TSetting = { } export const shrinkFile = (file: File, settings: TSetting): Promise => { - /*eslint no-async-promise-executor: "off"*/ + // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { if (!(URL && DataView && Blob)) { reject('Not support') @@ -62,12 +62,16 @@ export const shrinkFile = (file: File, settings: TSetting): Promise => { } canvasToBlob(canvas, format, quality, (blob) => { + if (!blob) { + reject('Failed to convert canvas to blob') + return + } canvas.width = canvas.height = 1 - let replaceChain = Promise.resolve(blob) + const replaceChain = Promise.resolve(blob) if (exif.value) { - replaceChain = replaceChain + replaceChain .then((blob) => replaceExif(blob, exif.value, isExifApplied.value) ) @@ -75,7 +79,7 @@ export const shrinkFile = (file: File, settings: TSetting): Promise => { } if (iccProfile?.value?.length > 0) { - replaceChain = replaceChain + replaceChain .then((blob) => replaceIccProfile(blob, iccProfile.value)) .catch(() => blob) } diff --git a/packages/image-shrink/src/utils/shrinkImage.ts b/packages/image-shrink/src/utils/shrinkImage.ts index a203c4d46..511fb5299 100644 --- a/packages/image-shrink/src/utils/shrinkImage.ts +++ b/packages/image-shrink/src/utils/shrinkImage.ts @@ -12,6 +12,7 @@ export const shrinkImage = ( settings: TSetting ): Promise => { return new Promise((resolve, reject) => { + // do not shrink image if original resolution / target resolution ratio falls behind 2.0 if (img.width * STEP * img.height * STEP < settings.size) { reject('Not required') } @@ -24,11 +25,13 @@ export const shrinkImage = ( const targetW = Math.floor(Math.sqrt(settings.size * ratio)) const targetH = Math.floor(settings.size / Math.sqrt(settings.size * ratio)) + // we test the last step because we can skip all intermediate steps return testCanvasSize(targetW, targetH) .then(() => { const { ctx } = createCanvas() const supportNative = 'imageSmoothingQuality' in ctx + // native scaling on ios gives blurry results const useNativeScaling = supportNative && !isIOS() && !isIpadOS return useNativeScaling diff --git a/packages/rest-client/src/api/conversion/convert.ts b/packages/rest-client/src/api/conversion/convert.ts index aeef529d6..4ad384b01 100644 --- a/packages/rest-client/src/api/conversion/convert.ts +++ b/packages/rest-client/src/api/conversion/convert.ts @@ -1,4 +1,8 @@ -import { ApiRequestSettings, makeApiRequest } from '../../makeApiRequest' +import { + ApiRequestBody, + ApiRequestSettings, + makeApiRequest +} from '../../makeApiRequest' import { storeValueToString } from '../../tools/storeValueToString' import { ConversionOptions } from '../../types/ConversionOptions' import { ConversionResponse } from '../../types/ConversionResponse' @@ -13,7 +17,7 @@ export async function convert>( ): Promise> { const isDocument = options.type === ConversionType.DOCUMENT - const body = { + const body: ApiRequestBody = { paths: options.paths, store: storeValueToString(options.store) } diff --git a/packages/upload-client/mock-server/controllers/base.ts b/packages/upload-client/mock-server/controllers/base.ts index 99c3e981b..531e9d771 100644 --- a/packages/upload-client/mock-server/controllers/base.ts +++ b/packages/upload-client/mock-server/controllers/base.ts @@ -1,12 +1,9 @@ +import { type Middleware } from 'koa' import json from '../data/base' import find from '../utils/find' -/** - * '/base/' - * - * @param {object} ctx - */ -const index = (ctx) => { +/** '/base/' */ +const index: Middleware = (ctx) => { ctx.body = find(json, 'info') } diff --git a/packages/upload-client/mock-server/controllers/from_url.ts b/packages/upload-client/mock-server/controllers/from_url.ts index e63a8b449..ae8ac32a2 100644 --- a/packages/upload-client/mock-server/controllers/from_url.ts +++ b/packages/upload-client/mock-server/controllers/from_url.ts @@ -4,6 +4,7 @@ import find from '../utils/find' import error from '../utils/error' import { PORT } from '../config' +import { type Middleware } from 'koa' interface State { isComputable: boolean @@ -20,24 +21,20 @@ const state: { [key: string]: State } = {} -/** - * '/from_url/?pub_key=XXXXXXXXXXXXXXXXXXXX' - * - * @param {object} ctx - */ -const index = (ctx) => { +/** '/from_url/?pub_key=XXXXXXXXXXXXXXXXXXXX' */ +const index: Middleware = (ctx) => { const isPrivateIP = (url: string): boolean => url.includes('192.168.') || (url.includes('localhost') && !url.includes(`http://localhost:${PORT}/`)) const doesNotExist = (url: string): boolean => url === 'https://1.com/1.jpg' const publicKey = ctx.query && ctx.query.pub_key - const sourceUrl = ctx.query && ctx.query.source_url + const sourceUrl = ctx.query && (ctx.query.source_url as string) const checkForUrlDuplicates = !!parseInt( - ctx.query && ctx.query.check_URL_duplicates + ctx.query && (ctx.query.check_URL_duplicates as string) ) const saveUrlForRecurrentUploads = !!parseInt( - ctx.query && ctx.query.save_URL_duplicates + ctx.query && (ctx.query.save_URL_duplicates as string) ) // Check params @@ -83,13 +80,9 @@ const index = (ctx) => { ctx.body = response } -/** - * '/from_url/status/?pub_key=XXXXXXXXXXXXXXXXXXXX&token=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' - * - * @param {object} ctx - */ -const status = (ctx) => { - const token = ctx.query && ctx.query.token +/** '/from_url/status/?pub_key=XXXXXXXXXXXXXXXXXXXX&token=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' */ +const status: Middleware = (ctx) => { + const token = ctx.query && (ctx.query.token as string) if (token) { const tokenState = state[token] diff --git a/packages/upload-client/mock-server/controllers/group.ts b/packages/upload-client/mock-server/controllers/group.ts index 6a4963e47..b3c3dcbcb 100644 --- a/packages/upload-client/mock-server/controllers/group.ts +++ b/packages/upload-client/mock-server/controllers/group.ts @@ -1,6 +1,7 @@ import json from '../data/group' import find from '../utils/find' import error from '../utils/error' +import { type Middleware } from 'koa' const UUID_REGEX = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}' @@ -52,13 +53,9 @@ const isValidFile = (file: string): boolean => { return isValidUuid(uuid) } -/** - * '/group/' - * - * @param {object} ctx - */ -const index = (ctx) => { - let files = ctx.request.body && ctx.request.body['files[]'] +/** '/group/' */ +const index: Middleware = (ctx) => { + let files = (ctx.request.body && ctx.request.body['files[]']) as string[] const publicKey = ctx.request.body && ctx.request.body.pub_key if (!files || files.length === 0) { @@ -99,13 +96,9 @@ const index = (ctx) => { ctx.body = response } -/** - * '/group/info/?pub_key=XXXXXXXXXXXXXXXXXXXX&group_id=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX~N' - * - * @param {object} ctx - */ -const info = (ctx) => { - const groupId = ctx.query && ctx.query.group_id +/** '/group/info/?pub_key=XXXXXXXXXXXXXXXXXXXX&group_id=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX~N' */ +const info: Middleware = (ctx) => { + const groupId = ctx.query && (ctx.query.group_id as string) if (!groupId) { return error(ctx, { diff --git a/packages/upload-client/mock-server/controllers/info.ts b/packages/upload-client/mock-server/controllers/info.ts index ee0ed9165..d69e81886 100644 --- a/packages/upload-client/mock-server/controllers/info.ts +++ b/packages/upload-client/mock-server/controllers/info.ts @@ -1,13 +1,10 @@ import json from '../data/info' import find from '../utils/find' import error from '../utils/error' +import { type Middleware } from 'koa' -/** - * '/info?pub_key=XXXXXXXXXXXXXXXXXXXX&file_id=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' - * - * @param {object} ctx - */ -const index = (ctx) => { +/** '/info?pub_key=XXXXXXXXXXXXXXXXXXXX&file_id=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX' */ +const index: Middleware = (ctx) => { if (ctx.query && ctx.query.file_id) { ctx.body = find(json, 'info') } else { diff --git a/packages/upload-client/mock-server/controllers/multipart.ts b/packages/upload-client/mock-server/controllers/multipart.ts index a26750238..2b1249003 100644 --- a/packages/upload-client/mock-server/controllers/multipart.ts +++ b/packages/upload-client/mock-server/controllers/multipart.ts @@ -2,13 +2,10 @@ import multipartJson from '../data/multipart' import infoJson from '../data/info' import find from '../utils/find' import error from '../utils/error' +import { type Middleware } from 'koa' -/** - * '/multipart/start/' - * - * @param {object} ctx - */ -const start = (ctx) => { +/** '/multipart/start/' */ +const start: Middleware = (ctx) => { if (ctx.request.body && !ctx.request.body.filename) { return error(ctx, { statusText: 'The "filename" parameter is missing.' @@ -41,21 +38,13 @@ const start = (ctx) => { ctx.body = find(multipartJson, 'start') } -/** - * '/multipart/upload/' - * - * @param {object} ctx - */ -const upload = (ctx) => { +/** '/multipart/upload/' */ +const upload: Middleware = (ctx) => { ctx.status = 200 } -/** - * '/multipart/complete/' - * - * @param {object} ctx - */ -const complete = (ctx) => { +/** '/multipart/complete/' */ +const complete: Middleware = (ctx) => { if (ctx.request.body && !ctx.request.body.uuid) { return error(ctx, { statusText: 'uuid is required.' diff --git a/packages/upload-client/mock-server/controllers/throttle.ts b/packages/upload-client/mock-server/controllers/throttle.ts index eaf791f91..a3f62cdf3 100644 --- a/packages/upload-client/mock-server/controllers/throttle.ts +++ b/packages/upload-client/mock-server/controllers/throttle.ts @@ -1,13 +1,10 @@ import error from '../utils/error' +import { type Middleware } from 'koa' let times = 0 -/** - * '/throttle/' - * - * @param {object} ctx - */ -const index = (ctx) => { +/** '/throttle/' */ +const index: Middleware = (ctx) => { times++ if (times === 2) { diff --git a/packages/upload-client/mock-server/middleware/auth.ts b/packages/upload-client/mock-server/middleware/auth.ts index df8550f7a..33dcbbbf6 100644 --- a/packages/upload-client/mock-server/middleware/auth.ts +++ b/packages/upload-client/mock-server/middleware/auth.ts @@ -1,6 +1,7 @@ import { ROUTES, RouteType } from '../routes' import { ALLOWED_PUBLIC_KEYS } from '../config' import error from '../utils/error' +import { type Middleware } from 'koa' /** Routes protected by auth. */ const protectedRoutes: Array = ROUTES.filter((route: RouteType) => { @@ -55,20 +56,16 @@ const isAuthorized = ({ url, publicKey }: IsAuthorizedParams) => { return !!(publicKey && ALLOWED_PUBLIC_KEYS.includes(publicKey)) } -/** - * Uploadcare Auth middleware. - * - * @param {object} ctx - * @param {function} next - */ -const auth = (ctx, next) => { - const urlWithSlash = ctx.url.split('?').shift() +/** Uploadcare Auth middleware. */ +const auth: Middleware = (ctx, next) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const urlWithSlash = ctx.url.split('?').shift()! const url = urlWithSlash.substring(0, urlWithSlash.length - 1) let key = 'pub_key' const params: IsAuthorizedParams = { url, - publicKey: getPublicKeyFromSource(ctx.query, key) + publicKey: getPublicKeyFromSource(ctx.query as Record, key) } // pub_key in body diff --git a/packages/upload-client/mock-server/middleware/delayer.ts b/packages/upload-client/mock-server/middleware/delayer.ts index 5a143d098..9d6683c7f 100644 --- a/packages/upload-client/mock-server/middleware/delayer.ts +++ b/packages/upload-client/mock-server/middleware/delayer.ts @@ -1,7 +1,9 @@ +import { type Middleware } from 'koa' + const delay = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)) -const delayer = async (ctx, next) => { +const delayer: Middleware = async (ctx, next) => { await delay(30) await next() } diff --git a/packages/upload-client/mock-server/middleware/logger.ts b/packages/upload-client/mock-server/middleware/logger.ts index e14ae4af4..05a6e03df 100644 --- a/packages/upload-client/mock-server/middleware/logger.ts +++ b/packages/upload-client/mock-server/middleware/logger.ts @@ -1,4 +1,5 @@ import chalk from 'chalk' +import { type Middleware } from 'koa' /** * Pretty print for JSON. @@ -18,13 +19,8 @@ const pretty = (json: Record): string => const isEmptyObject = (object: Record): boolean => Object.keys(object).length === 0 && object.constructor === Object -/** - * Logger for requests and responses. - * - * @param {object} ctx - * @param {function} next - */ -const logger = async (ctx, next) => { +/** Logger for requests and responses. */ +const logger: Middleware = async (ctx, next) => { await next() const request = `${chalk.gray('-->')} ${chalk.bold( diff --git a/packages/upload-client/mock-server/routes.ts b/packages/upload-client/mock-server/routes.ts index 196b33ea2..727eafe82 100644 --- a/packages/upload-client/mock-server/routes.ts +++ b/packages/upload-client/mock-server/routes.ts @@ -4,11 +4,12 @@ import * as info from './controllers/info' import * as group from './controllers/group' import * as throttle from './controllers/throttle' import * as multipart from './controllers/multipart' +import type { Context, Middleware } from 'koa' export type RouteType = { [path: string]: { - method: string - fn: (ctx: Record, next?: () => Promise) => void + method: 'get' | 'post' | 'put' | 'delete' + fn: Middleware isProtected: boolean isFake?: boolean description?: string @@ -16,7 +17,7 @@ export type RouteType = { } // this route need for health check -const index = (ctx) => { +const index = (ctx: Context) => { ctx.body = 'server is up' } diff --git a/packages/upload-client/mock-server/server.ts b/packages/upload-client/mock-server/server.ts index ac424a995..4963958d0 100644 --- a/packages/upload-client/mock-server/server.ts +++ b/packages/upload-client/mock-server/server.ts @@ -6,6 +6,7 @@ import chalk from 'chalk' // Middleware import cors from '@koa/cors' +// @ts-expect-error There is no types for this package import addTrailingSlashes from 'koa-add-trailing-slashes' // @ts-ignore import koaBody from 'koa-body' @@ -24,7 +25,7 @@ const app = new Koa() const router = new Router() const silent = process.argv.includes('--silent') -const noop = (_, next) => next() +const noop: Koa.Middleware = (_, next) => next() // Use middleware app.use(cors()) diff --git a/packages/upload-client/mock-server/utils/error.ts b/packages/upload-client/mock-server/utils/error.ts index 807bbe8d5..748294506 100644 --- a/packages/upload-client/mock-server/utils/error.ts +++ b/packages/upload-client/mock-server/utils/error.ts @@ -1,3 +1,5 @@ +import { type Context } from 'koa' + type ErrorType = { status?: number statusText: string @@ -5,7 +7,7 @@ type ErrorType = { } const error = ( - ctx, + ctx: Context, { status = 400, statusText, errorCode }: ErrorType ): void => { const isJson = !!ctx.query.jsonerrors diff --git a/packages/upload-client/package.json b/packages/upload-client/package.json index 42184d966..f7ee13593 100644 --- a/packages/upload-client/package.json +++ b/packages/upload-client/package.json @@ -40,7 +40,11 @@ } }, "sideEffects": false, - "files": ["dist/*", "README.md", "LICENSE"], + "files": [ + "dist/*", + "README.md", + "LICENSE" + ], "engines": { "node": ">=16" }, @@ -80,7 +84,11 @@ "@koa/router": "10.1.1", "@types/express-serve-static-core": "^4.17.28", "@types/koa": "2.13.4", + "@types/koa__cors": "^5.0.0", + "@types/koa__router": "^12.0.4", "@types/ws": "8.5.3", + "@uploadcare/api-client-utils": "^6.14.0", + "chalk": "^4.1.2", "data-uri-to-buffer": "3.0.1", "dataurl-to-blob": "0.0.1", "jest-environment-jsdom": "29.3.1", @@ -89,9 +97,7 @@ "koa-add-trailing-slashes": "2.0.1", "koa-body": "5.0.0", "mock-socket": "9.0.3", - "start-server-and-test": "1.14.0", - "@uploadcare/api-client-utils": "^6.14.0", - "chalk": "^4.1.2" + "start-server-and-test": "1.14.0" }, "dependencies": { "form-data": "^4.0.0", diff --git a/packages/upload-client/src/api/multipartStart.ts b/packages/upload-client/src/api/multipartStart.ts index 7aa12376a..b341c9d8b 100644 --- a/packages/upload-client/src/api/multipartStart.ts +++ b/packages/upload-client/src/api/multipartStart.ts @@ -104,7 +104,7 @@ export default function multipartStart( } else { // convert to array response.parts = Object.keys(response.parts).map( - (key) => response.parts[key] + (key) => response.parts[Number(key)] ) return response diff --git a/packages/upload-client/src/request/request.browser.ts b/packages/upload-client/src/request/request.browser.ts index 3b58c0bee..7f5262a7d 100644 --- a/packages/upload-client/src/request/request.browser.ts +++ b/packages/upload-client/src/request/request.browser.ts @@ -66,7 +66,7 @@ const request = ({ .split(/[\r\n]+/) // Create a map of header names to values - const responseHeaders = {} + const responseHeaders: Record = {} headersArray.forEach(function (line) { const parts = line.split(': ') diff --git a/packages/upload-client/src/request/request.node.ts b/packages/upload-client/src/request/request.node.ts index 2736ffa1a..1f0a6765f 100644 --- a/packages/upload-client/src/request/request.node.ts +++ b/packages/upload-client/src/request/request.node.ts @@ -2,7 +2,7 @@ import NodeFormData from 'form-data' import http from 'http' import https from 'https' -import { Readable, Transform } from 'stream' +import { Readable, Transform, TransformCallback } from 'stream' import { parse } from 'url' import { CancelError, onCancel } from '@uploadcare/api-client-utils' @@ -26,7 +26,11 @@ class ProgressEmitter extends Transform { this.size = size } - _transform(chunk, encoding, callback): void { + _transform( + chunk: Buffer, + encoding: BufferEncoding, + callback: TransformCallback + ): void { this._position += chunk.length this._onprogress({ isComputable: true, diff --git a/tsconfig.json b/tsconfig.json index e814e45cc..f44d6678d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "target": "ESNext", "module": "ESNext", "lib": ["DOM", "DOM.Iterable"], - "noImplicitAny": false, + "noImplicitAny": true, "strictNullChecks": true, "strict": true, "resolveJsonModule": true,