Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support async plugins #4671

Merged
merged 12 commits into from Oct 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/run/commandPlugins.ts
Expand Up @@ -23,7 +23,7 @@ export async function addPluginsFromCommandOption(
inputOptions: InputOptionsWithPlugins
): Promise<void> {
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
Expand Down
2 changes: 1 addition & 1 deletion cli/run/loadConfigFile.ts
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion cli/run/loadConfigFromCommand.ts
Expand Up @@ -13,7 +13,7 @@ export default async function loadConfigFromCommand(command: Record<string, unkn
if (!command.input && (command.stdin || !process.stdin.isTTY)) {
command.input = stdinName;
}
const options = mergeOptions({ input: [] }, command, warnings.add);
const options = await mergeOptions({ input: [] }, command, warnings.add);
await addCommandPluginsToInputOptions(options, command);
return { options: [options], warnings };
}
10 changes: 3 additions & 7 deletions cli/run/watch-cli.ts
Expand Up @@ -71,15 +71,11 @@ export async function watch(command: Record<string, any>): Promise<void> {
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<void> {
watcher = rollup.watch(configs as any);

watcher.on('event', event => {
switch (event.code) {
Expand Down
8 changes: 4 additions & 4 deletions docs/999-big-list-of-options.md
Expand Up @@ -258,9 +258,9 @@ this.a.b.c = ...

#### output.plugins

Type: `OutputPlugin | (OutputPlugin | OutputPlugin[] | void)[]`
Type: `MaybeArray<MaybePromise<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. 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.

Expand Down Expand Up @@ -288,9 +288,9 @@ export default {

#### plugins

Type: `Plugin | (Plugin | Plugin[] | void)[]`
Type: `MaybeArray<MaybePromise<Plugin | void>>`

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
Expand Down
25 changes: 15 additions & 10 deletions src/rollup/rollup.ts
Expand Up @@ -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);
Expand All @@ -138,7 +138,7 @@ function normalizePlugins(plugins: readonly Plugin[], anonymousPrefix: string):
}
}

function handleGenerateWrite(
async function handleGenerateWrite(
isWrite: boolean,
inputOptions: NormalizedInputOptions,
unsetInputOptions: ReadonlySet<string>,
Expand All @@ -149,7 +149,7 @@ function handleGenerateWrite(
options: outputOptions,
outputPluginDriver,
unsetOptions
} = getOutputOptionsAndPluginDriver(
} = await getOutputOptionsAndPluginDriver(
rawOutputOptions,
graph.pluginDriver,
inputOptions,
Expand All @@ -175,25 +175,30 @@ function handleGenerateWrite(
});
}

function getOutputOptionsAndPluginDriver(
async function getOutputOptionsAndPluginDriver(
rawOutputOptions: OutputOptions,
inputPluginDriver: PluginDriver,
inputOptions: NormalizedInputOptions,
unsetInputOptions: ReadonlySet<string>
): {
): Promise<{
options: NormalizedOutputOptions;
outputPluginDriver: PluginDriver;
unsetOptions: Set<string>;
} {
}> {
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
};
}
Expand All @@ -203,7 +208,7 @@ function getOutputOptions(
unsetInputOptions: ReadonlySet<string>,
rawOutputOptions: OutputOptions,
outputPluginDriver: PluginDriver
): { options: NormalizedOutputOptions; unsetOptions: Set<string> } {
): Promise<{ options: NormalizedOutputOptions; unsetOptions: Set<string> }> {
return normalizeOutputOptions(
outputPluginDriver.hookReduceArg0Sync(
'outputOptions',
Expand Down
8 changes: 6 additions & 2 deletions src/rollup/types.d.ts
@@ -1,5 +1,9 @@
export const VERSION: string;

type FalsyValue = false | null | undefined;
type MaybeArray<T> = T | T[];
type MaybePromise<T> = T | Promise<T>;

export interface RollupError extends RollupLog {
name?: string;
stack?: string;
Expand Down Expand Up @@ -495,7 +499,7 @@ export type SourcemapPathTransformOption = (
sourcemapPath: string
) => string;

export type InputPluginOption = Plugin | null | false | undefined | InputPluginOption[];
export type InputPluginOption = MaybePromise<Plugin | FalsyValue | InputPluginOption[]>;

export interface InputOptions {
acorn?: Record<string, unknown>;
Expand Down Expand Up @@ -619,7 +623,7 @@ export type NormalizedAmdOptions = (

type AddonFunction = (chunk: RenderedChunk) => string | Promise<string>;

type OutputPluginOption = OutputPlugin | null | false | undefined | OutputPluginOption[];
type OutputPluginOption = MaybePromise<OutputPlugin | FalsyValue | OutputPluginOption[]>;

export interface OutputOptions {
amd?: AmdOptions;
Expand Down
6 changes: 6 additions & 0 deletions src/utils/asyncFlatten.ts
@@ -0,0 +1,6 @@
export async function asyncFlatten<T>(array: T[]): Promise<T[]> {
do {
array = (await Promise.all(array)).flat(Infinity) as any;
} while (array.some((v: any) => v?.then));
return array;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder it would not make sense to integrate the filter(Boolean) here as well so you do not need the optional chaining in v?.then? But then maybe this should just become normalizePluginOption?

Copy link
Contributor Author

@sxzz sxzz Oct 14, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is copied from Vite.

I think we just keep the function simple and keep the falsy value (the fn is not referenced by now, but in the future, it could be used in other places). filter(Boolean) is done in normalizePluginOption.

26 changes: 13 additions & 13 deletions src/utils/options/mergeOptions.ts
@@ -1,10 +1,8 @@
import type {
ExternalOption,
InputOptions,
InputPluginOption,
MergedRollupOptions,
OutputOptions,
OutputPluginOption,
RollupCache,
RollupOptions,
WarningHandler,
Expand Down Expand Up @@ -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<MergedRollupOptions> {
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(
Expand Down Expand Up @@ -108,11 +108,11 @@ type CompleteInputOptions<U extends keyof InputOptions> = {
[K in U]: InputOptions[K];
};

function mergeInputOptions(
async function mergeInputOptions(
config: InputOptions,
overrides: CommandConfigObject,
defaultOnWarnHandler: WarningHandler
): InputOptions {
): Promise<InputOptions> {
const getOption = (name: keyof InputOptions): any => overrides[name] ?? config[name];
const inputOptions: CompleteInputOptions<keyof InputOptions> = {
acorn: getOption('acorn'),
Expand All @@ -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'),
Expand Down Expand Up @@ -218,11 +218,11 @@ type CompleteOutputOptions<U extends keyof OutputOptions> = {
[K in U]: OutputOptions[K];
};

function mergeOutputOptions(
async function mergeOutputOptions(
config: OutputOptions,
overrides: OutputOptions,
warn: WarningHandler
): OutputOptions {
): Promise<OutputOptions> {
const getOption = (name: keyof OutputOptions): any => overrides[name] ?? config[name];
const outputOptions: CompleteOutputOptions<keyof OutputOptions> = {
amd: getObjectOption(config, overrides, 'amd'),
Expand Down Expand Up @@ -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'),
Expand Down
6 changes: 3 additions & 3 deletions src/utils/options/normalizeInputOptions.ts
Expand Up @@ -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<string>;
} {
}> {
// These are options that may trigger special warnings or behaviour later
// if the user did not select an explicit value
const unsetOptions = new Set<string>();
Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/utils/options/normalizeOutputOptions.ts
Expand Up @@ -22,11 +22,11 @@ import {
warnUnknownOptions
} from './options';

export function normalizeOutputOptions(
export async function normalizeOutputOptions(
config: OutputOptions,
inputOptions: NormalizedInputOptions,
unsetInputOptions: ReadonlySet<string>
): { options: NormalizedOutputOptions; unsetOptions: Set<string> } {
): Promise<{ options: NormalizedOutputOptions; unsetOptions: Set<string> }> {
// 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);
Expand Down Expand Up @@ -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),
Expand Down
8 changes: 5 additions & 3 deletions src/utils/options/options.ts
Expand Up @@ -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';

Expand Down Expand Up @@ -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<Plugin[]>;
(plugins: OutputPluginOption): Promise<OutputPlugin[]>;
(plugins: unknown): Promise<any[]>;
} = async (plugins: any) => (await asyncFlatten([plugins])).filter(Boolean);
29 changes: 19 additions & 10 deletions 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<RollupOptions>, 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',
Expand All @@ -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);
}