refactor(core): remove duplicate types (#6359)

This PR removes the duplicate companion/provider bridge types
(CompanionClientProvider CompanionClientSearchProvider, and the`view:
any` escape hatch) and wires the real Provider/ProviderView types
directly, now possible since everything lives in `@uppy/core` Net gain
in type safety; no behavior change.

for more context on the approach see description of #6351
This commit is contained in:
Prakash 2026-06-30 16:23:23 +05:30 committed by GitHub
commit 6c05477b65
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 100 additions and 125 deletions

View file

@ -5,14 +5,9 @@ import {
type PluginOpts,
type Uppy,
} from '@uppy/core'
import type { RequestOptions } from '@uppy/core/companion-client'
import { RequestClient } from '@uppy/core/companion-client'
import type {
Body,
LocalUppyFile,
Meta,
RequestOptions,
UppyFile,
} from '@uppy/core/utils'
import type { Body, LocalUppyFile, Meta, UppyFile } from '@uppy/core/utils'
import {
createAbortError,
filterFilesToEmitUploadStarted,

View file

@ -7,9 +7,12 @@ import { nanoid } from 'nanoid/non-secure'
import type { h } from 'preact'
import packageJson from '../package.json' with { type: 'json' }
import type BasePlugin from './BasePlugin.js'
import type Provider from './companion-client/Provider.js'
import type SearchProvider from './companion-client/SearchProvider.js'
import getFileName from './getFileName.js'
import locale from './locale.js'
import { debugLogger, justErrorsLogger } from './loggers.js'
import type ProviderView from './provider-views/ProviderView/ProviderView.js'
import type { Restrictions, ValidateableFile } from './Restricter.js'
import {
defaultOptions as defaultRestrictionOptions,
@ -20,8 +23,6 @@ import DefaultStore, { type Store } from './store/index.js'
import supportsUploadProgress from './supportsUploadProgress.js'
import type {
Body,
CompanionClientProvider,
CompanionClientSearchProvider,
CompanionFile,
FileProgressNotStarted,
FileProgressStarted,
@ -165,8 +166,6 @@ export interface BaseProviderPlugin {
* UnknownProviderPlugin can be any Companion plugin (such as Google Drive)
* that uses the Companion-assisted OAuth flow.
* As the plugins are passed around throughout Uppy we need a generic type for this.
* It may seems like duplication, but this type safe. Changing the type of `storage`
* will error in the `Provider` class of @uppy/core/companion-client and vice versa.
*
* Note that this is the *plugin* class, not a version of the `Provider` class.
* `Provider` does operate on Companion plugins with `uppy.getPlugin()`.
@ -178,16 +177,25 @@ export type UnknownProviderPlugin<
BaseProviderPlugin & {
rootFolderId: string | null
files: UppyFile<M, B>[]
provider: CompanionClientProvider
// Can't be typed unfortunately, we can't depend on `provider-views` in `core`.
view: any
// Structural subset of the real `Provider` class,structural rather than the nominal class type so custom
// providers matching the public surface aren't forced to inherit from `Provider`.
provider: Pick<
Provider<M, B>,
| 'name'
| 'provider'
| 'login'
| 'logout'
| 'fetchPreAuthToken'
| 'fileUrl'
| 'list'
| 'search'
>
view: ProviderView<M, B>
}
/*
* UnknownSearchProviderPlugin can be any search Companion plugin (such as Unsplash).
* As the plugins are passed around throughout Uppy we need a generic type for this.
* It may seems like duplication, but this type safe. Changing the type of `title`
* will error in the `SearchProvider` class of @uppy/core/companion-client and vice versa.
*
* Note that this is the *plugin* class, not a version of the `SearchProvider` class.
* `SearchProvider` does operate on Companion plugins with `uppy.getPlugin()`.
@ -203,7 +211,11 @@ export type UnknownSearchProviderPlugin<
B extends Body,
> = UnknownPlugin<M, B, UnknownSearchProviderPluginState> &
BaseProviderPlugin & {
provider: CompanionClientSearchProvider
// Structural subset of the real `SearchProvider` class (see note above).
provider: Pick<
SearchProvider<M, B>,
'name' | 'provider' | 'fileUrl' | 'search'
>
}
// for better readability

View file

@ -5,13 +5,12 @@ import type {
UnknownProviderPlugin,
Uppy,
} from '../index.js'
import {
type CompanionClientProvider,
getSocketHost,
type RequestOptions,
} from '../utils/index.js'
import { getSocketHost } from '../utils/index.js'
import type { CompanionPluginOptions } from './index.js'
import RequestClient, { authErrorStatusCode } from './RequestClient.js'
import RequestClient, {
authErrorStatusCode,
type RequestOptions,
} from './RequestClient.js'
export interface Opts extends PluginOpts, CompanionPluginOptions {
pluginId: string
@ -31,10 +30,10 @@ function getOrigin() {
return location.origin
}
export default class Provider<M extends Meta, B extends Body>
extends RequestClient<M, B>
implements CompanionClientProvider
{
export default class Provider<
M extends Meta,
B extends Body,
> extends RequestClient<M, B> {
#refreshingTokenPromise: Promise<void> | undefined
provider: string
@ -273,11 +272,13 @@ export default class Provider<M extends Meta, B extends Body>
}
async login({
uppyVersions,
uppyVersions = '',
authFormData,
signal,
}: {
uppyVersions: string
// `uppyVersions` is optional because callers such as `ProviderView` do not
// have it on hand; the OAuth/simple-auth helpers always receive a string.
uppyVersions?: string
authFormData: unknown
signal: AbortSignal
}): Promise<void> {

View file

@ -1,13 +1,7 @@
import pRetry, { AbortError } from 'p-retry'
import packageJson from '../../package.json' with { type: 'json' }
import type Uppy from '../index.js'
import type {
Body,
Meta,
RemoteUppyFile,
RequestOptions,
UppyFile,
} from '../utils/index.js'
import type { Body, Meta, RemoteUppyFile, UppyFile } from '../utils/index.js'
import {
ErrorWithCause,
fetchWithNetworkError,
@ -16,6 +10,15 @@ import {
} from '../utils/index.js'
import AuthError from './AuthError.js'
export type RequestOptions = {
method?: string
data?: Record<string, unknown>
skipPostResponse?: boolean
signal?: AbortSignal
authFormData?: unknown
qs?: Record<string, string>
}
type CompanionHeaders = Record<string, string> | undefined
export type Opts = {

View file

@ -1,5 +1,4 @@
import type { Body, Meta, Uppy } from '../index.js'
import type { CompanionClientSearchProvider } from '../utils/index.js'
import RequestClient, { type Opts } from './RequestClient.js'
const getName = (id: string): string => {
@ -9,10 +8,10 @@ const getName = (id: string): string => {
.join(' ')
}
export default class SearchProvider<M extends Meta, B extends Body>
extends RequestClient<M, B>
implements CompanionClientSearchProvider
{
export default class SearchProvider<
M extends Meta,
B extends Body,
> extends RequestClient<M, B> {
provider: string
id: string

View file

@ -5,6 +5,9 @@
export type { CompanionPluginOptions } from './CompanionPluginOptions.js'
export { default as getAllowedHosts } from './getAllowedHosts.js'
export { default as Provider } from './Provider.js'
export { default as RequestClient } from './RequestClient.js'
export {
default as RequestClient,
type RequestOptions,
} from './RequestClient.js'
export { default as SearchProvider } from './SearchProvider.js'
export * as tokenStorage from './tokenStorage.js'

View file

@ -66,6 +66,17 @@ const getDefaultState = (
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>
/**
* Shape of the responses Companion returns from its `list` and `search`
* endpoints. The `Provider` methods are generic, so we pass this as the
* expected response type at the call sites.
*/
type ProviderListResponse = {
username: string
nextPagePath: string | null
items: CompanionFile[]
}
export interface Opts<M extends Meta, B extends Body> {
provider: UnknownProviderPlugin<M, B>['provider']
viewType: 'list' | 'grid'
@ -248,10 +259,13 @@ export default class ProviderView<M extends Meta, B extends Body> {
await this.#withAbort(async (signal) => {
const scopePath =
currentFolder.type === 'root' ? undefined : currentFolderId
const { items } = await this.provider.search!(searchString, {
signal,
path: scopePath,
})
const { items } = await this.provider.search<ProviderListResponse>(
searchString,
{
signal,
path: scopePath,
},
)
// For each searched file, build the entire path (from the root all the way to the leaf node)
// This is because we need to make sure all ancestor folders are present in the partialTree before we open the folder or check the file.
@ -389,10 +403,10 @@ export default class ProviderView<M extends Meta, B extends Body> {
let currentPagePath = folderId
let currentItems: CompanionFile[] = []
do {
const { username, nextPagePath, items } = await this.provider.list(
currentPagePath,
{ signal },
)
const { username, nextPagePath, items } =
await this.provider.list<ProviderListResponse>(currentPagePath, {
signal,
})
// It's important to set the username during one of our first fetches
this.plugin.setPluginState({ username })
@ -478,10 +492,11 @@ export default class ProviderView<M extends Meta, B extends Body> {
) {
this.isHandlingScroll = true
await this.#withAbort(async (signal) => {
const { nextPagePath, items } = await this.provider.list(
currentFolder.nextPagePath,
{ signal },
)
const { nextPagePath, items } =
await this.provider.list<ProviderListResponse>(
currentFolder.nextPagePath,
{ signal },
)
const newPartialTree = PartialTreeUtils.afterScrollFolder(
partialTree,
currentFolderId,

View file

@ -1,8 +1,10 @@
import type { UnknownPlugin } from '../../index.js'
import type {
UnknownPlugin,
UnknownProviderPlugin,
UnknownSearchProviderPlugin,
} from '../../index.js'
import type {
Body,
CompanionClientProvider,
CompanionClientSearchProvider,
CompanionFile,
Meta,
UppyFileNonGhost,
@ -13,7 +15,9 @@ import companionFileToUppyFile from './companionFileToUppyFile.js'
const addFiles = <M extends Meta, B extends Body>(
companionFiles: CompanionFile[],
plugin: UnknownPlugin<M, B>,
provider: CompanionClientProvider | CompanionClientSearchProvider,
provider:
| UnknownProviderPlugin<M, B>['provider']
| UnknownSearchProviderPlugin<M, B>['provider'],
): void => {
const uppyFiles = companionFiles.map((f) =>
companionFileToUppyFile<M, B>(f, plugin, provider),

View file

@ -1,8 +1,10 @@
import type { UnknownPlugin } from '../../index.js'
import type {
UnknownPlugin,
UnknownProviderPlugin,
UnknownSearchProviderPlugin,
} from '../../index.js'
import type {
Body,
CompanionClientProvider,
CompanionClientSearchProvider,
CompanionFile,
Meta,
RemoteUppyFile,
@ -11,7 +13,9 @@ import type {
const companionFileToUppyFile = <M extends Meta, B extends Body>(
file: CompanionFile,
plugin: UnknownPlugin<M, B>,
provider: CompanionClientProvider | CompanionClientSearchProvider,
provider:
| UnknownProviderPlugin<M, B>['provider']
| UnknownSearchProviderPlugin<M, B>['provider'],
): RemoteUppyFile<M, B> => {
const name = file.name || file.id

View file

@ -1,56 +0,0 @@
import type { CompanionFile } from './CompanionFile.js'
export type RequestOptions = {
method?: string
data?: Record<string, unknown>
skipPostResponse?: boolean
signal?: AbortSignal
authFormData?: unknown
qs?: Record<string, string>
}
/**
* CompanionClientProvider is subset of the types of the `Provider`
* class from @uppy/core/companion-client.
*
* This is needed as the `Provider` class is passed around in Uppy and we
* need to have shared types for it. Although we are duplicating some types,
* this is still safe as `Provider implements CompanionClientProvider`
* so any changes here will error there and vice versa.
*
* TODO: remove this once companion-client and provider-views are merged into a single plugin.
*/
export interface CompanionClientProvider {
name: string
provider: string
login(options?: RequestOptions): Promise<void>
logout<ResBody>(options?: RequestOptions): Promise<ResBody>
fetchPreAuthToken(): Promise<void>
fileUrl: (a: string) => string
list(
directory: string | null,
options: RequestOptions,
): Promise<{
username: string
nextPagePath: string | null
items: CompanionFile[]
}>
// Optional: not every provider implements server-side search.
search?: (
text: string,
options?: RequestOptions & {
path?: string | null | undefined
cursor?: string | undefined
},
) => Promise<{
username: string
nextPagePath: string | null
items: CompanionFile[]
}>
}
export interface CompanionClientSearchProvider {
name: string
provider: string
fileUrl: (a: string) => string
search<ResBody>(text: string, queries?: string): Promise<ResBody>
}

View file

@ -3,11 +3,6 @@ export {
AbortSignal,
createAbortError,
} from './AbortController.js'
export type {
CompanionClientProvider,
CompanionClientSearchProvider,
RequestOptions,
} from './CompanionClientProvider.js'
export type { CompanionFile } from './CompanionFile.js'
export { default as canvasToBlob } from './canvasToBlob.js'

View file

@ -27,10 +27,10 @@ class WebdavSimpleAuthProvider<M extends Meta, B extends Body> extends Provider<
> {
async login({
authFormData,
uppyVersions,
uppyVersions = '',
signal,
}: {
uppyVersions: string
uppyVersions?: string
authFormData: unknown
signal: AbortSignal
}) {