From 58cbc6c772f2f1f4cdd1a9d4252d13919551fa11 Mon Sep 17 00:00:00 2001 From: Tiger Oakes Date: Fri, 6 Mar 2020 10:10:45 -0800 Subject: [PATCH 1/2] Use strict types with PluginDriver --- src/rollup/rollup.ts | 4 +- src/rollup/types.d.ts | 47 ++++++++- src/utils/PluginDriver.ts | 209 +++++++++++++++++++++++++------------- src/utils/renderChunk.ts | 2 +- src/utils/transform.ts | 141 +++++++++++++------------ 5 files changed, 253 insertions(+), 150 deletions(-) diff --git a/src/rollup/rollup.ts b/src/rollup/rollup.ts index 51715ca69b5..17354ab465c 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 e30d97159fc..242dc958913 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -284,7 +284,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 @@ -348,6 +349,50 @@ export interface PluginHooks extends OutputPluginHooks { watchChange: (id: string) => void; } +export type AsyncPluginHooks = + | 'buildEnd' + | 'buildStart' + | 'renderError' + | 'renderStart' + | 'writeBundle' + | 'load' + | 'resolveDynamicImport' + | 'resolveId' + | 'transform' + | 'generateBundle' + | 'renderChunk'; + +export type PluginValueHooks = 'banner' | 'footer' | 'intro' | 'outro'; + +export type SyncPluginHooks = Exclude; + +export type FirstPluginHooks = + | 'load' + | 'resolveDynamicImport' + | 'resolveId' + | 'resolveFileUrl' + | 'resolveImportMeta'; + +export type SequentialPluginHooks = + | 'options' + | 'transform' + | 'watchChange' + | 'augmentChunkHash' + | 'generateBundle' + | 'outputOptions' + | 'renderChunk'; + +export type ParallelPluginHooks = + | 'buildEnd' + | 'buildStart' + | 'renderError' + | 'renderStart' + | 'writeBundle' + | 'banner' + | 'footer' + | 'intro' + | 'outro'; + interface OutputPluginValueHooks { banner: AddonHook; cacheKey: string; diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index 0879bf14b5b..b3f6a804a3d 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 f901984a253..39cf4eddb9b 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 b536cadb968..869aec0c681 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -77,80 +77,75 @@ export default function transform( } return graph.pluginDriver - .hookReduceArg0( - 'transform', - [curSource, id], - transformReducer, - (pluginContext, plugin) => { - curPlugin = plugin; - return { - ...pluginContext, - cache: customTransformCache - ? pluginContext.cache - : getTrackedPluginCache(pluginContext.cache, useCustomTransformCache), - warn(warning: RollupWarning | string, pos?: number | { column: number; line: number }) { - if (typeof warning === 'string') warning = { message: warning } as RollupWarning; - if (pos) augmentCodeLocation(warning, pos, curSource, id); - warning.id = id; - warning.hook = 'transform'; - pluginContext.warn(warning); - }, - error(err: RollupError | string, pos?: number | { column: number; line: number }): never { - if (typeof err === 'string') err = { message: err }; - if (pos) augmentCodeLocation(err, pos, curSource, id); - err.id = id; - err.hook = 'transform'; - return pluginContext.error(err); - }, - emitAsset(name: string, source?: string | Uint8Array) { - const emittedFile = { type: 'asset' as const, name, source }; - emittedFiles.push({ ...emittedFile }); - return graph.pluginDriver.emitFile(emittedFile); - }, - emitChunk(id, options) { - const emittedFile = { type: 'chunk' as const, id, name: options && options.name }; - emittedFiles.push({ ...emittedFile }); - return graph.pluginDriver.emitFile(emittedFile); - }, - emitFile(emittedFile: EmittedFile) { - emittedFiles.push(emittedFile); - return graph.pluginDriver.emitFile(emittedFile); - }, - addWatchFile(id: string) { - transformDependencies.push(id); - pluginContext.addWatchFile(id); - }, - setAssetSource() { - return this.error({ - code: 'INVALID_SETASSETSOURCE', - message: `setAssetSource cannot be called in transform for caching reasons. Use emitFile with a source, or call setAssetSource in another hook.` - }); - }, - getCombinedSourcemap() { - const combinedMap = collapseSourcemap( - graph, - id, - originalCode, - originalSourcemap, - sourcemapChain - ); - if (!combinedMap) { - const magicString = new MagicString(originalCode); - return magicString.generateMap({ includeContent: true, hires: true, source: id }); - } - if (originalSourcemap !== combinedMap) { - originalSourcemap = combinedMap; - sourcemapChain.length = 0; - } - return new SourceMap({ - ...combinedMap, - file: null as any, - sourcesContent: combinedMap.sourcesContent! - }); + .hookReduceArg0('transform', [curSource, id], transformReducer, (pluginContext, plugin) => { + curPlugin = plugin; + return { + ...pluginContext, + cache: customTransformCache + ? pluginContext.cache + : getTrackedPluginCache(pluginContext.cache, useCustomTransformCache), + warn(warning: RollupWarning | string, pos?: number | { column: number; line: number }) { + if (typeof warning === 'string') warning = { message: warning } as RollupWarning; + if (pos) augmentCodeLocation(warning, pos, curSource, id); + warning.id = id; + warning.hook = 'transform'; + pluginContext.warn(warning); + }, + error(err: RollupError | string, pos?: number | { column: number; line: number }): never { + if (typeof err === 'string') err = { message: err }; + if (pos) augmentCodeLocation(err, pos, curSource, id); + err.id = id; + err.hook = 'transform'; + return pluginContext.error(err); + }, + emitAsset(name: string, source?: string | Uint8Array) { + const emittedFile = { type: 'asset' as const, name, source }; + emittedFiles.push({ ...emittedFile }); + return graph.pluginDriver.emitFile(emittedFile); + }, + emitChunk(id, options) { + const emittedFile = { type: 'chunk' as const, id, name: options && options.name }; + emittedFiles.push({ ...emittedFile }); + return graph.pluginDriver.emitFile(emittedFile); + }, + emitFile(emittedFile: EmittedFile) { + emittedFiles.push(emittedFile); + return graph.pluginDriver.emitFile(emittedFile); + }, + addWatchFile(id: string) { + transformDependencies.push(id); + pluginContext.addWatchFile(id); + }, + setAssetSource() { + return this.error({ + code: 'INVALID_SETASSETSOURCE', + message: `setAssetSource cannot be called in transform for caching reasons. Use emitFile with a source, or call setAssetSource in another hook.` + }); + }, + getCombinedSourcemap() { + const combinedMap = collapseSourcemap( + graph, + id, + originalCode, + originalSourcemap, + sourcemapChain + ); + if (!combinedMap) { + const magicString = new MagicString(originalCode); + return magicString.generateMap({ includeContent: true, hires: true, source: id }); } - }; - } - ) + if (originalSourcemap !== combinedMap) { + originalSourcemap = combinedMap; + sourcemapChain.length = 0; + } + return new SourceMap({ + ...combinedMap, + file: null as any, + sourcesContent: combinedMap.sourcesContent! + }); + } + }; + }) .catch(err => throwPluginError(err, curPlugin.name, { hook: 'transform', id })) .then(code => { if (!customTransformCache) { From 100aa19ace6e5a29cd1f6fafa241f00348f00d46 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Tue, 10 Mar 2020 06:46:06 +0100 Subject: [PATCH 2/2] Fix type errors --- src/ast/nodes/MetaProperty.ts | 4 ++-- src/rollup/types.d.ts | 29 +++++++++++++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/ast/nodes/MetaProperty.ts b/src/ast/nodes/MetaProperty.ts index 3b29cb38b4d..e7b38ab8212 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/types.d.ts b/src/rollup/types.d.ts index 242dc958913..489433bcb27 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -352,15 +352,15 @@ export interface PluginHooks extends OutputPluginHooks { export type AsyncPluginHooks = | 'buildEnd' | 'buildStart' + | 'generateBundle' + | 'load' + | 'renderChunk' | 'renderError' | 'renderStart' - | 'writeBundle' - | 'load' | 'resolveDynamicImport' | 'resolveId' | 'transform' - | 'generateBundle' - | 'renderChunk'; + | 'writeBundle'; export type PluginValueHooks = 'banner' | 'footer' | 'intro' | 'outro'; @@ -368,30 +368,31 @@ export type SyncPluginHooks = Exclude; export type FirstPluginHooks = | 'load' + | 'resolveAssetUrl' | 'resolveDynamicImport' - | 'resolveId' | 'resolveFileUrl' + | 'resolveId' | 'resolveImportMeta'; export type SequentialPluginHooks = - | 'options' - | 'transform' - | 'watchChange' | 'augmentChunkHash' | 'generateBundle' + | 'options' | 'outputOptions' - | 'renderChunk'; + | 'renderChunk' + | 'transform' + | 'watchChange'; export type ParallelPluginHooks = + | 'banner' | 'buildEnd' | 'buildStart' - | 'renderError' - | 'renderStart' - | 'writeBundle' - | 'banner' | 'footer' | 'intro' - | 'outro'; + | 'outro' + | 'renderError' + | 'renderStart' + | 'writeBundle'; interface OutputPluginValueHooks { banner: AddonHook;