Forráskód Böngészése

meta: validate `defaultOptions` for stricter option types (#4901)

Antoine du Hamel 1 éve
szülő
commit
66bd526ab3

+ 2 - 1
packages/@uppy/companion-client/src/Provider.ts

@@ -1,5 +1,6 @@
 import type { Uppy, BasePlugin } from '@uppy/core'
 import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
+import type { PluginOpts } from '@uppy/core/lib/BasePlugin.ts'
 import RequestClient, {
   authErrorStatusCode,
   type RequestOptions,
@@ -7,7 +8,7 @@ import RequestClient, {
 import * as tokenStorage from './tokenStorage.ts'
 
 // TODO: remove deprecated options in next major release
-export type Opts = {
+export interface Opts extends PluginOpts {
   /** @deprecated */
   serverUrl?: string
   /** @deprecated */

+ 10 - 3
packages/@uppy/core/src/BasePlugin.ts

@@ -21,15 +21,22 @@ import type { State, Uppy } from './Uppy'
 export type PluginOpts = {
   locale?: Locale
   id?: string
-  [key: string]: unknown
 }
 
+export type OnlyOptionals<T> = Pick<
+  T,
+  {
+    // eslint-disable-next-line @typescript-eslint/ban-types
+    [K in keyof T]-?: {} extends Pick<T, K> ? K : never
+  }[keyof T]
+>
+
 /**
  * DefinePluginOpts marks all of the passed AlwaysDefinedKeys as “required” or “always defined”.
  */
 export type DefinePluginOpts<
-  Opts extends PluginOpts,
-  AlwaysDefinedKeys extends string,
+  Opts,
+  AlwaysDefinedKeys extends keyof OnlyOptionals<Opts>,
 > = Opts & Required<Pick<Opts, AlwaysDefinedKeys>>
 
 export default class BasePlugin<

+ 2 - 2
packages/@uppy/image-editor/src/Editor.tsx

@@ -8,12 +8,12 @@ import getCanvasDataThatFitsPerfectlyIntoContainer from './utils/getCanvasDataTh
 import getScaleFactorThatRemovesDarkCorners from './utils/getScaleFactorThatRemovesDarkCorners.ts'
 import limitCropboxMovementOnMove from './utils/limitCropboxMovementOnMove.ts'
 import limitCropboxMovementOnResize from './utils/limitCropboxMovementOnResize.ts'
-import type { ImageEditorOpts } from './ImageEditor.tsx'
+import type ImageEditor from './ImageEditor.tsx'
 
 type Props<M extends Meta, B extends Body> = {
   currentImage: UppyFile<M, B>
   storeCropperInstance: (cropper: Cropper) => void
-  opts: ImageEditorOpts
+  opts: ImageEditor<M, B>['opts']
   i18n: I18n
   // eslint-disable-next-line react/no-unused-prop-types
   save: () => void // eslint confused

+ 22 - 12
packages/@uppy/image-editor/src/ImageEditor.tsx

@@ -36,8 +36,8 @@ declare module '@uppy/core' {
   }
 }
 
-export interface Opts extends UIPluginOptions {
-  target: string | HTMLElement
+interface Opts extends UIPluginOptions {
+  target?: string | HTMLElement
   quality?: number
   cropperOptions?: Cropper.Options & {
     croppedCanvasOptions?: Cropper.GetCroppedCanvasOptions
@@ -54,13 +54,14 @@ export interface Opts extends UIPluginOptions {
     cropWidescreenVertical?: boolean
   }
 }
+export type { Opts as ImageEditorOptions }
 
 type PluginState<M extends Meta, B extends Body> = {
   currentImage: UppyFile<M, B> | null
 }
 
 const defaultCropperOptions = {
-  viewMode: 0,
+  viewMode: 0 as const,
   background: false,
   autoCropArea: 1,
   responsive: true,
@@ -68,7 +69,7 @@ const defaultCropperOptions = {
   minCropBoxHeight: 70,
   croppedCanvasOptions: {},
   initialAspectRatio: 0,
-} satisfies Opts['cropperOptions']
+} satisfies Partial<Opts['cropperOptions']>
 
 const defaultActions = {
   revert: true,
@@ -80,7 +81,7 @@ const defaultActions = {
   cropSquare: true,
   cropWidescreen: true,
   cropWidescreenVertical: true,
-} satisfies Opts['actions']
+} satisfies Partial<Opts['actions']>
 
 const defaultOptions = {
   target: 'body',
@@ -89,17 +90,26 @@ const defaultOptions = {
   quality: 0.8,
   actions: defaultActions,
   cropperOptions: defaultCropperOptions,
-} satisfies Opts
-
-export type ImageEditorOpts = DefinePluginOpts<
-  Opts,
-  keyof typeof defaultOptions
->
+} satisfies Partial<Opts>
+
+type InternalImageEditorOpts = Omit<
+  DefinePluginOpts<Opts, keyof typeof defaultOptions>,
+  'actions' | 'cropperOptions'
+> & {
+  actions: DefinePluginOpts<
+    NonNullable<Opts['actions']>,
+    keyof typeof defaultActions
+  >
+  cropperOptions: DefinePluginOpts<
+    NonNullable<Opts['cropperOptions']>,
+    keyof typeof defaultCropperOptions
+  >
+}
 
 export default class ImageEditor<
   M extends Meta,
   B extends Body,
-> extends UIPlugin<ImageEditorOpts, M, B, PluginState<M, B>> {
+> extends UIPlugin<InternalImageEditorOpts, M, B, PluginState<M, B>> {
   static VERSION = packageJson.version
 
   cropper: Cropper

+ 2 - 0
packages/@uppy/xhr-upload/src/index.test.ts

@@ -26,6 +26,7 @@ describe('XHRUpload', () => {
       core.use(XHRUpload, {
         id: 'XHRUpload',
         endpoint: 'https://fake-endpoint.uppy.io',
+        // @ts-expect-error that option does not exist
         some: 'option',
         getResponseData,
       })
@@ -65,6 +66,7 @@ describe('XHRUpload', () => {
       core.use(XHRUpload, {
         id: 'XHRUpload',
         endpoint: 'https://fake-endpoint.uppy.io',
+        // @ts-expect-error that option doesn't exist
         some: 'option',
         validateStatus,
         getResponseError(responseText) {

+ 5 - 2
packages/@uppy/xhr-upload/src/index.ts

@@ -105,7 +105,6 @@ function setTypeInBlob<M extends Meta, B extends Body>(file: UppyFile<M, B>) {
 }
 
 const defaultOptions = {
-  endpoint: '',
   formData: true,
   fieldName: 'file',
   method: 'post',
@@ -132,7 +131,7 @@ const defaultOptions = {
   validateStatus(status) {
     return status >= 200 && status < 300
   },
-} satisfies XhrUploadOpts<any, any>
+} satisfies Partial<XhrUploadOpts<any, any>>
 
 type Opts<M extends Meta, B extends Body> = DefinePluginOpts<
   XhrUploadOpts<M, B>,
@@ -169,6 +168,8 @@ export default class XHRUpload<
 
     // Simultaneous upload limiting is shared across all uploads with this plugin.
     if (internalRateLimitedQueue in this.opts) {
+      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+      // @ts-ignore untyped internal
       this.requests = this.opts[internalRateLimitedQueue]
     } else {
       this.requests = new RateLimitedQueue(this.opts.limit)
@@ -608,6 +609,8 @@ export default class XHRUpload<
 
     // No limit configured by the user, and no RateLimitedQueue passed in by a "parent" plugin
     // (basically just AwsS3) using the internal symbol
+    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+    // @ts-ignore untyped internal
     if (this.opts.limit === 0 && !this.opts[internalRateLimitedQueue]) {
       this.uppy.log(
         '[XHRUpload] When uploading multiple files at once, consider setting the `limit` option (to `10` for example), to limit the number of concurrent uploads, which helps prevent memory and network issues: https://uppy.io/docs/xhr-upload/#limit-0',