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
};