From 77dbe1f6843c034bf326254a58eb0b5c4d480e14 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Mon, 9 Mar 2020 22:58:12 -0700 Subject: [PATCH] Use strict types with PluginDriver (#3426) * Use strict types with PluginDriver * Fix type errors Co-authored-by: Lukas Taegert-Atkinson --- src/ast/nodes/MetaProperty.ts | 4 +- src/rollup/rollup.ts | 4 +- src/rollup/types.d.ts | 48 +++++++- src/utils/PluginDriver.ts | 209 ++++++++++++++++++++++------------ src/utils/renderChunk.ts | 2 +- src/utils/transform.ts | 2 +- 6 files changed, 189 insertions(+), 80 deletions(-) diff --git a/src/ast/nodes/MetaProperty.ts b/src/ast/nodes/MetaProperty.ts index 3b29cb38b..e7b38ab82 100644 --- a/src/ast/nodes/MetaProperty.ts +++ b/src/ast/nodes/MetaProperty.ts @@ -106,7 +106,7 @@ export default class MetaProperty extends NodeBase { ]); } if (!replacement) { - replacement = outputPluginDriver.hookFirstSync<'resolveFileUrl', string>('resolveFileUrl', [ + replacement = outputPluginDriver.hookFirstSync<'resolveFileUrl'>('resolveFileUrl', [ { assetReferenceId, chunkId, @@ -117,7 +117,7 @@ export default class MetaProperty extends NodeBase { referenceId: referenceId || assetReferenceId || chunkReferenceId!, relativePath } - ]); + ]) as string; } code.overwrite( diff --git a/src/rollup/rollup.ts b/src/rollup/rollup.ts index 51715ca69..17354ab46 100644 --- a/src/rollup/rollup.ts +++ b/src/rollup/rollup.ts @@ -362,8 +362,8 @@ function normalizeOutputOptions( const outputOptions = parseOutputOptions( outputPluginDriver.hookReduceArg0Sync( 'outputOptions', - [rawOutputOptions.output || inputOptions.output || rawOutputOptions], - (outputOptions: OutputOptions, result: OutputOptions) => result || outputOptions, + [rawOutputOptions.output || inputOptions.output || rawOutputOptions] as [OutputOptions], + (outputOptions, result) => result || outputOptions, pluginContext => { const emitError = () => pluginContext.error(errCannotEmitFromOptionsHook()); return { diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 352acf4ce..e88f5381d 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -288,7 +288,8 @@ export type ResolveFileUrlHook = ( } ) => string | null | undefined; -export type AddonHook = string | ((this: PluginContext) => string | Promise); +export type AddonHookFunction = (this: PluginContext) => string | Promise; +export type AddonHook = string | AddonHookFunction; /** * use this type for plugin annotation @@ -352,6 +353,51 @@ export interface PluginHooks extends OutputPluginHooks { watchChange: (id: string) => void; } +export type AsyncPluginHooks = + | 'buildEnd' + | 'buildStart' + | 'generateBundle' + | 'load' + | 'renderChunk' + | 'renderError' + | 'renderStart' + | 'resolveDynamicImport' + | 'resolveId' + | 'transform' + | 'writeBundle'; + +export type PluginValueHooks = 'banner' | 'footer' | 'intro' | 'outro'; + +export type SyncPluginHooks = Exclude; + +export type FirstPluginHooks = + | 'load' + | 'resolveAssetUrl' + | 'resolveDynamicImport' + | 'resolveFileUrl' + | 'resolveId' + | 'resolveImportMeta'; + +export type SequentialPluginHooks = + | 'augmentChunkHash' + | 'generateBundle' + | 'options' + | 'outputOptions' + | 'renderChunk' + | 'transform' + | 'watchChange'; + +export type ParallelPluginHooks = + | 'banner' + | 'buildEnd' + | 'buildStart' + | 'footer' + | 'intro' + | 'outro' + | 'renderError' + | 'renderStart' + | 'writeBundle'; + interface OutputPluginValueHooks { banner: AddonHook; cacheKey: string; diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index 0879bf14b..b3f6a804a 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -1,11 +1,18 @@ import Graph from '../Graph'; import { + AddonHookFunction, + AsyncPluginHooks, EmitFile, + FirstPluginHooks, OutputBundleWithPlaceholders, + ParallelPluginHooks, Plugin, PluginContext, PluginHooks, - SerializablePluginCache + PluginValueHooks, + SequentialPluginHooks, + SerializablePluginCache, + SyncPluginHooks } from '../rollup/types'; import { getRollupDefaultPlugin } from './defaultPlugin'; import { errInputHookInOutputPlugin, error } from './error'; @@ -13,11 +20,30 @@ import { FileEmitter } from './FileEmitter'; import { getPluginContexts } from './PluginContext'; import { throwPluginError, warnDeprecatedHooks } from './pluginUtils'; -type Args = T extends (...args: infer K) => any ? K : never; -type EnsurePromise = Promise ? K : T>; +/** + * Get the inner type from a promise + * @example ResolveValue> -> string + */ +type ResolveValue = T extends Promise ? K : T; +/** + * Coerce a promise union to always be a promise. + * @example EnsurePromise> -> Promise + */ +type EnsurePromise = Promise>; +/** + * Get the type of the first argument in a function. + * @example Arg0<(a: string, b: number) => void> -> string + */ +type Arg0 = Parameters[0]; -export type Reduce = (reduction: T, result: R, plugin: Plugin) => T; -export type ReplaceContext = (context: PluginContext, plugin: Plugin) => PluginContext; +type ReplaceContext = (context: PluginContext, plugin: Plugin) => PluginContext; + +function throwInvalidHookError(hookName: string, pluginName: string) { + return error({ + code: 'INVALID_PLUGIN_HOOK', + message: `Error running plugin hook ${hookName} for ${pluginName}, expected a function hook.` + }); +} export class PluginDriver { public emitFile: EmitFile; @@ -72,64 +98,69 @@ export class PluginDriver { } // chains, first non-null result stops and returns - hookFirst>( + hookFirst( hookName: H, - args: Args, + args: Parameters, replaceContext?: ReplaceContext | null, skip?: number | null - ): EnsurePromise { - let promise: Promise = Promise.resolve(); + ): EnsurePromise> { + let promise: EnsurePromise> = Promise.resolve(undefined as any); for (let i = 0; i < this.plugins.length; i++) { if (skip === i) continue; - promise = promise.then((result: any) => { + promise = promise.then(result => { if (result != null) return result; - return this.runHook(hookName, args as any[], i, false, replaceContext); + return this.runHook(hookName, args, i, false, replaceContext); }); } return promise; } // chains synchronously, first non-null result stops and returns - hookFirstSync>( + hookFirstSync( hookName: H, - args: Args, + args: Parameters, replaceContext?: ReplaceContext - ): R { + ): ReturnType { for (let i = 0; i < this.plugins.length; i++) { const result = this.runHookSync(hookName, args, i, replaceContext); - if (result != null) return result as any; + if (result != null) return result; } return null as any; } // parallel, ignores returns - hookParallel( + hookParallel( hookName: H, - args: Args, + args: Parameters, replaceContext?: ReplaceContext ): Promise { const promises: Promise[] = []; for (let i = 0; i < this.plugins.length; i++) { - const hookPromise = this.runHook(hookName, args as any[], i, false, replaceContext); + const hookPromise = this.runHook(hookName, args, i, false, replaceContext); if (!hookPromise) continue; promises.push(hookPromise); } return Promise.all(promises).then(() => {}); } - // chains, reduces returns of type R, to type T, handling the reduced value as the first hook argument - hookReduceArg0>( + // chains, reduces returned value, handling the reduced value as the first hook argument + hookReduceArg0( hookName: H, - [arg0, ...args]: any[], - reduce: Reduce, + [arg0, ...rest]: Parameters, + reduce: ( + reduction: Arg0, + result: ResolveValue>, + plugin: Plugin + ) => Arg0, replaceContext?: ReplaceContext - ) { + ): Promise> { let promise = Promise.resolve(arg0); for (let i = 0; i < this.plugins.length; i++) { promise = promise.then(arg0 => { - const hookPromise = this.runHook(hookName, [arg0, ...args], i, false, replaceContext); + const args = [arg0, ...rest] as Parameters; + const hookPromise = this.runHook(hookName, args, i, false, replaceContext); if (!hookPromise) return arg0; - return hookPromise.then((result: any) => + return hookPromise.then(result => reduce.call(this.pluginContexts[i], arg0, result, this.plugins[i]) ); }); @@ -137,26 +168,31 @@ export class PluginDriver { return promise; } - // chains synchronously, reduces returns of type R, to type T, handling the reduced value as the first hook argument - hookReduceArg0Sync>( + // chains synchronously, reduces returned value, handling the reduced value as the first hook argument + hookReduceArg0Sync( hookName: H, - [arg0, ...args]: any[], - reduce: Reduce, + [arg0, ...rest]: Parameters, + reduce: (reduction: Arg0, result: ReturnType, plugin: Plugin) => Arg0, replaceContext?: ReplaceContext - ): R { + ): Arg0 { for (let i = 0; i < this.plugins.length; i++) { - const result: any = this.runHookSync(hookName, [arg0, ...args], i, replaceContext); + const args = [arg0, ...rest] as Parameters; + const result = this.runHookSync(hookName, args, i, replaceContext); arg0 = reduce.call(this.pluginContexts[i], arg0, result, this.plugins[i]); } return arg0; } - // chains, reduces returns of type R, to type T, handling the reduced value separately. permits hooks as values. - hookReduceValue( + // chains, reduces returned value to type T, handling the reduced value separately. permits hooks as values. + hookReduceValue( hookName: H, initialValue: T | Promise, - args: any[], - reduce: Reduce, + args: Parameters, + reduce: ( + reduction: T, + result: ResolveValue>, + plugin: Plugin + ) => T, replaceContext?: ReplaceContext ): Promise { let promise = Promise.resolve(initialValue); @@ -164,7 +200,7 @@ export class PluginDriver { promise = promise.then(value => { const hookPromise = this.runHook(hookName, args, i, true, replaceContext); if (!hookPromise) return value; - return hookPromise.then((result: any) => + return hookPromise.then(result => reduce.call(this.pluginContexts[i], value, result, this.plugins[i]) ); }); @@ -172,101 +208,128 @@ export class PluginDriver { return promise; } - // chains, reduces returns of type R, to type T, handling the reduced value separately. permits hooks as values. - hookReduceValueSync( + // chains synchronously, reduces returned value to type T, handling the reduced value separately. permits hooks as values. + hookReduceValueSync( hookName: H, initialValue: T, - args: any[], - reduce: Reduce, + args: Parameters, + reduce: (reduction: T, result: ReturnType, plugin: Plugin) => T, replaceContext?: ReplaceContext ): T { let acc = initialValue; for (let i = 0; i < this.plugins.length; i++) { - const result: any = this.runHookSync(hookName, args, i, replaceContext); + const result = this.runHookSync(hookName, args, i, replaceContext); acc = reduce.call(this.pluginContexts[i], acc, result, this.plugins[i]); } return acc; } // chains, ignores returns - async hookSeq( + hookSeq( hookName: H, - args: Args, + args: Parameters, replaceContext?: ReplaceContext ): Promise { - let promise: Promise = Promise.resolve(); - for (let i = 0; i < this.plugins.length; i++) - promise = promise.then(() => - this.runHook(hookName, args as any[], i, false, replaceContext) + let promise = Promise.resolve(); + for (let i = 0; i < this.plugins.length; i++) { + promise = promise.then( + () => this.runHook(hookName, args, i, false, replaceContext) as Promise ); + } return promise; } - // chains, ignores returns - hookSeqSync( + // chains synchronously, ignores returns + hookSeqSync( hookName: H, - args: Args, + args: Parameters, replaceContext?: ReplaceContext ): void { - for (let i = 0; i < this.plugins.length; i++) - this.runHookSync(hookName, args as any[], i, replaceContext); + for (let i = 0; i < this.plugins.length; i++) { + this.runHookSync(hookName, args, i, replaceContext); + } } - private runHook( - hookName: string, - args: any[], + /** + * Run an async plugin hook and return the result. + * @param hookName Name of the plugin hook. Must be either in `PluginHooks` or `OutputPluginValueHooks`. + * @param args Arguments passed to the plugin hook. + * @param pluginIndex Index of the plugin inside `this.plugins[]`. + * @param permitValues If true, values can be passed instead of functions for the plugin hook. + * @param hookContext When passed, the plugin context can be overridden. + */ + private runHook( + hookName: H, + args: Parameters, + pluginIndex: number, + permitValues: true, + hookContext?: ReplaceContext | null + ): EnsurePromise>; + private runHook( + hookName: H, + args: Parameters, + pluginIndex: number, + permitValues: false, + hookContext?: ReplaceContext | null + ): EnsurePromise>; + private runHook( + hookName: H, + args: Parameters, pluginIndex: number, permitValues: boolean, hookContext?: ReplaceContext | null - ): Promise { + ): EnsurePromise> { this.previousHooks.add(hookName); const plugin = this.plugins[pluginIndex]; - const hook = (plugin as any)[hookName]; + const hook = plugin[hookName]; if (!hook) return undefined as any; let context = this.pluginContexts[pluginIndex]; if (hookContext) { context = hookContext(context, plugin); } + return Promise.resolve() .then(() => { // permit values allows values to be returned instead of a functional hook if (typeof hook !== 'function') { if (permitValues) return hook; - return error({ - code: 'INVALID_PLUGIN_HOOK', - message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.` - }); + return throwInvalidHookError(hookName, plugin.name); } - return hook.apply(context, args); + return (hook as Function).apply(context, args); }) .catch(err => throwPluginError(err, plugin.name, { hook: hookName })); } - private runHookSync( - hookName: string, - args: any[], + /** + * Run a sync plugin hook and return the result. + * @param hookName Name of the plugin hook. Must be in `PluginHooks`. + * @param args Arguments passed to the plugin hook. + * @param pluginIndex Index of the plugin inside `this.plugins[]`. + * @param hookContext When passed, the plugin context can be overridden. + */ + private runHookSync( + hookName: H, + args: Parameters, pluginIndex: number, hookContext?: ReplaceContext - ): T { + ): ReturnType { this.previousHooks.add(hookName); const plugin = this.plugins[pluginIndex]; - let context = this.pluginContexts[pluginIndex]; - const hook = (plugin as any)[hookName]; + const hook = plugin[hookName]; if (!hook) return undefined as any; + let context = this.pluginContexts[pluginIndex]; if (hookContext) { context = hookContext(context, plugin); } + try { // permit values allows values to be returned instead of a functional hook if (typeof hook !== 'function') { - return error({ - code: 'INVALID_PLUGIN_HOOK', - message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.` - }); + return throwInvalidHookError(hookName, plugin.name); } - return hook.apply(context, args); + return (hook as Function).apply(context, args); } catch (err) { return throwPluginError(err, plugin.name, { hook: hookName }); } diff --git a/src/utils/renderChunk.ts b/src/utils/renderChunk.ts index f901984a2..39cf4eddb 100644 --- a/src/utils/renderChunk.ts +++ b/src/utils/renderChunk.ts @@ -23,7 +23,7 @@ export default function renderChunk({ }): Promise { const renderChunkReducer = ( code: string, - result: { code: string; map?: SourceMapInput }, + result: { code: string; map?: SourceMapInput } | string | null, plugin: Plugin ): string => { if (result == null) return code; diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 5433bafd8..c7a0b9b3d 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -78,7 +78,7 @@ export default function transform( } return graph.pluginDriver - .hookReduceArg0( + .hookReduceArg0( 'transform', [curSource, id], transformReducer,