Конфигурационный файл Rollup не ограничивается обычным объектом. Вместо статической структуры допускается использование:
Promise;Благодаря этому конфигурация превращается в полноценный программный модуль, способный:
Асинхронная конфигурация особенно полезна при сложной инфраструктуре сборки.
Rollup умеет ожидать завершения промиса, экспортированного из конфигурационного файла.
Простейший пример:
export default Promise.resolve({
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
}
});
Rollup дождётся выполнения промиса и только после этого начнёт сборку.
Подобный подход используется редко, поскольку асинхронная функция предоставляет более гибкий синтаксис.
Наиболее распространённый вариант:
export default async () => {
return {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'esm'
}
};
};
Rollup вызывает функцию автоматически.
Преимущества такого подхода:
await;Функция конфигурации получает аргументы.
export default async commandLineArgs => {
console.log(commandLineArgs);
return {
input: 'src/index.js',
output: {
file: 'dist/app.js',
format: 'esm'
}
};
};
При запуске:
rollup -c --environment BUILD:production
аргументы будут доступны внутри конфигурации.
Вторым параметром передаётся информация о среде выполнения.
export default async (args, context) => {
console.log(context);
return {
input: 'src/index.js',
output: {
file: 'dist/app.js',
format: 'esm'
}
};
};
Внутри context доступны сведения о:
Одна из самых распространённых задач — загрузка внешнего JSON-файла.
import { readFile } from 'node:fs/promises';
export default async () => {
const pkg = JSON.parse(
await readFile('./package.json', 'utf8')
);
return {
input: pkg.source,
output: {
file: pkg.main,
format: 'cjs'
}
};
};
Такой подход позволяет:
package.json как единый источник
конфигурации;Асинхронная конфигурация часто используется вместе с
.env.
import dotenv from 'dotenv';
export default async () => {
dotenv.config();
const production =
process.env.NODE_ENV === 'production';
return {
input: 'src/index.js',
output: {
file: production
? 'dist/prod.js'
: 'dist/dev.js',
format: 'esm'
}
};
};
Асинхронная функция особенно удобна для ветвления логики.
export default async () => {
const production =
process.env.NODE_ENV === 'production';
if (production) {
return {
input: 'src/index.js',
output: {
file: 'dist/app.min.js',
format: 'esm'
}
};
}
return {
input: 'src/index.js',
output: {
file: 'dist/app.js',
format: 'esm'
}
};
};
Rollup поддерживает множественные сборки.
Асинхронная функция может вернуть массив:
export default async () => {
return [
{
input: 'src/index.js',
output: {
file: 'dist/index.esm.js',
format: 'esm'
}
},
{
input: 'src/index.js',
output: {
file: 'dist/index.cjs.js',
format: 'cjs'
}
}
];
};
Конфигурации могут формироваться программно.
const formats = ['esm', 'cjs', 'umd'];
export default async () => {
return formats.map(format => ({
input: 'src/index.js',
output: {
file: `dist/bundle.${format}.js`,
format
}
}));
};
Подобная схема существенно уменьшает дублирование.
Асинхронность особенно полезна при сканировании директорий.
import { readdir } from 'node:fs/promises';
export default async () => {
const files = await readdir('./src/pages');
const configs = files.map(file => ({
input: `src/pages/${file}`,
output: {
file: `dist/${file}`,
format: 'esm'
}
}));
return configs;
};
Такая архитектура часто используется:
Rollup позволяет формировать точки входа динамически.
import { glob } from 'glob';
export default async () => {
const entries = await glob('src/**/*.js');
const input = Object.fromEntries(
entries.map(file => [
file
.replace('src/', '')
.replace('.js', ''),
file
])
);
return {
input,
output: {
dir: 'dist',
format: 'esm'
}
};
};
Иногда конфигурация зависит от внешнего API.
const response = await fetch(
'https://example.com/build-config'
);
const remoteConfig = await response.json();
export default async () => {
return {
input: remoteConfig.entry,
output: {
file: remoteConfig.output,
format: 'esm'
}
};
};
Подобные сценарии встречаются редко, однако возможны в:
Плагины могут зависеть от вычисляемых данных.
import replace from '@rollup/plugin-replace';
export default async () => {
const version = process.env.APP_VERSION;
return {
input: 'src/index.js',
plugins: [
replace({
preventAssignment: true,
__VERSION__: JSON.stringify(version)
})
],
output: {
file: 'dist/app.js',
format: 'esm'
}
};
};
Асинхронность позволяет автоматически анализировать зависимости.
import { readFile } from 'node:fs/promises';
export default async () => {
const pkg = JSON.parse(
await readFile('./package.json', 'utf8')
);
return {
input: 'src/index.js',
external: [
...Object.keys(pkg.dependencies || {}),
...Object.keys(pkg.peerDependencies || {})
],
output: {
file: 'dist/index.js',
format: 'esm'
}
};
};
Это распространённый шаблон библиотечной сборки.
В монорепозиториях конфигурация часто строится автоматически.
import { readdir } from 'node:fs/promises';
export default async () => {
const packages = await readdir('./packages');
return packages.map(name => ({
input: `packages/${name}/src/index.js`,
output: {
file: `packages/${name}/dist/index.js`,
format: 'esm'
}
}));
};
Асинхронная логика помогает управлять тяжёлыми плагинами.
import terser from '@rollup/plugin-terser';
export default async () => {
const production =
process.env.NODE_ENV === 'production';
return {
input: 'src/index.js',
plugins: [
production && terser()
],
output: {
file: 'dist/app.js',
format: 'esm'
}
};
};
Обычно затем применяется фильтрация:
plugins: [
production && terser()
].filter(Boolean)
При ESM-конфигурации возможен top-level await.
import { readFile } from 'node:fs/promises';
const pkg = JSON.parse(
await readFile('./package.json', 'utf8')
);
export default {
input: pkg.source,
output: {
file: pkg.main,
format: 'esm'
}
};
Такой вариант работает только при:
Важно понимать особенность watch-режима:
Например:
export default async () => {
console.log('CONFIG EXECUTED');
return {
input: 'src/index.js',
output: {
file: 'dist/app.js',
format: 'esm'
}
};
};
Сообщение обычно выводится один раз при старте watch-процесса.
Ошибки автоматически пробрасываются в Rollup.
export default async () => {
throw new Error('Invalid build configuration');
};
Rollup завершит сборку с ошибкой.
Более безопасный вариант:
export default async () => {
try {
const config = await loadConfig();
return config;
} catch (error) {
console.error(error);
process.exit(1);
}
};
Конфигурация может разделяться на отдельные генераторы.
async function createConfig(format) {
return {
input: 'src/index.js',
output: {
file: `dist/index.${format}.js`,
format
}
};
}
export default async () => {
return Promise.all([
createConfig('esm'),
createConfig('cjs'),
createConfig('umd')
]);
};
Асинхронная конфигурация может эффективно выполнять параллельные операции.
import { readFile } from 'node:fs/promises';
export default async () => {
const [
pkg,
tsconfig
] = await Promise.all([
readFile('./package.json', 'utf8'),
readFile('./tsconfig.json', 'utf8')
]);
return {
input: 'src/index.js',
output: {
file: 'dist/app.js',
format: 'esm'
}
};
};
При тяжёлых вычислениях конфигурация может использовать локальный кэш.
let cachedConfig;
export default async () => {
if (cachedConfig) {
return cachedConfig;
}
cachedConfig = {
input: 'src/index.js',
output: {
file: 'dist/app.js',
format: 'esm'
}
};
return cachedConfig;
};
Несмотря на гибкость, асинхронная конфигурация имеет ряд недостатков:
Каждый await замедляет старт сборки.
Особенно это заметно при:
Динамическая генерация усложняет понимание итоговой конфигурации.
Например:
return createConfigsFromWorkspace(
await scanPackages()
);
В подобных системах становится труднее:
Конфигурация может зависеть от:
Это ухудшает воспроизводимость сборки.
import { readFile } from 'node:fs/promises';
import replace from '@rollup/plugin-replace';
import terser from '@rollup/plugin-terser';
export default async () => {
const pkg = JSON.parse(
await readFile('./package.json', 'utf8')
);
const production =
process.env.NODE_ENV === 'production';
return {
input: pkg.source,
external: [
...Object.keys(pkg.dependencies || {})
],
plugins: [
replace({
preventAssignment: true,
__VERSION__: JSON.stringify(
pkg.version
)
}),
production && terser()
].filter(Boolean),
output: [
{
file: pkg.module,
format: 'esm'
},
{
file: pkg.main,
format: 'cjs'
}
]
};
};
Такая архитектура сочетает: