🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Développement de Plugins Rollup
Exemples de développement de plugins Rollup incluant des plugins personnalisés, des transformations et des patterns de bundling
💻 Bases des Plugins Rollup javascript
🟢 simple
⭐⭐
Patterns essentiels de développement de plugins Rollup et exemples 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 Avancés Rollup javascript
🟡 intermediate
⭐⭐⭐⭐
Patterns de plugins complexes, transformation de code et développement avancé 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
};
💻 Écosystème de Plugins Rollup javascript
🟡 intermediate
⭐⭐⭐⭐
Création de plugins qui s'intègrent avec l'écosystème Rollup plus large et patterns communs
⏱️ 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
};