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

refactor: stricter plugin types #1

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions packages/runtime-core/src/apiCreateApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ import { ObjectEmitsOptions } from './componentEmits'
export interface App<HostElement = any> {
version: string
config: AppConfig
use<Option>(plugin: Plugin<Option>, ...options: Option[]): this

use<Options extends unknown[]>(
plugin: Plugin<Options>,
...options: Options
): this
use<Options>(plugin: Plugin<Options>, options: Options): this

mixin(mixin: ComponentOptions): this
component(name: string): Component | undefined
component(name: string, component: Component): this
Expand Down Expand Up @@ -137,12 +143,18 @@ export interface AppContext {
filters?: Record<string, Function>
}

type PluginInstallFunction<Option> = (app: App, ...options: Option[]) => any
type PluginInstallFunction<Options> = Options extends unknown[]
? (app: App, ...options: Options) => any
: Options extends undefined
? (app: App, options?: Options) => any
: (app: App, options: Options) => any

export type Plugin<Option = any> =
| PluginInstallFunction<Option> & { install?: PluginInstallFunction<Option> }
export type Plugin<Options = any[]> =
| (PluginInstallFunction<Options> & {
install?: PluginInstallFunction<Options>
})
| {
install: PluginInstallFunction<Option>
install: PluginInstallFunction<Options>
}

export function createAppContext(): AppContext {
Expand Down
132 changes: 78 additions & 54 deletions test-dts/appUse.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,71 +1,95 @@
import { createApp } from './index'
import { createApp, App, Plugin } from './index'

type App = ReturnType<typeof createApp>
const app = createApp({})

type PluginAOptionType = {
// Plugin without types accept anything
const PluginWithoutType: Plugin = {
install(app: App) {}
}

app.use(PluginWithoutType)
app.use(PluginWithoutType, 2)
app.use(PluginWithoutType, { anything: 'goes' }, true)

type PluginOptions = {
option1?: string
option2: number
option3: boolean
}

const PluginA = {
install(app: App, ...options: PluginAOptionType[]) {
options[0].option1
options[0].option2
options[0].option3
const PluginWithObjectOptions = {
install(app: App, options: PluginOptions) {
options.option1
options.option2
options.option3
}
}

const PluginB = {
install(app: App) {}
}

const PluginC = (app: App, ...options: string[]) => {}
for (const Plugin of [
PluginWithObjectOptions,
PluginWithObjectOptions.install
]) {
// @ts-expect-error: no params
app.use(Plugin)

createApp({})
.use(PluginA)
// @ts-expect-error option2 and option3 (required) missing
.use(PluginA, {})
// @ts-expect-error type mismatch
.use(PluginA, true)
// @ts-expect-error type mismatch
.use(PluginA, undefined)
// @ts-expect-error type mismatch
.use(PluginA, null)
app.use(Plugin, {})
// @ts-expect-error type mismatch
.use(PluginA, 'foo')
// @ts-expect-error type mismatch
.use(PluginA, 1)
.use(PluginA, { option2: 1, option3: true })
.use(PluginA, { option1: 'foo', option2: 1, option3: true })
.use(PluginA, { option1: 'foo', option2: 1, option3: true }, { option2: 1, option3: true })
app.use(Plugin, undefined)
// valid options
app.use(Plugin, { option2: 1, option3: true })
app.use(Plugin, { option1: 'foo', option2: 1, option3: true })
}

// @ts-expect-error option2 (required) missing
.use(PluginA, { option3: true })
const PluginNoOptions = {
install(app: App) {}
}

.use(PluginB)
// @ts-expect-error unexpected plugin option
.use(PluginB, {})
for (const Plugin of [PluginNoOptions, PluginNoOptions.install]) {
// no args
app.use(Plugin)
// @ts-expect-error unexpected plugin option
.use(PluginB, true)
// @ts-expect-error unexpected plugin option
.use(PluginB, undefined)
// @ts-expect-error unexpected plugin option
.use(PluginB, null)
// @ts-expect-error type mismatch
.use(PluginB, 'foo')
// @ts-expect-error type mismatch
.use(PluginB, 1)
app.use(Plugin, {})
// @ts-expect-error only no options is valid
app.use(Plugin, undefined)
}

.use(PluginC)
// @ts-expect-error unexpected plugin option
.use(PluginC, {})
// @ts-expect-error unexpected plugin option
.use(PluginC, true)
// @ts-expect-error unexpected plugin option
.use(PluginC, undefined)
// @ts-expect-error unexpected plugin option
.use(PluginC, null)
.use(PluginC, 'foo')
// @ts-expect-error type mismatch
.use(PluginC, 1)
const PluginMultipleArgs = {
install: (app: App, a: string, b: number) => {}
}

for (const Plugin of [PluginMultipleArgs, PluginMultipleArgs.install]) {
// @ts-expect-error: 2 arguments expected
app.use(Plugin, 'hey')
app.use(Plugin, 'hey', 2)
}

const PluginOptionalOptions = {
install(
app: App,
options: PluginOptions = { option2: 2, option3: true, option1: 'foo' }
) {
options.option1
options.option2
options.option3
}
}

for (const Plugin of [PluginOptionalOptions, PluginOptionalOptions.install]) {
// both version are valid
app.use(Plugin)
app.use(Plugin, undefined)

// @ts-expect-error option2 and option3 (required) missing
app.use(Plugin, {})
// valid options
app.use(Plugin, { option2: 1, option3: true })
app.use(Plugin, { option1: 'foo', option2: 1, option3: true })
}

// still valid but it's better to use the regular function because this one can accept an optional param
const PluginTyped: Plugin<PluginOptions> = (app, options) => {}

// @ts-expect-error: needs options
app.use(PluginTyped)
app.use(PluginTyped, { option2: 2, option3: true })