From 4b9bc4d464ad02a23a1be1bfa300e32876e1af3f Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 21 Oct 2019 18:03:18 +0200 Subject: [PATCH 01/15] Extract plugin cache to separate file --- src/Graph.ts | 2 +- src/ModuleLoader.ts | 2 +- src/ast/nodes/MetaProperty.ts | 2 +- src/rollup/index.ts | 3 +- src/utils/PluginCache.ts | 98 ++++++++++++++++ .../{pluginDriver.ts => PluginDriver.ts} | 107 +----------------- src/utils/pluginConstants.ts | 9 ++ src/utils/transform.ts | 5 +- 8 files changed, 119 insertions(+), 109 deletions(-) create mode 100644 src/utils/PluginCache.ts rename src/utils/{pluginDriver.ts => PluginDriver.ts} (84%) create mode 100644 src/utils/pluginConstants.ts diff --git a/src/Graph.ts b/src/Graph.ts index 0ffa5a11fed..2dae80789c8 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -29,7 +29,7 @@ import { Uint8ArrayToHexString } from './utils/entryHashing'; import { errDeprecation, error } from './utils/error'; import { analyseModuleExecution, sortByExecutionOrder } from './utils/executionOrder'; import { resolve } from './utils/path'; -import { createPluginDriver, PluginDriver } from './utils/pluginDriver'; +import { createPluginDriver, PluginDriver } from './utils/PluginDriver'; import relativeId from './utils/relativeId'; import { timeEnd, timeStart } from './utils/timers'; diff --git a/src/ModuleLoader.ts b/src/ModuleLoader.ts index ab2a8b38983..9c6e21f8256 100644 --- a/src/ModuleLoader.ts +++ b/src/ModuleLoader.ts @@ -26,7 +26,7 @@ import { errUnresolvedImportTreatedAsExternal } from './utils/error'; import { isRelative, resolve } from './utils/path'; -import { PluginDriver } from './utils/pluginDriver'; +import { PluginDriver } from './utils/PluginDriver'; import relativeId from './utils/relativeId'; import { timeEnd, timeStart } from './utils/timers'; import transform from './utils/transform'; diff --git a/src/ast/nodes/MetaProperty.ts b/src/ast/nodes/MetaProperty.ts index 9a328513d62..5e3781e6bbc 100644 --- a/src/ast/nodes/MetaProperty.ts +++ b/src/ast/nodes/MetaProperty.ts @@ -1,7 +1,7 @@ import MagicString from 'magic-string'; import { accessedFileUrlGlobals, accessedMetaUrlGlobals } from '../../utils/defaultPlugin'; import { dirname, normalize, relative } from '../../utils/path'; -import { PluginDriver } from '../../utils/pluginDriver'; +import { PluginDriver } from '../../utils/PluginDriver'; import { ObjectPathKey } from '../utils/PathTracker'; import Identifier from './Identifier'; import MemberExpression from './MemberExpression'; diff --git a/src/rollup/index.ts b/src/rollup/index.ts index 2ff3c1db41a..04fa87991e5 100644 --- a/src/rollup/index.ts +++ b/src/rollup/index.ts @@ -10,7 +10,8 @@ import { writeFile } from '../utils/fs'; import getExportMode from '../utils/getExportMode'; import mergeOptions, { GenericConfigObject } from '../utils/mergeOptions'; import { basename, dirname, isAbsolute, resolve } from '../utils/path'; -import { ANONYMOUS_PLUGIN_PREFIX, PluginDriver } from '../utils/pluginDriver'; +import { ANONYMOUS_PLUGIN_PREFIX } from '../utils/pluginConstants'; +import { PluginDriver } from '../utils/PluginDriver'; import { SOURCEMAPPING_URL } from '../utils/sourceMappingURL'; import { getTimings, initialiseTimers, timeEnd, timeStart } from '../utils/timers'; import { diff --git a/src/utils/PluginCache.ts b/src/utils/PluginCache.ts new file mode 100644 index 00000000000..6599545390e --- /dev/null +++ b/src/utils/PluginCache.ts @@ -0,0 +1,98 @@ +import { PluginCache, SerializablePluginCache } from '../rollup/types'; +import { error } from './error'; +import { ANONYMOUS_PLUGIN_PREFIX } from './pluginConstants'; + +export function createPluginCache(cache: SerializablePluginCache): PluginCache { + return { + has(id: string) { + const item = cache[id]; + if (!item) return false; + item[0] = 0; + return true; + }, + get(id: string) { + const item = cache[id]; + if (!item) return undefined; + item[0] = 0; + return item[1]; + }, + set(id: string, value: any) { + cache[id] = [0, value]; + }, + delete(id: string) { + return delete cache[id]; + } + }; +} + +export function getTrackedPluginCache(pluginCache: PluginCache) { + const trackedCache = { + cache: { + has(id: string) { + trackedCache.used = true; + return pluginCache.has(id); + }, + get(id: string) { + trackedCache.used = true; + return pluginCache.get(id); + }, + set(id: string, value: any) { + trackedCache.used = true; + return pluginCache.set(id, value); + }, + delete(id: string) { + trackedCache.used = true; + return pluginCache.delete(id); + } + }, + used: false + }; + return trackedCache; +} + +export const NO_CACHE: PluginCache = { + has() { + return false; + }, + get() { + return undefined as any; + }, + set() {}, + delete() { + return false; + } +}; + +function uncacheablePluginError(pluginName: string) { + if (pluginName.startsWith(ANONYMOUS_PLUGIN_PREFIX)) + error({ + code: 'ANONYMOUS_PLUGIN_CACHE', + message: + 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.' + }); + else + error({ + code: 'DUPLICATE_PLUGIN_NAME', + message: `The plugin name ${pluginName} is being used twice in the same build. Plugin names must be distinct or provide a cacheKey (please post an issue to the plugin if you are a plugin user).` + }); +} + +export function getCacheForUncacheablePlugin(pluginName: string): PluginCache { + return { + has() { + uncacheablePluginError(pluginName); + return false; + }, + get() { + uncacheablePluginError(pluginName); + return undefined as any; + }, + set() { + uncacheablePluginError(pluginName); + }, + delete() { + uncacheablePluginError(pluginName); + return false; + } + }; +} diff --git a/src/utils/pluginDriver.ts b/src/utils/PluginDriver.ts similarity index 84% rename from src/utils/pluginDriver.ts rename to src/utils/PluginDriver.ts index e9e3b10260e..52a7c50272b 100644 --- a/src/utils/pluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -20,6 +20,8 @@ import { BuildPhase } from './buildPhase'; import { getRollupDefaultPlugin } from './defaultPlugin'; import { errInvalidRollupPhaseForAddWatchFile, error, Errors } from './error'; import { FileEmitter } from './FileEmitter'; +import { createPluginCache, getCacheForUncacheablePlugin, NO_CACHE } from './PluginCache'; +import { ANONYMOUS_PLUGIN_PREFIX, deprecatedHooks } from './pluginConstants'; type Args = T extends (...args: infer K) => any ? K : never; type EnsurePromise = Promise ? K : T>; @@ -86,16 +88,6 @@ export interface PluginDriver { export type Reduce = (reduction: T, result: R, plugin: Plugin) => T; export type HookContext = (context: PluginContext, plugin: Plugin) => PluginContext; -export const ANONYMOUS_PLUGIN_PREFIX = 'at position '; - -const deprecatedHooks: { active: boolean; deprecated: string; replacement: string }[] = [ - { active: true, deprecated: 'ongenerate', replacement: 'generateBundle' }, - { active: true, deprecated: 'onwrite', replacement: 'generateBundle/writeBundle' }, - { active: true, deprecated: 'transformBundle', replacement: 'renderChunk' }, - { active: true, deprecated: 'transformChunk', replacement: 'renderChunk' }, - { active: false, deprecated: 'resolveAssetUrl', replacement: 'resolveFileUrl' } -]; - function warnDeprecatedHooks(plugins: Plugin[], graph: Graph) { for (const { active, deprecated, replacement } of deprecatedHooks) { for (const plugin of plugins) { @@ -182,14 +174,14 @@ export function createPluginDriver( let cacheInstance: PluginCache; if (!pluginCache) { - cacheInstance = noCache; + cacheInstance = NO_CACHE; } else if (cacheable) { const cacheKey = plugin.cacheKey || plugin.name; cacheInstance = createPluginCache( pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null)) ); } else { - cacheInstance = uncacheablePlugin(plugin.name); + cacheInstance = getCacheForUncacheablePlugin(plugin.name); } const context: PluginContext = { @@ -490,94 +482,3 @@ export function createPluginDriver( return pluginDriver; } - -export function createPluginCache(cache: SerializablePluginCache): PluginCache { - return { - has(id: string) { - const item = cache[id]; - if (!item) return false; - item[0] = 0; - return true; - }, - get(id: string) { - const item = cache[id]; - if (!item) return undefined; - item[0] = 0; - return item[1]; - }, - set(id: string, value: any) { - cache[id] = [0, value]; - }, - delete(id: string) { - return delete cache[id]; - } - }; -} - -export function trackPluginCache(pluginCache: PluginCache) { - const result = { used: false, cache: (undefined as any) as PluginCache }; - result.cache = { - has(id: string) { - result.used = true; - return pluginCache.has(id); - }, - get(id: string) { - result.used = true; - return pluginCache.get(id); - }, - set(id: string, value: any) { - result.used = true; - return pluginCache.set(id, value); - }, - delete(id: string) { - result.used = true; - return pluginCache.delete(id); - } - }; - return result; -} - -const noCache: PluginCache = { - has() { - return false; - }, - get() { - return undefined as any; - }, - set() {}, - delete() { - return false; - } -}; - -function uncacheablePluginError(pluginName: string) { - if (pluginName.startsWith(ANONYMOUS_PLUGIN_PREFIX)) - error({ - code: 'ANONYMOUS_PLUGIN_CACHE', - message: - 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.' - }); - else - error({ - code: 'DUPLICATE_PLUGIN_NAME', - message: `The plugin name ${pluginName} is being used twice in the same build. Plugin names must be distinct or provide a cacheKey (please post an issue to the plugin if you are a plugin user).` - }); -} - -const uncacheablePlugin: (pluginName: string) => PluginCache = pluginName => ({ - has() { - uncacheablePluginError(pluginName); - return false; - }, - get() { - uncacheablePluginError(pluginName); - return undefined as any; - }, - set() { - uncacheablePluginError(pluginName); - }, - delete() { - uncacheablePluginError(pluginName); - return false; - } -}); diff --git a/src/utils/pluginConstants.ts b/src/utils/pluginConstants.ts new file mode 100644 index 00000000000..cf85f779fc2 --- /dev/null +++ b/src/utils/pluginConstants.ts @@ -0,0 +1,9 @@ +export const ANONYMOUS_PLUGIN_PREFIX = 'at position '; + +export const deprecatedHooks: { active: boolean; deprecated: string; replacement: string }[] = [ + { active: true, deprecated: 'ongenerate', replacement: 'generateBundle' }, + { active: true, deprecated: 'onwrite', replacement: 'generateBundle/writeBundle' }, + { active: true, deprecated: 'transformBundle', replacement: 'renderChunk' }, + { active: true, deprecated: 'transformChunk', replacement: 'renderChunk' }, + { active: false, deprecated: 'resolveAssetUrl', replacement: 'resolveFileUrl' } +]; diff --git a/src/utils/transform.ts b/src/utils/transform.ts index 2e6d6f382bb..cfda38697f7 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -17,7 +17,8 @@ import { collapseSourcemap } from './collapseSourcemaps'; import { decodedSourcemap } from './decodedSourcemap'; import { augmentCodeLocation } from './error'; import { dirname, resolve } from './path'; -import { throwPluginError, trackPluginCache } from './pluginDriver'; +import { getTrackedPluginCache } from './PluginCache'; +import { throwPluginError } from './PluginDriver'; export default function transform( graph: Graph, @@ -106,7 +107,7 @@ export default function transform( (pluginContext, plugin) => { curPlugin = plugin; if (curPlugin.cacheKey) customTransformCache = true; - else trackedPluginCache = trackPluginCache(pluginContext.cache); + else trackedPluginCache = getTrackedPluginCache(pluginContext.cache); return { ...pluginContext, cache: trackedPluginCache ? trackedPluginCache.cache : pluginContext.cache, From 3dfe63c30f7e8832a21bc5b080a8b792b8573f06 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Tue, 22 Oct 2019 08:12:20 +0200 Subject: [PATCH 02/15] Extract plugin context to separate file --- src/rollup/index.ts | 2 +- src/utils/PluginCache.ts | 2 +- src/utils/PluginContext.ts | 202 ++++++++++++++++++++++++++++++++ src/utils/PluginDriver.ts | 218 +++-------------------------------- src/utils/pluginConstants.ts | 9 -- src/utils/pluginUtils.ts | 24 ++++ src/utils/transform.ts | 2 +- 7 files changed, 244 insertions(+), 215 deletions(-) create mode 100644 src/utils/PluginContext.ts delete mode 100644 src/utils/pluginConstants.ts create mode 100644 src/utils/pluginUtils.ts diff --git a/src/rollup/index.ts b/src/rollup/index.ts index 04fa87991e5..5cf1807c984 100644 --- a/src/rollup/index.ts +++ b/src/rollup/index.ts @@ -10,8 +10,8 @@ import { writeFile } from '../utils/fs'; import getExportMode from '../utils/getExportMode'; import mergeOptions, { GenericConfigObject } from '../utils/mergeOptions'; import { basename, dirname, isAbsolute, resolve } from '../utils/path'; -import { ANONYMOUS_PLUGIN_PREFIX } from '../utils/pluginConstants'; import { PluginDriver } from '../utils/PluginDriver'; +import { ANONYMOUS_PLUGIN_PREFIX } from '../utils/pluginUtils'; import { SOURCEMAPPING_URL } from '../utils/sourceMappingURL'; import { getTimings, initialiseTimers, timeEnd, timeStart } from '../utils/timers'; import { diff --git a/src/utils/PluginCache.ts b/src/utils/PluginCache.ts index 6599545390e..7974947f3d9 100644 --- a/src/utils/PluginCache.ts +++ b/src/utils/PluginCache.ts @@ -1,6 +1,6 @@ import { PluginCache, SerializablePluginCache } from '../rollup/types'; import { error } from './error'; -import { ANONYMOUS_PLUGIN_PREFIX } from './pluginConstants'; +import { ANONYMOUS_PLUGIN_PREFIX } from './pluginUtils'; export function createPluginCache(cache: SerializablePluginCache): PluginCache { return { diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts new file mode 100644 index 00000000000..4bb12dc5cbb --- /dev/null +++ b/src/utils/PluginContext.ts @@ -0,0 +1,202 @@ +import { EventEmitter } from 'events'; +import { version as rollupVersion } from 'package.json'; +import ExternalModule from '../ExternalModule'; +import Graph from '../Graph'; +import Module from '../Module'; +import { + Plugin, + PluginCache, + PluginContext, + RollupWarning, + RollupWatcher, + SerializablePluginCache +} from '../rollup/types'; +import { BuildPhase } from './buildPhase'; +import { errInvalidRollupPhaseForAddWatchFile } from './error'; +import { FileEmitter } from './FileEmitter'; +import { createPluginCache, getCacheForUncacheablePlugin, NO_CACHE } from './PluginCache'; +import { ANONYMOUS_PLUGIN_PREFIX, throwPluginError } from './pluginUtils'; + +function getDeprecatedContextHandler( + handler: H, + handlerName: string, + newHandlerName: string, + pluginName: string, + activeDeprecation: boolean, + graph: Graph +): H { + let deprecationWarningShown = false; + return (((...args: any[]) => { + if (!deprecationWarningShown) { + deprecationWarningShown = true; + graph.warnDeprecation( + { + message: `The "this.${handlerName}" plugin context function used by plugin ${pluginName} is deprecated. The "this.${newHandlerName}" plugin context function should be used instead.`, + plugin: pluginName + }, + activeDeprecation + ); + } + return handler(...args); + }) as unknown) as H; +} + +export function getPluginContexts( + pluginCache: Record | void, + graph: Graph, + fileEmitter: FileEmitter, + watcher: RollupWatcher | undefined +): (plugin: Plugin, pluginIndex: number) => PluginContext { + const existingPluginKeys = new Set(); + return (plugin, pidx) => { + let cacheable = true; + if (typeof plugin.cacheKey !== 'string') { + if (plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) || existingPluginKeys.has(plugin.name)) { + cacheable = false; + } else { + existingPluginKeys.add(plugin.name); + } + } + + let cacheInstance: PluginCache; + if (!pluginCache) { + cacheInstance = NO_CACHE; + } else if (cacheable) { + const cacheKey = plugin.cacheKey || plugin.name; + cacheInstance = createPluginCache( + pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null)) + ); + } else { + cacheInstance = getCacheForUncacheablePlugin(plugin.name); + } + + const context: PluginContext = { + addWatchFile(id) { + if (graph.phase >= BuildPhase.GENERATE) this.error(errInvalidRollupPhaseForAddWatchFile()); + graph.watchFiles[id] = true; + }, + cache: cacheInstance, + emitAsset: getDeprecatedContextHandler( + (name: string, source?: string | Buffer) => + fileEmitter.emitFile({ type: 'asset', name, source }), + 'emitAsset', + 'emitFile', + plugin.name, + false, + graph + ), + emitChunk: getDeprecatedContextHandler( + (id: string, options?: { name?: string }) => + fileEmitter.emitFile({ type: 'chunk', id, name: options && options.name }), + 'emitChunk', + 'emitFile', + plugin.name, + false, + graph + ), + emitFile: fileEmitter.emitFile, + error(err): never { + return throwPluginError(err, plugin.name); + }, + getAssetFileName: getDeprecatedContextHandler( + fileEmitter.getFileName, + 'getAssetFileName', + 'getFileName', + plugin.name, + false, + graph + ), + getChunkFileName: getDeprecatedContextHandler( + fileEmitter.getFileName, + 'getChunkFileName', + 'getFileName', + plugin.name, + false, + graph + ), + getFileName: fileEmitter.getFileName, + getModuleInfo(moduleId) { + const foundModule = graph.moduleById.get(moduleId); + if (foundModule == null) { + throw new Error(`Unable to find module ${moduleId}`); + } + + return { + hasModuleSideEffects: foundModule.moduleSideEffects, + id: foundModule.id, + importedIds: + foundModule instanceof ExternalModule + ? [] + : foundModule.sources.map(id => foundModule.resolvedIds[id].id), + isEntry: foundModule instanceof Module && foundModule.isEntryPoint, + isExternal: foundModule instanceof ExternalModule + }; + }, + isExternal: getDeprecatedContextHandler( + (id: string, parentId: string, isResolved = false) => + graph.moduleLoader.isExternal(id, parentId, isResolved), + 'isExternal', + 'resolve', + plugin.name, + false, + graph + ), + meta: { + rollupVersion + }, + get moduleIds() { + return graph.moduleById.keys(); + }, + parse: graph.contextParse, + resolve(source, importer, options?: { skipSelf: boolean }) { + return graph.moduleLoader.resolveId( + source, + importer, + options && options.skipSelf ? pidx : null + ); + }, + resolveId: getDeprecatedContextHandler( + (source: string, importer: string) => + graph.moduleLoader + .resolveId(source, importer) + .then(resolveId => resolveId && resolveId.id), + 'resolveId', + 'resolve', + plugin.name, + false, + graph + ), + setAssetSource: fileEmitter.setAssetSource, + warn(warning) { + if (typeof warning === 'string') warning = { message: warning } as RollupWarning; + if (warning.code) warning.pluginCode = warning.code; + warning.code = 'PLUGIN_WARNING'; + warning.plugin = plugin.name; + graph.warn(warning); + }, + watcher: watcher + ? (() => { + let deprecationWarningShown = false; + + function deprecatedWatchListener(event: string, handler: () => void): EventEmitter { + if (!deprecationWarningShown) { + context.warn({ + code: 'PLUGIN_WATCHER_DEPRECATED', + message: `this.watcher usage is deprecated in plugins. Use the watchChange plugin hook and this.addWatchFile() instead.` + }); + deprecationWarningShown = true; + } + return (watcher as RollupWatcher).on(event, handler); + } + + return { + ...(watcher as EventEmitter), + addListener: deprecatedWatchListener, + on: deprecatedWatchListener + }; + })() + : (undefined as any) + }; + return context; + }; +} diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index 52a7c50272b..8292916c2b2 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -1,27 +1,19 @@ -import { EventEmitter } from 'events'; -import { version as rollupVersion } from 'package.json'; -import ExternalModule from '../ExternalModule'; import Graph from '../Graph'; -import Module from '../Module'; import { EmitFile, InputOptions, OutputBundleWithPlaceholders, Plugin, - PluginCache, PluginContext, PluginHooks, - RollupError, - RollupWarning, RollupWatcher, SerializablePluginCache } from '../rollup/types'; -import { BuildPhase } from './buildPhase'; import { getRollupDefaultPlugin } from './defaultPlugin'; -import { errInvalidRollupPhaseForAddWatchFile, error, Errors } from './error'; +import { error } from './error'; import { FileEmitter } from './FileEmitter'; -import { createPluginCache, getCacheForUncacheablePlugin, NO_CACHE } from './PluginCache'; -import { ANONYMOUS_PLUGIN_PREFIX, deprecatedHooks } from './pluginConstants'; +import { getPluginContexts } from './PluginContext'; +import { throwPluginError } from './pluginUtils'; type Args = T extends (...args: infer K) => any ? K : never; type EnsurePromise = Promise ? K : T>; @@ -88,6 +80,14 @@ export interface PluginDriver { export type Reduce = (reduction: T, result: R, plugin: Plugin) => T; export type HookContext = (context: PluginContext, plugin: Plugin) => PluginContext; +export const deprecatedHooks: { active: boolean; deprecated: string; replacement: string }[] = [ + { active: true, deprecated: 'ongenerate', replacement: 'generateBundle' }, + { active: true, deprecated: 'onwrite', replacement: 'generateBundle/writeBundle' }, + { active: true, deprecated: 'transformBundle', replacement: 'renderChunk' }, + { active: true, deprecated: 'transformChunk', replacement: 'renderChunk' }, + { active: false, deprecated: 'resolveAssetUrl', replacement: 'resolveFileUrl' } +]; + function warnDeprecatedHooks(plugins: Plugin[], graph: Graph) { for (const { active, deprecated, replacement } of deprecatedHooks) { for (const plugin of plugins) { @@ -104,26 +104,6 @@ function warnDeprecatedHooks(plugins: Plugin[], graph: Graph) { } } -export function throwPluginError( - err: string | RollupError, - plugin: string, - { hook, id }: { hook?: string; id?: string } = {} -): never { - if (typeof err === 'string') err = { message: err }; - if (err.code && err.code !== Errors.PLUGIN_ERROR) { - err.pluginCode = err.code; - } - err.code = Errors.PLUGIN_ERROR; - err.plugin = plugin; - if (hook) { - err.hook = hook; - } - if (id) { - err.id = id; - } - return error(err); -} - export function createPluginDriver( graph: Graph, options: InputOptions, @@ -132,181 +112,15 @@ export function createPluginDriver( ): PluginDriver { warnDeprecatedHooks(options.plugins as Plugin[], graph); - function getDeprecatedHookHandler( - handler: H, - handlerName: string, - newHandlerName: string, - pluginName: string, - acitveDeprecation: boolean - ): H { - let deprecationWarningShown = false; - return (((...args: any[]) => { - if (!deprecationWarningShown) { - deprecationWarningShown = true; - graph.warnDeprecation( - { - message: `The "this.${handlerName}" plugin context function used by plugin ${pluginName} is deprecated. The "this.${newHandlerName}" plugin context function should be used instead.`, - plugin: pluginName - }, - acitveDeprecation - ); - } - return handler(...args); - }) as unknown) as H; - } - const plugins = [ ...(options.plugins as Plugin[]), getRollupDefaultPlugin(options.preserveSymlinks as boolean) ]; const fileEmitter = new FileEmitter(graph); - const existingPluginKeys = new Set(); - - const pluginContexts: PluginContext[] = plugins.map((plugin, pidx) => { - let cacheable = true; - if (typeof plugin.cacheKey !== 'string') { - if (plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) || existingPluginKeys.has(plugin.name)) { - cacheable = false; - } else { - existingPluginKeys.add(plugin.name); - } - } - - let cacheInstance: PluginCache; - if (!pluginCache) { - cacheInstance = NO_CACHE; - } else if (cacheable) { - const cacheKey = plugin.cacheKey || plugin.name; - cacheInstance = createPluginCache( - pluginCache[cacheKey] || (pluginCache[cacheKey] = Object.create(null)) - ); - } else { - cacheInstance = getCacheForUncacheablePlugin(plugin.name); - } - const context: PluginContext = { - addWatchFile(id) { - if (graph.phase >= BuildPhase.GENERATE) this.error(errInvalidRollupPhaseForAddWatchFile()); - graph.watchFiles[id] = true; - }, - cache: cacheInstance, - emitAsset: getDeprecatedHookHandler( - (name: string, source?: string | Buffer) => - fileEmitter.emitFile({ type: 'asset', name, source }), - 'emitAsset', - 'emitFile', - plugin.name, - false - ), - emitChunk: getDeprecatedHookHandler( - (id: string, options?: { name?: string }) => - fileEmitter.emitFile({ type: 'chunk', id, name: options && options.name }), - 'emitChunk', - 'emitFile', - plugin.name, - false - ), - emitFile: fileEmitter.emitFile, - error(err): never { - return throwPluginError(err, plugin.name); - }, - getAssetFileName: getDeprecatedHookHandler( - fileEmitter.getFileName, - 'getAssetFileName', - 'getFileName', - plugin.name, - false - ), - getChunkFileName: getDeprecatedHookHandler( - fileEmitter.getFileName, - 'getChunkFileName', - 'getFileName', - plugin.name, - false - ), - getFileName: fileEmitter.getFileName, - getModuleInfo(moduleId) { - const foundModule = graph.moduleById.get(moduleId); - if (foundModule == null) { - throw new Error(`Unable to find module ${moduleId}`); - } - - return { - hasModuleSideEffects: foundModule.moduleSideEffects, - id: foundModule.id, - importedIds: - foundModule instanceof ExternalModule - ? [] - : Array.from(foundModule.sources).map(id => foundModule.resolvedIds[id].id), - isEntry: foundModule instanceof Module && foundModule.isEntryPoint, - isExternal: foundModule instanceof ExternalModule - }; - }, - isExternal: getDeprecatedHookHandler( - (id: string, parentId: string, isResolved = false) => - graph.moduleLoader.isExternal(id, parentId, isResolved), - 'isExternal', - 'resolve', - plugin.name, - false - ), - meta: { - rollupVersion - }, - get moduleIds() { - return graph.moduleById.keys(); - }, - parse: graph.contextParse, - resolve(source, importer, options?: { skipSelf: boolean }) { - return graph.moduleLoader.resolveId( - source, - importer, - options && options.skipSelf ? pidx : null - ); - }, - resolveId: getDeprecatedHookHandler( - (source: string, importer: string) => - graph.moduleLoader - .resolveId(source, importer) - .then(resolveId => resolveId && resolveId.id), - 'resolveId', - 'resolve', - plugin.name, - false - ), - setAssetSource: fileEmitter.setAssetSource, - warn(warning) { - if (typeof warning === 'string') warning = { message: warning } as RollupWarning; - if (warning.code) warning.pluginCode = warning.code; - warning.code = 'PLUGIN_WARNING'; - warning.plugin = plugin.name; - graph.warn(warning); - }, - watcher: watcher - ? (() => { - let deprecationWarningShown = false; - - function deprecatedWatchListener(event: string, handler: () => void): EventEmitter { - if (!deprecationWarningShown) { - context.warn({ - code: 'PLUGIN_WATCHER_DEPRECATED', - message: `this.watcher usage is deprecated in plugins. Use the watchChange plugin hook and this.addWatchFile() instead.` - }); - deprecationWarningShown = true; - } - return (watcher as RollupWatcher).on(event, handler); - } - - return { - ...(watcher as EventEmitter), - addListener: deprecatedWatchListener, - on: deprecatedWatchListener - }; - })() - : (undefined as any) - }; - return context; - }); + const pluginContexts: PluginContext[] = plugins.map( + getPluginContexts(pluginCache, graph, fileEmitter, watcher) + ); function runHookSync( hookName: string, @@ -372,7 +186,7 @@ export function createPluginDriver( .catch(err => throwPluginError(err, plugin.name, { hook: hookName })); } - const pluginDriver: PluginDriver = { + return { emitFile: fileEmitter.emitFile, finaliseAssets() { fileEmitter.assertAssetsFinalized(); @@ -479,6 +293,4 @@ export function createPluginDriver( fileEmitter.startOutput(outputBundle, assetFileNames); } }; - - return pluginDriver; } diff --git a/src/utils/pluginConstants.ts b/src/utils/pluginConstants.ts deleted file mode 100644 index cf85f779fc2..00000000000 --- a/src/utils/pluginConstants.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const ANONYMOUS_PLUGIN_PREFIX = 'at position '; - -export const deprecatedHooks: { active: boolean; deprecated: string; replacement: string }[] = [ - { active: true, deprecated: 'ongenerate', replacement: 'generateBundle' }, - { active: true, deprecated: 'onwrite', replacement: 'generateBundle/writeBundle' }, - { active: true, deprecated: 'transformBundle', replacement: 'renderChunk' }, - { active: true, deprecated: 'transformChunk', replacement: 'renderChunk' }, - { active: false, deprecated: 'resolveAssetUrl', replacement: 'resolveFileUrl' } -]; diff --git a/src/utils/pluginUtils.ts b/src/utils/pluginUtils.ts new file mode 100644 index 00000000000..3233ca2092c --- /dev/null +++ b/src/utils/pluginUtils.ts @@ -0,0 +1,24 @@ +import { RollupError } from '../rollup/types'; +import { error, Errors } from './error'; + +export const ANONYMOUS_PLUGIN_PREFIX = 'at position '; + +export function throwPluginError( + err: string | RollupError, + plugin: string, + { hook, id }: { hook?: string; id?: string } = {} +): never { + if (typeof err === 'string') err = { message: err }; + if (err.code && err.code !== Errors.PLUGIN_ERROR) { + err.pluginCode = err.code; + } + err.code = Errors.PLUGIN_ERROR; + err.plugin = plugin; + if (hook) { + err.hook = hook; + } + if (id) { + err.id = id; + } + return error(err); +} diff --git a/src/utils/transform.ts b/src/utils/transform.ts index cfda38697f7..3e16df29471 100644 --- a/src/utils/transform.ts +++ b/src/utils/transform.ts @@ -18,7 +18,7 @@ import { decodedSourcemap } from './decodedSourcemap'; import { augmentCodeLocation } from './error'; import { dirname, resolve } from './path'; import { getTrackedPluginCache } from './PluginCache'; -import { throwPluginError } from './PluginDriver'; +import { throwPluginError } from './pluginUtils'; export default function transform( graph: Graph, From 0dcfff9adf8823a2e5463a5ac8b7163e87030112 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Wed, 23 Oct 2019 16:31:22 +0200 Subject: [PATCH 03/15] Extract more code from plugin driver --- src/Graph.ts | 8 +++++++- src/utils/PluginDriver.ts | 38 +++++--------------------------------- src/utils/pluginUtils.ts | 27 ++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 35 deletions(-) diff --git a/src/Graph.ts b/src/Graph.ts index 2dae80789c8..2871120e82f 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -143,7 +143,13 @@ export default class Graph { ...this.acornOptions }) as any; - this.pluginDriver = createPluginDriver(this, options, this.pluginCache, watcher); + this.pluginDriver = createPluginDriver( + this, + options.plugins as Plugin[], + this.pluginCache, + options.preserveSymlinks === true, + watcher + ); if (watcher) { const handleChange = (id: string) => this.pluginDriver.hookSeqSync('watchChange', [id]); diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index 8292916c2b2..b518d737fdc 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -1,7 +1,6 @@ import Graph from '../Graph'; import { EmitFile, - InputOptions, OutputBundleWithPlaceholders, Plugin, PluginContext, @@ -13,7 +12,7 @@ import { getRollupDefaultPlugin } from './defaultPlugin'; import { error } from './error'; import { FileEmitter } from './FileEmitter'; import { getPluginContexts } from './PluginContext'; -import { throwPluginError } from './pluginUtils'; +import { throwPluginError, warnDeprecatedHooks } from './pluginUtils'; type Args = T extends (...args: infer K) => any ? K : never; type EnsurePromise = Promise ? K : T>; @@ -80,42 +79,15 @@ export interface PluginDriver { export type Reduce = (reduction: T, result: R, plugin: Plugin) => T; export type HookContext = (context: PluginContext, plugin: Plugin) => PluginContext; -export const deprecatedHooks: { active: boolean; deprecated: string; replacement: string }[] = [ - { active: true, deprecated: 'ongenerate', replacement: 'generateBundle' }, - { active: true, deprecated: 'onwrite', replacement: 'generateBundle/writeBundle' }, - { active: true, deprecated: 'transformBundle', replacement: 'renderChunk' }, - { active: true, deprecated: 'transformChunk', replacement: 'renderChunk' }, - { active: false, deprecated: 'resolveAssetUrl', replacement: 'resolveFileUrl' } -]; - -function warnDeprecatedHooks(plugins: Plugin[], graph: Graph) { - for (const { active, deprecated, replacement } of deprecatedHooks) { - for (const plugin of plugins) { - if (deprecated in plugin) { - graph.warnDeprecation( - { - message: `The "${deprecated}" hook used by plugin ${plugin.name} is deprecated. The "${replacement}" hook should be used instead.`, - plugin: plugin.name - }, - active - ); - } - } - } -} - export function createPluginDriver( graph: Graph, - options: InputOptions, + userPlugins: Plugin[], pluginCache: Record | void, + preserveSymlinks: boolean, watcher?: RollupWatcher ): PluginDriver { - warnDeprecatedHooks(options.plugins as Plugin[], graph); - - const plugins = [ - ...(options.plugins as Plugin[]), - getRollupDefaultPlugin(options.preserveSymlinks as boolean) - ]; + warnDeprecatedHooks(userPlugins, graph); + const plugins = userPlugins.concat([getRollupDefaultPlugin(preserveSymlinks)]); const fileEmitter = new FileEmitter(graph); const pluginContexts: PluginContext[] = plugins.map( diff --git a/src/utils/pluginUtils.ts b/src/utils/pluginUtils.ts index 3233ca2092c..7542df2a376 100644 --- a/src/utils/pluginUtils.ts +++ b/src/utils/pluginUtils.ts @@ -1,4 +1,5 @@ -import { RollupError } from '../rollup/types'; +import Graph from '../Graph'; +import { Plugin, RollupError } from '../rollup/types'; import { error, Errors } from './error'; export const ANONYMOUS_PLUGIN_PREFIX = 'at position '; @@ -22,3 +23,27 @@ export function throwPluginError( } return error(err); } + +export const deprecatedHooks: { active: boolean; deprecated: string; replacement: string }[] = [ + { active: true, deprecated: 'ongenerate', replacement: 'generateBundle' }, + { active: true, deprecated: 'onwrite', replacement: 'generateBundle/writeBundle' }, + { active: true, deprecated: 'transformBundle', replacement: 'renderChunk' }, + { active: true, deprecated: 'transformChunk', replacement: 'renderChunk' }, + { active: false, deprecated: 'resolveAssetUrl', replacement: 'resolveFileUrl' } +]; + +export function warnDeprecatedHooks(plugins: Plugin[], graph: Graph) { + for (const { active, deprecated, replacement } of deprecatedHooks) { + for (const plugin of plugins) { + if (deprecated in plugin) { + graph.warnDeprecation( + { + message: `The "${deprecated}" hook used by plugin ${plugin.name} is deprecated. The "${replacement}" hook should be used instead.`, + plugin: plugin.name + }, + active + ); + } + } + } +} From 3fec0fce022943782cda101720a8941ffd8fcad7 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Thu, 24 Oct 2019 16:41:48 +0200 Subject: [PATCH 04/15] Introduce a virtual "outputPluginDriver" that we can replace to have per-output plugins TODO: Add phases (build/generate) to hooks in documentation TODO: Think about creating an Output interface containing options, driver, bundle? --- src/Chunk.ts | 42 +++++++++++------- src/ast/nodes/MetaProperty.ts | 8 ++-- src/rollup/index.ts | 84 ++++++++++++++++++++--------------- src/utils/PluginDriver.ts | 10 ++--- src/utils/addons.ts | 16 ++++--- src/utils/assignChunkIds.ts | 6 ++- src/utils/renderChunk.ts | 18 ++++---- 7 files changed, 106 insertions(+), 78 deletions(-) diff --git a/src/Chunk.ts b/src/Chunk.ts index bd077275922..ad9db0eec20 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -32,6 +32,7 @@ import { sortByExecutionOrder } from './utils/executionOrder'; import getIndentString from './utils/getIndentString'; import { makeLegal } from './utils/identifierHelpers'; import { basename, dirname, extname, isAbsolute, normalize, resolve } from './utils/path'; +import { PluginDriver } from './utils/PluginDriver'; import relativeId, { getAliasName } from './utils/relativeId'; import renderChunk from './utils/renderChunk'; import { RenderOptions } from './utils/renderHelpers'; @@ -256,7 +257,8 @@ export default class Chunk { addons: Addons, options: OutputOptions, existingNames: Record, - includeHash: boolean + includeHash: boolean, + outputPluginDriver: PluginDriver ): string { if (this.fileName !== null) { return this.fileName; @@ -270,7 +272,12 @@ export default class Chunk { format: () => (options.format === 'es' ? 'esm' : (options.format as string)), hash: () => includeHash - ? this.computeContentHashWithDependencies(addons, options, existingNames) + ? this.computeContentHashWithDependencies( + addons, + options, + existingNames, + outputPluginDriver + ) : '[hash]', name: () => this.getChunkName() }), @@ -363,11 +370,11 @@ export default class Chunk { return this.dependencies.map(chunk => chunk.id).filter(Boolean) as string[]; } - getRenderedHash(): string { + getRenderedHash(outputPluginDriver: PluginDriver): string { if (this.renderedHash) return this.renderedHash; if (!this.renderedSource) return ''; const hash = createHash(); - const hashAugmentation = this.calculateHashAugmentation(); + const hashAugmentation = this.calculateHashAugmentation(outputPluginDriver); hash.update(hashAugmentation); hash.update(this.renderedSource.toString()); hash.update( @@ -629,7 +636,12 @@ export default class Chunk { timeEnd('render modules', 3); } - render(options: OutputOptions, addons: Addons, outputChunk: RenderedChunk) { + render( + options: OutputOptions, + addons: Addons, + outputChunk: RenderedChunk, + outputPluginDriver: PluginDriver + ) { timeStart('render format', 3); if (!this.renderedSource) @@ -665,7 +677,7 @@ export default class Chunk { } this.finaliseDynamicImports(format); - this.finaliseImportMetas(format); + this.finaliseImportMetas(format, outputPluginDriver); const hasExports = this.renderedDeclarations.exports.length !== 0 || @@ -726,8 +738,8 @@ export default class Chunk { return renderChunk({ chunk: this, code: prevCode, - graph: this.graph, options, + outputPluginDriver: this.graph.pluginDriver, renderChunk: outputChunk, sourcemapChain: chunkSourcemapChain }).then((code: string) => { @@ -826,7 +838,7 @@ export default class Chunk { } } - private calculateHashAugmentation(): string { + private calculateHashAugmentation(outputPluginDriver: PluginDriver): string { const facadeModule = this.facadeModule; const getChunkName = this.getChunkName.bind(this); const preRenderedChunk = { @@ -841,7 +853,7 @@ export default class Chunk { return getChunkName(); } } as PreRenderedChunk; - const hashAugmentation = this.graph.pluginDriver.hookReduceValueSync( + return outputPluginDriver.hookReduceValueSync( 'augmentChunkHash', '', [preRenderedChunk], @@ -852,13 +864,13 @@ export default class Chunk { return hashAugmentation; } ); - return hashAugmentation; } private computeContentHashWithDependencies( addons: Addons, options: OutputOptions, - existingNames: Record + existingNames: Record, + outputPluginDriver: PluginDriver ): string { const hash = createHash(); hash.update( @@ -869,8 +881,8 @@ export default class Chunk { if (dep instanceof ExternalModule) { hash.update(':' + dep.renderPath); } else { - hash.update(dep.getRenderedHash()); - hash.update(dep.generateId(addons, options, existingNames, false)); + hash.update(dep.getRenderedHash(outputPluginDriver)); + hash.update(dep.generateId(addons, options, existingNames, false, outputPluginDriver)); } }); @@ -907,10 +919,10 @@ export default class Chunk { } } - private finaliseImportMetas(format: string): void { + private finaliseImportMetas(format: string, outputPluginDriver: PluginDriver): void { for (const [module, code] of this.renderedModuleSources) { for (const importMeta of module.importMetas) { - importMeta.renderFinalMechanism(code, this.id as string, format, this.graph.pluginDriver); + importMeta.renderFinalMechanism(code, this.id as string, format, outputPluginDriver); } } } diff --git a/src/ast/nodes/MetaProperty.ts b/src/ast/nodes/MetaProperty.ts index 5e3781e6bbc..9694af2d554 100644 --- a/src/ast/nodes/MetaProperty.ts +++ b/src/ast/nodes/MetaProperty.ts @@ -59,7 +59,7 @@ export default class MetaProperty extends NodeBase { code: MagicString, chunkId: string, format: string, - pluginDriver: PluginDriver + outputPluginDriver: PluginDriver ): void { if (!this.included) return; const parent = this.parent; @@ -96,7 +96,7 @@ export default class MetaProperty extends NodeBase { const relativePath = normalize(relative(dirname(chunkId), fileName)); let replacement; if (assetReferenceId !== null) { - replacement = pluginDriver.hookFirstSync('resolveAssetUrl', [ + replacement = outputPluginDriver.hookFirstSync('resolveAssetUrl', [ { assetFileName: fileName, chunkId, @@ -107,7 +107,7 @@ export default class MetaProperty extends NodeBase { ]); } if (!replacement) { - replacement = pluginDriver.hookFirstSync<'resolveFileUrl', string>('resolveFileUrl', [ + replacement = outputPluginDriver.hookFirstSync<'resolveFileUrl', string>('resolveFileUrl', [ { assetReferenceId, chunkId, @@ -130,7 +130,7 @@ export default class MetaProperty extends NodeBase { return; } - const replacement = pluginDriver.hookFirstSync('resolveImportMeta', [ + const replacement = outputPluginDriver.hookFirstSync('resolveImportMeta', [ metaProperty, { chunkId, diff --git a/src/rollup/index.ts b/src/rollup/index.ts index 5cf1807c984..0a9c2b45944 100644 --- a/src/rollup/index.ts +++ b/src/rollup/index.ts @@ -215,27 +215,34 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom // ensure we only do one optimization pass per build let optimized = false; - function getOutputOptions(rawOutputOptions: GenericConfigObject) { + function getOutputOptions( + rawOutputOptions: GenericConfigObject, + outputPluginDriver: PluginDriver + ) { return normalizeOutputOptions( inputOptions as GenericConfigObject, rawOutputOptions, chunks.length > 1, - graph.pluginDriver + outputPluginDriver ); } - async function generate(outputOptions: OutputOptions, isWrite: boolean): Promise { + async function generate( + outputOptions: OutputOptions, + isWrite: boolean, + outputPluginDriver: PluginDriver + ): Promise { timeStart('GENERATE', 1); const assetFileNames = outputOptions.assetFileNames || 'assets/[name]-[hash][extname]'; const outputBundleWithPlaceholders: OutputBundleWithPlaceholders = Object.create(null); let outputBundle; const inputBase = commondir(getAbsoluteEntryModulePaths(chunks)); - graph.pluginDriver.startOutput(outputBundleWithPlaceholders, assetFileNames); + outputPluginDriver.startOutput(outputBundleWithPlaceholders, assetFileNames); try { - await graph.pluginDriver.hookParallel('renderStart', []); - const addons = await createAddons(graph, outputOptions); + await outputPluginDriver.hookParallel('renderStart', []); + const addons = await createAddons(outputOptions, outputPluginDriver); for (const chunk of chunks) { if (!inputOptions.preserveModules) chunk.generateInternalExports(outputOptions); if (chunk.facadeModule && chunk.facadeModule.isEntryPoint) @@ -254,29 +261,32 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom outputOptions, inputBase, addons, - outputBundleWithPlaceholders + outputBundleWithPlaceholders, + outputPluginDriver ); outputBundle = assignChunksToBundle(chunks, outputBundleWithPlaceholders); await Promise.all( chunks.map(chunk => { const outputChunk = outputBundleWithPlaceholders[chunk.id as string] as OutputChunk; - return chunk.render(outputOptions, addons, outputChunk).then(rendered => { - outputChunk.code = rendered.code; - outputChunk.map = rendered.map; - - return graph.pluginDriver.hookParallel('ongenerate', [ - { bundle: outputChunk, ...outputOptions }, - outputChunk - ]); - }); + return chunk + .render(outputOptions, addons, outputChunk, outputPluginDriver) + .then(rendered => { + outputChunk.code = rendered.code; + outputChunk.map = rendered.map; + + return outputPluginDriver.hookParallel('ongenerate', [ + { bundle: outputChunk, ...outputOptions }, + outputChunk + ]); + }); }) ); } catch (error) { - await graph.pluginDriver.hookParallel('renderError', [error]); + await outputPluginDriver.hookParallel('renderError', [error]); throw error; } - await graph.pluginDriver.hookSeq('generateBundle', [outputOptions, outputBundle, isWrite]); + await outputPluginDriver.hookSeq('generateBundle', [outputOptions, outputBundle, isWrite]); for (const key of Object.keys(outputBundle)) { const file = outputBundle[key] as any; if (!file.type) { @@ -287,7 +297,7 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom file.type = 'asset'; } } - graph.pluginDriver.finaliseAssets(); + outputPluginDriver.finaliseAssets(); timeEnd('GENERATE', 1); return outputBundle; @@ -297,31 +307,35 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom const result: RollupBuild = { cache: cache as RollupCache, generate: ((rawOutputOptions: GenericConfigObject) => { - const promise = generate(getOutputOptions(rawOutputOptions), false).then(result => - createOutput(result) - ); + const outputPluginDriver = graph.pluginDriver; + const promise = generate( + getOutputOptions(rawOutputOptions, outputPluginDriver), + false, + outputPluginDriver + ).then(result => createOutput(result)); Object.defineProperty(promise, 'code', throwAsyncGenerateError); Object.defineProperty(promise, 'map', throwAsyncGenerateError); return promise; }) as any, watchFiles: Object.keys(graph.watchFiles), write: ((rawOutputOptions: GenericConfigObject) => { - const outputOptions = getOutputOptions(rawOutputOptions); + const outputPluginDriver = graph.pluginDriver; + const outputOptions = getOutputOptions(rawOutputOptions, outputPluginDriver); if (!outputOptions.dir && !outputOptions.file) { error({ code: 'MISSING_OPTION', message: 'You must specify "output.file" or "output.dir" for the build.' }); } - return generate(outputOptions, true).then(async bundle => { - let chunkCnt = 0; + return generate(outputOptions, true, outputPluginDriver).then(async bundle => { + let chunkCount = 0; for (const fileName of Object.keys(bundle)) { const file = bundle[fileName]; if (file.type === 'asset') continue; - chunkCnt++; - if (chunkCnt > 1) break; + chunkCount++; + if (chunkCount > 1) break; } - if (chunkCnt > 1) { + if (chunkCount > 1) { if (outputOptions.sourcemapFile) error({ code: 'INVALID_OPTION', @@ -340,10 +354,10 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom } await Promise.all( Object.keys(bundle).map(chunkId => - writeOutputFile(graph, result, bundle[chunkId], outputOptions) + writeOutputFile(result, bundle[chunkId], outputOptions, outputPluginDriver) ) ); - await graph.pluginDriver.hookParallel('writeBundle', [bundle]); + await outputPluginDriver.hookParallel('writeBundle', [bundle]); return createOutput(bundle); }); }) as any @@ -384,10 +398,10 @@ function createOutput(outputBundle: Record { const fileName = resolve( outputOptions.dir || dirname(outputOptions.file as string), @@ -418,7 +432,7 @@ function writeOutputFile( .then( (): any => outputFile.type === 'chunk' && - graph.pluginDriver.hookSeq('onwrite', [ + outputPluginDriver.hookSeq('onwrite', [ { bundle: build, ...outputOptions @@ -433,7 +447,7 @@ function normalizeOutputOptions( inputOptions: GenericConfigObject, rawOutputOptions: GenericConfigObject, hasMultipleChunks: boolean, - pluginDriver: PluginDriver + outputPluginDriver: PluginDriver ): OutputOptions { if (!rawOutputOptions) { throw new Error('You must supply an options object'); @@ -454,7 +468,7 @@ function normalizeOutputOptions( const mergedOutputOptions = mergedOptions.outputOptions[0]; const outputOptionsReducer = (outputOptions: OutputOptions, result: OutputOptions) => result || outputOptions; - const outputOptions = pluginDriver.hookReduceArg0Sync( + const outputOptions = outputPluginDriver.hookReduceArg0Sync( 'outputOptions', [mergedOutputOptions], outputOptionsReducer, diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index b518d737fdc..079b517f5a4 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -49,15 +49,15 @@ export interface PluginDriver { reduce: Reduce, hookContext?: HookContext ): R; - hookReduceValue( - hook: string, + hookReduceValue( + hook: H, value: T | Promise, args: any[], reduce: Reduce, hookContext?: HookContext ): Promise; - hookReduceValueSync( - hook: string, + hookReduceValueSync( + hook: H, value: T, args: any[], reduce: Reduce, @@ -108,8 +108,6 @@ export function createPluginDriver( if (hookContext) { context = hookContext(context, plugin); - if (!context || context === pluginContexts[pluginIndex]) - throw new Error('Internal Rollup error: hookContext must return a new context object.'); } try { // permit values allows values to be returned instead of a functional hook diff --git a/src/utils/addons.ts b/src/utils/addons.ts index 80b7a0f5970..890175c524e 100644 --- a/src/utils/addons.ts +++ b/src/utils/addons.ts @@ -1,6 +1,6 @@ -import Graph from '../Graph'; import { OutputOptions } from '../rollup/types'; import { error } from './error'; +import { PluginDriver } from './PluginDriver'; export interface Addons { banner?: string; @@ -25,13 +25,15 @@ function evalIfFn( const concatSep = (out: string, next: string) => (next ? `${out}\n${next}` : out); const concatDblSep = (out: string, next: string) => (next ? `${out}\n\n${next}` : out); -export function createAddons(graph: Graph, options: OutputOptions): Promise { - const pluginDriver = graph.pluginDriver; +export function createAddons( + options: OutputOptions, + outputPluginDriver: PluginDriver +): Promise { return Promise.all([ - pluginDriver.hookReduceValue('banner', evalIfFn(options.banner), [], concatSep), - pluginDriver.hookReduceValue('footer', evalIfFn(options.footer), [], concatSep), - pluginDriver.hookReduceValue('intro', evalIfFn(options.intro), [], concatDblSep), - pluginDriver.hookReduceValue('outro', evalIfFn(options.outro), [], concatDblSep) + outputPluginDriver.hookReduceValue('banner', evalIfFn(options.banner), [], concatSep), + outputPluginDriver.hookReduceValue('footer', evalIfFn(options.footer), [], concatSep), + outputPluginDriver.hookReduceValue('intro', evalIfFn(options.intro), [], concatDblSep), + outputPluginDriver.hookReduceValue('outro', evalIfFn(options.outro), [], concatDblSep) ]) .then(([banner, footer, intro, outro]) => { if (intro) intro += '\n\n'; diff --git a/src/utils/assignChunkIds.ts b/src/utils/assignChunkIds.ts index ae479ae4684..48dd39917e8 100644 --- a/src/utils/assignChunkIds.ts +++ b/src/utils/assignChunkIds.ts @@ -3,6 +3,7 @@ import { InputOptions, OutputBundleWithPlaceholders, OutputOptions } from '../ro import { Addons } from './addons'; import { FILE_PLACEHOLDER } from './FileEmitter'; import { basename } from './path'; +import { PluginDriver } from './PluginDriver'; export function assignChunkIds( chunks: Chunk[], @@ -10,7 +11,8 @@ export function assignChunkIds( outputOptions: OutputOptions, inputBase: string, addons: Addons, - bundle: OutputBundleWithPlaceholders + bundle: OutputBundleWithPlaceholders, + outputPluginDriver: PluginDriver ) { const entryChunks: Chunk[] = []; const otherChunks: Chunk[] = []; @@ -29,7 +31,7 @@ export function assignChunkIds( } else if (inputOptions.preserveModules) { chunk.id = chunk.generateIdPreserveModules(inputBase, outputOptions, bundle); } else { - chunk.id = chunk.generateId(addons, outputOptions, bundle, true); + chunk.id = chunk.generateId(addons, outputOptions, bundle, true, outputPluginDriver); } bundle[chunk.id] = FILE_PLACEHOLDER; } diff --git a/src/utils/renderChunk.ts b/src/utils/renderChunk.ts index d53be7c376b..64e58abf9f5 100644 --- a/src/utils/renderChunk.ts +++ b/src/utils/renderChunk.ts @@ -1,5 +1,4 @@ import Chunk from '../Chunk'; -import Graph from '../Graph'; import { DecodedSourceMapOrMissing, OutputOptions, @@ -9,19 +8,20 @@ import { } from '../rollup/types'; import { decodedSourcemap } from './decodedSourcemap'; import { error } from './error'; +import { PluginDriver } from './PluginDriver'; export default function renderChunk({ - graph, chunk, - renderChunk, code, - sourcemapChain, - options + options, + outputPluginDriver, + renderChunk, + sourcemapChain }: { chunk: Chunk; code: string; - graph: Graph; options: OutputOptions; + outputPluginDriver: PluginDriver; renderChunk: RenderedChunk; sourcemapChain: DecodedSourceMapOrMissing[]; }): Promise { @@ -49,11 +49,11 @@ export default function renderChunk({ let inTransformBundle = false; let inRenderChunk = true; - return graph.pluginDriver + return outputPluginDriver .hookReduceArg0('renderChunk', [code, renderChunk, options], renderChunkReducer) .then(code => { inRenderChunk = false; - return graph.pluginDriver.hookReduceArg0( + return outputPluginDriver.hookReduceArg0( 'transformChunk', [code, options, chunk], renderChunkReducer @@ -61,7 +61,7 @@ export default function renderChunk({ }) .then(code => { inTransformBundle = true; - return graph.pluginDriver.hookReduceArg0( + return outputPluginDriver.hookReduceArg0( 'transformBundle', [code, options, chunk], renderChunkReducer From 4394a72e804a58371327a45e620db678eec10d2f Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 25 Oct 2019 12:06:48 +0200 Subject: [PATCH 05/15] Improve some names --- src/utils/PluginContext.ts | 8 ++++---- src/utils/PluginDriver.ts | 24 ++++++++++++------------ 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts index 4bb12dc5cbb..471966027da 100644 --- a/src/utils/PluginContext.ts +++ b/src/utils/PluginContext.ts @@ -47,14 +47,14 @@ export function getPluginContexts( fileEmitter: FileEmitter, watcher: RollupWatcher | undefined ): (plugin: Plugin, pluginIndex: number) => PluginContext { - const existingPluginKeys = new Set(); + const existingPluginNames = new Set(); return (plugin, pidx) => { let cacheable = true; if (typeof plugin.cacheKey !== 'string') { - if (plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) || existingPluginKeys.has(plugin.name)) { + if (plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) || existingPluginNames.has(plugin.name)) { cacheable = false; } else { - existingPluginKeys.add(plugin.name); + existingPluginNames.add(plugin.name); } } @@ -127,7 +127,7 @@ export function getPluginContexts( importedIds: foundModule instanceof ExternalModule ? [] - : foundModule.sources.map(id => foundModule.resolvedIds[id].id), + : Array.from(foundModule.sources).map(id => foundModule.resolvedIds[id].id), isEntry: foundModule instanceof Module && foundModule.isEntryPoint, isExternal: foundModule instanceof ExternalModule }; diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index 079b517f5a4..b13d2b6bcb8 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -24,60 +24,60 @@ export interface PluginDriver { hookFirst>( hook: H, args: Args, - hookContext?: HookContext | null, + replaceContext?: ReplaceContext | null, skip?: number | null ): EnsurePromise; hookFirstSync>( hook: H, args: Args, - hookContext?: HookContext + replaceContext?: ReplaceContext ): R; hookParallel( hook: H, args: Args, - hookContext?: HookContext + replaceContext?: ReplaceContext ): Promise; hookReduceArg0>( hook: H, args: any[], reduce: Reduce, - hookContext?: HookContext + replaceContext?: ReplaceContext ): EnsurePromise; hookReduceArg0Sync>( hook: H, args: any[], reduce: Reduce, - hookContext?: HookContext + replaceContext?: ReplaceContext ): R; hookReduceValue( hook: H, value: T | Promise, args: any[], reduce: Reduce, - hookContext?: HookContext + replaceContext?: ReplaceContext ): Promise; hookReduceValueSync( hook: H, value: T, args: any[], reduce: Reduce, - hookContext?: HookContext + replaceContext?: ReplaceContext ): T; hookSeq( hook: H, args: Args, - context?: HookContext + replaceContext?: ReplaceContext ): Promise; hookSeqSync( hook: H, args: Args, - context?: HookContext + replaceContext?: ReplaceContext ): void; startOutput(outputBundle: OutputBundleWithPlaceholders, assetFileNames: string): void; } export type Reduce = (reduction: T, result: R, plugin: Plugin) => T; -export type HookContext = (context: PluginContext, plugin: Plugin) => PluginContext; +export type ReplaceContext = (context: PluginContext, plugin: Plugin) => PluginContext; export function createPluginDriver( graph: Graph, @@ -99,7 +99,7 @@ export function createPluginDriver( args: any[], pluginIndex: number, permitValues = false, - hookContext?: HookContext + hookContext?: ReplaceContext ): T { const plugin = plugins[pluginIndex]; let context = pluginContexts[pluginIndex]; @@ -129,7 +129,7 @@ export function createPluginDriver( args: any[], pluginIndex: number, permitValues = false, - hookContext?: HookContext | null + hookContext?: ReplaceContext | null ): Promise { const plugin = plugins[pluginIndex]; let context = pluginContexts[pluginIndex]; From e38f1f384c75d93190d83f756c6527769afada25 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sat, 2 Nov 2019 07:58:31 +0100 Subject: [PATCH 06/15] Make PluginDriver a class --- src/Graph.ts | 4 +- src/utils/PluginDriver.ts | 354 ++++++++++++++++++-------------------- 2 files changed, 172 insertions(+), 186 deletions(-) diff --git a/src/Graph.ts b/src/Graph.ts index 2871120e82f..49c6913ddfb 100644 --- a/src/Graph.ts +++ b/src/Graph.ts @@ -29,7 +29,7 @@ import { Uint8ArrayToHexString } from './utils/entryHashing'; import { errDeprecation, error } from './utils/error'; import { analyseModuleExecution, sortByExecutionOrder } from './utils/executionOrder'; import { resolve } from './utils/path'; -import { createPluginDriver, PluginDriver } from './utils/PluginDriver'; +import { PluginDriver } from './utils/PluginDriver'; import relativeId from './utils/relativeId'; import { timeEnd, timeStart } from './utils/timers'; @@ -143,7 +143,7 @@ export default class Graph { ...this.acornOptions }) as any; - this.pluginDriver = createPluginDriver( + this.pluginDriver = new PluginDriver( this, options.plugins as Plugin[], this.pluginCache, diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index b13d2b6bcb8..9065c0f4563 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -17,129 +17,193 @@ import { throwPluginError, warnDeprecatedHooks } from './pluginUtils'; type Args = T extends (...args: infer K) => any ? K : never; type EnsurePromise = Promise ? K : T>; -export interface PluginDriver { - emitFile: EmitFile; - finaliseAssets(): void; - getFileName(assetReferenceId: string): string; +export type Reduce = (reduction: T, result: R, plugin: Plugin) => T; +export type ReplaceContext = (context: PluginContext, plugin: Plugin) => PluginContext; + +export class PluginDriver { + public emitFile: EmitFile; + public finaliseAssets: () => void; + public getFileName: (fileReferenceId: string) => string; + public startOutput: (outputBundle: OutputBundleWithPlaceholders, assetFileNames: string) => void; + + private fileEmitter: FileEmitter; + private pluginContexts: PluginContext[]; + private plugins: Plugin[]; + + constructor( + graph: Graph, + userPlugins: Plugin[], + pluginCache: Record | void, + preserveSymlinks: boolean, + watcher?: RollupWatcher + ) { + warnDeprecatedHooks(userPlugins, graph); + this.fileEmitter = new FileEmitter(graph); + this.emitFile = this.fileEmitter.emitFile.bind(this.fileEmitter); + this.getFileName = this.fileEmitter.getFileName.bind(this.fileEmitter); + this.finaliseAssets = this.fileEmitter.assertAssetsFinalized.bind(this.fileEmitter); + this.startOutput = this.fileEmitter.startOutput.bind(this.fileEmitter); + this.plugins = userPlugins.concat([getRollupDefaultPlugin(preserveSymlinks)]); + this.pluginContexts = this.plugins.map( + getPluginContexts(pluginCache, graph, this.fileEmitter, watcher) + ); + } + + // chains, first non-null result stops and returns hookFirst>( - hook: H, + hookName: H, args: Args, replaceContext?: ReplaceContext | null, skip?: number | null - ): EnsurePromise; + ): EnsurePromise { + let promise: Promise = Promise.resolve(); + for (let i = 0; i < this.plugins.length; i++) { + if (skip === i) continue; + promise = promise.then((result: any) => { + if (result != null) return result; + return this.runHook(hookName, args as any[], i, false, replaceContext); + }); + } + return promise; + } + + // chains synchronously, first non-null result stops and returns hookFirstSync>( - hook: H, + hookName: H, args: Args, replaceContext?: ReplaceContext - ): R; + ): R { + for (let i = 0; i < this.plugins.length; i++) { + const result = this.runHookSync(hookName, args, i, false, replaceContext); + if (result != null) return result as any; + } + return null as any; + } + + // parallel, ignores returns hookParallel( - hook: H, + hookName: H, args: Args, replaceContext?: ReplaceContext - ): Promise; + ): Promise { + const promises: Promise[] = []; + for (let i = 0; i < this.plugins.length; i++) { + const hookPromise = this.runHook(hookName, args as any[], 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>( - hook: H, - args: any[], + hookName: H, + [arg0, ...args]: any[], reduce: Reduce, replaceContext?: ReplaceContext - ): EnsurePromise; + ) { + 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); + if (!hookPromise) return arg0; + return hookPromise.then((result: any) => + reduce.call(this.pluginContexts[i], arg0, result, this.plugins[i]) + ); + }); + } + return promise; + } + + // chains synchronously, reduces returns of type R, to type T, handling the reduced value as the first hook argument hookReduceArg0Sync>( - hook: H, - args: any[], + hookName: H, + [arg0, ...args]: any[], reduce: Reduce, replaceContext?: ReplaceContext - ): R; + ): R { + for (let i = 0; i < this.plugins.length; i++) { + const result: any = this.runHookSync(hookName, [arg0, ...args], i, false, 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( - hook: H, - value: T | Promise, + hookName: H, + initialValue: T | Promise, args: any[], reduce: Reduce, replaceContext?: ReplaceContext - ): Promise; + ): Promise { + let promise = Promise.resolve(initialValue); + for (let i = 0; i < this.plugins.length; i++) { + promise = promise.then(value => { + const hookPromise = this.runHook(hookName, args, i, true, replaceContext); + if (!hookPromise) return value; + return hookPromise.then((result: any) => + reduce.call(this.pluginContexts[i], value, result, this.plugins[i]) + ); + }); + } + return promise; + } + + // chains, reduces returns of type R, to type T, handling the reduced value separately. permits hooks as values. hookReduceValueSync( - hook: H, - value: T, + hookName: H, + initialValue: T, args: any[], reduce: Reduce, replaceContext?: ReplaceContext - ): T; - hookSeq( - hook: H, + ): T { + let acc = initialValue; + for (let i = 0; i < this.plugins.length; i++) { + const result: any = this.runHookSync(hookName, args, i, true, replaceContext); + acc = reduce.call(this.pluginContexts[i], acc, result, this.plugins[i]); + } + return acc; + } + + // chains, ignores returns + async hookSeq( + hookName: H, args: Args, replaceContext?: ReplaceContext - ): Promise; + ): Promise { + let promise: Promise = Promise.resolve() as any; + for (let i = 0; i < this.plugins.length; i++) + promise = promise.then(() => + this.runHook(hookName, args as any[], i, false, replaceContext) + ); + return promise; + } + + // chains, ignores returns hookSeqSync( - hook: H, + hookName: H, args: Args, replaceContext?: ReplaceContext - ): void; - startOutput(outputBundle: OutputBundleWithPlaceholders, assetFileNames: string): void; -} - -export type Reduce = (reduction: T, result: R, plugin: Plugin) => T; -export type ReplaceContext = (context: PluginContext, plugin: Plugin) => PluginContext; - -export function createPluginDriver( - graph: Graph, - userPlugins: Plugin[], - pluginCache: Record | void, - preserveSymlinks: boolean, - watcher?: RollupWatcher -): PluginDriver { - warnDeprecatedHooks(userPlugins, graph); - const plugins = userPlugins.concat([getRollupDefaultPlugin(preserveSymlinks)]); - const fileEmitter = new FileEmitter(graph); - - const pluginContexts: PluginContext[] = plugins.map( - getPluginContexts(pluginCache, graph, fileEmitter, watcher) - ); - - function runHookSync( - hookName: string, - args: any[], - pluginIndex: number, - permitValues = false, - hookContext?: ReplaceContext - ): T { - const plugin = plugins[pluginIndex]; - let context = pluginContexts[pluginIndex]; - const hook = (plugin as any)[hookName]; - if (!hook) return undefined as any; - - if (hookContext) { - context = hookContext(context, plugin); - } - try { - // permit values allows values to be returned instead of a functional hook - if (typeof hook !== 'function') { - if (permitValues) return hook; - error({ - code: 'INVALID_PLUGIN_HOOK', - message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.` - }); - } - return hook.apply(context, args); - } catch (err) { - return throwPluginError(err, plugin.name, { hook: hookName }); - } + ): void { + for (let i = 0; i < this.plugins.length; i++) + this.runHookSync(hookName, args as any[], i, false, replaceContext); } - function runHook( + private runHook( hookName: string, args: any[], pluginIndex: number, permitValues = false, hookContext?: ReplaceContext | null ): Promise { - const plugin = plugins[pluginIndex]; - let context = pluginContexts[pluginIndex]; + const plugin = this.plugins[pluginIndex]; const hook = (plugin as any)[hookName]; if (!hook) return undefined as any; + let context = this.pluginContexts[pluginIndex]; if (hookContext) { context = hookContext(context, plugin); - if (!context || context === pluginContexts[pluginIndex]) - throw new Error('Internal Rollup error: hookContext must return a new context object.'); } return Promise.resolve() .then(() => { @@ -156,111 +220,33 @@ export function createPluginDriver( .catch(err => throwPluginError(err, plugin.name, { hook: hookName })); } - return { - emitFile: fileEmitter.emitFile, - finaliseAssets() { - fileEmitter.assertAssetsFinalized(); - }, - getFileName: fileEmitter.getFileName, - - // chains, ignores returns - hookSeq(name, args, hookContext) { - let promise: Promise = Promise.resolve() as any; - for (let i = 0; i < plugins.length; i++) - promise = promise.then(() => runHook(name, args as any[], i, false, hookContext)); - return promise; - }, - - // chains, ignores returns - hookSeqSync(name, args, hookContext) { - for (let i = 0; i < plugins.length; i++) - runHookSync(name, args as any[], i, false, hookContext); - }, - - // chains, first non-null result stops and returns - hookFirst(name, args, hookContext, skip) { - let promise: Promise = Promise.resolve(); - for (let i = 0; i < plugins.length; i++) { - if (skip === i) continue; - promise = promise.then((result: any) => { - if (result != null) return result; - return runHook(name, args as any[], i, false, hookContext); - }); - } - return promise; - }, - - // chains synchronously, first non-null result stops and returns - hookFirstSync(name, args?, hookContext?) { - for (let i = 0; i < plugins.length; i++) { - const result = runHookSync(name, args, i, false, hookContext); - if (result != null) return result as any; - } - return null; - }, - - // parallel, ignores returns - hookParallel(name, args, hookContext) { - const promises: Promise[] = []; - for (let i = 0; i < plugins.length; i++) { - const hookPromise = runHook(name, args as any[], i, false, hookContext); - 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(name, [arg0, ...args], reduce, hookContext) { - let promise = Promise.resolve(arg0); - for (let i = 0; i < plugins.length; i++) { - promise = promise.then(arg0 => { - const hookPromise = runHook(name, [arg0, ...args], i, false, hookContext); - if (!hookPromise) return arg0; - return hookPromise.then((result: any) => - reduce.call(pluginContexts[i], arg0, result, plugins[i]) - ); - }); - } - return promise; - }, - - // chains synchronously, reduces returns of type R, to type T, handling the reduced value as the first hook argument - hookReduceArg0Sync(name, [arg0, ...args], reduce, hookContext) { - for (let i = 0; i < plugins.length; i++) { - const result: any = runHookSync(name, [arg0, ...args], i, false, hookContext); - arg0 = reduce.call(pluginContexts[i], arg0, result, plugins[i]); - } - return arg0; - }, + private runHookSync( + hookName: string, + args: any[], + pluginIndex: number, + permitValues = false, + hookContext?: ReplaceContext + ): T { + const plugin = this.plugins[pluginIndex]; + let context = this.pluginContexts[pluginIndex]; + const hook = (plugin as any)[hookName]; + if (!hook) return undefined as any; - // chains, reduces returns of type R, to type T, handling the reduced value separately. permits hooks as values. - hookReduceValue(name, initial, args, reduce, hookContext) { - let promise = Promise.resolve(initial); - for (let i = 0; i < plugins.length; i++) { - promise = promise.then(value => { - const hookPromise = runHook(name, args, i, true, hookContext); - if (!hookPromise) return value; - return hookPromise.then((result: any) => - reduce.call(pluginContexts[i], value, result, plugins[i]) - ); + if (hookContext) { + context = hookContext(context, plugin); + } + try { + // permit values allows values to be returned instead of a functional hook + if (typeof hook !== 'function') { + if (permitValues) return hook; + error({ + code: 'INVALID_PLUGIN_HOOK', + message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.` }); } - return promise; - }, - - // chains, reduces returns of type R, to type T, handling the reduced value separately. permits hooks as values. - hookReduceValueSync(name, initial, args, reduce, hookContext) { - let acc = initial; - for (let i = 0; i < plugins.length; i++) { - const result: any = runHookSync(name, args, i, true, hookContext); - acc = reduce.call(pluginContexts[i], acc, result, plugins[i]); - } - return acc; - }, - - startOutput(outputBundle: OutputBundleWithPlaceholders, assetFileNames: string): void { - fileEmitter.startOutput(outputBundle, assetFileNames); + return hook.apply(context, args); + } catch (err) { + return throwPluginError(err, plugin.name, { hook: hookName }); } - }; + } } From 8fec280ede31df0c0f8f2a3eb665eb95a2660f31 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Sun, 3 Nov 2019 19:31:53 +0100 Subject: [PATCH 07/15] Use separate file emitters for different outputs --- src/Chunk.ts | 2 +- src/Module.ts | 2 -- src/ast/nodes/MetaProperty.ts | 6 ++-- src/rollup/index.ts | 8 ++--- src/utils/FileEmitter.ts | 65 ++++++++++++++++++----------------- src/utils/PluginDriver.ts | 43 ++++++++++++++++++----- test/hooks/index.js | 29 ++++++++++++++++ 7 files changed, 105 insertions(+), 50 deletions(-) diff --git a/src/Chunk.ts b/src/Chunk.ts index ad9db0eec20..73b48b867d1 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -739,7 +739,7 @@ export default class Chunk { chunk: this, code: prevCode, options, - outputPluginDriver: this.graph.pluginDriver, + outputPluginDriver, renderChunk: outputChunk, sourcemapChain: chunkSourcemapChain }).then((code: string) => { diff --git a/src/Module.ts b/src/Module.ts index fbc651fd75c..11ac9fce855 100644 --- a/src/Module.ts +++ b/src/Module.ts @@ -91,7 +91,6 @@ export interface AstContext { error: (props: RollupError, pos: number) => void; fileName: string; getExports: () => string[]; - getFileName: (fileReferenceId: string) => string; getModuleExecIndex: () => number; getModuleName: () => string; getReexports: () => string[]; @@ -586,7 +585,6 @@ export default class Module { error: this.error.bind(this), fileName, // Needed for warnings getExports: this.getExports.bind(this), - getFileName: this.graph.pluginDriver.getFileName, getModuleExecIndex: () => this.execIndex, getModuleName: this.basename.bind(this), getReexports: this.getReexports.bind(this), diff --git a/src/ast/nodes/MetaProperty.ts b/src/ast/nodes/MetaProperty.ts index 9694af2d554..bccedbddeb9 100644 --- a/src/ast/nodes/MetaProperty.ts +++ b/src/ast/nodes/MetaProperty.ts @@ -77,21 +77,21 @@ export default class MetaProperty extends NodeBase { let fileName: string; if (metaProperty.startsWith(FILE_PREFIX)) { referenceId = metaProperty.substr(FILE_PREFIX.length); - fileName = this.context.getFileName(referenceId); + fileName = outputPluginDriver.getFileName(referenceId); } else if (metaProperty.startsWith(ASSET_PREFIX)) { this.context.warnDeprecation( `Using the "${ASSET_PREFIX}" prefix to reference files is deprecated. Use the "${FILE_PREFIX}" prefix instead.`, false ); assetReferenceId = metaProperty.substr(ASSET_PREFIX.length); - fileName = this.context.getFileName(assetReferenceId); + fileName = outputPluginDriver.getFileName(assetReferenceId); } else { this.context.warnDeprecation( `Using the "${CHUNK_PREFIX}" prefix to reference files is deprecated. Use the "${FILE_PREFIX}" prefix instead.`, false ); chunkReferenceId = metaProperty.substr(CHUNK_PREFIX.length); - fileName = this.context.getFileName(chunkReferenceId); + fileName = outputPluginDriver.getFileName(chunkReferenceId); } const relativePath = normalize(relative(dirname(chunkId), fileName)); let replacement; diff --git a/src/rollup/index.ts b/src/rollup/index.ts index 0a9c2b45944..b5cf6f90aa4 100644 --- a/src/rollup/index.ts +++ b/src/rollup/index.ts @@ -235,10 +235,10 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom timeStart('GENERATE', 1); const assetFileNames = outputOptions.assetFileNames || 'assets/[name]-[hash][extname]'; + const inputBase = commondir(getAbsoluteEntryModulePaths(chunks)); const outputBundleWithPlaceholders: OutputBundleWithPlaceholders = Object.create(null); + outputPluginDriver.setOutputBundle(outputBundleWithPlaceholders, assetFileNames); let outputBundle; - const inputBase = commondir(getAbsoluteEntryModulePaths(chunks)); - outputPluginDriver.startOutput(outputBundleWithPlaceholders, assetFileNames); try { await outputPluginDriver.hookParallel('renderStart', []); @@ -307,7 +307,7 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom const result: RollupBuild = { cache: cache as RollupCache, generate: ((rawOutputOptions: GenericConfigObject) => { - const outputPluginDriver = graph.pluginDriver; + const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver([]); const promise = generate( getOutputOptions(rawOutputOptions, outputPluginDriver), false, @@ -319,7 +319,7 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom }) as any, watchFiles: Object.keys(graph.watchFiles), write: ((rawOutputOptions: GenericConfigObject) => { - const outputPluginDriver = graph.pluginDriver; + const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver([]); const outputOptions = getOutputOptions(rawOutputOptions, outputPluginDriver); if (!outputOptions.dir && !outputOptions.file) { error({ diff --git a/src/utils/FileEmitter.ts b/src/utils/FileEmitter.ts index 5ddb3b14e78..ab11649aa76 100644 --- a/src/utils/FileEmitter.ts +++ b/src/utils/FileEmitter.ts @@ -48,7 +48,7 @@ function generateAssetFileName( } function reserveFileNameInBundle(fileName: string, bundle: OutputBundleWithPlaceholders) { - // TODO this should warn if the fileName is already in the bundle, + // TODO Lukas this should warn if the fileName is already in the bundle, // but until #3174 is fixed, this raises spurious warnings and is disabled bundle[fileName] = FILE_PLACEHOLDER; } @@ -132,16 +132,24 @@ function getChunkFileName(file: ConsumedChunk): string { } export class FileEmitter { - private filesByReferenceId = new Map(); - // tslint:disable member-ordering - private buildFilesByReferenceId = this.filesByReferenceId; + private filesByReferenceId: Map; private graph: Graph; private output: OutputSpecificFileData | null = null; - constructor(graph: Graph) { + constructor(graph: Graph, baseFileEmitter?: FileEmitter) { this.graph = graph; + this.filesByReferenceId = baseFileEmitter + ? new Map(baseFileEmitter.filesByReferenceId) + : new Map(); } + public assertAssetsFinalized = (): void => { + for (const [referenceId, emittedFile] of this.filesByReferenceId.entries()) { + if (emittedFile.type === 'asset' && typeof emittedFile.fileName !== 'string') + error(errNoAssetSourceSet(emittedFile.name || referenceId)); + } + }; + public emitFile = (emittedFile: unknown): string => { if (!hasValidType(emittedFile)) { return error( @@ -166,7 +174,7 @@ export class FileEmitter { } }; - public getFileName = (fileReferenceId: string) => { + public getFileName = (fileReferenceId: string): string => { const emittedFile = this.filesByReferenceId.get(fileReferenceId); if (!emittedFile) return error(errFileReferenceIdNotFoundForFilename(fileReferenceId)); if (emittedFile.type === 'chunk') { @@ -176,7 +184,7 @@ export class FileEmitter { } }; - public setAssetSource = (referenceId: string, requestedSource: unknown) => { + public setAssetSource = (referenceId: string, requestedSource: unknown): void => { const consumedFile = this.filesByReferenceId.get(referenceId); if (!consumedFile) return error(errAssetReferenceIdNotFoundForSetSource(referenceId)); if (consumedFile.type !== 'asset') { @@ -197,8 +205,10 @@ export class FileEmitter { } }; - public startOutput(outputBundle: OutputBundleWithPlaceholders, assetFileNames: string) { - this.filesByReferenceId = new Map(this.buildFilesByReferenceId); + public setOutputBundle = ( + outputBundle: OutputBundleWithPlaceholders, + assetFileNames: string + ): void => { this.output = { assetFileNames, bundle: outputBundle @@ -213,13 +223,21 @@ export class FileEmitter { this.finalizeAsset(consumedFile, consumedFile.source, referenceId, this.output); } } - } + }; - public assertAssetsFinalized() { - for (const [referenceId, emittedFile] of this.filesByReferenceId.entries()) { - if (emittedFile.type === 'asset' && typeof emittedFile.fileName !== 'string') - error(errNoAssetSourceSet(emittedFile.name || referenceId)); - } + private assignReferenceId(file: ConsumedFile, idBase: string): string { + let referenceId: string | undefined; + do { + const hash = createHash(); + if (referenceId) { + hash.update(referenceId); + } else { + hash.update(idBase); + } + referenceId = hash.digest('hex').substr(0, 8); + } while (this.filesByReferenceId.has(referenceId)); + this.filesByReferenceId.set(referenceId, file); + return referenceId; } private emitAsset(emittedAsset: EmittedFile): string { @@ -287,27 +305,12 @@ export class FileEmitter { return this.assignReferenceId(consumedChunk, emittedChunk.id); } - private assignReferenceId(file: ConsumedFile, idBase: string): string { - let referenceId: string | undefined; - do { - const hash = createHash(); - if (referenceId) { - hash.update(referenceId); - } else { - hash.update(idBase); - } - referenceId = hash.digest('hex').substr(0, 8); - } while (this.filesByReferenceId.has(referenceId)); - this.filesByReferenceId.set(referenceId, file); - return referenceId; - } - private finalizeAsset( consumedFile: ConsumedFile, source: string | Buffer, referenceId: string, output: OutputSpecificFileData - ) { + ): void { const fileName = consumedFile.fileName || this.findExistingAssetFileNameWithSource(output.bundle, source) || diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index 9065c0f4563..62f7b1428db 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -24,31 +24,56 @@ export class PluginDriver { public emitFile: EmitFile; public finaliseAssets: () => void; public getFileName: (fileReferenceId: string) => string; - public startOutput: (outputBundle: OutputBundleWithPlaceholders, assetFileNames: string) => void; + public setOutputBundle: ( + outputBundle: OutputBundleWithPlaceholders, + assetFileNames: string + ) => void; private fileEmitter: FileEmitter; + private graph: Graph; + private pluginCache: Record | undefined; private pluginContexts: PluginContext[]; private plugins: Plugin[]; + private preserveSymlinks: boolean; + private watcher: RollupWatcher | undefined; constructor( graph: Graph, userPlugins: Plugin[], - pluginCache: Record | void, + pluginCache: Record | undefined, preserveSymlinks: boolean, - watcher?: RollupWatcher + watcher: RollupWatcher | undefined, + basePluginDriver?: PluginDriver ) { warnDeprecatedHooks(userPlugins, graph); - this.fileEmitter = new FileEmitter(graph); - this.emitFile = this.fileEmitter.emitFile.bind(this.fileEmitter); - this.getFileName = this.fileEmitter.getFileName.bind(this.fileEmitter); - this.finaliseAssets = this.fileEmitter.assertAssetsFinalized.bind(this.fileEmitter); - this.startOutput = this.fileEmitter.startOutput.bind(this.fileEmitter); - this.plugins = userPlugins.concat([getRollupDefaultPlugin(preserveSymlinks)]); + this.graph = graph; + this.pluginCache = pluginCache; + this.preserveSymlinks = preserveSymlinks; + this.watcher = watcher; + this.fileEmitter = new FileEmitter(graph, basePluginDriver && basePluginDriver.fileEmitter); + this.emitFile = this.fileEmitter.emitFile; + this.getFileName = this.fileEmitter.getFileName; + this.finaliseAssets = this.fileEmitter.assertAssetsFinalized; + this.setOutputBundle = this.fileEmitter.setOutputBundle; + this.plugins = userPlugins.concat( + basePluginDriver ? basePluginDriver.plugins : [getRollupDefaultPlugin(preserveSymlinks)] + ); this.pluginContexts = this.plugins.map( getPluginContexts(pluginCache, graph, this.fileEmitter, watcher) ); } + public createOutputPluginDriver(plugins: Plugin[]): PluginDriver { + return new PluginDriver( + this.graph, + plugins, + this.pluginCache, + this.preserveSymlinks, + this.watcher, + this + ); + } + // chains, first non-null result stops and returns hookFirst>( hookName: H, diff --git a/test/hooks/index.js b/test/hooks/index.js index 2e4d800ca83..84555d91047 100644 --- a/test/hooks/index.js +++ b/test/hooks/index.js @@ -322,6 +322,35 @@ describe('hooks', () => { }); }); + it('does not overwrite files in other outputs when emitting assets during generate', () => { + return rollup + .rollup({ + input: 'input', + plugins: [ + loader({ input: 'export default 42;' }), + { + generateBundle(outputOptions) { + this.emitFile({ type: 'asset', source: outputOptions.format }); + } + } + ] + }) + .then(bundle => + Promise.all([ + bundle.generate({ format: 'es', assetFileNames: 'asset' }), + bundle.generate({ format: 'cjs', assetFileNames: 'asset' }) + ]) + ) + .then(([{ output: output1 }, { output: output2 }]) => { + assert.equal(output1.length, 2, 'output1'); + assert.equal(output1[1].fileName, 'asset'); + assert.equal(output1[1].source, 'es'); + assert.equal(output2.length, 2, 'output2'); + assert.equal(output2[1].fileName, 'asset'); + assert.equal(output2[1].source, 'cjs'); + }); + }); + it('caches asset emission in transform hook', () => { let cache; return rollup From c4e529d02c485a7808c2c7b8f14fb242b91d6dff Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Tue, 5 Nov 2019 06:16:12 +0100 Subject: [PATCH 08/15] Warn when reemitting files of the same name --- src/utils/FileEmitter.ts | 16 ++++++---- src/utils/error.ts | 2 +- .../samples/emit-same-file/_config.js | 15 ---------- .../emit-file/emit-same-file/_config.js | 30 +++++++++++++++++++ .../emit-same-file/_expected/amd/main.js | 0 .../emit-same-file/_expected/amd/myfile | 0 .../emit-same-file/_expected/cjs/main.js | 0 .../emit-same-file/_expected/cjs/myfile | 0 .../emit-same-file/_expected/es/main.js | 0 .../emit-same-file/_expected/es/myfile | 0 .../emit-same-file/_expected/system/main.js | 0 .../emit-same-file/_expected/system/myfile | 0 .../samples/emit-file}/emit-same-file/main.js | 0 13 files changed, 42 insertions(+), 21 deletions(-) delete mode 100644 test/chunking-form/samples/emit-same-file/_config.js create mode 100644 test/function/samples/emit-file/emit-same-file/_config.js rename test/{chunking-form/samples => function/samples/emit-file}/emit-same-file/_expected/amd/main.js (100%) rename test/{chunking-form/samples => function/samples/emit-file}/emit-same-file/_expected/amd/myfile (100%) rename test/{chunking-form/samples => function/samples/emit-file}/emit-same-file/_expected/cjs/main.js (100%) rename test/{chunking-form/samples => function/samples/emit-file}/emit-same-file/_expected/cjs/myfile (100%) rename test/{chunking-form/samples => function/samples/emit-file}/emit-same-file/_expected/es/main.js (100%) rename test/{chunking-form/samples => function/samples/emit-file}/emit-same-file/_expected/es/myfile (100%) rename test/{chunking-form/samples => function/samples/emit-file}/emit-same-file/_expected/system/main.js (100%) rename test/{chunking-form/samples => function/samples/emit-file}/emit-same-file/_expected/system/myfile (100%) rename test/{chunking-form/samples => function/samples/emit-file}/emit-same-file/main.js (100%) diff --git a/src/utils/FileEmitter.ts b/src/utils/FileEmitter.ts index ab11649aa76..88e34a8bb3d 100644 --- a/src/utils/FileEmitter.ts +++ b/src/utils/FileEmitter.ts @@ -10,6 +10,7 @@ import { errAssetSourceAlreadySet, errChunkNotGeneratedForFileName, errFailedValidation, + errFileNameConflict, errFileReferenceIdNotFoundForFilename, errInvalidRollupPhaseForChunkEmission, errNoAssetSourceSet, @@ -47,9 +48,14 @@ function generateAssetFileName( ); } -function reserveFileNameInBundle(fileName: string, bundle: OutputBundleWithPlaceholders) { - // TODO Lukas this should warn if the fileName is already in the bundle, - // but until #3174 is fixed, this raises spurious warnings and is disabled +function reserveFileNameInBundle( + fileName: string, + bundle: OutputBundleWithPlaceholders, + graph: Graph +) { + if (fileName in bundle) { + graph.warn(errFileNameConflict(fileName)); + } bundle[fileName] = FILE_PLACEHOLDER; } @@ -215,7 +221,7 @@ export class FileEmitter { }; for (const emittedFile of this.filesByReferenceId.values()) { if (emittedFile.fileName) { - reserveFileNameInBundle(emittedFile.fileName, this.output.bundle); + reserveFileNameInBundle(emittedFile.fileName, this.output.bundle, this.graph); } } for (const [referenceId, consumedFile] of this.filesByReferenceId.entries()) { @@ -257,7 +263,7 @@ export class FileEmitter { ); if (this.output) { if (emittedAsset.fileName) { - reserveFileNameInBundle(emittedAsset.fileName, this.output.bundle); + reserveFileNameInBundle(emittedAsset.fileName, this.output.bundle, this.graph); } if (source !== undefined) { this.finalizeAsset(consumedAsset, source, referenceId, this.output); diff --git a/src/utils/error.ts b/src/utils/error.ts index 7010eb883c2..aba6a6ee463 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -122,7 +122,7 @@ export function errFileReferenceIdNotFoundForFilename(assetReferenceId: string) export function errFileNameConflict(fileName: string) { return { code: Errors.FILE_NAME_CONFLICT, - message: `Could not emit file "${fileName}" as it conflicts with an already emitted file.` + message: `The emitted file "${fileName}" overwrites a previously emitted file of the same name.` }; } diff --git a/test/chunking-form/samples/emit-same-file/_config.js b/test/chunking-form/samples/emit-same-file/_config.js deleted file mode 100644 index bad65a0f0d7..00000000000 --- a/test/chunking-form/samples/emit-same-file/_config.js +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - description: - 'does not throw an error if multiple files with the same name are emitted (until #3174 is fixed)', - options: { - input: 'main.js', - plugins: [ - { - generateBundle() { - this.emitFile({ type: 'asset', fileName: 'myfile', source: 'abc' }); - this.emitFile({ type: 'asset', fileName: 'myfile', source: 'abc' }); - } - } - ] - } -}; diff --git a/test/function/samples/emit-file/emit-same-file/_config.js b/test/function/samples/emit-file/emit-same-file/_config.js new file mode 100644 index 00000000000..a0266d5791b --- /dev/null +++ b/test/function/samples/emit-file/emit-same-file/_config.js @@ -0,0 +1,30 @@ +module.exports = { + description: 'warns if multiple files with the same name are emitted', + options: { + input: 'main.js', + plugins: [ + { + buildStart() { + this.emitFile({ type: 'asset', fileName: 'buildStart', source: 'abc' }); + }, + generateBundle() { + this.emitFile({ type: 'asset', fileName: 'buildStart', source: 'abc' }); + this.emitFile({ type: 'asset', fileName: 'generateBundle', source: 'abc' }); + this.emitFile({ type: 'asset', fileName: 'generateBundle', source: 'abc' }); + } + } + ] + }, + warnings: [ + { + code: 'FILE_NAME_CONFLICT', + message: + 'The emitted file "buildStart" overwrites a previously emitted file of the same name.' + }, + { + code: 'FILE_NAME_CONFLICT', + message: + 'The emitted file "generateBundle" overwrites a previously emitted file of the same name.' + } + ] +}; diff --git a/test/chunking-form/samples/emit-same-file/_expected/amd/main.js b/test/function/samples/emit-file/emit-same-file/_expected/amd/main.js similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/amd/main.js rename to test/function/samples/emit-file/emit-same-file/_expected/amd/main.js diff --git a/test/chunking-form/samples/emit-same-file/_expected/amd/myfile b/test/function/samples/emit-file/emit-same-file/_expected/amd/myfile similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/amd/myfile rename to test/function/samples/emit-file/emit-same-file/_expected/amd/myfile diff --git a/test/chunking-form/samples/emit-same-file/_expected/cjs/main.js b/test/function/samples/emit-file/emit-same-file/_expected/cjs/main.js similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/cjs/main.js rename to test/function/samples/emit-file/emit-same-file/_expected/cjs/main.js diff --git a/test/chunking-form/samples/emit-same-file/_expected/cjs/myfile b/test/function/samples/emit-file/emit-same-file/_expected/cjs/myfile similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/cjs/myfile rename to test/function/samples/emit-file/emit-same-file/_expected/cjs/myfile diff --git a/test/chunking-form/samples/emit-same-file/_expected/es/main.js b/test/function/samples/emit-file/emit-same-file/_expected/es/main.js similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/es/main.js rename to test/function/samples/emit-file/emit-same-file/_expected/es/main.js diff --git a/test/chunking-form/samples/emit-same-file/_expected/es/myfile b/test/function/samples/emit-file/emit-same-file/_expected/es/myfile similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/es/myfile rename to test/function/samples/emit-file/emit-same-file/_expected/es/myfile diff --git a/test/chunking-form/samples/emit-same-file/_expected/system/main.js b/test/function/samples/emit-file/emit-same-file/_expected/system/main.js similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/system/main.js rename to test/function/samples/emit-file/emit-same-file/_expected/system/main.js diff --git a/test/chunking-form/samples/emit-same-file/_expected/system/myfile b/test/function/samples/emit-file/emit-same-file/_expected/system/myfile similarity index 100% rename from test/chunking-form/samples/emit-same-file/_expected/system/myfile rename to test/function/samples/emit-file/emit-same-file/_expected/system/myfile diff --git a/test/chunking-form/samples/emit-same-file/main.js b/test/function/samples/emit-file/emit-same-file/main.js similarity index 100% rename from test/chunking-form/samples/emit-same-file/main.js rename to test/function/samples/emit-file/emit-same-file/main.js From bbb91871563c2bfa499c63ba9398c483e7a5579c Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Tue, 5 Nov 2019 06:18:38 +0100 Subject: [PATCH 09/15] Generate outputs in parallel again --- cli/run/build.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cli/run/build.ts b/cli/run/build.ts index a744bec485e..ec2e5abe104 100644 --- a/cli/run/build.ts +++ b/cli/run/build.ts @@ -66,10 +66,9 @@ export default function build( }); } - return outputOptions.reduce( - (prev, output) => prev.then(() => bundle.write(output) as Promise), - Promise.resolve() - ).then(() => bundle) + return Promise.all(outputOptions.map(output => bundle.write(output) as Promise)).then( + () => bundle + ); }) .then((bundle: RollupBuild | null) => { if (!silent) { From 3ec3d960a4fa190db9d85e1698451946745cd905 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Wed, 6 Nov 2019 06:19:36 +0100 Subject: [PATCH 10/15] Enable per-output plugins --- src/rollup/index.ts | 27 ++++++++++++------- .../samples/per-output-plugins/_config.js | 12 +++++++++ .../per-output-plugins/_expected/amd.js | 5 ++++ .../per-output-plugins/_expected/cjs.js | 3 +++ .../per-output-plugins/_expected/es.js | 1 + .../per-output-plugins/_expected/iife.js | 6 +++++ .../per-output-plugins/_expected/system.js | 10 +++++++ .../per-output-plugins/_expected/umd.js | 8 ++++++ test/form/samples/per-output-plugins/main.js | 1 + 9 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 test/form/samples/per-output-plugins/_config.js create mode 100644 test/form/samples/per-output-plugins/_expected/amd.js create mode 100644 test/form/samples/per-output-plugins/_expected/cjs.js create mode 100644 test/form/samples/per-output-plugins/_expected/es.js create mode 100644 test/form/samples/per-output-plugins/_expected/iife.js create mode 100644 test/form/samples/per-output-plugins/_expected/system.js create mode 100644 test/form/samples/per-output-plugins/_expected/umd.js create mode 100644 test/form/samples/per-output-plugins/main.js diff --git a/src/rollup/index.ts b/src/rollup/index.ts index b5cf6f90aa4..1330bfdf8b4 100644 --- a/src/rollup/index.ts +++ b/src/rollup/index.ts @@ -82,6 +82,17 @@ function ensureArray(items: (T | null | undefined)[] | T | null | undefined): return []; } +function normalizePlugins(rawPlugins: any): Plugin[] { + const plugins = ensureArray(rawPlugins); + for (let pluginIndex = 0; pluginIndex < plugins.length; pluginIndex++) { + const plugin = plugins[pluginIndex]; + if (!plugin.name) { + plugin.name = `${ANONYMOUS_PLUGIN_PREFIX}${pluginIndex + 1}`; + } + } + return plugins; +} + function getInputOptions(rawInputOptions: GenericConfigObject): InputOptions { if (!rawInputOptions) { throw new Error('You must supply an options object to rollup'); @@ -94,13 +105,7 @@ function getInputOptions(rawInputOptions: GenericConfigObject): InputOptions { (inputOptions.onwarn as WarningHandler)({ message: optionError, code: 'UNKNOWN_OPTION' }); inputOptions = ensureArray(inputOptions.plugins).reduce(applyOptionHook, inputOptions); - inputOptions.plugins = ensureArray(inputOptions.plugins); - for (let pluginIndex = 0; pluginIndex < inputOptions.plugins.length; pluginIndex++) { - const plugin = inputOptions.plugins[pluginIndex]; - if (!plugin.name) { - plugin.name = `${ANONYMOUS_PLUGIN_PREFIX}${pluginIndex + 1}`; - } - } + inputOptions.plugins = normalizePlugins(inputOptions.plugins); if (inputOptions.inlineDynamicImports) { if (inputOptions.preserveModules) @@ -307,7 +312,9 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom const result: RollupBuild = { cache: cache as RollupCache, generate: ((rawOutputOptions: GenericConfigObject) => { - const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver([]); + const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver( + normalizePlugins(rawOutputOptions.plugins) + ); const promise = generate( getOutputOptions(rawOutputOptions, outputPluginDriver), false, @@ -319,7 +326,9 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom }) as any, watchFiles: Object.keys(graph.watchFiles), write: ((rawOutputOptions: GenericConfigObject) => { - const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver([]); + const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver( + normalizePlugins(rawOutputOptions.plugins) + ); const outputOptions = getOutputOptions(rawOutputOptions, outputPluginDriver); if (!outputOptions.dir && !outputOptions.file) { error({ diff --git a/test/form/samples/per-output-plugins/_config.js b/test/form/samples/per-output-plugins/_config.js new file mode 100644 index 00000000000..a74b21e4306 --- /dev/null +++ b/test/form/samples/per-output-plugins/_config.js @@ -0,0 +1,12 @@ +module.exports = { + description: 'allows specifying per-output plugins', + options: { + output: { + plugins: { + renderChunk(code, chunkDescription, { format }) { + return code.replace(42, `'${format}'`); + } + } + } + } +}; diff --git a/test/form/samples/per-output-plugins/_expected/amd.js b/test/form/samples/per-output-plugins/_expected/amd.js new file mode 100644 index 00000000000..1d43a8d4670 --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/amd.js @@ -0,0 +1,5 @@ +define(function () { 'use strict'; + + console.log('amd'); + +}); diff --git a/test/form/samples/per-output-plugins/_expected/cjs.js b/test/form/samples/per-output-plugins/_expected/cjs.js new file mode 100644 index 00000000000..ff37dabca27 --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/cjs.js @@ -0,0 +1,3 @@ +'use strict'; + +console.log('cjs'); diff --git a/test/form/samples/per-output-plugins/_expected/es.js b/test/form/samples/per-output-plugins/_expected/es.js new file mode 100644 index 00000000000..cbe9810363d --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/es.js @@ -0,0 +1 @@ +console.log('es'); diff --git a/test/form/samples/per-output-plugins/_expected/iife.js b/test/form/samples/per-output-plugins/_expected/iife.js new file mode 100644 index 00000000000..b64ab69e189 --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/iife.js @@ -0,0 +1,6 @@ +(function () { + 'use strict'; + + console.log('iife'); + +}()); diff --git a/test/form/samples/per-output-plugins/_expected/system.js b/test/form/samples/per-output-plugins/_expected/system.js new file mode 100644 index 00000000000..9cd89c4c3a0 --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/system.js @@ -0,0 +1,10 @@ +System.register([], function () { + 'use strict'; + return { + execute: function () { + + console.log('system'); + + } + }; +}); diff --git a/test/form/samples/per-output-plugins/_expected/umd.js b/test/form/samples/per-output-plugins/_expected/umd.js new file mode 100644 index 00000000000..cece26c2f48 --- /dev/null +++ b/test/form/samples/per-output-plugins/_expected/umd.js @@ -0,0 +1,8 @@ +(function (factory) { + typeof define === 'function' && define.amd ? define(factory) : + factory(); +}((function () { 'use strict'; + + console.log('umd'); + +}))); diff --git a/test/form/samples/per-output-plugins/main.js b/test/form/samples/per-output-plugins/main.js new file mode 100644 index 00000000000..753a47d529e --- /dev/null +++ b/test/form/samples/per-output-plugins/main.js @@ -0,0 +1 @@ +console.log(42); From ddbefc8fd2c7be9cace0e6a2da5c9ae7147698af Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Wed, 6 Nov 2019 08:40:25 +0100 Subject: [PATCH 11/15] Warn if build-time hooks are used in an output plugin --- src/rollup/index.ts | 56 ++++++++------- src/utils/PluginCache.ts | 7 +- src/utils/PluginContext.ts | 12 +++- src/utils/PluginDriver.ts | 14 +++- src/utils/error.ts | 8 +++ src/utils/pluginUtils.ts | 1 + .../samples/per-output-plugins/_config.js | 16 +++-- .../per-output-plugins/_expected/amd.js | 2 +- .../per-output-plugins/_expected/cjs.js | 2 +- .../per-output-plugins/_expected/es.js | 2 +- .../per-output-plugins/_expected/iife.js | 2 +- .../per-output-plugins/_expected/system.js | 2 +- .../per-output-plugins/_expected/umd.js | 2 +- test/form/samples/per-output-plugins/main.js | 2 +- .../per-output-plugins-warn-hooks/_config.js | 72 +++++++++++++++++++ .../_expected/amd.js | 5 ++ .../_expected/cjs.js | 3 + .../_expected/es.js | 1 + .../_expected/iife.js | 6 ++ .../_expected/system.js | 10 +++ .../_expected/umd.js | 8 +++ .../per-output-plugins-warn-hooks/main.js | 1 + 22 files changed, 192 insertions(+), 42 deletions(-) create mode 100644 test/function/samples/per-output-plugins-warn-hooks/_config.js create mode 100644 test/function/samples/per-output-plugins-warn-hooks/_expected/amd.js create mode 100644 test/function/samples/per-output-plugins-warn-hooks/_expected/cjs.js create mode 100644 test/function/samples/per-output-plugins-warn-hooks/_expected/es.js create mode 100644 test/function/samples/per-output-plugins-warn-hooks/_expected/iife.js create mode 100644 test/function/samples/per-output-plugins-warn-hooks/_expected/system.js create mode 100644 test/function/samples/per-output-plugins-warn-hooks/_expected/umd.js create mode 100644 test/function/samples/per-output-plugins-warn-hooks/main.js diff --git a/src/rollup/index.ts b/src/rollup/index.ts index 1330bfdf8b4..dabe9b4bba8 100644 --- a/src/rollup/index.ts +++ b/src/rollup/index.ts @@ -11,7 +11,7 @@ import getExportMode from '../utils/getExportMode'; import mergeOptions, { GenericConfigObject } from '../utils/mergeOptions'; import { basename, dirname, isAbsolute, resolve } from '../utils/path'; import { PluginDriver } from '../utils/PluginDriver'; -import { ANONYMOUS_PLUGIN_PREFIX } from '../utils/pluginUtils'; +import { ANONYMOUS_OUTPUT_PLUGIN_PREFIX, ANONYMOUS_PLUGIN_PREFIX } from '../utils/pluginUtils'; import { SOURCEMAPPING_URL } from '../utils/sourceMappingURL'; import { getTimings, initialiseTimers, timeEnd, timeStart } from '../utils/timers'; import { @@ -82,12 +82,12 @@ function ensureArray(items: (T | null | undefined)[] | T | null | undefined): return []; } -function normalizePlugins(rawPlugins: any): Plugin[] { +function normalizePlugins(rawPlugins: any, anonymousPrefix: string): Plugin[] { const plugins = ensureArray(rawPlugins); for (let pluginIndex = 0; pluginIndex < plugins.length; pluginIndex++) { const plugin = plugins[pluginIndex]; if (!plugin.name) { - plugin.name = `${ANONYMOUS_PLUGIN_PREFIX}${pluginIndex + 1}`; + plugin.name = `${anonymousPrefix}${pluginIndex + 1}`; } } return plugins; @@ -105,7 +105,7 @@ function getInputOptions(rawInputOptions: GenericConfigObject): InputOptions { (inputOptions.onwarn as WarningHandler)({ message: optionError, code: 'UNKNOWN_OPTION' }); inputOptions = ensureArray(inputOptions.plugins).reduce(applyOptionHook, inputOptions); - inputOptions.plugins = normalizePlugins(inputOptions.plugins); + inputOptions.plugins = normalizePlugins(inputOptions.plugins, ANONYMOUS_PLUGIN_PREFIX); if (inputOptions.inlineDynamicImports) { if (inputOptions.preserveModules) @@ -220,16 +220,25 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom // ensure we only do one optimization pass per build let optimized = false; - function getOutputOptions( - rawOutputOptions: GenericConfigObject, - outputPluginDriver: PluginDriver - ) { - return normalizeOutputOptions( - inputOptions as GenericConfigObject, - rawOutputOptions, - chunks.length > 1, - outputPluginDriver + function getOutputOptionsAndPluginDriver( + rawOutputOptions: GenericConfigObject + ): { outputOptions: OutputOptions; outputPluginDriver: PluginDriver } { + if (!rawOutputOptions) { + throw new Error('You must supply an options object'); + } + const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver( + normalizePlugins(rawOutputOptions.plugins, ANONYMOUS_OUTPUT_PLUGIN_PREFIX) ); + + return { + outputOptions: normalizeOutputOptions( + inputOptions as GenericConfigObject, + rawOutputOptions, + chunks.length > 1, + outputPluginDriver + ), + outputPluginDriver + }; } async function generate( @@ -312,24 +321,21 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom const result: RollupBuild = { cache: cache as RollupCache, generate: ((rawOutputOptions: GenericConfigObject) => { - const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver( - normalizePlugins(rawOutputOptions.plugins) + const { outputOptions, outputPluginDriver } = getOutputOptionsAndPluginDriver( + rawOutputOptions + ); + const promise = generate(outputOptions, false, outputPluginDriver).then(result => + createOutput(result) ); - const promise = generate( - getOutputOptions(rawOutputOptions, outputPluginDriver), - false, - outputPluginDriver - ).then(result => createOutput(result)); Object.defineProperty(promise, 'code', throwAsyncGenerateError); Object.defineProperty(promise, 'map', throwAsyncGenerateError); return promise; }) as any, watchFiles: Object.keys(graph.watchFiles), write: ((rawOutputOptions: GenericConfigObject) => { - const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver( - normalizePlugins(rawOutputOptions.plugins) + const { outputOptions, outputPluginDriver } = getOutputOptionsAndPluginDriver( + rawOutputOptions ); - const outputOptions = getOutputOptions(rawOutputOptions, outputPluginDriver); if (!outputOptions.dir && !outputOptions.file) { error({ code: 'MISSING_OPTION', @@ -458,9 +464,6 @@ function normalizeOutputOptions( hasMultipleChunks: boolean, outputPluginDriver: PluginDriver ): OutputOptions { - if (!rawOutputOptions) { - throw new Error('You must supply an options object'); - } const mergedOptions = mergeOptions({ config: { output: { @@ -477,6 +480,7 @@ function normalizeOutputOptions( const mergedOutputOptions = mergedOptions.outputOptions[0]; const outputOptionsReducer = (outputOptions: OutputOptions, result: OutputOptions) => result || outputOptions; + // TODO Lukas add inputOptions to hook const outputOptions = outputPluginDriver.hookReduceArg0Sync( 'outputOptions', [mergedOutputOptions], diff --git a/src/utils/PluginCache.ts b/src/utils/PluginCache.ts index 7974947f3d9..02b631eab4f 100644 --- a/src/utils/PluginCache.ts +++ b/src/utils/PluginCache.ts @@ -1,6 +1,6 @@ import { PluginCache, SerializablePluginCache } from '../rollup/types'; import { error } from './error'; -import { ANONYMOUS_PLUGIN_PREFIX } from './pluginUtils'; +import { ANONYMOUS_OUTPUT_PLUGIN_PREFIX, ANONYMOUS_PLUGIN_PREFIX } from './pluginUtils'; export function createPluginCache(cache: SerializablePluginCache): PluginCache { return { @@ -64,7 +64,10 @@ export const NO_CACHE: PluginCache = { }; function uncacheablePluginError(pluginName: string) { - if (pluginName.startsWith(ANONYMOUS_PLUGIN_PREFIX)) + if ( + pluginName.startsWith(ANONYMOUS_PLUGIN_PREFIX) || + pluginName.startsWith(ANONYMOUS_OUTPUT_PLUGIN_PREFIX) + ) error({ code: 'ANONYMOUS_PLUGIN_CACHE', message: diff --git a/src/utils/PluginContext.ts b/src/utils/PluginContext.ts index 471966027da..62bbdf64191 100644 --- a/src/utils/PluginContext.ts +++ b/src/utils/PluginContext.ts @@ -15,7 +15,11 @@ import { BuildPhase } from './buildPhase'; import { errInvalidRollupPhaseForAddWatchFile } from './error'; import { FileEmitter } from './FileEmitter'; import { createPluginCache, getCacheForUncacheablePlugin, NO_CACHE } from './PluginCache'; -import { ANONYMOUS_PLUGIN_PREFIX, throwPluginError } from './pluginUtils'; +import { + ANONYMOUS_OUTPUT_PLUGIN_PREFIX, + ANONYMOUS_PLUGIN_PREFIX, + throwPluginError +} from './pluginUtils'; function getDeprecatedContextHandler( handler: H, @@ -51,7 +55,11 @@ export function getPluginContexts( return (plugin, pidx) => { let cacheable = true; if (typeof plugin.cacheKey !== 'string') { - if (plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) || existingPluginNames.has(plugin.name)) { + if ( + plugin.name.startsWith(ANONYMOUS_PLUGIN_PREFIX) || + plugin.name.startsWith(ANONYMOUS_OUTPUT_PLUGIN_PREFIX) || + existingPluginNames.has(plugin.name) + ) { cacheable = false; } else { existingPluginNames.add(plugin.name); diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index 62f7b1428db..38e9339cc0c 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -9,7 +9,7 @@ import { SerializablePluginCache } from '../rollup/types'; import { getRollupDefaultPlugin } from './defaultPlugin'; -import { error } from './error'; +import { errInputHookInOutputPlugin, error } from './error'; import { FileEmitter } from './FileEmitter'; import { getPluginContexts } from './PluginContext'; import { throwPluginError, warnDeprecatedHooks } from './pluginUtils'; @@ -35,6 +35,7 @@ export class PluginDriver { private pluginContexts: PluginContext[]; private plugins: Plugin[]; private preserveSymlinks: boolean; + private previousHooks = new Set(['options']); private watcher: RollupWatcher | undefined; constructor( @@ -61,6 +62,15 @@ export class PluginDriver { this.pluginContexts = this.plugins.map( getPluginContexts(pluginCache, graph, this.fileEmitter, watcher) ); + if (basePluginDriver) { + for (const plugin of userPlugins) { + for (const hook of basePluginDriver.previousHooks) { + if (hook in plugin) { + graph.warn(errInputHookInOutputPlugin(plugin.name, hook)); + } + } + } + } } public createOutputPluginDriver(plugins: Plugin[]): PluginDriver { @@ -222,6 +232,7 @@ export class PluginDriver { permitValues = false, hookContext?: ReplaceContext | null ): Promise { + this.previousHooks.add(hookName); const plugin = this.plugins[pluginIndex]; const hook = (plugin as any)[hookName]; if (!hook) return undefined as any; @@ -252,6 +263,7 @@ export class PluginDriver { permitValues = false, hookContext?: ReplaceContext ): T { + this.previousHooks.add(hookName); const plugin = this.plugins[pluginIndex]; let context = this.pluginContexts[pluginIndex]; const hook = (plugin as any)[hookName]; diff --git a/src/utils/error.ts b/src/utils/error.ts index aba6a6ee463..351436f487d 100644 --- a/src/utils/error.ts +++ b/src/utils/error.ts @@ -42,6 +42,7 @@ export enum Errors { DEPRECATED_FEATURE = 'DEPRECATED_FEATURE', FILE_NOT_FOUND = 'FILE_NOT_FOUND', FILE_NAME_CONFLICT = 'FILE_NAME_CONFLICT', + INPUT_HOOK_IN_OUTPUT_PLUGIN = 'INPUT_HOOK_IN_OUTPUT_PLUGIN', INVALID_CHUNK = 'INVALID_CHUNK', INVALID_EXTERNAL_ID = 'INVALID_EXTERNAL_ID', INVALID_OPTION = 'INVALID_OPTION', @@ -126,6 +127,13 @@ export function errFileNameConflict(fileName: string) { }; } +export function errInputHookInOutputPlugin(pluginName: string, hookName: string) { + return { + code: Errors.INPUT_HOOK_IN_OUTPUT_PLUGIN, + message: `The "${hookName}" hook used by the output plugin ${pluginName} is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.` + }; +} + export function errCannotAssignModuleToChunk( moduleId: string, assignToAlias: string, diff --git a/src/utils/pluginUtils.ts b/src/utils/pluginUtils.ts index 7542df2a376..e855212bf4b 100644 --- a/src/utils/pluginUtils.ts +++ b/src/utils/pluginUtils.ts @@ -3,6 +3,7 @@ import { Plugin, RollupError } from '../rollup/types'; import { error, Errors } from './error'; export const ANONYMOUS_PLUGIN_PREFIX = 'at position '; +export const ANONYMOUS_OUTPUT_PLUGIN_PREFIX = 'at output position '; export function throwPluginError( err: string | RollupError, diff --git a/test/form/samples/per-output-plugins/_config.js b/test/form/samples/per-output-plugins/_config.js index a74b21e4306..8045837e438 100644 --- a/test/form/samples/per-output-plugins/_config.js +++ b/test/form/samples/per-output-plugins/_config.js @@ -2,11 +2,19 @@ module.exports = { description: 'allows specifying per-output plugins', options: { output: { - plugins: { - renderChunk(code, chunkDescription, { format }) { - return code.replace(42, `'${format}'`); + plugins: [ + { + name: 'test-plugin', + renderChunk(code, chunkDescription, { format }) { + return code.replace(42, `'${format}'`); + } + }, + { + renderChunk(code, chunkDescription, { format }) { + return code.replace(43, `'!${format}!'`); + } } - } + ] } } }; diff --git a/test/form/samples/per-output-plugins/_expected/amd.js b/test/form/samples/per-output-plugins/_expected/amd.js index 1d43a8d4670..a12305aff0d 100644 --- a/test/form/samples/per-output-plugins/_expected/amd.js +++ b/test/form/samples/per-output-plugins/_expected/amd.js @@ -1,5 +1,5 @@ define(function () { 'use strict'; - console.log('amd'); + console.log('amd', '!amd!'); }); diff --git a/test/form/samples/per-output-plugins/_expected/cjs.js b/test/form/samples/per-output-plugins/_expected/cjs.js index ff37dabca27..9b162a6eb87 100644 --- a/test/form/samples/per-output-plugins/_expected/cjs.js +++ b/test/form/samples/per-output-plugins/_expected/cjs.js @@ -1,3 +1,3 @@ 'use strict'; -console.log('cjs'); +console.log('cjs', '!cjs!'); diff --git a/test/form/samples/per-output-plugins/_expected/es.js b/test/form/samples/per-output-plugins/_expected/es.js index cbe9810363d..dd41435976a 100644 --- a/test/form/samples/per-output-plugins/_expected/es.js +++ b/test/form/samples/per-output-plugins/_expected/es.js @@ -1 +1 @@ -console.log('es'); +console.log('es', '!es!'); diff --git a/test/form/samples/per-output-plugins/_expected/iife.js b/test/form/samples/per-output-plugins/_expected/iife.js index b64ab69e189..42c0f688f00 100644 --- a/test/form/samples/per-output-plugins/_expected/iife.js +++ b/test/form/samples/per-output-plugins/_expected/iife.js @@ -1,6 +1,6 @@ (function () { 'use strict'; - console.log('iife'); + console.log('iife', '!iife!'); }()); diff --git a/test/form/samples/per-output-plugins/_expected/system.js b/test/form/samples/per-output-plugins/_expected/system.js index 9cd89c4c3a0..600f1bdfbd1 100644 --- a/test/form/samples/per-output-plugins/_expected/system.js +++ b/test/form/samples/per-output-plugins/_expected/system.js @@ -3,7 +3,7 @@ System.register([], function () { return { execute: function () { - console.log('system'); + console.log('system', '!system!'); } }; diff --git a/test/form/samples/per-output-plugins/_expected/umd.js b/test/form/samples/per-output-plugins/_expected/umd.js index cece26c2f48..0fddc9f20f8 100644 --- a/test/form/samples/per-output-plugins/_expected/umd.js +++ b/test/form/samples/per-output-plugins/_expected/umd.js @@ -3,6 +3,6 @@ factory(); }((function () { 'use strict'; - console.log('umd'); + console.log('umd', '!umd!'); }))); diff --git a/test/form/samples/per-output-plugins/main.js b/test/form/samples/per-output-plugins/main.js index 753a47d529e..fdd64120dc6 100644 --- a/test/form/samples/per-output-plugins/main.js +++ b/test/form/samples/per-output-plugins/main.js @@ -1 +1 @@ -console.log(42); +console.log(42, 43); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_config.js b/test/function/samples/per-output-plugins-warn-hooks/_config.js new file mode 100644 index 00000000000..3583c674719 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_config.js @@ -0,0 +1,72 @@ +module.exports = { + description: 'warns when input hooks are used in output plugins', + options: { + output: { + plugins: [ + { + name: 'test-plugin', + options() {}, + buildStart() {}, + resolveId() {}, + load() {}, + transform() {}, + buildEnd() {}, + outputOptions() {}, + renderStart() {}, + banner() {}, + footer() {}, + intro() {}, + outro() {}, + resolveDynamicImport() {}, + resolveFileUrl() {}, + resolveImportMeta() {}, + augmentChunkHash() {}, + renderChunk() {}, + generateBundle() {}, + writeBundle() {}, + renderError() {} + }, + { + buildStart() {} + } + ] + } + }, + warnings: [ + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "options" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "buildStart" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "resolveId" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "load" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "transform" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "buildEnd" hook used by the output plugin test-plugin is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + }, + { + code: 'INPUT_HOOK_IN_OUTPUT_PLUGIN', + message: + 'The "buildStart" hook used by the output plugin at output position 2 is a build time hook and will not be run for that plugin. Either this plugin cannot be used as an output plugin, or it should have an option to configure it as an output plugin.' + } + ] +}; diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/amd.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/amd.js new file mode 100644 index 00000000000..1d43a8d4670 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/amd.js @@ -0,0 +1,5 @@ +define(function () { 'use strict'; + + console.log('amd'); + +}); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/cjs.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/cjs.js new file mode 100644 index 00000000000..ff37dabca27 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/cjs.js @@ -0,0 +1,3 @@ +'use strict'; + +console.log('cjs'); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/es.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/es.js new file mode 100644 index 00000000000..cbe9810363d --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/es.js @@ -0,0 +1 @@ +console.log('es'); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/iife.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/iife.js new file mode 100644 index 00000000000..b64ab69e189 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/iife.js @@ -0,0 +1,6 @@ +(function () { + 'use strict'; + + console.log('iife'); + +}()); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/system.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/system.js new file mode 100644 index 00000000000..9cd89c4c3a0 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/system.js @@ -0,0 +1,10 @@ +System.register([], function () { + 'use strict'; + return { + execute: function () { + + console.log('system'); + + } + }; +}); diff --git a/test/function/samples/per-output-plugins-warn-hooks/_expected/umd.js b/test/function/samples/per-output-plugins-warn-hooks/_expected/umd.js new file mode 100644 index 00000000000..cece26c2f48 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/_expected/umd.js @@ -0,0 +1,8 @@ +(function (factory) { + typeof define === 'function' && define.amd ? define(factory) : + factory(); +}((function () { 'use strict'; + + console.log('umd'); + +}))); diff --git a/test/function/samples/per-output-plugins-warn-hooks/main.js b/test/function/samples/per-output-plugins-warn-hooks/main.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/per-output-plugins-warn-hooks/main.js @@ -0,0 +1 @@ +export default 42; From 6379e3d257455c3be8fcc38444c082f9deca71c8 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Thu, 7 Nov 2019 07:31:37 +0100 Subject: [PATCH 12/15] Pass output and intput options to the renderStart hook --- src/rollup/index.ts | 3 +- src/rollup/types.d.ts | 48 ++++++++++++------- .../samples/options-in-renderstart/_config.js | 33 +++++++++++++ .../samples/options-in-renderstart/main.js | 1 + 4 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 test/function/samples/options-in-renderstart/_config.js create mode 100644 test/function/samples/options-in-renderstart/main.js diff --git a/src/rollup/index.ts b/src/rollup/index.ts index dabe9b4bba8..b4503c660b3 100644 --- a/src/rollup/index.ts +++ b/src/rollup/index.ts @@ -255,7 +255,7 @@ export default async function rollup(rawInputOptions: GenericConfigObject): Prom let outputBundle; try { - await outputPluginDriver.hookParallel('renderStart', []); + await outputPluginDriver.hookParallel('renderStart', [outputOptions, inputOptions]); const addons = await createAddons(outputOptions, outputPluginDriver); for (const chunk of chunks) { if (!inputOptions.preserveModules) chunk.generateInternalExports(outputOptions); @@ -480,7 +480,6 @@ function normalizeOutputOptions( const mergedOutputOptions = mergedOptions.outputOptions[0]; const outputOptionsReducer = (outputOptions: OutputOptions, result: OutputOptions) => result || outputOptions; - // TODO Lukas add inputOptions to hook const outputOptions = outputPluginDriver.hookReduceArg0Sync( 'outputOptions', [mergedOutputOptions], diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 32c3e5e237c..343491308f0 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -332,17 +332,14 @@ interface OnWriteOptions extends OutputOptions { bundle: RollupBuild; } -export interface PluginHooks { +interface OutputPluginHooks { augmentChunkHash: (this: PluginContext, chunk: PreRenderedChunk) => string | void; - buildEnd: (this: PluginContext, err?: Error) => Promise | void; - buildStart: (this: PluginContext, options: InputOptions) => Promise | void; generateBundle: ( this: PluginContext, options: OutputOptions, bundle: OutputBundle, isWrite: boolean ) => void | Promise; - load: LoadHook; /** @deprecated Use `generateBundle` instead */ ongenerate: ( this: PluginContext, @@ -355,33 +352,50 @@ export interface PluginHooks { options: OnWriteOptions, chunk: OutputChunk ) => void | Promise; - options: (this: MinimalPluginContext, options: InputOptions) => InputOptions | null | undefined; outputOptions: (this: PluginContext, options: OutputOptions) => OutputOptions | null | undefined; renderChunk: RenderChunkHook; renderError: (this: PluginContext, err?: Error) => Promise | void; - renderStart: (this: PluginContext) => Promise | void; + renderStart: ( + this: PluginContext, + outputOptions: OutputOptions, + inputOptions: InputOptions + ) => Promise | void; /** @deprecated Use `resolveFileUrl` instead */ resolveAssetUrl: ResolveAssetUrlHook; resolveDynamicImport: ResolveDynamicImportHook; resolveFileUrl: ResolveFileUrlHook; - resolveId: ResolveIdHook; - resolveImportMeta: ResolveImportMetaHook; - transform: TransformHook; /** @deprecated Use `renderChunk` instead */ transformBundle: TransformChunkHook; /** @deprecated Use `renderChunk` instead */ transformChunk: TransformChunkHook; - watchChange: (id: string) => void; writeBundle: (this: PluginContext, bundle: OutputBundle) => void | Promise; } -export interface Plugin extends Partial { - banner?: AddonHook; - cacheKey?: string; - footer?: AddonHook; - intro?: AddonHook; +export interface PluginHooks extends OutputPluginHooks { + buildEnd: (this: PluginContext, err?: Error) => Promise | void; + buildStart: (this: PluginContext, options: InputOptions) => Promise | void; + load: LoadHook; + options: (this: MinimalPluginContext, options: InputOptions) => InputOptions | null | undefined; + resolveId: ResolveIdHook; + resolveImportMeta: ResolveImportMetaHook; + transform: TransformHook; + watchChange: (id: string) => void; +} + +interface OutputPluginValueHooks { + banner: AddonHook; + cacheKey: string; + footer: AddonHook; + intro: AddonHook; + outro: AddonHook; +} + +export interface Plugin extends Partial, Partial { + name: string; +} + +export interface OutputPlugin extends Partial, Partial { name: string; - outro?: AddonHook; } export interface TreeshakingOptions { @@ -475,6 +489,7 @@ export interface OutputOptions { noConflict?: boolean; outro?: string | (() => string | Promise); paths?: OptionsPaths; + plugins?: OutputPlugin[]; preferConst?: boolean; sourcemap?: boolean | 'inline' | 'hidden'; sourcemapExcludeSources?: boolean; @@ -553,6 +568,7 @@ export interface RollupBuild { } export interface RollupOptions extends InputOptions { + // This is included for compatibility with config files but ignored by rollup.rollup output?: OutputOptions | OutputOptions[]; } diff --git a/test/function/samples/options-in-renderstart/_config.js b/test/function/samples/options-in-renderstart/_config.js new file mode 100644 index 00000000000..3129a8b36ea --- /dev/null +++ b/test/function/samples/options-in-renderstart/_config.js @@ -0,0 +1,33 @@ +const assert = require('assert'); +const checkedOptions = []; + +module.exports = { + description: 'makes input and output options available in renderStart', + options: { + context: 'global', + plugins: { + name: 'input-plugin', + renderStart(outputOptions, inputOptions) { + checkedOptions.push('input-plugin', outputOptions.format, inputOptions.context); + } + }, + output: { + plugins: { + name: 'output-plugin', + renderStart(outputOptions, inputOptions) { + checkedOptions.push('output-plugin', outputOptions.format, inputOptions.context); + } + } + } + }, + exports: () => { + assert.deepStrictEqual(checkedOptions, [ + 'output-plugin', + 'cjs', + 'global', + 'input-plugin', + 'cjs', + 'global' + ]); + } +}; diff --git a/test/function/samples/options-in-renderstart/main.js b/test/function/samples/options-in-renderstart/main.js new file mode 100644 index 00000000000..5dff5e2ec72 --- /dev/null +++ b/test/function/samples/options-in-renderstart/main.js @@ -0,0 +1 @@ +assert.ok(this); From 1b84fdaf2c9f72f1a5bceb381df85ef7e590ac21 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Fri, 8 Nov 2019 08:43:59 +0100 Subject: [PATCH 13/15] Make sure the CLI supports output plugins --- src/utils/mergeOptions.ts | 14 ++++++++------ .../_config.js | 4 ++++ .../_expected/main.js | 7 +++++++ .../_expected/minified.js | 1 + .../multiple-targets-different-plugins/main.js | 3 +++ .../rollup.config.js | 16 ++++++++++++++++ test/misc/optionList.js | 2 +- 7 files changed, 40 insertions(+), 7 deletions(-) create mode 100644 test/cli/samples/multiple-targets-different-plugins/_config.js create mode 100644 test/cli/samples/multiple-targets-different-plugins/_expected/main.js create mode 100644 test/cli/samples/multiple-targets-different-plugins/_expected/minified.js create mode 100644 test/cli/samples/multiple-targets-different-plugins/main.js create mode 100644 test/cli/samples/multiple-targets-different-plugins/rollup.config.js diff --git a/src/utils/mergeOptions.ts b/src/utils/mergeOptions.ts index 4d8b4a92d95..d6332a16f77 100644 --- a/src/utils/mergeOptions.ts +++ b/src/utils/mergeOptions.ts @@ -168,14 +168,15 @@ function addUnknownOptionErrors( optionType: string, ignoredKeys: RegExp = /$./ ) { - const unknownOptions = options.filter( - key => validOptions.indexOf(key) === -1 && !ignoredKeys.test(key) - ); + const validOptionSet = new Set(validOptions); + const unknownOptions = options.filter(key => !validOptionSet.has(key) && !ignoredKeys.test(key)); if (unknownOptions.length > 0) errors.push( - `Unknown ${optionType}: ${unknownOptions.join( - ', ' - )}. Allowed options: ${validOptions.sort().join(', ')}` + `Unknown ${optionType}: ${unknownOptions.join(', ')}. Allowed options: ${Array.from( + validOptionSet + ) + .sort() + .join(', ')}` ); } @@ -283,6 +284,7 @@ function getOutputOptions( noConflict: getOption('noConflict'), outro: getOption('outro'), paths: getOption('paths'), + plugins: config.plugins as any, preferConst: getOption('preferConst'), sourcemap: getOption('sourcemap'), sourcemapExcludeSources: getOption('sourcemapExcludeSources'), diff --git a/test/cli/samples/multiple-targets-different-plugins/_config.js b/test/cli/samples/multiple-targets-different-plugins/_config.js new file mode 100644 index 00000000000..e9ded7da1c6 --- /dev/null +++ b/test/cli/samples/multiple-targets-different-plugins/_config.js @@ -0,0 +1,4 @@ +module.exports = { + description: 'generates multiple output files, only one of which is minified', + command: 'rollup -c' +}; diff --git a/test/cli/samples/multiple-targets-different-plugins/_expected/main.js b/test/cli/samples/multiple-targets-different-plugins/_expected/main.js new file mode 100644 index 00000000000..7fb07e7ab0f --- /dev/null +++ b/test/cli/samples/multiple-targets-different-plugins/_expected/main.js @@ -0,0 +1,7 @@ +'use strict'; + +const Hello = 1; +console.log(Hello); +var main = 0; + +module.exports = main; diff --git a/test/cli/samples/multiple-targets-different-plugins/_expected/minified.js b/test/cli/samples/multiple-targets-different-plugins/_expected/minified.js new file mode 100644 index 00000000000..d5fc32bfdf8 --- /dev/null +++ b/test/cli/samples/multiple-targets-different-plugins/_expected/minified.js @@ -0,0 +1 @@ +"use strict";const Hello=1;console.log(1);var main=0;module.exports=main; diff --git a/test/cli/samples/multiple-targets-different-plugins/main.js b/test/cli/samples/multiple-targets-different-plugins/main.js new file mode 100644 index 00000000000..d9c8e76411d --- /dev/null +++ b/test/cli/samples/multiple-targets-different-plugins/main.js @@ -0,0 +1,3 @@ +const Hello = 1; +console.log(Hello); +export default 0; diff --git a/test/cli/samples/multiple-targets-different-plugins/rollup.config.js b/test/cli/samples/multiple-targets-different-plugins/rollup.config.js new file mode 100644 index 00000000000..a0d9108c282 --- /dev/null +++ b/test/cli/samples/multiple-targets-different-plugins/rollup.config.js @@ -0,0 +1,16 @@ +import { terser } from 'rollup-plugin-terser'; + +export default { + input: 'main.js', + output: [ + { + format: 'cjs', + file: '_actual/main.js' + }, + { + format: 'cjs', + file: '_actual/minified.js', + plugins: [terser()] + } + ] +}; diff --git a/test/misc/optionList.js b/test/misc/optionList.js index 4a28a584e70..17211adb8b5 100644 --- a/test/misc/optionList.js +++ b/test/misc/optionList.js @@ -1,3 +1,3 @@ exports.input = 'acorn, acornInjectPlugins, cache, chunkGroupingSize, context, experimentalCacheExpiry, experimentalOptimizeChunks, experimentalTopLevelAwait, external, inlineDynamicImports, input, manualChunks, moduleContext, onwarn, perf, plugins, preserveModules, preserveSymlinks, shimMissingExports, strictDeprecations, treeshake, watch'; exports.flags = 'acorn, acornInjectPlugins, amd, assetFileNames, banner, c, cache, chunkFileNames, chunkGroupingSize, compact, config, context, d, dir, dynamicImportFunction, e, entryFileNames, environment, esModule, experimentalCacheExpiry, experimentalOptimizeChunks, experimentalTopLevelAwait, exports, extend, external, externalLiveBindings, f, file, footer, format, freeze, g, globals, h, i, indent, inlineDynamicImports, input, interop, intro, m, manualChunks, moduleContext, n, name, namespaceToStringTag, noConflict, o, onwarn, outro, paths, perf, plugins, preferConst, preserveModules, preserveSymlinks, shimMissingExports, silent, sourcemap, sourcemapExcludeSources, sourcemapFile, strict, strictDeprecations, treeshake, v, w, watch'; -exports.output = 'amd, assetFileNames, banner, chunkFileNames, compact, dir, dynamicImportFunction, entryFileNames, esModule, exports, extend, externalLiveBindings, file, footer, format, freeze, globals, indent, interop, intro, name, namespaceToStringTag, noConflict, outro, paths, preferConst, sourcemap, sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, strict'; +exports.output = 'amd, assetFileNames, banner, chunkFileNames, compact, dir, dynamicImportFunction, entryFileNames, esModule, exports, extend, externalLiveBindings, file, footer, format, freeze, globals, indent, interop, intro, name, namespaceToStringTag, noConflict, outro, paths, plugins, preferConst, sourcemap, sourcemapExcludeSources, sourcemapFile, sourcemapPathTransform, strict'; From b0a05987c6acbf15946b4b5982392de279f5b657 Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 11 Nov 2019 08:43:25 +0100 Subject: [PATCH 14/15] Add documentation --- docs/01-command-line-reference.md | 1 + docs/02-javascript-api.md | 1 + docs/04-tutorial.md | 59 +++++++++++++++++++++-- docs/05-plugin-development.md | 77 ++++++++++++++++++++----------- docs/999-big-list-of-options.md | 37 ++++++++++++++- 5 files changed, 144 insertions(+), 31 deletions(-) diff --git a/docs/01-command-line-reference.md b/docs/01-command-line-reference.md index 8970e765fa4..e4e76c3687e 100755 --- a/docs/01-command-line-reference.md +++ b/docs/01-command-line-reference.md @@ -66,6 +66,7 @@ export default { // can be an array (for multiple inputs) format, // required globals, name, + plugins, // advanced output options assetFileNames, diff --git a/docs/02-javascript-api.md b/docs/02-javascript-api.md index a209163c0de..992802dbe16 100755 --- a/docs/02-javascript-api.md +++ b/docs/02-javascript-api.md @@ -116,6 +116,7 @@ const outputOptions = { format, // required globals, name, + plugins, // advanced output options assetFileNames, diff --git a/docs/04-tutorial.md b/docs/04-tutorial.md index 73b3e811300..6ed34605e60 100755 --- a/docs/04-tutorial.md +++ b/docs/04-tutorial.md @@ -227,17 +227,70 @@ Run Rollup with `npm run build`. The result should look like this: ```js 'use strict'; -const version = "1.0.0"; +var version = "1.0.0"; -const main = function () { +function main () { console.log('version ' + version); -}; +} module.exports = main; ``` _Note: Only the data we actually need gets imported – `name` and `devDependencies` and other parts of `package.json` are ignored. That's **tree-shaking** in action._ +### Using output plugins + +Some plugins can also be applied specifically to some outputs. See [plugin hooks](guide/en/#hooks) for the technical details of what output-specific plugins can do. In a nut-shell, those plugins can only modify code after the main analysis of Rollup has completed. Rollup will warn if an incompatible plugin is used as an output-specific plugin. One possible use-case is minification of bundles to be consumed in a browser. + +Let us extend the previous example to provide a minified build together with the non-minified one. To that end, we install `rollup-plugin-terser`: + +```console +npm install --save-dev rollup-plugin-terser +``` + +Edit your `rollup.config.js` file to add a second minified output. As format, we choose `iife`. This format wraps the code so that it can be consumed via a `script` tag in the browser while avoiding unwanted interactions with other code. As we have an export, we need to provide the name of a global variable that will be created by our bundle so that other code can access our export via this variable. + +```js +// rollup.config.js +import json from 'rollup-plugin-json'; +import {terser} from 'rollup-plugin-terser'; + +export default { + input: 'src/main.js', + output: [ + { + file: 'bundle.js', + format: 'cjs' + }, + { + file: 'bundle.min.js', + format: 'iife', + name: 'version', + plugins: [terser()] + } + ], + plugins: [ json() ] +}; +``` + +Besides `bundle.js`, Rollup will now create a second file `bundle.min.js`: + +```js +var version = (function () { + 'use strict'; + + var version = "1.0.0"; + + function main () { + console.log('version ' + version); + } + + return main; + +}()); +``` + + ### Code Splitting To use the code splitting feature, we got back to the original example and modify `src/main.js` to load `src/foo.js` dynamically instead of statically: diff --git a/docs/05-plugin-development.md b/docs/05-plugin-development.md index 974f6a45e41..ecb4d46eccd 100644 --- a/docs/05-plugin-development.md +++ b/docs/05-plugin-development.md @@ -72,9 +72,12 @@ In addition to properties defining the identity of your plugin, you may also spe * `sequential`: If this hook returns a promise, then other hooks of this kind will only be executed once this hook has resolved * `parallel`: If this hook returns a promise, then other hooks of this kind will not wait for this hook to be resolved +Furthermore, hooks can be run either during the `build` phase of the Rollup build, which is triggered by `rollup.rollup()`, or during the `generate` phase, which is triggered by `bundle.generate()` or `bundle.write()`. Plugins that only use `generate` phase hooks can also be passed in via the output options to `bundle.generate()` or `bundle.write()` and therefore run only for certain outputs. + #### `augmentChunkHash` Type: `(preRenderedChunk: PreRenderedChunk) => string`
-Kind: `sync, sequential` +Kind: `sync, sequential`
+Phase: `generate` Can be used to augment the hash of individual chunks. Called for each Rollup output chunk. Returning a falsy value will not modify the hash. @@ -91,31 +94,36 @@ augmentChunkHash(chunkInfo) { #### `banner` Type: `string | (() => string)`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `generate` Cf. [`output.banner/output.footer`](guide/en/#outputbanneroutputfooter). #### `buildEnd` Type: `(error?: Error) => void`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `build` Called when rollup has finished bundling, but before `generate` or `write` is called; you can also return a Promise. If an error occurred during the build, it is passed on to this hook. #### `buildStart` Type: `(options: InputOptions) => void`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `build` -Called on each `rollup.rollup` build. +Called on each `rollup.rollup` build. This is the recommended hook to use when you need access to the options passed to `rollup.rollup()` as it will take the transformations by all [`options`](guide/en/#options) hooks into account. #### `footer` Type: `string | (() => string)`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `generate` Cf. [`output.banner/output.footer`](guide/en/#outputbanneroutputfooter). #### `generateBundle` Type: `(options: OutputOptions, bundle: { [fileName: string]: AssetInfo | ChunkInfo }, isWrite: boolean) => void`
-Kind: `async, sequential` +Kind: `async, sequential`
+Phase: `generate` Called at the end of `bundle.generate()` or immediately before the files are written in `bundle.write()`. To modify the files after they have been written, use the [`writeBundle`](guide/en/#writebundle) hook. `bundle` provides the full list of files being written or generated along with their details: @@ -155,13 +163,15 @@ You can prevent files from being emitted by deleting them from the bundle object #### `intro` Type: `string | (() => string)`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `generate` Cf. [`output.intro/output.outro`](guide/en/#outputintrooutputoutro). #### `load` Type: `(id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null }`
-Kind: `async, first` +Kind: `async, first`
+Phase: `build` Defines a custom loader. Returning `null` defers to other `load` functions (and eventually the default behavior of loading from the file system). To prevent additional parsing overhead in case e.g. this hook already used `this.parse` to generate an AST for some reason, this hook can optionally return a `{ code, ast }` object. The `ast` must be a standard ESTree AST with `start` and `end` properties for each node. @@ -171,43 +181,52 @@ You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--m #### `options` Type: `(options: InputOptions) => InputOptions | null`
-Kind: `sync, sequential` +Kind: `sync, sequential`
+Phase: `build` + +Replaces or manipulates the options object passed to `rollup.rollup`. Returning `null` does not replace anything. If you just need to read the options, it is recommended to use the [`buildStart`](guide/en/#buildstart) hook as that hook has access to the options after the transformations from all `options` hooks have been taken into account. -Reads and replaces or manipulates the options object passed to `rollup.rollup`. Returning `null` does not replace anything. This is the only hook that does not have access to most [plugin context](guide/en/#plugin-context) utility functions as it is run before rollup is fully configured. +This is the only hook that does not have access to most [plugin context](guide/en/#plugin-context) utility functions as it is run before rollup is fully configured. #### `outputOptions` Type: `(outputOptions: OutputOptions) => OutputOptions | null`
-Kind: `sync, sequential` +Kind: `sync, sequential`
+Phase: `generate` -Reads and replaces or manipulates the output options object passed to `bundle.generate`. Returning `null` does not replace anything. +Replaces or manipulates the output options object passed to `bundle.generate()` or `bundle.write()`. Returning `null` does not replace anything. If you just need to read the output options, it is recommended to use the [`renderStart`](guide/en/#renderstart) hook as this hook has access to the output options after the transformations from all `outputOptions` hooks have been taken into account. #### `outro` Type: `string | (() => string)`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `generate` Cf. [`output.intro/output.outro`](guide/en/#outputintrooutputoutro). #### `renderChunk` Type: `(code: string, chunk: ChunkInfo, options: OutputOptions) => string | { code: string, map: SourceMap } | null`
-Kind: `async, sequential` +Kind: `async, sequential`
+Phase: `generate` Can be used to transform individual chunks. Called for each Rollup output chunk file. Returning `null` will apply no transformations. #### `renderError` Type: `(error: Error) => void`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `generate` Called when rollup encounters an error during `bundle.generate()` or `bundle.write()`. The error is passed to this hook. To get notified when generation completes successfully, use the `generateBundle` hook. #### `renderStart` -Type: `() => void`
-Kind: `async, parallel` +Type: `(outputOptions: OutputOptions, inputOptions: InputOptions) => void`
+Kind: `async, parallel`
+Phase: `generate` -Called initially each time `bundle.generate()` or `bundle.write()` is called. To get notified when generation has completed, use the `generateBundle` and `renderError` hooks. +Called initially each time `bundle.generate()` or `bundle.write()` is called. To get notified when generation has completed, use the `generateBundle` and `renderError` hooks. This is the recommended hook to use when you need access to the output options passed to `bundle.generate()` or `bundle.write()` as it will take the transformations by all [`outputOptions`](guide/en/#outputoptions) hooks into account. It also receives the input options passed to `rollup.rollup()` so that plugins that can be used as output plugins, i.e. plugins that only use `generate` phase hooks, can get access to them. #### `resolveDynamicImport` Type: `(specifier: string | ESTree.Node, importer: string) => string | false | null | {id: string, external?: boolean}`
-Kind: `async, first` +Kind: `async, first`
+Phase: `generate` Defines a custom resolver for dynamic imports. Returning `false` signals that the import should be kept as it is and not be passed to other resolvers thus making it external. Similar to the [`resolveId`](guide/en/#resolveid) hook, you can also return an object to resolve the import to a different id while marking it as external at the same time. @@ -222,7 +241,8 @@ Note that the return value of this hook will not be passed to `resolveId` afterw #### `resolveFileUrl` Type: `({chunkId: string, fileName: string, format: string, moduleId: string, referenceId: string, relativePath: string}) => string | null`
-Kind: `sync, first` +Kind: `sync, first`
+Phase: `generate` Allows to customize how Rollup resolves URLs of files that were emitted by plugins via `this.emitAsset` or `this.emitChunk`. By default, Rollup will generate code for `import.meta.ROLLUP_ASSET_URL_assetReferenceId` and `import.meta.ROLLUP_CHUNK_URL_chunkReferenceId` that should correctly generate absolute URLs of emitted files independent of the output format and the host system where the code is deployed. @@ -249,7 +269,8 @@ resolveFileUrl({fileName}) { #### `resolveId` Type: `(source: string, importer: string) => string | false | null | {id: string, external?: boolean, moduleSideEffects?: boolean | null}`
-Kind: `async, first` +Kind: `async, first`
+Phase: `build` Defines a custom resolver. A resolver can be useful for e.g. locating third-party dependencies. Returning `null` defers to other `resolveId` functions and eventually the default resolution behavior; returning `false` signals that `source` should be treated as an external module and not included in the bundle. If this happens for a relative import, the id will be renormalized the same way as when the `external` option is used. @@ -270,7 +291,8 @@ If `false` is returned for `moduleSideEffects` in the first hook that resolves a #### `resolveImportMeta` Type: `(property: string | null, {chunkId: string, moduleId: string, format: string}) => string | null`
-Kind: `sync, first` +Kind: `sync, first`
+Phase: `generate` Allows to customize how Rollup handles `import.meta` and `import.meta.someProperty`, in particular `import.meta.url`. In ES modules, `import.meta` is an object and `import.meta.url` contains the URL of the current module, e.g. `http://server.net/bundle.js` for browsers or `file:///path/to/bundle.js` in Node. @@ -292,7 +314,8 @@ Note that since this hook has access to the filename of the current chunk, its r #### `transform` Type: `(code: string, id: string) => string | null | { code: string, map?: string | SourceMap, ast? : ESTree.Program, moduleSideEffects?: boolean | null }`
-Kind: `async, sequential` +Kind: `async, sequential`
+Phase: `build` Can be used to transform individual modules. To prevent additional parsing overhead in case e.g. this hook already used `this.parse` to generate an AST for some reason, this hook can optionally return a `{ code, ast }` object. The `ast` must be a standard ESTree AST with `start` and `end` properties for each node. @@ -304,13 +327,15 @@ You can use [`this.getModuleInfo`](guide/en/#thisgetmoduleinfomoduleid-string--m #### `watchChange` Type: `(id: string) => void`
-Kind: `sync, sequential` +Kind: `sync, sequential`
+Phase: `build` (can also be triggered during `generate` but cannot be used by output plugins) Notifies a plugin whenever rollup has detected a change to a monitored file in `--watch` mode. #### `writeBundle` Type: `( bundle: { [fileName: string]: AssetInfo | ChunkInfo }) => void`
-Kind: `async, parallel` +Kind: `async, parallel`
+Phase: `generate` Called only at the end of `bundle.write()` once all files have been written. Similar to the [`generateBundle`](guide/en/#generatebundle) hook, `bundle` provides the full list of files being written along with their details. diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index 2838105e143..fbf70361497 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -226,6 +226,35 @@ this.a.b.c = ... */ ``` +#### output.plugins +Type: `OutputPlugin | (OutputPlugin | void)[]` + +Adds a plugin just to this output. See [Using output plugins](guide/en/#using-output-plugins) for more information on how to use output-specific plugins and [Plugins](guide/en/#plugin-development) on how to write your own. For plugins imported from packages, remember to call the imported plugin function (i.e. `commonjs()`, not just `commonjs`). Falsy plugins will be ignored, which can be used to easily activate or deactivate plugins. + +Not every plugin can be used here. `output.plugins` is limited to plugins that only use hooks that run during `bundle.generate()` or `bundle.write()`, i.e. after Rollup's main analysis is complete. If you are a plugin author, see [Plugin hooks](guide/en/#hooks) to find out which hooks can be used. + +The following will add minifaction to one of the outputs: + +```js +// rollup.config.js +import {terser} from 'rollup-plugin-terser'; + +export default { + input: 'main.js', + output: [ + { + file: 'bundle.js', + format: 'esm' + }, + { + file: 'bundle.min.js', + format: 'esm', + plugins: [terser()] + } + ] +}; +``` + #### plugins Type: `Plugin | (Plugin | void)[]` @@ -239,12 +268,16 @@ import commonjs from 'rollup-plugin-commonjs'; const isProduction = process.env.NODE_ENV === 'production'; export default (async () => ({ - entry: 'main.js', + input: 'main.js', plugins: [ resolve(), commonjs(), isProduction && (await import('rollup-plugin-terser')).terser() - ] + ], + output: { + file: 'bundle.js', + format: 'cjs' + } }))(); ``` From a3a23155ea838485c64226b6b8c2ff947127d03c Mon Sep 17 00:00:00 2001 From: Lukas Taegert-Atkinson Date: Mon, 11 Nov 2019 17:43:54 +0100 Subject: [PATCH 15/15] Improve coverage --- src/Chunk.ts | 13 +- src/utils/PluginCache.ts | 27 +- src/utils/PluginDriver.ts | 12 +- test/function/samples/file-and-dir/_config.js | 11 + test/function/samples/file-and-dir/foo.js | 1 + test/function/samples/file-and-dir/main.js | 1 + .../samples/iife-code-splitting/_config.js | 10 + .../samples/iife-code-splitting/foo.js | 1 + .../samples/iife-code-splitting/main.js | 1 + .../invalid-top-level-await/_config.js | 11 + .../samples/invalid-top-level-await/main.js | 1 + .../non-function-hook-async/_config.js | 15 + .../samples/non-function-hook-async/foo.js | 1 + .../samples/non-function-hook-async/main.js | 1 + .../samples/non-function-hook-sync/_config.js | 15 + .../samples/non-function-hook-sync/foo.js | 1 + .../samples/non-function-hook-sync/main.js | 1 + .../plugin-cache/anonymous-delete/_config.js | 18 + .../plugin-cache/anonymous-delete/main.js | 1 + .../plugin-cache/anonymous-get/_config.js | 18 + .../plugin-cache/anonymous-get/main.js | 1 + .../plugin-cache/anonymous-has/_config.js | 18 + .../plugin-cache/anonymous-has/main.js | 1 + .../plugin-cache/anonymous-set/_config.js | 18 + .../plugin-cache/anonymous-set/main.js | 1 + .../duplicate-names-no-cache/_config.js | 15 + .../duplicate-names-no-cache/main.js | 1 + .../plugin-cache/duplicate-names/_config.js | 27 ++ .../plugin-cache/duplicate-names/main.js | 1 + test/hooks/index.js | 430 +++++++++--------- 30 files changed, 430 insertions(+), 243 deletions(-) create mode 100644 test/function/samples/file-and-dir/_config.js create mode 100644 test/function/samples/file-and-dir/foo.js create mode 100644 test/function/samples/file-and-dir/main.js create mode 100644 test/function/samples/iife-code-splitting/_config.js create mode 100644 test/function/samples/iife-code-splitting/foo.js create mode 100644 test/function/samples/iife-code-splitting/main.js create mode 100644 test/function/samples/invalid-top-level-await/_config.js create mode 100644 test/function/samples/invalid-top-level-await/main.js create mode 100644 test/function/samples/non-function-hook-async/_config.js create mode 100644 test/function/samples/non-function-hook-async/foo.js create mode 100644 test/function/samples/non-function-hook-async/main.js create mode 100644 test/function/samples/non-function-hook-sync/_config.js create mode 100644 test/function/samples/non-function-hook-sync/foo.js create mode 100644 test/function/samples/non-function-hook-sync/main.js create mode 100644 test/function/samples/plugin-cache/anonymous-delete/_config.js create mode 100644 test/function/samples/plugin-cache/anonymous-delete/main.js create mode 100644 test/function/samples/plugin-cache/anonymous-get/_config.js create mode 100644 test/function/samples/plugin-cache/anonymous-get/main.js create mode 100644 test/function/samples/plugin-cache/anonymous-has/_config.js create mode 100644 test/function/samples/plugin-cache/anonymous-has/main.js create mode 100644 test/function/samples/plugin-cache/anonymous-set/_config.js create mode 100644 test/function/samples/plugin-cache/anonymous-set/main.js create mode 100644 test/function/samples/plugin-cache/duplicate-names-no-cache/_config.js create mode 100644 test/function/samples/plugin-cache/duplicate-names-no-cache/main.js create mode 100644 test/function/samples/plugin-cache/duplicate-names/_config.js create mode 100644 test/function/samples/plugin-cache/duplicate-names/main.js diff --git a/src/Chunk.ts b/src/Chunk.ts index 73b48b867d1..b6ead70e594 100644 --- a/src/Chunk.ts +++ b/src/Chunk.ts @@ -644,19 +644,8 @@ export default class Chunk { ) { timeStart('render format', 3); - if (!this.renderedSource) - throw new Error('Internal error: Chunk render called before preRender'); - const format = options.format as string; const finalise = finalisers[format]; - if (!finalise) { - error({ - code: 'INVALID_OPTION', - message: `Invalid format: ${format} - valid options are ${Object.keys(finalisers).join( - ', ' - )}.` - }); - } if (options.dynamicImportFunction && format !== 'es') { this.graph.warn({ code: 'INVALID_OPTION', @@ -709,7 +698,7 @@ export default class Chunk { } const magicString = finalise( - this.renderedSource, + this.renderedSource as MagicStringBundle, { accessedGlobals, dependencies: this.renderedDeclarations.dependencies, diff --git a/src/utils/PluginCache.ts b/src/utils/PluginCache.ts index 02b631eab4f..54feb3cbcf6 100644 --- a/src/utils/PluginCache.ts +++ b/src/utils/PluginCache.ts @@ -63,39 +63,36 @@ export const NO_CACHE: PluginCache = { } }; -function uncacheablePluginError(pluginName: string) { +function uncacheablePluginError(pluginName: string): never { if ( pluginName.startsWith(ANONYMOUS_PLUGIN_PREFIX) || pluginName.startsWith(ANONYMOUS_OUTPUT_PLUGIN_PREFIX) - ) - error({ + ) { + return error({ code: 'ANONYMOUS_PLUGIN_CACHE', message: 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.' }); - else - error({ - code: 'DUPLICATE_PLUGIN_NAME', - message: `The plugin name ${pluginName} is being used twice in the same build. Plugin names must be distinct or provide a cacheKey (please post an issue to the plugin if you are a plugin user).` - }); + } + return error({ + code: 'DUPLICATE_PLUGIN_NAME', + message: `The plugin name ${pluginName} is being used twice in the same build. Plugin names must be distinct or provide a cacheKey (please post an issue to the plugin if you are a plugin user).` + }); } export function getCacheForUncacheablePlugin(pluginName: string): PluginCache { return { has() { - uncacheablePluginError(pluginName); - return false; + return uncacheablePluginError(pluginName); }, get() { - uncacheablePluginError(pluginName); - return undefined as any; + return uncacheablePluginError(pluginName); }, set() { - uncacheablePluginError(pluginName); + return uncacheablePluginError(pluginName); }, delete() { - uncacheablePluginError(pluginName); - return false; + return uncacheablePluginError(pluginName); } }; } diff --git a/src/utils/PluginDriver.ts b/src/utils/PluginDriver.ts index 38e9339cc0c..fc080e7f3e8 100644 --- a/src/utils/PluginDriver.ts +++ b/src/utils/PluginDriver.ts @@ -109,7 +109,7 @@ export class PluginDriver { replaceContext?: ReplaceContext ): R { for (let i = 0; i < this.plugins.length; i++) { - const result = this.runHookSync(hookName, args, i, false, replaceContext); + const result = this.runHookSync(hookName, args, i, replaceContext); if (result != null) return result as any; } return null as any; @@ -158,7 +158,7 @@ export class PluginDriver { replaceContext?: ReplaceContext ): R { for (let i = 0; i < this.plugins.length; i++) { - const result: any = this.runHookSync(hookName, [arg0, ...args], i, false, replaceContext); + const result: any = this.runHookSync(hookName, [arg0, ...args], i, replaceContext); arg0 = reduce.call(this.pluginContexts[i], arg0, result, this.plugins[i]); } return arg0; @@ -195,7 +195,7 @@ export class PluginDriver { ): T { let acc = initialValue; for (let i = 0; i < this.plugins.length; i++) { - const result: any = this.runHookSync(hookName, args, i, true, replaceContext); + const result: any = this.runHookSync(hookName, args, i, replaceContext); acc = reduce.call(this.pluginContexts[i], acc, result, this.plugins[i]); } return acc; @@ -222,14 +222,14 @@ export class PluginDriver { replaceContext?: ReplaceContext ): void { for (let i = 0; i < this.plugins.length; i++) - this.runHookSync(hookName, args as any[], i, false, replaceContext); + this.runHookSync(hookName, args as any[], i, replaceContext); } private runHook( hookName: string, args: any[], pluginIndex: number, - permitValues = false, + permitValues: boolean, hookContext?: ReplaceContext | null ): Promise { this.previousHooks.add(hookName); @@ -260,7 +260,6 @@ export class PluginDriver { hookName: string, args: any[], pluginIndex: number, - permitValues = false, hookContext?: ReplaceContext ): T { this.previousHooks.add(hookName); @@ -275,7 +274,6 @@ export class PluginDriver { try { // permit values allows values to be returned instead of a functional hook if (typeof hook !== 'function') { - if (permitValues) return hook; error({ code: 'INVALID_PLUGIN_HOOK', message: `Error running plugin hook ${hookName} for ${plugin.name}, expected a function hook.` diff --git a/test/function/samples/file-and-dir/_config.js b/test/function/samples/file-and-dir/_config.js new file mode 100644 index 00000000000..5a777946e19 --- /dev/null +++ b/test/function/samples/file-and-dir/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'throws when using both the file and the dir option', + options: { + output: { file: 'bundle.js', dir: 'dist' } + }, + generateError: { + code: 'INVALID_OPTION', + message: + 'You must set either "output.file" for a single-file build or "output.dir" when generating multiple chunks.' + } +}; diff --git a/test/function/samples/file-and-dir/foo.js b/test/function/samples/file-and-dir/foo.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/file-and-dir/foo.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/function/samples/file-and-dir/main.js b/test/function/samples/file-and-dir/main.js new file mode 100644 index 00000000000..a25cfbd9058 --- /dev/null +++ b/test/function/samples/file-and-dir/main.js @@ -0,0 +1 @@ +export default () => import('./foo.js'); diff --git a/test/function/samples/iife-code-splitting/_config.js b/test/function/samples/iife-code-splitting/_config.js new file mode 100644 index 00000000000..ae4602851f7 --- /dev/null +++ b/test/function/samples/iife-code-splitting/_config.js @@ -0,0 +1,10 @@ +module.exports = { + description: 'throws when generating multiple chunks for an IIFE build', + options: { + output: { format: 'iife' } + }, + generateError: { + code: 'INVALID_OPTION', + message: 'UMD and IIFE output formats are not supported for code-splitting builds.' + } +}; diff --git a/test/function/samples/iife-code-splitting/foo.js b/test/function/samples/iife-code-splitting/foo.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/iife-code-splitting/foo.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/function/samples/iife-code-splitting/main.js b/test/function/samples/iife-code-splitting/main.js new file mode 100644 index 00000000000..a25cfbd9058 --- /dev/null +++ b/test/function/samples/iife-code-splitting/main.js @@ -0,0 +1 @@ +export default () => import('./foo.js'); diff --git a/test/function/samples/invalid-top-level-await/_config.js b/test/function/samples/invalid-top-level-await/_config.js new file mode 100644 index 00000000000..d0ffc48dbd9 --- /dev/null +++ b/test/function/samples/invalid-top-level-await/_config.js @@ -0,0 +1,11 @@ +module.exports = { + description: 'throws for invalid top-level-await format', + options: { + experimentalTopLevelAwait: true + }, + generateError: { + code: 'INVALID_TLA_FORMAT', + message: + 'Module format cjs does not support top-level await. Use the "es" or "system" output formats rather.' + } +}; diff --git a/test/function/samples/invalid-top-level-await/main.js b/test/function/samples/invalid-top-level-await/main.js new file mode 100644 index 00000000000..fb59627860e --- /dev/null +++ b/test/function/samples/invalid-top-level-await/main.js @@ -0,0 +1 @@ +await Promise.resolve(); diff --git a/test/function/samples/non-function-hook-async/_config.js b/test/function/samples/non-function-hook-async/_config.js new file mode 100644 index 00000000000..d17469d3926 --- /dev/null +++ b/test/function/samples/non-function-hook-async/_config.js @@ -0,0 +1,15 @@ +module.exports = { + description: 'throws when providing a value for an async function hook', + options: { + plugins: { + resolveId: 'value' + } + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'resolveId', + message: 'Error running plugin hook resolveId for at position 1, expected a function hook.', + plugin: 'at position 1', + pluginCode: 'INVALID_PLUGIN_HOOK' + } +}; diff --git a/test/function/samples/non-function-hook-async/foo.js b/test/function/samples/non-function-hook-async/foo.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/non-function-hook-async/foo.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/function/samples/non-function-hook-async/main.js b/test/function/samples/non-function-hook-async/main.js new file mode 100644 index 00000000000..a25cfbd9058 --- /dev/null +++ b/test/function/samples/non-function-hook-async/main.js @@ -0,0 +1 @@ +export default () => import('./foo.js'); diff --git a/test/function/samples/non-function-hook-sync/_config.js b/test/function/samples/non-function-hook-sync/_config.js new file mode 100644 index 00000000000..c447f84ec77 --- /dev/null +++ b/test/function/samples/non-function-hook-sync/_config.js @@ -0,0 +1,15 @@ +module.exports = { + description: 'throws when providing a value for a sync function hook', + options: { + plugins: { + outputOptions: 'value' + } + }, + generateError: { + code: 'PLUGIN_ERROR', + hook: 'outputOptions', + message: 'Error running plugin hook outputOptions for at position 1, expected a function hook.', + plugin: 'at position 1', + pluginCode: 'INVALID_PLUGIN_HOOK' + } +}; diff --git a/test/function/samples/non-function-hook-sync/foo.js b/test/function/samples/non-function-hook-sync/foo.js new file mode 100644 index 00000000000..7a4e8a723a4 --- /dev/null +++ b/test/function/samples/non-function-hook-sync/foo.js @@ -0,0 +1 @@ +export default 42; diff --git a/test/function/samples/non-function-hook-sync/main.js b/test/function/samples/non-function-hook-sync/main.js new file mode 100644 index 00000000000..a25cfbd9058 --- /dev/null +++ b/test/function/samples/non-function-hook-sync/main.js @@ -0,0 +1 @@ +export default () => import('./foo.js'); diff --git a/test/function/samples/plugin-cache/anonymous-delete/_config.js b/test/function/samples/plugin-cache/anonymous-delete/_config.js new file mode 100644 index 00000000000..8d9a9ef6e1a --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-delete/_config.js @@ -0,0 +1,18 @@ +module.exports = { + description: 'throws for anonymous plugins deleting from the cache', + options: { + plugins: { + buildStart() { + this.cache.delete('asdf'); + } + } + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'buildStart', + message: + 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.', + plugin: 'at position 1', + pluginCode: 'ANONYMOUS_PLUGIN_CACHE' + } +}; diff --git a/test/function/samples/plugin-cache/anonymous-delete/main.js b/test/function/samples/plugin-cache/anonymous-delete/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-delete/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/function/samples/plugin-cache/anonymous-get/_config.js b/test/function/samples/plugin-cache/anonymous-get/_config.js new file mode 100644 index 00000000000..9a25655aa12 --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-get/_config.js @@ -0,0 +1,18 @@ +module.exports = { + description: 'throws for anonymous plugins reading the cache', + options: { + plugins: { + buildStart() { + this.cache.get('asdf'); + } + } + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'buildStart', + message: + 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.', + plugin: 'at position 1', + pluginCode: 'ANONYMOUS_PLUGIN_CACHE' + } +}; diff --git a/test/function/samples/plugin-cache/anonymous-get/main.js b/test/function/samples/plugin-cache/anonymous-get/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-get/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/function/samples/plugin-cache/anonymous-has/_config.js b/test/function/samples/plugin-cache/anonymous-has/_config.js new file mode 100644 index 00000000000..f6d37f0485e --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-has/_config.js @@ -0,0 +1,18 @@ +module.exports = { + description: 'throws for anonymous plugins checking the cache', + options: { + plugins: { + buildStart() { + this.cache.has('asdf'); + } + } + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'buildStart', + message: + 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.', + plugin: 'at position 1', + pluginCode: 'ANONYMOUS_PLUGIN_CACHE' + } +}; diff --git a/test/function/samples/plugin-cache/anonymous-has/main.js b/test/function/samples/plugin-cache/anonymous-has/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-has/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/function/samples/plugin-cache/anonymous-set/_config.js b/test/function/samples/plugin-cache/anonymous-set/_config.js new file mode 100644 index 00000000000..c12e533586c --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-set/_config.js @@ -0,0 +1,18 @@ +module.exports = { + description: 'throws for anonymous plugins adding to the cache', + options: { + plugins: { + buildStart() { + this.cache.set('asdf', 'asdf'); + } + } + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'buildStart', + message: + 'A plugin is trying to use the Rollup cache but is not declaring a plugin name or cacheKey.', + plugin: 'at position 1', + pluginCode: 'ANONYMOUS_PLUGIN_CACHE' + } +}; diff --git a/test/function/samples/plugin-cache/anonymous-set/main.js b/test/function/samples/plugin-cache/anonymous-set/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/anonymous-set/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/function/samples/plugin-cache/duplicate-names-no-cache/_config.js b/test/function/samples/plugin-cache/duplicate-names-no-cache/_config.js new file mode 100644 index 00000000000..5acf2e8b233 --- /dev/null +++ b/test/function/samples/plugin-cache/duplicate-names-no-cache/_config.js @@ -0,0 +1,15 @@ +module.exports = { + description: 'allows plugins to have the same name if they do not access the cache', + options: { + plugins: [ + { + name: 'test-plugin', + buildStart() {} + }, + { + name: 'test-plugin', + buildStart() {} + } + ] + } +}; diff --git a/test/function/samples/plugin-cache/duplicate-names-no-cache/main.js b/test/function/samples/plugin-cache/duplicate-names-no-cache/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/duplicate-names-no-cache/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/function/samples/plugin-cache/duplicate-names/_config.js b/test/function/samples/plugin-cache/duplicate-names/_config.js new file mode 100644 index 00000000000..bf9f5a7af39 --- /dev/null +++ b/test/function/samples/plugin-cache/duplicate-names/_config.js @@ -0,0 +1,27 @@ +module.exports = { + description: 'throws if two plugins with the same name and no cache key access the cache', + options: { + plugins: [ + { + name: 'test-plugin', + buildStart() { + this.cache.set('asdf', 'asdf'); + } + }, + { + name: 'test-plugin', + buildStart() { + this.cache.set('asdf', 'asdf'); + } + } + ] + }, + error: { + code: 'PLUGIN_ERROR', + hook: 'buildStart', + message: + 'The plugin name test-plugin is being used twice in the same build. Plugin names must be distinct or provide a cacheKey (please post an issue to the plugin if you are a plugin user).', + plugin: 'test-plugin', + pluginCode: 'DUPLICATE_PLUGIN_NAME' + } +}; diff --git a/test/function/samples/plugin-cache/duplicate-names/main.js b/test/function/samples/plugin-cache/duplicate-names/main.js new file mode 100644 index 00000000000..65804ade90a --- /dev/null +++ b/test/function/samples/plugin-cache/duplicate-names/main.js @@ -0,0 +1 @@ +assert.equal( 1, 1 ); diff --git a/test/hooks/index.js b/test/hooks/index.js index 84555d91047..c8e083a3b2a 100644 --- a/test/hooks/index.js +++ b/test/hooks/index.js @@ -58,7 +58,7 @@ describe('hooks', () => { }) ) .then(({ output }) => { - assert.equal(output[0].code, `new banner\n'use strict';\n\nalert('hello');\n`); + assert.strictEqual(output[0].code, `new banner\n'use strict';\n\nalert('hello');\n`); })); it('allows to replace file with dir in the outputOptions hook', () => @@ -118,8 +118,8 @@ describe('hooks', () => { ] }) .then(bundle => { - assert.equal(buildStartCnt, 1); - assert.equal(buildEndCnt, 1); + assert.strictEqual(buildStartCnt, 1); + assert.strictEqual(buildEndCnt, 1); return rollup.rollup({ input: 'input', @@ -140,8 +140,8 @@ describe('hooks', () => { assert.ok(err); }) .then(() => { - assert.equal(buildStartCnt, 2); - assert.equal(buildEndCnt, 2); + assert.strictEqual(buildStartCnt, 2); + assert.strictEqual(buildEndCnt, 2); }); }); @@ -152,10 +152,10 @@ describe('hooks', () => { input: 'input', onwarn(warning) { if (callCnt === 0) { - assert.equal(warning.message, 'build start'); + assert.strictEqual(warning.message, 'build start'); callCnt++; } else if (callCnt === 1) { - assert.equal(warning.message, 'build end'); + assert.strictEqual(warning.message, 'build end'); callCnt++; } }, @@ -172,7 +172,7 @@ describe('hooks', () => { ] }) .then(() => { - assert.equal(callCnt, 2); + assert.strictEqual(callCnt, 2); }); }); @@ -188,7 +188,7 @@ describe('hooks', () => { this.error('build start error'); }, buildEnd(error) { - assert.equal(error.message, 'build start error'); + assert.strictEqual(error.message, 'build start error'); handledError = true; } } @@ -196,7 +196,7 @@ describe('hooks', () => { }) .catch(error => { assert.ok(handledError); - assert.equal(error.message, 'build start error'); + assert.strictEqual(error.message, 'build start error'); }) .then(() => { assert.ok(handledError); @@ -211,8 +211,8 @@ describe('hooks', () => { loader({ input: `alert('hello')` }), { buildStart() { - assert.equal(this.isExternal('test'), true); - assert.equal(this.isExternal('another'), false); + assert.strictEqual(this.isExternal('test'), true); + assert.strictEqual(this.isExternal('another'), false); } } ] @@ -239,7 +239,7 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output: [output] }) => { - assert.equal(output.code, `alert('hello');\n`); + assert.strictEqual(output.code, `alert('hello');\n`); })); it('caches chunk emission in transform hook', () => { @@ -266,12 +266,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); return rollup.rollup({ cache, @@ -291,12 +291,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); return rollup.rollup({ cache, @@ -313,12 +313,12 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); }); }); @@ -342,12 +342,12 @@ describe('hooks', () => { ]) ) .then(([{ output: output1 }, { output: output2 }]) => { - assert.equal(output1.length, 2, 'output1'); - assert.equal(output1[1].fileName, 'asset'); - assert.equal(output1[1].source, 'es'); - assert.equal(output2.length, 2, 'output2'); - assert.equal(output2[1].fileName, 'asset'); - assert.equal(output2[1].source, 'cjs'); + assert.strictEqual(output1.length, 2, 'output1'); + assert.strictEqual(output1[1].fileName, 'asset'); + assert.strictEqual(output1[1].source, 'es'); + assert.strictEqual(output2.length, 2, 'output2'); + assert.strictEqual(output2[1].fileName, 'asset'); + assert.strictEqual(output2[1].source, 'cjs'); }); }); @@ -374,12 +374,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -399,12 +399,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -421,12 +421,12 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); }); }); @@ -468,14 +468,14 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `console.log('imported');\n\n` + `var input = new URL('assets/test-09aeb845.ext', import.meta.url).href;\n\n` + `export default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-09aeb845.ext'); - assert.equal(output[1].source, 'first run'); + assert.strictEqual(output[1].fileName, 'assets/test-09aeb845.ext'); + assert.strictEqual(output[1].source, 'first run'); return rollup.rollup({ cache, @@ -501,14 +501,14 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `console.log('imported');\n\n` + `var input = new URL('assets/test-ce5fc71b.ext', import.meta.url).href;\n\n` + `export default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-ce5fc71b.ext'); - assert.equal(output[1].source, 'second run'); + assert.strictEqual(output[1].fileName, 'assets/test-ce5fc71b.ext'); + assert.strictEqual(output[1].source, 'second run'); }); }); @@ -540,14 +540,14 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -566,9 +566,9 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal(runs, 2); - assert.equal(output[0].code.trim(), `alert('hello world');`); - assert.equal(output.length, 1); + assert.strictEqual(runs, 2); + assert.strictEqual(output[0].code.trim(), `alert('hello world');`); + assert.strictEqual(output.length, 1); }); }); @@ -587,14 +587,14 @@ describe('hooks', () => { const chunk = outputBundle['input.js']; // can detect that b has been tree-shaken this way - assert.equal(chunk.modules['dep'].renderedExports[0], 'a'); - assert.equal(chunk.modules['dep'].renderedExports.length, 1); + assert.strictEqual(chunk.modules['dep'].renderedExports[0], 'a'); + assert.strictEqual(chunk.modules['dep'].renderedExports.length, 1); - assert.equal(chunk.modules['dep'].removedExports[0], 'b'); - assert.equal(chunk.modules['dep'].removedExports.length, 1); + assert.strictEqual(chunk.modules['dep'].removedExports[0], 'b'); + assert.strictEqual(chunk.modules['dep'].removedExports.length, 1); - assert.equal(chunk.modules['dep'].renderedLength, 10); - assert.equal(chunk.modules['dep'].originalLength, 35); + assert.strictEqual(chunk.modules['dep'].renderedLength, 10); + assert.strictEqual(chunk.modules['dep'].originalLength, 35); } } ] @@ -658,78 +658,15 @@ describe('hooks', () => { name: 'cachePlugin', buildStart() { assert.ok(this.cache.has('asdf')); - assert.equal(this.cache.get('asdf'), 'asdf'); + assert.strictEqual(this.cache.get('asdf'), 'asdf'); + assert.strictEqual(this.cache.delete('asdf'), true); + assert.ok(!this.cache.has('asdf')); } } ] }) )); - it('throws for anonymous plugins using the cache', () => - rollup - .rollup({ - input: 'input', - plugins: [ - loader({ input: `alert('hello')` }), - { - buildStart() { - this.cache.set('asdf', 'asdf'); - } - } - ] - }) - .then(() => { - assert.fail('Should have thrown'); - }) - .catch(err => { - assert.equal(err.code, 'PLUGIN_ERROR'); - assert.equal(err.pluginCode, 'ANONYMOUS_PLUGIN_CACHE'); - })); - - it('throws for two plugins using the same name and the cache', () => { - // we don't throw for duplicate names unless there is cache access - return rollup - .rollup({ - input: 'input', - plugins: [ - loader({ input: `alert('hello')` }), - { - name: 'a' - }, - { - name: 'a' - } - ] - }) - .then(() => { - const name = 'MyTestPluginName'; - return rollup - .rollup({ - input: 'input', - plugins: [ - loader({ input: `alert('hello')` }), - { - name, - buildStart() { - this.cache.set('asdf', 'asdf'); - } - }, - { - name, - buildStart() { - this.cache.set('asdf', 'asdf'); - } - } - ] - }) - .catch(err => { - assert.equal(err.code, 'PLUGIN_ERROR'); - assert.equal(err.pluginCode, 'DUPLICATE_PLUGIN_NAME'); - assert.equal(err.message.includes(name), true); - }); - }); - }); - it('Allows plugins with any names using a shared cacheKey', () => rollup.rollup({ input: 'input', @@ -746,14 +683,14 @@ describe('hooks', () => { name: 'a', cacheKey: 'a9b6', buildEnd() { - assert.equal(this.cache.get('asdf'), 'asdf'); + assert.strictEqual(this.cache.get('asdf'), 'asdf'); } }, { name: 'b', cacheKey: 'a9b6', buildEnd() { - assert.equal(this.cache.get('asdf'), 'asdf'); + assert.strictEqual(this.cache.get('asdf'), 'asdf'); } } ] @@ -788,7 +725,7 @@ describe('hooks', () => { { name: 'x', buildStart() { - if (i === 4) assert.equal(this.cache.has('second'), true); + if (i === 4) assert.strictEqual(this.cache.has('second'), true); } } ] @@ -806,9 +743,9 @@ describe('hooks', () => { { name: 'x', buildStart() { - assert.equal(this.cache.has('first'), false); - assert.equal(this.cache.get('first'), undefined); - assert.equal(this.cache.get('second'), 'second'); + assert.strictEqual(this.cache.has('first'), false); + assert.strictEqual(this.cache.get('first'), undefined); + assert.strictEqual(this.cache.get('second'), 'second'); } } ] @@ -826,14 +763,15 @@ describe('hooks', () => { name: 'x', buildStart() { this.cache.set('x', 'x'); - assert.equal(this.cache.has('x'), false); - assert.equal(this.cache.get('x'), undefined); + assert.ok(!this.cache.has('x')); + assert.strictEqual(this.cache.get('x'), undefined); + this.cache.delete('x'); } } ] }) .then(bundle => { - assert.equal(bundle.cache, undefined); + assert.strictEqual(bundle.cache, undefined); })); it('Disables the default transform cache when using cache in transform only', () => @@ -860,7 +798,10 @@ describe('hooks', () => { { name: 'x', transform() { - assert.equal(this.cache.get('asdf'), 'asdf'); + assert.ok(this.cache.has('asdf')); + assert.strictEqual(this.cache.get('asdf'), 'asdf'); + this.cache.delete('asdf'); + assert.ok(!this.cache.has('asdf')); return `alert('hello')`; } } @@ -873,7 +814,7 @@ describe('hooks', () => { }) ) .then(({ output }) => { - assert.equal(output[0].code.trim(), `alert('hello');`); + assert.strictEqual(output[0].code.trim(), `alert('hello');`); })); it('supports renderStart hook', () => { @@ -888,13 +829,13 @@ describe('hooks', () => { { renderStart() { renderStartCount++; - assert.equal(generateBundleCount, 0); - assert.equal(renderErrorCount, 0); + assert.strictEqual(generateBundleCount, 0); + assert.strictEqual(renderErrorCount, 0); }, generateBundle() { generateBundleCount++; - assert.equal(renderStartCount, 1); - assert.equal(renderErrorCount, 0); + assert.strictEqual(renderStartCount, 1); + assert.strictEqual(renderErrorCount, 0); }, renderError() { renderErrorCount++; @@ -904,9 +845,9 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'esm' })) .then(() => { - assert.equal(renderStartCount, 1, 'renderStart count'); - assert.equal(generateBundleCount, 1, 'generateBundle count'); - assert.equal(renderErrorCount, 0, 'renderError count'); + assert.strictEqual(renderStartCount, 1, 'renderStart count'); + assert.strictEqual(generateBundleCount, 1, 'generateBundle count'); + assert.strictEqual(renderErrorCount, 0, 'renderError count'); }); }); @@ -931,8 +872,8 @@ describe('hooks', () => { }, renderError(error) { assert(error); - assert.equal(error.message, 'renderChunk error'); - assert.equal(renderStartCount, 1); + assert.strictEqual(error.message, 'renderChunk error'); + assert.strictEqual(renderStartCount, 1); renderErrorCount++; } } @@ -943,24 +884,18 @@ describe('hooks', () => { assert.ok(err); }) .then(() => { - assert.equal(renderStartCount, 1, 'renderStart count'); - assert.equal(generateBundleCount, 0, 'generateBundle count'); - assert.equal(renderErrorCount, 1, 'renderError count'); + assert.strictEqual(renderStartCount, 1, 'renderStart count'); + assert.strictEqual(generateBundleCount, 0, 'generateBundle count'); + assert.strictEqual(renderErrorCount, 1, 'renderError count'); }); }); - it('Warns when using deprecated this.watcher in plugins', () => { - let warned = false; + it('Warns once when using deprecated this.watcher in plugins', () => { + const warnings = []; const watcher = rollup.watch({ input: 'input', onwarn(warning) { - warned = true; - assert.equal(warning.code, 'PLUGIN_WARNING'); - assert.equal(warning.pluginCode, 'PLUGIN_WATCHER_DEPRECATED'); - assert.equal( - warning.message, - 'this.watcher usage is deprecated in plugins. Use the watchChange plugin hook and this.addWatchFile() instead.' - ); + warnings.push(warning); }, output: { format: 'esm' @@ -971,18 +906,95 @@ describe('hooks', () => { name: 'x', buildStart() { this.watcher.on('change', () => {}); + this.watcher.on('change', () => {}); } } ] }); return new Promise((resolve, reject) => { - watcher.on('event', evt => { - if (evt.code === 'BUNDLE_END') resolve(); - else if (evt.code === 'ERROR' || evt.code === 'FATAL') reject(evt.error); + watcher.on('event', event => { + if (event.code === 'BUNDLE_END') resolve(); + else if (event.code === 'ERROR' || event.code === 'FATAL') reject(event.error); + }); + }).catch(err => { + assert.strictEqual( + err.message, + 'You must specify "output.file" or "output.dir" for the build.' + ); + assert.strictEqual(warnings.length, 1); + const warning = warnings[0]; + assert.strictEqual(warning.code, 'PLUGIN_WARNING'); + assert.strictEqual(warning.pluginCode, 'PLUGIN_WATCHER_DEPRECATED'); + assert.strictEqual( + warning.message, + 'this.watcher usage is deprecated in plugins. Use the watchChange plugin hook and this.addWatchFile() instead.' + ); + }); + }); + + it('Throws when not specifying "file" or "dir"', () => { + const watcher = rollup.watch({ + input: 'input', + output: { + format: 'esm' + }, + plugins: [loader({ input: `alert('hello')` })] + }); + return new Promise((resolve, reject) => { + watcher.on('event', event => { + if (event.code === 'BUNDLE_END') reject(new Error('Expected an error')); + else if (event.code === 'ERROR') reject(event.error); + }); + }).catch(err => { + assert.strictEqual( + err.message, + 'You must specify "output.file" or "output.dir" for the build.' + ); + }); + }); + + it('Throws when using the "file"" option for multiple chunks', () => { + const watcher = rollup.watch({ + input: 'input', + output: { + format: 'esm', + file: 'bundle.js' + }, + plugins: [loader({ input: `import('dep')`, dep: `console.log('dep')` })] + }); + return new Promise((resolve, reject) => { + watcher.on('event', event => { + if (event.code === 'BUNDLE_END') reject(new Error('Expected an error')); + else if (event.code === 'ERROR') reject(event.error); + }); + }).catch(err => { + assert.strictEqual( + err.message, + 'You must set "output.dir" instead of "output.file" when generating multiple chunks.' + ); + }); + }); + + it('Throws when using the "sourcemapFile" option for multiple chunks', () => { + const watcher = rollup.watch({ + input: 'input', + output: { + format: 'esm', + sourcemapFile: 'bundle.map', + dir: 'ignored' + }, + plugins: [loader({ input: `import('dep')`, dep: `console.log('dep')` })] + }); + return new Promise((resolve, reject) => { + watcher.on('event', event => { + if (event.code === 'BUNDLE_END') reject(new Error('Expected an error')); + else if (event.code === 'ERROR') reject(event.error); }); }).catch(err => { - assert.equal(err.message, 'You must specify "output.file" or "output.dir" for the build.'); - assert.equal(warned, true); + assert.strictEqual( + err.message, + '"output.sourcemapFile" is only supported for single-file builds.' + ); }); }); @@ -1069,7 +1081,7 @@ describe('hooks', () => { }) ) .then(output => { - assert.equal(augmentChunkHashCalls, 1); + assert.strictEqual(augmentChunkHashCalls, 1); }); }); @@ -1083,14 +1095,14 @@ describe('hooks', () => { onwarn(warning) { deprecationCnt++; if (deprecationCnt === 1) { - assert.equal(warning.code, 'DEPRECATED_FEATURE'); - assert.equal( + assert.strictEqual(warning.code, 'DEPRECATED_FEATURE'); + assert.strictEqual( warning.message, 'The "ongenerate" hook used by plugin at position 2 is deprecated. The "generateBundle" hook should be used instead.' ); } else { - assert.equal(warning.code, 'DEPRECATED_FEATURE'); - assert.equal( + assert.strictEqual(warning.code, 'DEPRECATED_FEATURE'); + assert.strictEqual( warning.message, 'The "onwrite" hook used by plugin at position 2 is deprecated. The "generateBundle/writeBundle" hook should be used instead.' ); @@ -1104,7 +1116,7 @@ describe('hooks', () => { }, onwrite(bundle, out) { - assert.equal(out.ongenerate, true); + assert.strictEqual(out.ongenerate, true); } } ] @@ -1116,7 +1128,7 @@ describe('hooks', () => { }) ) .then(() => { - assert.equal(deprecationCnt, 2); + assert.strictEqual(deprecationCnt, 2); return sander.rimraf(TEMP_DIR); }); }); @@ -1193,8 +1205,8 @@ describe('hooks', () => { input: 'input', onwarn(warning) { deprecationCnt++; - assert.equal(warning.code, 'DEPRECATED_FEATURE'); - assert.equal( + assert.strictEqual(warning.code, 'DEPRECATED_FEATURE'); + assert.strictEqual( warning.message, 'The "transformChunk" hook used by plugin at position 2 is deprecated. The "renderChunk" hook should be used instead.' ); @@ -1207,7 +1219,7 @@ describe('hooks', () => { try { this.emitAsset('test.ext', 'hello world'); } catch (e) { - assert.equal(e.code, 'ASSETS_ALREADY_FINALISED'); + assert.strictEqual(e.code, 'ASSETS_ALREADY_FINALISED'); } } } @@ -1220,8 +1232,8 @@ describe('hooks', () => { }) ) .then(() => { - assert.equal(deprecationCnt, 1); - assert.equal(calledHook, true); + assert.strictEqual(deprecationCnt, 1); + assert.strictEqual(calledHook, true); }); }); @@ -1237,8 +1249,8 @@ describe('hooks', () => { return `export default import.meta.ROLLUP_ASSET_URL_${assetId};`; }, generateBundle(options, outputBundle, isWrite) { - assert.equal(outputBundle['assets/test-0a676135.ext'].source, 'hello world'); - assert.equal( + assert.strictEqual(outputBundle['assets/test-0a676135.ext'].source, 'hello world'); + assert.strictEqual( outputBundle['input.js'].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); @@ -1269,12 +1281,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); return rollup.rollup({ cache, @@ -1294,12 +1306,12 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); return rollup.rollup({ cache, @@ -1316,12 +1328,12 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('chunk-928cb70b.js', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'chunk-928cb70b.js'); - assert.equal(output[1].code, `console.log('chunk');\n`); + assert.strictEqual(output[1].fileName, 'chunk-928cb70b.js'); + assert.strictEqual(output[1].code, `console.log('chunk');\n`); }); }); @@ -1345,14 +1357,14 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -1372,14 +1384,14 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -1396,14 +1408,14 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); }); }); @@ -1431,14 +1443,14 @@ describe('hooks', () => { return bundle.generate({ format: 'es' }); }) .then(({ output }) => { - assert.equal( + assert.strictEqual( output[0].code, `var input = new URL('assets/test-0a676135.ext', import.meta.url).href;\n\nexport default input;\n` ); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); - assert.equal(output[1].fileName, 'assets/test-0a676135.ext'); - assert.equal(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); + assert.strictEqual(output[1].fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output[1].source, 'hello world'); return rollup.rollup({ cache, @@ -1457,9 +1469,9 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output }) => { - assert.equal(runs, 2); - assert.equal(output[0].code.trim(), `alert('hello world');`); - assert.equal(output.length, 1); + assert.strictEqual(runs, 2); + assert.strictEqual(output[0].code.trim(), `alert('hello world');`); + assert.strictEqual(output.length, 1); }); }); @@ -1483,7 +1495,7 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output: [, output] }) => { - assert.equal(output.source, 'hello world'); + assert.strictEqual(output.source, 'hello world'); }); }); @@ -1507,8 +1519,8 @@ describe('hooks', () => { }) .then(bundle => bundle.generate({ format: 'es' })) .then(({ output: [, output] }) => { - assert.equal(output.fileName, 'assets/test-0a676135.ext'); - assert.equal(output.source, 'hello world'); + assert.strictEqual(output.fileName, 'assets/test-0a676135.ext'); + assert.strictEqual(output.source, 'hello world'); }); }); });