diff --git a/cli/run/commandPlugins.ts b/cli/run/commandPlugins.ts index c59d82e9070..2076dd590b7 100644 --- a/cli/run/commandPlugins.ts +++ b/cli/run/commandPlugins.ts @@ -23,7 +23,7 @@ export async function addPluginsFromCommandOption( inputOptions: InputOptionsWithPlugins ): Promise { if (commandPlugin) { - const plugins: any[] = normalizePluginOption(commandPlugin as any); + const plugins = await normalizePluginOption(commandPlugin); for (const plugin of plugins) { if (/[={}]/.test(plugin)) { // -p plugin=value diff --git a/cli/run/loadConfigFile.ts b/cli/run/loadConfigFile.ts index f134db0af2c..ac4940e8394 100644 --- a/cli/run/loadConfigFile.ts +++ b/cli/run/loadConfigFile.ts @@ -31,7 +31,7 @@ export async function loadConfigFile( try { const normalizedConfigs: MergedRollupOptions[] = []; for (const config of configs) { - const options = mergeOptions(config, commandOptions, warnings.add); + const options = await mergeOptions(config, commandOptions, warnings.add); await addCommandPluginsToInputOptions(options, commandOptions); normalizedConfigs.push(options); } diff --git a/cli/run/loadConfigFromCommand.ts b/cli/run/loadConfigFromCommand.ts index 73da0098cd2..6fd8298bf38 100644 --- a/cli/run/loadConfigFromCommand.ts +++ b/cli/run/loadConfigFromCommand.ts @@ -13,7 +13,7 @@ export default async function loadConfigFromCommand(command: Record): Promise { await loadConfigFromFileAndTrack(configFile); } else { const { options, warnings } = await loadConfigFromCommand(command); - start(options, warnings); + await start(options, warnings); } - function start(configs: MergedRollupOptions[], warnings: BatchWarnings): void { - try { - watcher = rollup.watch(configs as any); - } catch (error: any) { - return handleError(error); - } + async function start(configs: MergedRollupOptions[], warnings: BatchWarnings): Promise { + watcher = rollup.watch(configs as any); watcher.on('event', event => { switch (event.code) { diff --git a/docs/999-big-list-of-options.md b/docs/999-big-list-of-options.md index a3479ee4e8f..0eff6b40e3c 100755 --- a/docs/999-big-list-of-options.md +++ b/docs/999-big-list-of-options.md @@ -258,9 +258,9 @@ this.a.b.c = ... #### output.plugins -Type: `OutputPlugin | (OutputPlugin | OutputPlugin[] | void)[]` +Type: `MaybeArray>` -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. Nested plugins will be flatten. +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. Nested plugins will be flatten. Async plugin will be awaited and resolved. 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 [output generation hooks](guide/en/#output-generation-hooks) to find out which hooks can be used. @@ -288,9 +288,9 @@ export default { #### plugins -Type: `Plugin | (Plugin | Plugin[] | void)[]` +Type: `MaybeArray>` -See [Using plugins](guide/en/#using-plugins) for more information on how to use plugins and [Plugins](guide/en/#plugin-development) on how to write your own (try it out, it's not as difficult as it may sound and very much extends what you can do with Rollup). 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. Nested plugins will be flatten. +See [Using plugins](guide/en/#using-plugins) for more information on how to use plugins and [Plugins](guide/en/#plugin-development) on how to write your own (try it out, it's not as difficult as it may sound and very much extends what you can do with Rollup). 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. Nested plugins will be flatten. Async plugins will be awaited and resolved. ```js // rollup.config.js diff --git a/src/rollup/rollup.ts b/src/rollup/rollup.ts index de060205b10..914f5d72548 100644 --- a/src/rollup/rollup.ts +++ b/src/rollup/rollup.ts @@ -111,9 +111,9 @@ async function getInputOptions( } const rawPlugins = getSortedValidatedPlugins( 'options', - normalizePluginOption(rawInputOptions.plugins) + await normalizePluginOption(rawInputOptions.plugins) ); - const { options, unsetOptions } = normalizeInputOptions( + const { options, unsetOptions } = await normalizeInputOptions( await rawPlugins.reduce(applyOptionHook(watchMode), Promise.resolve(rawInputOptions)) ); normalizePlugins(options.plugins, ANONYMOUS_PLUGIN_PREFIX); @@ -138,7 +138,7 @@ function normalizePlugins(plugins: readonly Plugin[], anonymousPrefix: string): } } -function handleGenerateWrite( +async function handleGenerateWrite( isWrite: boolean, inputOptions: NormalizedInputOptions, unsetInputOptions: ReadonlySet, @@ -149,7 +149,7 @@ function handleGenerateWrite( options: outputOptions, outputPluginDriver, unsetOptions - } = getOutputOptionsAndPluginDriver( + } = await getOutputOptionsAndPluginDriver( rawOutputOptions, graph.pluginDriver, inputOptions, @@ -175,25 +175,30 @@ function handleGenerateWrite( }); } -function getOutputOptionsAndPluginDriver( +async function getOutputOptionsAndPluginDriver( rawOutputOptions: OutputOptions, inputPluginDriver: PluginDriver, inputOptions: NormalizedInputOptions, unsetInputOptions: ReadonlySet -): { +): Promise<{ options: NormalizedOutputOptions; outputPluginDriver: PluginDriver; unsetOptions: Set; -} { +}> { if (!rawOutputOptions) { throw new Error('You must supply an options object'); } - const rawPlugins = normalizePluginOption(rawOutputOptions.plugins); + const rawPlugins = await normalizePluginOption(rawOutputOptions.plugins); normalizePlugins(rawPlugins, ANONYMOUS_OUTPUT_PLUGIN_PREFIX); const outputPluginDriver = inputPluginDriver.createOutputPluginDriver(rawPlugins); return { - ...getOutputOptions(inputOptions, unsetInputOptions, rawOutputOptions, outputPluginDriver), + ...(await getOutputOptions( + inputOptions, + unsetInputOptions, + rawOutputOptions, + outputPluginDriver + )), outputPluginDriver }; } @@ -203,7 +208,7 @@ function getOutputOptions( unsetInputOptions: ReadonlySet, rawOutputOptions: OutputOptions, outputPluginDriver: PluginDriver -): { options: NormalizedOutputOptions; unsetOptions: Set } { +): Promise<{ options: NormalizedOutputOptions; unsetOptions: Set }> { return normalizeOutputOptions( outputPluginDriver.hookReduceArg0Sync( 'outputOptions', diff --git a/src/rollup/types.d.ts b/src/rollup/types.d.ts index 146c2db6e5a..0ebdc347db9 100644 --- a/src/rollup/types.d.ts +++ b/src/rollup/types.d.ts @@ -1,5 +1,9 @@ export const VERSION: string; +type FalsyValue = false | null | undefined; +type MaybeArray = T | T[]; +type MaybePromise = T | Promise; + export interface RollupError extends RollupLog { name?: string; stack?: string; @@ -495,7 +499,7 @@ export type SourcemapPathTransformOption = ( sourcemapPath: string ) => string; -export type InputPluginOption = Plugin | null | false | undefined | InputPluginOption[]; +export type InputPluginOption = MaybePromise; export interface InputOptions { acorn?: Record; @@ -619,7 +623,7 @@ export type NormalizedAmdOptions = ( type AddonFunction = (chunk: RenderedChunk) => string | Promise; -type OutputPluginOption = OutputPlugin | null | false | undefined | OutputPluginOption[]; +type OutputPluginOption = MaybePromise; export interface OutputOptions { amd?: AmdOptions; diff --git a/src/utils/asyncFlatten.ts b/src/utils/asyncFlatten.ts new file mode 100644 index 00000000000..06e58e3ed67 --- /dev/null +++ b/src/utils/asyncFlatten.ts @@ -0,0 +1,6 @@ +export async function asyncFlatten(array: T[]): Promise { + do { + array = (await Promise.all(array)).flat(Infinity) as any; + } while (array.some((v: any) => v?.then)); + return array; +} diff --git a/src/utils/options/mergeOptions.ts b/src/utils/options/mergeOptions.ts index 32ce1c3cd1c..5071374422d 100644 --- a/src/utils/options/mergeOptions.ts +++ b/src/utils/options/mergeOptions.ts @@ -1,10 +1,8 @@ import type { ExternalOption, InputOptions, - InputPluginOption, MergedRollupOptions, OutputOptions, - OutputPluginOption, RollupCache, RollupOptions, WarningHandler, @@ -41,21 +39,23 @@ export const commandAliases: { [key: string]: string } = { const EMPTY_COMMAND_OPTIONS = { external: [], globals: undefined }; -export function mergeOptions( +export async function mergeOptions( config: RollupOptions, rawCommandOptions: GenericConfigObject = EMPTY_COMMAND_OPTIONS, defaultOnWarnHandler: WarningHandler = defaultOnWarn -): MergedRollupOptions { +): Promise { const command = getCommandOptions(rawCommandOptions); - const inputOptions = mergeInputOptions(config, command, defaultOnWarnHandler); + const inputOptions = await mergeInputOptions(config, command, defaultOnWarnHandler); const warn = inputOptions.onwarn as WarningHandler; if (command.output) { Object.assign(command, command.output); } const outputOptionsArray = ensureArray(config.output); if (outputOptionsArray.length === 0) outputOptionsArray.push({}); - const outputOptions = outputOptionsArray.map(singleOutputOptions => - mergeOutputOptions(singleOutputOptions, command, warn) + const outputOptions = await Promise.all( + outputOptionsArray.map(singleOutputOptions => + mergeOutputOptions(singleOutputOptions, command, warn) + ) ); warnUnknownOptions( @@ -108,11 +108,11 @@ type CompleteInputOptions = { [K in U]: InputOptions[K]; }; -function mergeInputOptions( +async function mergeInputOptions( config: InputOptions, overrides: CommandConfigObject, defaultOnWarnHandler: WarningHandler -): InputOptions { +): Promise { const getOption = (name: keyof InputOptions): any => overrides[name] ?? config[name]; const inputOptions: CompleteInputOptions = { acorn: getOption('acorn'), @@ -133,7 +133,7 @@ function mergeInputOptions( moduleContext: getOption('moduleContext'), onwarn: getOnWarn(config, defaultOnWarnHandler), perf: getOption('perf'), - plugins: normalizePluginOption(config.plugins as InputPluginOption), + plugins: await normalizePluginOption(config.plugins), preserveEntrySignatures: getOption('preserveEntrySignatures'), preserveModules: getOption('preserveModules'), preserveSymlinks: getOption('preserveSymlinks'), @@ -218,11 +218,11 @@ type CompleteOutputOptions = { [K in U]: OutputOptions[K]; }; -function mergeOutputOptions( +async function mergeOutputOptions( config: OutputOptions, overrides: OutputOptions, warn: WarningHandler -): OutputOptions { +): Promise { const getOption = (name: keyof OutputOptions): any => overrides[name] ?? config[name]; const outputOptions: CompleteOutputOptions = { amd: getObjectOption(config, overrides, 'amd'), @@ -262,7 +262,7 @@ function mergeOutputOptions( noConflict: getOption('noConflict'), outro: getOption('outro'), paths: getOption('paths'), - plugins: normalizePluginOption(config.plugins as OutputPluginOption), + plugins: await normalizePluginOption(config.plugins), preferConst: getOption('preferConst'), preserveModules: getOption('preserveModules'), preserveModulesRoot: getOption('preserveModulesRoot'), diff --git a/src/utils/options/normalizeInputOptions.ts b/src/utils/options/normalizeInputOptions.ts index 114c5ea3cdf..d28675db19d 100644 --- a/src/utils/options/normalizeInputOptions.ts +++ b/src/utils/options/normalizeInputOptions.ts @@ -26,10 +26,10 @@ export interface CommandConfigObject { globals: { [id: string]: string } | undefined; } -export function normalizeInputOptions(config: InputOptions): { +export async function normalizeInputOptions(config: InputOptions): Promise<{ options: NormalizedInputOptions; unsetOptions: Set; -} { +}> { // These are options that may trigger special warnings or behaviour later // if the user did not select an explicit value const unsetOptions = new Set(); @@ -54,7 +54,7 @@ export function normalizeInputOptions(config: InputOptions): { moduleContext: getModuleContext(config, context), onwarn, perf: config.perf || false, - plugins: normalizePluginOption(config.plugins), + plugins: await normalizePluginOption(config.plugins), preserveEntrySignatures: config.preserveEntrySignatures ?? 'exports-only', preserveModules: getPreserveModules(config, onwarn, strictDeprecations), preserveSymlinks: config.preserveSymlinks || false, diff --git a/src/utils/options/normalizeOutputOptions.ts b/src/utils/options/normalizeOutputOptions.ts index c0904bd8cd9..3e0797e9908 100644 --- a/src/utils/options/normalizeOutputOptions.ts +++ b/src/utils/options/normalizeOutputOptions.ts @@ -22,11 +22,11 @@ import { warnUnknownOptions } from './options'; -export function normalizeOutputOptions( +export async function normalizeOutputOptions( config: OutputOptions, inputOptions: NormalizedInputOptions, unsetInputOptions: ReadonlySet -): { options: NormalizedOutputOptions; unsetOptions: Set } { +): Promise<{ options: NormalizedOutputOptions; unsetOptions: Set }> { // These are options that may trigger special warnings or behaviour later // if the user did not select an explicit value const unsetOptions = new Set(unsetInputOptions); @@ -72,7 +72,7 @@ export function normalizeOutputOptions( noConflict: config.noConflict || false, outro: getAddon(config, 'outro'), paths: config.paths || {}, - plugins: normalizePluginOption(config.plugins), + plugins: await normalizePluginOption(config.plugins), preferConst, preserveModules, preserveModulesRoot: getPreserveModulesRoot(config), diff --git a/src/utils/options/options.ts b/src/utils/options/options.ts index de2e986d28d..c85ef2dfd0c 100644 --- a/src/utils/options/options.ts +++ b/src/utils/options/options.ts @@ -10,6 +10,7 @@ import type { Plugin, WarningHandler } from '../../rollup/types'; +import { asyncFlatten } from '../asyncFlatten'; import { error, errorInvalidOption, errorUnknownOption } from '../error'; import { printQuotedStringList } from '../printStringList'; @@ -151,6 +152,7 @@ const getHashFromObjectOption = (optionName: string): string => optionName.split('.').join('').toLowerCase(); export const normalizePluginOption: { - (plugins: InputPluginOption): Plugin[]; - (plugins: OutputPluginOption): OutputPlugin[]; -} = (plugins: any) => [plugins].flat(Infinity).filter(Boolean); + (plugins: InputPluginOption): Promise; + (plugins: OutputPluginOption): Promise; + (plugins: unknown): Promise; +} = async (plugins: any) => (await asyncFlatten([plugins])).filter(Boolean); diff --git a/src/watch/watch-proxy.ts b/src/watch/watch-proxy.ts index eafb0102632..98d65616ac3 100644 --- a/src/watch/watch-proxy.ts +++ b/src/watch/watch-proxy.ts @@ -1,15 +1,25 @@ -import type { RollupWatcher } from '../rollup/types'; +import { handleError } from '../../cli/logging'; +import type { MaybeArray, RollupOptions, RollupWatcher } from '../rollup/types'; import { ensureArray } from '../utils/ensureArray'; import { error, errorInvalidOption } from '../utils/error'; -import type { GenericConfigObject } from '../utils/options/options'; +import { mergeOptions } from '../utils/options/mergeOptions'; import { WatchEmitter } from './WatchEmitter'; import { loadFsEvents } from './fsevents-importer'; -export default function watch(configs: GenericConfigObject[] | GenericConfigObject): RollupWatcher { +export default function watch(configs: RollupOptions[] | RollupOptions): RollupWatcher { const emitter = new WatchEmitter() as RollupWatcher; - const configArray = ensureArray(configs); - const watchConfigs = configArray.filter(config => config.watch !== false); - if (watchConfigs.length === 0) { + + watchInternal(configs, emitter).catch(error => { + handleError(error); + }); + + return emitter; +} + +async function watchInternal(configs: MaybeArray, emitter: RollupWatcher) { + const optionsList = await Promise.all(ensureArray(configs).map(config => mergeOptions(config))); + const watchOptionsList = optionsList.filter(config => config.watch !== false); + if (watchOptionsList.length === 0) { return error( errorInvalidOption( 'watch', @@ -18,8 +28,7 @@ export default function watch(configs: GenericConfigObject[] | GenericConfigObje ) ); } - loadFsEvents() - .then(() => import('./watch')) - .then(({ Watcher }) => new Watcher(watchConfigs, emitter)); - return emitter; + await loadFsEvents(); + const { Watcher } = await import('./watch'); + new Watcher(watchOptionsList, emitter); } diff --git a/src/watch/watch.ts b/src/watch/watch.ts index efd868a418a..5179082f8e1 100644 --- a/src/watch/watch.ts +++ b/src/watch/watch.ts @@ -11,8 +11,6 @@ import type { RollupWatcher, WatcherOptions } from '../rollup/types'; -import { mergeOptions } from '../utils/options/mergeOptions'; -import type { GenericConfigObject } from '../utils/options/options'; import { FileWatcher } from './fileWatcher'; const eventsRewrites: Record> = { @@ -44,13 +42,13 @@ export class Watcher { private running = true; private readonly tasks: Task[]; - constructor(configs: readonly GenericConfigObject[], emitter: RollupWatcher) { + constructor(optionsList: readonly MergedRollupOptions[], emitter: RollupWatcher) { this.emitter = emitter; emitter.close = this.close.bind(this); - this.tasks = configs.map(config => new Task(this, config)); - for (const { watch } of configs) { - if (watch && typeof (watch as WatcherOptions).buildDelay === 'number') { - this.buildDelay = Math.max(this.buildDelay, (watch as WatcherOptions).buildDelay!); + this.tasks = optionsList.map(options => new Task(this, options)); + for (const { watch } of optionsList) { + if (watch && typeof watch.buildDelay === 'number') { + this.buildDelay = Math.max(this.buildDelay, watch.buildDelay!); } } process.nextTick(() => this.run()); @@ -148,11 +146,11 @@ export class Task { private watched = new Set(); private readonly watcher: Watcher; - constructor(watcher: Watcher, config: GenericConfigObject) { + constructor(watcher: Watcher, options: MergedRollupOptions) { this.watcher = watcher; + this.options = options; - this.skipWrite = Boolean(config.watch && (config.watch as GenericConfigObject).skipWrite); - this.options = mergeOptions(config); + this.skipWrite = Boolean(options.watch && options.watch.skipWrite); this.outputs = this.options.output; this.outputFiles = this.outputs.map(output => { if (output.file || output.dir) return resolve(output.file || output.dir!); diff --git a/test/function/samples/nested-plugin/_config.js b/test/function/samples/nested-and-async-plugin/_config.js similarity index 72% rename from test/function/samples/nested-plugin/_config.js rename to test/function/samples/nested-and-async-plugin/_config.js index 60be2ce1083..414bab3cf72 100644 --- a/test/function/samples/nested-plugin/_config.js +++ b/test/function/samples/nested-and-async-plugin/_config.js @@ -8,17 +8,17 @@ const pluginA = { } }; -const pluginB = { - name: 'nested-plugin-2', +const pluginB = Promise.resolve({ + name: 'async-plugin-2', transform(code) { return code.replace('answer = 41', 'answer = 42'); } -}; +}); module.exports = { description: 'works when nested plugin', options: { // eslint-disable-next-line no-sparse-arrays - plugins: [[pluginA], [undefined, [null]], ,] + plugins: [[Promise.resolve(pluginA)], [undefined, Promise.resolve([null])], ,] } }; diff --git a/test/function/samples/nested-plugin/main.js b/test/function/samples/nested-and-async-plugin/main.js similarity index 100% rename from test/function/samples/nested-plugin/main.js rename to test/function/samples/nested-and-async-plugin/main.js