🎯 Ejemplos recomendados
Balanced sample collections from various categories for you to explore
Desarrollo de Plugins Rollup
Ejemplos de desarrollo de plugins Rollup incluyendo plugins personalizados, transformaciones y patrones de bundling
💻 Conceptos Básicos de Plugins Rollup javascript
🟢 simple
⭐⭐
Patrones esenciales de desarrollo de plugins Rollup y ejemplos de plugins simples
⏱️ 25 min
🏷️ rollup, plugins, development
Prerequisites:
JavaScript ES6+, Module bundlers, Rollup basics
// Rollup Plugin Development Basics
// Understanding the plugin API and common patterns
// ===== BASIC PLUGIN STRUCTURE =====
// 1. Simple Hello World Plugin
// hello-world-plugin.js
export default function helloWorldPlugin(options = {}) {
const name = 'hello-world';
const { greeting = 'Hello' } = options;
return {
name,
// Plugin hooks
buildStart() {
console.log(`${greeting}! Rollup build started.`);
},
generateBundle(options, bundle) {
console.log(`${greeting}! Generating bundle with ${Object.keys(bundle).length} files.`);
},
writeBundle() {
console.log(`${greeting}! Bundle written successfully.`);
}
};
}
// 2. File Size Reporter Plugin
// file-size-plugin.js
export default function fileSizePlugin(options = {}) {
const { format = 'json', limit = 1024 * 1024 } = options; // 1MB default limit
return {
name: 'file-size',
generateBundle(outputOptions, bundle) {
let totalSize = 0;
const fileSizes = {};
for (const [fileName, chunk] of Object.entries(bundle)) {
const size = chunk.code ? chunk.code.length : chunk.source.length;
fileSizes[fileName] = size;
totalSize += size;
console.log(`${fileName}: ${(size / 1024).toFixed(2)} KB`);
if (size > limit) {
this.warn(`File ${fileName} exceeds size limit of ${(limit / 1024).toFixed(2)} KB`);
}
}
console.log(`Total bundle size: ${(totalSize / 1024).toFixed(2)} KB`);
if (format === 'json') {
this.emitFile({
type: 'asset',
fileName: 'bundle-sizes.json',
source: JSON.stringify({ fileSizes, totalSize }, null, 2)
});
}
}
};
}
// 3. Banner/Footer Plugin
// banner-plugin.js
export default function bannerPlugin(options = {}) {
const { banner = '', footer = '', includeVersion = false } = options;
const version = includeVersion ? require('../package.json').version : '';
return {
name: 'banner',
renderChunk(code, chunk) {
const bannerText = banner.replace('%%VERSION%%', version);
const footerText = footer.replace('%%VERSION%%', version);
return {
code: `${bannerText}${code}${footerText}`,
map: null
};
}
};
}
// 4. Environment Variables Plugin
// env-plugin.js
export default function envPlugin(options = {}) {
const { prefix = 'process.env.' } = options;
return {
name: 'env',
transform(code, id) {
// Simple regex to find process.env.VARIABLE
const regex = new RegExp(`${prefix}([A-Z_]+)`, 'g');
let hasMatches = false;
const result = code.replace(regex, (match, envVar) => {
hasMatches = true;
if (process.env[envVar] !== undefined) {
return JSON.stringify(process.env[envVar]);
}
// Keep as is if environment variable is not defined
return match;
});
if (hasMatches) {
return {
code: result,
map: null
};
}
}
};
}
// ===== TRANSFORM PLUGIN EXAMPLES =====
// 5. JSON to ES Module Plugin
// json-esm-plugin.js
export default function jsonEsmPlugin() {
return {
name: 'json-esm',
transform(code, id) {
if (!id.endsWith('.json')) {
return null;
}
try {
const json = JSON.parse(code);
const esModuleCode = `// Generated from ${id}
export default ${JSON.stringify(json, null, 2)};`;
// Add named exports for top-level properties
const namedExports = Object.keys(json)
.map(key => `export const ${key} = default.${key};`)
.join('\n');
return {
code: esModuleCode + '\n' + namedExports,
map: null
};
} catch (error) {
this.error(`Invalid JSON in file ${id}: ${error.message}`);
}
}
};
}
// 6. CSS Injection Plugin
// css-inject-plugin.js
export default function cssInjectPlugin(options = {}) {
const { styleId = 'injected-styles' } = options;
let cssCode = '';
return {
name: 'css-inject',
transform(code, id) {
if (!id.endsWith('.css')) {
return null;
}
// Collect CSS code
cssCode += code + '\n';
// Return empty module (CSS will be injected separately)
return {
code: 'export default null;',
map: null
};
},
generateBundle(outputOptions, bundle) {
if (cssCode.trim()) {
const injectorCode = `
// CSS Injection Script
(function() {
const css = ${JSON.stringify(cssCode)};
const styleId = ${JSON.stringify(styleId)};
if (!document.getElementById(styleId)) {
const style = document.createElement('style');
style.id = styleId;
style.textContent = css;
document.head.appendChild(style);
}
})();
`;
this.emitFile({
type: 'chunk',
id: 'css-injector',
fileName: 'css-injector.js',
code: injectorCode
});
}
}
};
}
// 7. Image Asset Plugin
// image-asset-plugin.js
import { createHash } from 'crypto';
import { readFileSync, statSync } from 'fs';
import { extname, basename } from 'path';
export default function imageAssetPlugin(options = {}) {
const { limit = 8192, emitFiles = true, publicPath = '/' } = options;
const assetCache = new Map();
return {
name: 'image-asset',
load(id) {
const ext = extname(id).toLowerCase();
if (!['.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp'].includes(ext)) {
return null;
}
const buffer = readFileSync(id);
const hash = createHash('md5').update(buffer).digest('hex');
const size = statSync(id).size;
// Check cache
if (assetCache.has(hash)) {
return assetCache.get(hash);
}
const fileName = `${basename(id, ext)}.${hash.slice(0, 8)}${ext}`;
let result;
if (size <= limit) {
// Inline as base64
const base64 = buffer.toString('base64');
const mimeType = getMimeType(ext);
result = `export default "data:${mimeType};base64,${base64}";`;
} else if (emitFiles) {
// Emit as file and return URL
this.emitFile({
type: 'asset',
fileName,
source: buffer
});
result = `export default "${publicPath}${fileName}";`;
} else {
// Keep as external reference
result = `export default "${id}";`;
}
assetCache.set(hash, result);
return result;
}
};
}
function getMimeType(ext) {
const mimeTypes = {
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.webp': 'image/webp'
};
return mimeTypes[ext] || 'application/octet-stream';
}
// ===== ROLLUP CONFIGURATION EXAMPLES =====
// rollup.config.js - Using the plugins
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
// Import custom plugins
import helloWorldPlugin from './plugins/hello-world-plugin.js';
import fileSizePlugin from './plugins/file-size-plugin.js';
import bannerPlugin from './plugins/banner-plugin.js';
import envPlugin from './plugins/env-plugin.js';
import jsonEsmPlugin from './plugins/json-esm-plugin.js';
import cssInjectPlugin from './plugins/css-inject-plugin.js';
import imageAssetPlugin from './plugins/image-asset-plugin.js';
const isProduction = process.env.NODE_ENV === 'production';
export default {
input: 'src/index.js',
output: {
file: 'dist/bundle.js',
format: 'es',
sourcemap: !isProduction
},
plugins: [
// Community plugins
nodeResolve({
browser: true,
preferBuiltins: false
}),
commonjs(),
typescript(),
json(),
// Custom plugins
helloWorldPlugin({
greeting: '🚀 Starting build'
}),
bannerPlugin({
banner: `/*!
* My Library %%VERSION%%
* Built with Rollup
* Copyright (c) ${new Date().getFullYear()}
*/`,
footer: '\n//# sourceMappingURL=bundle.js.map',
includeVersion: true
}),
envPlugin({
prefix: 'process.env.'
}),
jsonEsmPlugin(),
cssInjectPlugin({
styleId: 'my-app-styles'
}),
imageAssetPlugin({
limit: 4096,
publicPath: '/assets/'
}),
fileSizePlugin({
format: 'json',
limit: 1024 * 512 // 512KB
}),
// Production-only plugins
isProduction && terser({
format: {
comments: /^!/
}
})
].filter(Boolean)
};
// ===== TESTING CUSTOM PLUGINS =====
// test-plugins.js
import { rollup } from 'rollup';
async function testPlugin(plugin, inputFile = 'test-input.js') {
try {
const bundle = await rollup({
input: inputFile,
plugins: [plugin]
});
const { output } = await bundle.generate({
format: 'es'
});
console.log('Plugin test successful!');
console.log('Generated files:', output.map(o => o.fileName));
await bundle.close();
return true;
} catch (error) {
console.error('Plugin test failed:', error);
return false;
}
}
// Test individual plugins
async function runTests() {
console.log('Testing custom Rollup plugins...\n');
const tests = [
{
name: 'Hello World Plugin',
plugin: helloWorldPlugin({ greeting: '🧪 Test' })
},
{
name: 'File Size Plugin',
plugin: fileSizePlugin({ limit: 1000 })
},
{
name: 'Banner Plugin',
plugin: bannerPlugin({ banner: '// Test Banner' })
},
{
name: 'JSON ESM Plugin',
plugin: jsonEsmPlugin()
}
];
for (const test of tests) {
console.log(`Testing ${test.name}...`);
const success = await testPlugin(test.plugin);
console.log(`${test.name}: ${success ? '✅ PASSED' : '❌ FAILED'}\n`);
}
}
// Export for use in other files
export {
helloWorldPlugin,
fileSizePlugin,
bannerPlugin,
envPlugin,
jsonEsmPlugin,
cssInjectPlugin,
imageAssetPlugin,
testPlugin,
runTests
};
💻 Plugins Avanzados de Rollup javascript
🟡 intermediate
⭐⭐⭐⭐
Patrones de plugins complejos, transformación de código y desarrollo avanzado de plugins Rollup
⏱️ 45 min
🏷️ rollup, plugins, advanced
Prerequisites:
Rollup basics, JavaScript AST, Plugin architecture
// Advanced Rollup Plugin Development
// Complex patterns, code transformation, and advanced techniques
import { createHash } from 'crypto';
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { join, dirname, basename, extname } from 'path';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
// ===== ADVANCED TRANSFORMATION PLUGINS =====
// 1. Tree Shaking for Unused Exports
// tree-shaking-plugin.js
export default function treeShakingPlugin(options = {}) {
const { analyzeOnly = false, warnings = true } = options;
const usedExports = new Set();
const declaredExports = new Map();
return {
name: 'tree-shaking-analyzer',
// First pass: collect declared exports
buildStart(options) {
this.declaredExports = declaredExports;
this.usedExports = usedExports;
},
moduleParsed(moduleInfo) {
// Track declared exports
moduleInfo.exports.forEach(name => {
declaredExports.set(moduleInfo.id, (declaredExports.get(moduleInfo.id) || new Set()).add(name));
});
},
// Second pass: track used exports
resolveId(source, importer) {
if (importer && declaredExports.has(source)) {
return source;
}
return null;
},
load(id) {
if (declaredExports.has(id)) {
return readFileSync(id, 'utf8');
}
return null;
},
transform(code, id) {
if (!declaredExports.has(id)) {
return null;
}
try {
const ast = parse(code, {
sourceType: 'module',
plugins: ['typescript', 'jsx']
});
// Find import statements
traverse.default(ast, {
ImportDeclaration(path) {
const source = path.node.source.value;
if (declaredExports.has(source)) {
path.node.specifiers.forEach(spec => {
if (spec.type === 'ImportDefaultSpecifier') {
usedExports.add({ module: source, name: 'default' });
} else if (spec.type === 'ImportSpecifier') {
usedExports.add({ module: source, name: spec.imported.name });
}
});
}
}
});
if (analyzeOnly) {
return null;
}
// Remove unused exports (this is simplified)
let result = code;
const moduleExports = declaredExports.get(id) || new Set();
moduleExports.forEach(exportName => {
const isUsed = Array.from(usedExports).some(
used => used.module === id && used.name === exportName
);
if (!isUsed && exportName !== 'default') {
// Remove export (simplified regex approach)
const exportRegex = new RegExp(`export\\s*\\{[^}]*\\b${exportName}\\b[^}]*\\}[^;]*;?`, 'g');
result = result.replace(exportRegex, '');
// Also handle named exports
const namedExportRegex = new RegExp(`export\\s+(const|let|var|function|class)\\s+${exportName}\\b[^;]*;?`, 'g');
result = result.replace(namedExportRegex, '');
if (warnings) {
this.warn(`Unused export '${exportName}' removed from ${id}`);
}
}
});
return {
code: result,
map: null
};
} catch (error) {
this.warn(`Failed to analyze ${id}: ${error.message}`);
return null;
}
},
generateBundle() {
// Report unused exports
if (warnings) {
const allUsed = new Set(Array.from(usedExports).map(u => u.module));
declaredExports.forEach((exports, module) => {
if (!allUsed.has(module)) {
this.warn(`Module ${module} has no used exports`);
}
});
}
}
};
}
// 2. Bundle Analyzer Plugin (Advanced)
// bundle-analyzer-plugin.js
export default function bundleAnalyzerPlugin(options = {}) {
const {
outputPath = 'bundle-analysis.json',
excludePatterns = [],
includeDependencies = true,
createVisualization = false
} = options;
return {
name: 'bundle-analyzer',
buildStart() {
this.moduleGraph = new Map();
this.dependencyGraph = new Map();
this.importCounts = new Map();
},
moduleParsed(moduleInfo) {
// Store module information
this.moduleGraph.set(moduleInfo.id, {
id: moduleInfo.id,
size: moduleInfo.code ? moduleInfo.code.length : 0,
imports: moduleInfo.importedIds || [],
exports: moduleInfo.exports || [],
isExternal: moduleInfo.isExternal,
dynamicImports: moduleInfo.dynamicImportIds || []
});
// Track dependencies
moduleInfo.importedIds.forEach(dep => {
if (!this.dependencyGraph.has(moduleInfo.id)) {
this.dependencyGraph.set(moduleInfo.id, new Set());
}
this.dependencyGraph.get(moduleInfo.id).add(dep);
// Count imports
this.importCounts.set(dep, (this.importCounts.get(dep) || 0) + 1);
});
},
generateBundle(outputOptions, bundle) {
const analysis = this.performAnalysis(bundle);
// Save analysis
this.emitFile({
type: 'asset',
fileName: outputPath,
source: JSON.stringify(analysis, null, 2)
});
// Generate HTML visualization
if (createVisualization) {
const html = this.generateVisualization(analysis);
this.emitFile({
type: 'asset',
fileName: 'bundle-analysis.html',
source: html
});
}
// Print summary
this.printSummary(analysis);
},
performAnalysis(bundle) {
const modules = Array.from(this.moduleGraph.values());
const totalSize = modules.reduce((sum, m) => sum + m.size, 0);
// Find largest modules
const largestModules = modules
.sort((a, b) => b.size - a.size)
.slice(0, 10);
// Find most imported modules
const mostImported = Array.from(this.importCounts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10);
// Analyze dependencies
const dependencyChains = this.findDependencyChains();
// Detect circular dependencies
const circularDependencies = this.detectCircularDependencies();
// Calculate bundle metrics
const metrics = this.calculateMetrics(bundle, modules);
return {
timestamp: new Date().toISOString(),
summary: {
totalModules: modules.length,
totalSize,
averageModuleSize: totalSize / modules.length,
circularDependencies: circularDependencies.length
},
modules,
largestModules,
mostImported,
dependencyChains,
circularDependencies,
metrics,
bundle: {
fileName: Object.keys(bundle),
formats: Object.keys(bundle).map(key => bundle[key].type)
}
};
},
findDependencyChains() {
const chains = [];
const visited = new Set();
function findChains(module, path = []) {
if (visited.has(module)) {
return;
}
visited.add(module);
path.push(module);
const dependencies = this.dependencyGraph.get(module) || new Set();
for (const dep of dependencies) {
if (path.length > 1) {
chains.push([...path, dep]);
}
findChains(dep, [...path]);
}
path.pop();
}
for (const module of this.moduleGraph.keys()) {
if (!visited.has(module)) {
findChains(module);
}
}
return chains;
},
detectCircularDependencies() {
const visiting = new Set();
const visited = new Set();
const cycles = [];
function detect(node, path = []) {
if (visiting.has(node)) {
const cycleStart = path.indexOf(node);
cycles.push(path.slice(cycleStart).concat(node));
return;
}
if (visited.has(node)) {
return;
}
visiting.add(node);
path.push(node);
const dependencies = this.dependencyGraph.get(node) || new Set();
for (const dep of dependencies) {
detect(dep, [...path]);
}
visiting.delete(node);
visited.add(node);
path.pop();
}
for (const module of this.moduleGraph.keys()) {
if (!visited.has(module)) {
detect(module);
}
}
return cycles;
},
calculateMetrics(bundle, modules) {
const externalModules = modules.filter(m => m.isExternal);
const internalModules = modules.filter(m => !m.isExternal);
return {
externalModules: externalModules.length,
internalModules: internalModules.length,
externalSize: externalModules.reduce((sum, m) => sum + m.size, 0),
internalSize: internalModules.reduce((sum, m) => sum + m.size, 0),
dynamicImports: modules.reduce((sum, m) => sum + (m.dynamicImports?.length || 0), 0),
averageExports: modules.reduce((sum, m) => sum + (m.exports?.length || 0), 0) / modules.length
};
},
printSummary(analysis) {
console.log('\n📊 Bundle Analysis Summary');
console.log('========================');
console.log(`Total Modules: ${analysis.summary.totalModules}`);
console.log(`Total Size: ${(analysis.summary.totalSize / 1024).toFixed(2)} KB`);
console.log(`Average Module Size: ${(analysis.summary.averageModuleSize / 1024).toFixed(2)} KB`);
console.log(`Circular Dependencies: ${analysis.summary.circularDependencies}`);
console.log(`\n🔝 Largest Modules:`);
analysis.largestModules.forEach((m, i) => {
console.log(` ${i + 1}. ${m.id}: ${(m.size / 1024).toFixed(2)} KB`);
});
console.log(`\n📈 Most Imported:`);
analysis.mostImported.forEach(([module, count], i) => {
console.log(` ${i + 1}. ${module}: ${count} imports`);
});
},
generateVisualization(analysis) {
return `
<!DOCTYPE html>
<html>
<head>
<title>Bundle Analysis</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.chart { margin: 20px 0; }
.bar { background: #007bff; margin: 2px 0; }
.module { font-size: 12px; padding: 2px; }
.cycle { color: #dc3545; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
th { background-color: #f2f2f2; }
</style>
</head>
<body>
<h1>Bundle Analysis</h1>
<p>Generated on ${analysis.timestamp}</p>
<h2>Summary</h2>
<table>
<tr><th>Metric</th><th>Value</th></tr>
<tr><td>Total Modules</td><td>${analysis.summary.totalModules}</td></tr>
<tr><td>Total Size</td><td>${(analysis.summary.totalSize / 1024).toFixed(2)} KB</td></tr>
<tr><td>Circular Dependencies</td><td>${analysis.summary.circularDependencies}</td></tr>
</table>
<h2>Largest Modules</h2>
<div class="chart">
${analysis.largestModules.map(m => {
const width = (m.size / analysis.summary.totalSize) * 100;
return `
<div>
<div class="bar" style="width: ${width}%; height: 20px;">
<span class="module">${m.id} (${(m.size / 1024).toFixed(2)} KB)</span>
</div>
</div>
`;
}).join('')}
</div>
<h2>Circular Dependencies</h2>
${analysis.circularDependencies.length > 0 ? `
<ul>
${analysis.circularDependencies.map(cycle =>
`<li class="cycle">${cycle.join(' → ')}</li>`
).join('')}
</ul>
` : '<p>No circular dependencies detected.</p>'}
</body>
</html>
`;
}
};
}
// 3. Code Splitting Plugin
// code-splitting-plugin.js
export default function codeSplittingPlugin(options = {}) {
const {
strategy = 'auto',
maxSize = 250000, // 250KB
minSize = 10000, // 10KB
includePatterns = [],
excludePatterns = [/node_modules/]
} = options;
return {
name: 'code-splitting',
buildStart() {
this.moduleGroups = new Map();
this.splitPoints = new Set();
},
moduleParsed(moduleInfo) {
if (shouldSplit(moduleInfo.id, includePatterns, excludePatterns)) {
const group = getModuleGroup(moduleInfo.id);
if (!this.moduleGroups.has(group)) {
this.moduleGroups.set(group, []);
}
this.moduleGroups.get(group).push(moduleInfo);
}
},
generateBundle(outputOptions, bundle) {
if (strategy === 'manual') {
return;
}
// Automatic splitting based on size
const largeModules = Object.values(bundle).filter(
chunk => chunk.type === 'chunk' && chunk.code.length > maxSize
);
largeModules.forEach(chunk => {
const parts = splitChunk(chunk.code, maxSize);
parts.forEach((part, index) => {
if (index > 0) {
this.emitFile({
type: 'chunk',
id: `${chunk.fileName}.part${index}`,
fileName: `${chunk.fileName}.part${index}.js`,
code: part
});
}
});
});
}
};
}
function shouldSplit(id, includePatterns, excludePatterns) {
const shouldInclude = includePatterns.length === 0 ||
includePatterns.some(pattern => pattern.test(id));
const shouldExclude = excludePatterns.some(pattern => pattern.test(id));
return shouldInclude && !shouldExclude;
}
function getModuleGroup(id) {
// Group by directory
const parts = id.split(/[/\\]/);
return parts.length > 1 ? parts[parts.length - 2] : 'default';
}
function splitChunk(code, maxSize) {
const parts = [];
let currentPart = '';
const lines = code.split('\n');
for (const line of lines) {
if (currentPart.length + line.length > maxSize && currentPart) {
parts.push(currentPart);
currentPart = line + '\n';
} else {
currentPart += line + '\n';
}
}
if (currentPart) {
parts.push(currentPart);
}
return parts;
}
// 4. Plugin Composition and Orchestration
// plugin-composer.js
export function composePlugins(...plugins) {
return {
name: 'composed-plugins',
plugins: plugins.filter(Boolean),
buildStart(...args) {
return this.plugins.reduce((promise, plugin) => {
return promise.then(() => {
return plugin.buildStart && plugin.buildStart.apply(this, args);
});
}, Promise.resolve());
},
resolveId(...args) {
let result = null;
for (const plugin of this.plugins) {
if (plugin.resolveId) {
const pluginResult = plugin.resolveId.apply(this, args);
if (pluginResult) {
result = pluginResult;
break;
}
}
}
return result;
},
load(...args) {
for (const plugin of this.plugins) {
if (plugin.load) {
const result = plugin.load.apply(this, args);
if (result) {
return result;
}
}
}
return null;
},
transform(...args) {
let code = args[0];
let map = null;
for (const plugin of this.plugins) {
if (plugin.transform) {
const result = plugin.transform.apply(this, [code, ...args.slice(1)]);
if (result) {
code = result.code || result;
map = result.map || null;
}
}
}
return { code, map };
},
generateBundle(...args) {
return Promise.all(
this.plugins.map(plugin =>
plugin.generateBundle && plugin.generateBundle.apply(this, args)
)
);
}
};
}
// Usage with advanced plugins
import { nodeResolve } from '@rollup/plugin-node-resolve';
import typescript from '@rollup/plugin-typescript';
import { terser } from 'rollup-plugin-terser';
import treeShakingPlugin from './plugins/tree-shaking-plugin.js';
import bundleAnalyzerPlugin from './plugins/bundle-analyzer-plugin.js';
import codeSplittingPlugin from './plugins/code-splitting-plugin.js';
import { composePlugins } from './plugins/plugin-composer.js';
export default {
input: 'src/index.js',
output: {
dir: 'dist',
format: 'es',
sourcemap: true
},
plugins: [
nodeResolve(),
typescript(),
// Compose multiple plugins
composePlugins(
treeShakingPlugin({
analyzeOnly: false,
warnings: true
}),
codeSplittingPlugin({
strategy: 'auto',
maxSize: 200000,
includePatterns: [/src\/components/],
excludePatterns: [/node_modules/, /src\/utils\/index\.js$/]
})
),
bundleAnalyzerPlugin({
outputPath: 'bundle-analysis.json',
createVisualization: true,
includeDependencies: true
}),
terser({
compress: {
drop_console: true,
drop_debugger: true
}
})
]
};
// ===== PLUGIN TESTING FRAMEWORK =====
// plugin-tester.js
export class PluginTester {
constructor(options = {}) {
this.options = {
timeout: 5000,
verbose: false,
...options
};
}
async testPlugin(plugin, testCases) {
console.log(`🧪 Testing plugin: ${plugin.name}`);
const results = [];
for (const testCase of testCases) {
try {
const result = await this.runSingleTest(plugin, testCase);
results.push({
name: testCase.name,
passed: true,
result
});
if (this.options.verbose) {
console.log(` ✅ ${testCase.name}`);
}
} catch (error) {
results.push({
name: testCase.name,
passed: false,
error: error.message
});
console.log(` ❌ ${testCase.name}: ${error.message}`);
}
}
const passed = results.filter(r => r.passed).length;
const total = results.length;
console.log(`📊 Plugin Test Results: ${passed}/${total} passed\n`);
return results;
}
async runSingleTest(plugin, testCase) {
const { input, expectedTransform, expectedOutput } = testCase;
// Create a minimal Rollup bundle for testing
const bundle = await rollup({
input,
plugins: [plugin],
onwarn: (warning, warn) => {
if (!testCase.expectedWarnings?.includes(warning.code)) {
throw new Error(`Unexpected warning: ${warning.message}`);
}
}
});
try {
// Test transformation if expected
if (expectedTransform) {
const module = await bundle.cache.get(input);
if (module && expectedTransform) {
if (typeof expectedTransform === 'function') {
const passes = expectedTransform(module.code);
if (!passes) {
throw new Error('Transform did not match expectations');
}
} else {
if (module.code !== expectedTransform) {
throw new Error('Transform output mismatch');
}
}
}
}
// Test output generation
if (expectedOutput) {
const { output } = await bundle.generate({
format: 'es'
});
if (typeof expectedOutput === 'function') {
const passes = expectedOutput(output);
if (!passes) {
throw new Error('Output does not match expectations');
}
} else {
if (JSON.stringify(output) !== JSON.stringify(expectedOutput)) {
throw new Error('Generated output mismatch');
}
}
}
await bundle.close();
return { success: true };
} finally {
await bundle.close();
}
}
}
// Export for use
export {
treeShakingPlugin,
bundleAnalyzerPlugin,
codeSplittingPlugin,
composePlugins,
PluginTester
};
💻 Ecosistema de Plugins Rollup javascript
🟡 intermediate
⭐⭐⭐⭐
Construcción de plugins que se integran con el ecosistema más amplio de Rollup y patrones comunes
⏱️ 40 min
🏷️ rollup, plugins, ecosystem
Prerequisites:
Rollup plugin development, JavaScript ecosystems, Build tools
// Rollup Plugin Ecosystem Integration
// Building plugins that work seamlessly with other Rollup plugins and tools
// ===== PLUGIN INTEROPERABILITY =====
// 1. Plugin Communication System
// plugin-communication.js
export class PluginCommunication {
constructor() {
this.channels = new Map();
this.globalState = new Map();
}
// Subscribe to a channel
subscribe(channel, callback) {
if (!this.channels.has(channel)) {
this.channels.set(channel, []);
}
this.channels.get(channel).push(callback);
}
// Publish to a channel
publish(channel, data) {
const subscribers = this.channels.get(channel) || [];
subscribers.forEach(callback => callback(data));
}
// Set global state
setState(key, value) {
this.globalState.set(key, value);
this.publish('stateChange', { key, value });
}
// Get global state
getState(key) {
return this.globalState.get(key);
}
}
// 2. Plugin Registry System
// plugin-registry.js
export class PluginRegistry {
constructor() {
this.plugins = new Map();
this.hooks = new Map();
this.communication = new PluginCommunication();
}
// Register a plugin
register(plugin) {
if (!plugin.name) {
throw new Error('Plugin must have a name');
}
if (this.plugins.has(plugin.name)) {
throw new Error(`Plugin ${plugin.name} is already registered`);
}
// Initialize plugin hooks
this.initializeHooks(plugin);
// Store plugin
this.plugins.set(plugin.name, plugin);
// Notify other plugins
this.communication.publish('pluginRegistered', { plugin });
return this;
}
// Initialize plugin hooks
initializeHooks(plugin) {
const hookNames = [
'buildStart', 'buildEnd', 'moduleParsed', 'resolveId',
'load', 'transform', 'generateBundle', 'writeBundle',
'renderChunk', 'renderStart', 'renderError'
];
hookNames.forEach(hookName => {
if (plugin[hookName]) {
if (!this.hooks.has(hookName)) {
this.hooks.set(hookName, []);
}
this.hooks.get(hookName).push({
plugin: plugin.name,
handler: plugin[hookName].bind(plugin)
});
}
});
}
// Execute hooks
async executeHook(hookName, ...args) {
const hooks = this.hooks.get(hookName) || [];
const results = [];
for (const { plugin, handler } of hooks) {
try {
const result = await handler(...args);
results.push({ plugin, result });
} catch (error) {
console.error(`Error in hook ${hookName} for plugin ${plugin}:`, error);
results.push({ plugin, error });
}
}
return results;
}
}
// 3. Plugin Lifecycle Manager
// lifecycle-manager.js
export class PluginLifecycleManager {
constructor(registry) {
this.registry = registry;
this.phases = new Map();
}
// Register a phase
registerPhase(name, plugins = []) {
this.phases.set(name, plugins);
return this;
}
// Execute a phase
async executePhase(name, context = {}) {
const phasePlugins = this.phases.get(name) || [];
for (const pluginName of phasePlugins) {
const plugin = this.registry.plugins.get(pluginName);
if (plugin && plugin.phaseHandler) {
await plugin.phaseHandler(name, context);
}
}
}
}
// ===== INTEGRATION WITH COMMON PLUGINS =====
// 4. Alias Resolution Plugin
// alias-resolver-plugin.js
export default function aliasResolverPlugin(options = {}) {
const { alias = {}, preserveSymlinks = false } = options;
return {
name: 'alias-resolver',
resolveId(id, importer) {
// Check if the ID matches any alias
for (const [key, value] of Object.entries(alias)) {
if (id === key || id.startsWith(key + '/')) {
const replacement = id.replace(key, value);
return this.resolve(replacement, importer, { preserveSymlinks });
}
}
return null;
},
// Integrate with node-resolve
async buildStart() {
// Ensure node-resolve is available
if (!this.getPluginInfo('node-resolve')) {
this.warn('alias-resolver works best with @rollup/plugin-node-resolve');
}
}
};
}
// 5. Virtual Modules Plugin
// virtual-modules-plugin.js
export default function virtualModulesPlugin(modules = {}) {
const virtualModulePrefix = '\0virtual:';
const virtualModules = new Map();
// Initialize with provided modules
Object.entries(modules).forEach(([id, content]) => {
virtualModules.set(virtualModulePrefix + id, content);
});
return {
name: 'virtual-modules',
resolveId(id) {
if (virtualModules.has(virtualModulePrefix + id)) {
return virtualModulePrefix + id;
}
return null;
},
load(id) {
if (virtualModules.has(id)) {
return virtualModules.get(id);
}
return null;
},
// API to add virtual modules at runtime
api: {
addModule(id, content) {
virtualModules.set(virtualModulePrefix + id, content);
},
hasModule(id) {
return virtualModules.has(virtualModulePrefix + id);
}
}
};
}
// 6. CSS Processing Plugin (with PostCSS integration)
// css-processor-plugin.js
import postcss from 'postcss';
import autoprefixer from 'autoprefixer';
import cssnano from 'cssnano';
export default function cssProcessorPlugin(options = {}) {
const {
plugins = [autoprefixer, cssnano],
inject = false,
outputName = 'styles.css',
minimize = process.env.NODE_ENV === 'production'
} = options;
let cssCode = '';
const processedCSS = new Set();
return {
name: 'css-processor',
transform(code, id) {
if (!id.endsWith('.css') && !id.endsWith('.scss') && !id.endsWith('.sass')) {
return null;
}
// Collect CSS code
cssCode += code + '\n';
return {
code: 'export default null;',
map: null
};
},
buildEnd() {
if (!cssCode.trim()) return;
return postcss(minimize ? [...plugins, cssnano] : plugins)
.process(cssCode, { from: undefined })
.then(result => {
const processedResult = result.css;
processedCSS.add(processedResult);
if (inject) {
// Create CSS injector
const injector = `
(function() {
var css = ${JSON.stringify(processedResult)};
if (typeof document !== 'undefined') {
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
}
})();
`;
this.emitFile({
type: 'chunk',
id: 'css-injector',
fileName: 'css-injector.js',
code: injector
});
} else {
// Emit CSS file
this.emitFile({
type: 'asset',
fileName: outputName,
source: processedResult
});
}
})
.catch(error => {
this.error(`CSS processing failed: ${error.message}`);
});
}
};
}
// 7. Environment Configuration Plugin
// environment-config-plugin.js
export default function environmentConfigPlugin(options = {}) {
const {
environments = {},
defaultEnv = 'development',
configFile = '.rollup.env.js'
} = options;
let currentEnv = defaultEnv;
let envConfig = {};
return {
name: 'environment-config',
buildStart() {
// Load environment configuration
const fs = require('fs');
const path = require('path');
if (fs.existsSync(configFile)) {
delete require.cache[require.resolve(path.resolve(configFile))];
const userConfig = require(path.resolve(configFile));
currentEnv = process.env.ROLLUP_ENV || userConfig.env || defaultEnv;
envConfig = { ...userConfig, ...environments[currentEnv] };
} else {
currentEnv = process.env.ROLLUP_ENV || defaultEnv;
envConfig = environments[currentEnv] || {};
}
console.log(`🌍 Using environment: ${currentEnv}`);
},
resolveId(id, importer) {
// Handle environment-specific modules
const envMatch = id.match(/^(.*)\.env\.(\w+)$/);
if (envMatch) {
const [, baseId, env] = envMatch;
if (env === currentEnv) {
return this.resolve(baseId, importer);
}
return null;
}
return null;
},
transform(code, id) {
// Replace environment variables in code
const envRegex = /__ENV_([A-Z_]+)__|__CURRENT_ENV__/g;
let hasReplacements = false;
const result = code.replace(envRegex, (match, varName) => {
hasReplacements = true;
if (varName) {
return JSON.stringify(envConfig[varName.toLowerCase()] || process.env[varName] || '');
}
return JSON.stringify(currentEnv);
});
if (hasReplacements) {
return {
code: result,
map: null
};
}
},
api: {
getEnv() {
return currentEnv;
},
getConfig() {
return envConfig;
},
isDevelopment() {
return currentEnv === 'development';
},
isProduction() {
return currentEnv === 'production';
}
}
};
}
// ===== ADVANCED ECOSYSTEM INTEGRATION =====
// 8. Plugin Dependency Manager
// dependency-manager-plugin.js
export default function dependencyManagerPlugin(options = {}) {
const { dependencies = {}, autoInstall = false } = options;
return {
name: 'dependency-manager',
buildStart() {
// Check plugin dependencies
for (const [pluginName, requiredVersion] of Object.entries(dependencies)) {
const pluginInfo = this.getPluginInfo(pluginName);
if (!pluginInfo) {
if (autoInstall) {
this.warn(`Installing missing plugin: ${pluginName}`);
// In a real implementation, you might want to install the plugin
} else {
this.error(`Required plugin not found: ${pluginName}`);
}
} else {
// Version check (simplified)
const version = pluginInfo.version || '0.0.0';
if (!this.satisfiesVersion(version, requiredVersion)) {
this.warn(`Plugin version mismatch: ${pluginName} requires ${requiredVersion}, found ${version}`);
}
}
}
},
satisfiesVersion(current, required) {
// Simplified version comparison
const currentParts = current.split('.').map(Number);
const requiredParts = required.split('.').map(Number);
for (let i = 0; i < Math.max(currentParts.length, requiredParts.length); i++) {
const currentPart = currentParts[i] || 0;
const requiredPart = requiredParts[i] || 0;
if (currentPart > requiredPart) return true;
if (currentPart < requiredPart) return false;
}
return true;
}
};
}
// 9. Plugin Configuration Validator
// config-validator-plugin.js
export default function configValidatorPlugin(options = {}) {
const {
requiredOptions = [],
optionValidators = {},
strict = false
} = options;
return {
name: 'config-validator',
buildStart() {
// Validate required options
for (const option of requiredOptions) {
if (!(option in this.options)) {
const message = `Required option '${option}' is missing`;
if (strict) {
this.error(message);
} else {
this.warn(message);
}
}
}
// Run custom validators
for (const [option, validator] of Object.entries(optionValidators)) {
if (option in this.options) {
try {
const result = validator(this.options[option]);
if (!result) {
this.warn(`Option '${option}' failed validation`);
}
} catch (error) {
this.warn(`Validation error for option '${option}': ${error.message}`);
}
}
}
}
};
}
// 10. Bundle Optimization Pipeline
// optimization-pipeline-plugin.js
export default function optimizationPipelinePlugin(options = {}) {
const {
stages = [],
parallel = false,
timeout = 30000
} = options;
return {
name: 'optimization-pipeline',
generateBundle(outputOptions, bundle) {
const pipeline = stages.map(stage => {
if (typeof stage === 'string') {
return this.getOptimizationStage(stage);
}
return stage;
});
let optimizedBundle = { ...bundle };
if (parallel) {
// Run stages in parallel
return Promise.all(
pipeline.map(stage => this.runOptimizationStage(stage, optimizedBundle))
).then(() => {
// Merge results
this.mergeOptimizationResults(optimizedBundle, pipeline);
});
} else {
// Run stages sequentially
return pipeline.reduce((promise, stage) => {
return promise.then(() => this.runOptimizationStage(stage, optimizedBundle));
}, Promise.resolve());
}
},
getOptimizationStage(name) {
const stages = {
'dedupe': this.dedupeStage.bind(this),
'compress': this.compressStage.bind(this),
'mangle': this.mangleStage.bind(this),
'treeshake': this.treeshakeStage.bind(this),
'optimize-imports': this.optimizeImportsStage.bind(this)
};
return stages[name] || (() => Promise.resolve());
},
runOptimizationStage(stage, bundle) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error('Optimization stage timeout'));
}, timeout);
try {
const result = stage(bundle);
if (result && typeof result.then === 'function') {
result.finally(() => clearTimeout(timer)).then(resolve).catch(reject);
} else {
clearTimeout(timer);
resolve();
}
} catch (error) {
clearTimeout(timer);
reject(error);
}
});
},
// Optimization stage implementations
dedupeStage(bundle) {
// Remove duplicate modules
const seen = new Set();
const deduped = {};
for (const [fileName, chunk] of Object.entries(bundle)) {
const hash = this.createChunkHash(chunk);
if (!seen.has(hash)) {
seen.add(hash);
deduped[fileName] = chunk;
}
}
// Update bundle in place
Object.keys(bundle).forEach(key => delete bundle[key]);
Object.assign(bundle, deduped);
},
compressStage(bundle) {
// Compress chunks (simplified)
for (const chunk of Object.values(bundle)) {
if (chunk.type === 'chunk' && chunk.code) {
// Basic compression - in practice, you'd use a proper minifier
chunk.code = chunk.code
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments
.replace(/\s+/g, ' ') // Collapse whitespace
.trim();
}
}
},
createChunkHash(chunk) {
const crypto = require('crypto');
const content = chunk.code || chunk.source || '';
return crypto.createHash('md5').update(content).digest('hex');
}
};
}
// ===== ROLLUP CONFIGURATION WITH ECOSYSTEM INTEGRATION =====
// rollup.config.js - Complete ecosystem example
import { nodeResolve } from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import json from '@rollup/plugin-json';
import { terser } from 'rollup-plugin-terser';
import alias from '@rollup/plugin-alias';
// Import custom plugins
import aliasResolverPlugin from './plugins/alias-resolver-plugin.js';
import virtualModulesPlugin from './plugins/virtual-modules-plugin.js';
import cssProcessorPlugin from './plugins/css-processor-plugin.js';
import environmentConfigPlugin from './plugins/environment-config-plugin.js';
import dependencyManagerPlugin from './plugins/dependency-manager-plugin.js';
import configValidatorPlugin from './plugins/config-validator-plugin.js';
import optimizationPipelinePlugin from './plugins/optimization-pipeline-plugin.js';
// Virtual modules
const virtualModules = {
'config': `
export const API_URL = __ENV_API_URL__;
export const VERSION = '__CURRENT_ENV__-v1.0.0';
`,
'constants': `
export const APP_NAME = 'My App';
export const BUILD_DATE = new Date().toISOString();
`
};
export default {
input: 'src/index.js',
output: [
{
file: 'dist/bundle.esm.js',
format: 'es',
sourcemap: true
},
{
file: 'dist/bundle.cjs.js',
format: 'cjs',
sourcemap: true
},
{
file: 'dist/bundle.umd.js',
format: 'umd',
name: 'MyApp',
sourcemap: true
}
],
plugins: [
// Configuration validation
configValidatorPlugin({
requiredOptions: ['input'],
optionValidators: {
input: (value) => typeof value === 'string',
output: (value) => Array.isArray(value) || typeof value === 'object'
},
strict: true
}),
// Dependency management
dependencyManagerPlugin({
dependencies: {
'node-resolve': '^13.0.0',
'typescript': '^8.0.0'
},
autoInstall: false
}),
// Environment configuration
environmentConfigPlugin({
environments: {
development: {
API_URL: 'http://localhost:3000/api',
DEBUG: true
},
production: {
API_URL: 'https://api.myapp.com',
DEBUG: false
},
test: {
API_URL: 'http://test-api:3000/api',
DEBUG: true
}
}
}),
// Core plugins
nodeResolve({
browser: true,
preferBuiltins: false
}),
commonjs(),
typescript(),
json(),
// Alias resolution
alias({
entries: [
{ find: '@', replacement: './src' },
{ find: '@utils', replacement: './src/utils' },
{ find: '@components', replacement: './src/components' }
]
}),
// Custom alias resolver (works with node-resolve)
aliasResolverPlugin({
alias: {
'react': 'preact/compat',
'react-dom': 'preact/compat'
}
}),
// Virtual modules
virtualModulesPlugin(virtualModules),
// CSS processing
cssProcessorPlugin({
inject: process.env.NODE_ENV === 'development',
minimize: process.env.NODE_ENV === 'production',
plugins: [
require('autoprefixer'),
require('cssnano')({
preset: 'default'
})
]
}),
// Optimization pipeline
optimizationPipelinePlugin({
stages: ['dedupe', 'optimize-imports', 'compress'],
parallel: true
}),
// Production optimizations
process.env.NODE_ENV === 'production' && terser({
format: {
comments: /^!/,
ascii_only: true
},
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log', 'console.info'],
passes: 2
},
mangle: {
properties: {
regex: /^_/
}
}
})
].filter(Boolean)
};
// Export for use in other files
export {
aliasResolverPlugin,
virtualModulesPlugin,
cssProcessorPlugin,
environmentConfigPlugin,
dependencyManagerPlugin,
configValidatorPlugin,
optimizationPipelinePlugin
};