# API Integration Standards for Rollup
This document outlines the coding standards for integrating Rollup with backend services and external APIs. It provides guidance on best practices, common anti-patterns, and specific code examples to ensure maintainable, performant, and secure code. These standards will help developers and AI coding assistants write high-quality Rollup plugins and applications that interact with APIs.
## 1. General Principles of API Integration with Rollup
These general principles underpin all subsequent specific standards. They emphasize clear separation of concerns, robust error handling, and optimized data fetching strategies.
**Why These Principles Matter:**
* **Maintainability:** A well-defined API integration layer makes it easier to update and change backend services without impacting the Rollup build process.
* **Performance:** Efficient API calls and data caching reduce build times and improve the overall developer experience.
* **Security:** Secure authentication and authorization mechanisms protect sensitive data during API interactions.
**Standards:**
* **Do This:** Separate configuration, API call execution, and data transformation into distinct, modular components. This improves testability and reusability.
* **Don't Do This:** Embed API calls directly within Rollup plugin "transform" or "generateBundle" hooks. This makes the code difficult to maintain and test.
* **Do This:** Use environment variables for API keys and sensitive configuration data during development and production.
* **Don't Do This:** Hardcode API keys directly in the source code. This is a major security risk.
* **Do This:** Implement robust error handling and retry mechanisms for API calls.
* **Don't Do This:** Ignore API errors, which can lead to unexpected build failures.
* **Do This:** Implement caching strategies to avoid redundant API calls. Consider using persistent caching for static data and in-memory caching for frequently accessed data during a single build process.
* **Don't Do This:** Always fetch data from the API, even when it's unlikely to have changed.
* **Do This:** Utilize asynchronous operations (Promises, "async/await") for API calls to avoid blocking the Rollup build process.
* **Don't Do This:** Use synchronous API calls, which can cause Rollup to freeze or become unresponsive.
## 2. Architectural Patterns for API Integration
Choosing the right architectural pattern is essential for managing complex API integrations within Rollup plugins. Consider the trade-offs between simplicity, flexibility, and performance when selecting a pattern.
### 2.1. Dedicated API Client Module
*This is often the preferred approach for simple integrations where you need to make several API requests.*
**Standards:**
* **Do This:** Create a dedicated module or class that encapsulates all API-related logic, including URL construction, request headers, authentication, and response parsing.
* **Don't Do This:** Scatter API logic throughout the Rollup plugin.
* **Do This:** Use a well-established HTTP client library like "axios" or "node-fetch" for making API requests.
* **Don't Do This:** Use built-in Node.js "http" or "https" modules directly, unless you require low-level control. Using a library provides a more streamlined interface.
* **Do This:** Implement a consistent error handling strategy within the API client module, such as throwing custom exceptions or returning standardized error objects.
**Example:**
"""javascript
// api-client.js
import axios from 'axios';
const API_URL = process.env.MY_API_URL || 'https://api.example.com';
const API_KEY = process.env.MY_API_KEY;
class ApiClient {
constructor() {
this.client = axios.create({
baseURL: API_URL,
timeout: 10000,
headers: {
'Authorization': "Bearer ${API_KEY}",
'Content-Type': 'application/json',
},
});
}
async getData(id) {
try {
const response = await this.client.get("/data/${id}");
return response.data;
} catch (error) {
console.error('API error:', error);
throw new Error("Failed to fetch data for id ${id}: ${error.message}"); // Custom Error
}
}
// Other API methods...
}
export default new ApiClient();
"""
"""javascript
// rollup-plugin.js
import apiClient from './api-client.js';
export default function myRollupPlugin() {
return {
name: 'my-rollup-plugin',
async transform(code, id) {
if (id.endsWith('.my-file')) {
try {
const data = await apiClient.getData(id);
const transformedCode = "export default ${JSON.stringify(data)};";
return { code: transformedCode, map: null };
} catch (error) {
this.error(error); // Use this.error for Rollup error handling.
}
}
return null;
},
};
}
"""
### 2.2. Service Abstraction Layer
*Useful when you need to interact with multiple APIs or backend services, potentially from different providers.*
**Standards:**
* **Do This:** Define a consistent interface (abstract class or TypeScript interface) for interacting with different backend services.
* **Don't Do This:** Directly couple Rollup plugins to specific API implementations.
* **Do This:** Create separate service adapter implementations for each backend service.
* **Don't Do This:** Mix different API client logic within a single service adapter.
* **Do This:** Inject service adapters into Rollup plugins using dependency injection.
* **Don't Do This:** Instantiate service adapters directly within the plugin scope. Dependency injection simplifies testing and configuration.
**Example:**
"""typescript
// service-interface.ts
export interface IDataService {
fetchData(id: string): Promise;
}
"""
"""typescript
// api-service-adapter.ts
import axios from 'axios';
import { IDataService } from './service-interface';
export class ApiDataService implements IDataService {
private readonly client;
private readonly baseUrl:string;
private readonly apiKey: string | undefined;
constructor(baseUrl: string, apiKey?: string) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
this.client = axios.create({
baseURL: this.baseUrl,
timeout: 5000,
headers:{
'Content-Type': 'application/json',
...(this.apiKey ? {'Authorization': "Bearer ${this.apiKey}"} : {})
}
});
}
async fetchData(id: string): Promise {
try {
const response = await this.client.get("/data/${id}");
return response.data;
} catch (error) {
console.error('API error:', error);
throw new Error("Failed to fetch data for id ${id}: ${error.message}");
}
}
}
"""
"""javascript
// rollup-plugin.js
import { ApiDataService } from './api-service-adapter';
export default function myRollupPlugin(options = {}) {
const dataService = options.dataService || new ApiDataService(process.env.API_URL, process.env.API_KEY);
return {
name: 'my-rollup-plugin',
async transform(code, id) {
if (id.endsWith('.my-file')) {
try {
const data = await dataService.fetchData(id);
const transformedCode = "export default ${JSON.stringify(data)};";
return { code: transformedCode, map: null };
} catch (error) {
this.error(error); // Use this.error for Rollup error handling.
}
}
return null;
},
};
}
"""
### 2.3. GraphQL Client Integration
*For interacting with GraphQL APIs*
GraphQL provides a flexible and efficient way to fetch data, especially when you need to retrieve specific fields or aggregate data from multiple sources. Integrating a GraphQL client into your Rollup plugin can significantly simplify data fetching from GraphQL APIs.
**Standards:**
* **Do This:** Use a GraphQL client library like "apollo-client" or "graphql-request" to interact with GraphQL APIs.
* **Don't Do This:** Construct GraphQL queries manually as strings, prone to errors and difficult to maintain.
* **Do This:** Define GraphQL queries as separate files or template literals to enhance readability.
**Example:**
"""javascript
// graphql-client.js
import { GraphQLClient, gql } from 'graphql-request';
const API_URL = process.env.GRAPHQL_API_URL || 'https://api.example.com/graphql';
const API_KEY = process.env.MY_API_KEY;
const client = new GraphQLClient(API_URL, {
headers: {
Authorization: "Bearer ${API_KEY}",
},
});
const GET_DATA = gql"
query GetData($id: ID!) {
data(id: $id) {
field1
field2
field3
}
}
";
export async function fetchData(id) {
try {
const data = await client.request(GET_DATA, { id });
return data.data;
} catch (error) {
console.error('GraphQL error:', error);
throw new Error("Failed to fetch data for id ${id}: ${error.message}");
}
}
"""
"""javascript
// rollup-plugin.js
import { fetchData } from './graphql-client.js';
export default function myRollupPlugin() {
return {
name: 'my-rollup-plugin',
async transform(code, id) {
if (id.endsWith('.my-file')) {
try {
const data = await fetchData(id);
const transformedCode = "export default ${JSON.stringify(data)};";
return { code: transformedCode, map: null };
} catch (error) {
this.error(error); // Use this.error for Rollup error handling.
}
}
return null;
},
};
}
"""
## 3. Data Caching and Optimization
Effective data caching is critical for minimizing API calls and optimizing Rollup build times. Consider using both in-memory and persistent caching strategies.
**Standards:**
* **Do This:** Cache API responses in memory during a single Rollup build to avoid redundant calls. A simple "Map" object can often suffice.
* **Don't Do This:** Fetch the same data from the API multiple times within the same build.
* **Do This:** Implement persistent caching (e.g., using a file system cache or a database) to store API responses between builds. Use a cache key based on relevant parameters to invalidate the cache when underlying data changes. Libraries like "node-persist" simplify persistent caching.
* **Don't Do This:** Neglect persistent caching, which can significantly improve build times for projects with static or infrequently changing API data.
* **Do This:** Use appropriate cache invalidation strategies: implement time-based expiration, etags, or webhooks (if the API provides them).
* **Don't Do This:** Cache data indefinitely without any mechanism for invalidation.
* **Do This:** Utilize memoization techniques for frequently called functions that fetch data. Libraries like "lodash.memoize" can be helpful.
* **Don't Do This:** Over-memoize, as unnecessary memoization can add overhead.
**Example (In-Memory Caching):**
"""javascript
// rollup-plugin.js
import apiClient from './api-client.js';
export default function myRollupPlugin() {
const cache = new Map(); // In-memory cache
return {
name: 'my-rollup-plugin',
async transform(code, id) {
if (id.endsWith('.my-file')) {
const cacheKey = id; // Simple cache key
if (cache.has(cacheKey)) {
return { code: cache.get(cacheKey), map: null }; // Return from cache
}
try {
const data = await apiClient.getData(id);
const transformedCode = "export default ${JSON.stringify(data)};";
cache.set(cacheKey, transformedCode); // Store in cache
return { code: transformedCode, map: null };
} catch (error) {
this.error(error);
}
}
return null;
},
};
}
"""
**Example (Persistent Caching with "node-persist"):**
"""javascript
// rollup-plugin.js
import apiClient from './api-client.js';
import storage from 'node-persist';
export default function myRollupPlugin() {
let cache;
return {
name: 'my-rollup-plugin',
async buildStart() {
// Initialize cache on build start (once per build)
await storage.init({ dir: '.rollup_cache' });
cache = storage;
},
async transform(code, id) {
if (id.endsWith('.my-file')) {
const cacheKey = id; // Example cache key
const cachedData = await cache.getItem(cacheKey);
if (cachedData) {
console.log ("Cache Hit")
return { code: cachedData, map: null }; // Return from cache
}
try {
const data = await apiClient.getData(id);
const transformedCode = "export default ${JSON.stringify(data)};";
await cache.setItem(cacheKey, transformedCode); // Store in storage
return { code: transformedCode, map: null };
} catch (error) {
this.error(error);
}
}
return null;
},
};
}
"""
## 4. Error Handling and Resilience
Robust error handling is essential for preventing unexpected build failures and providing informative error messages to developers.
**Standards:**
* **Do This:** Use "try...catch" blocks to handle potential errors during API calls.
* **Don't Do This:** Ignore API errors, which can lead to silent build failures.
* **Do This:** Use Rollup's "this.error()" and "this.warn()" methods to report errors and warnings during the build process.
* **Don't Do This:** Use "console.log()" or "console.error()" for logging errors, use the Rollup logging mechanisms instead.
* **Do This:** Implement retry mechanisms with exponential backoff for handling transient API errors (e.g., network timeouts). Libraries like "p-retry" are particularly useful.
* **Don't Do This:** Retry API calls indefinitely without any limits or backoff strategy.
* **Do This:** Provide informative error messages that include the API endpoint, request parameters, and the underlying error details.
* **Don't Do This:** Display cryptic or unhelpful error messages.
**Example:**
"""javascript
//rollup-plugin.js
import apiClient from './api-client.js';
import pRetry from 'p-retry';
export default function myRollupPlugin() {
return {
name: 'my-rollup-plugin',
async transform(code, id) {
if (id.endsWith('.my-file')) {
const fetchDataWithRetry = async () => {
try {
return await apiClient.getData(id);
} catch (error) {
console.log("Retrying API call");
throw error; // Re-throw the error for pRetry to handle
}
};
try {
const data = await pRetry(fetchDataWithRetry, {
retries: 3,
onFailedAttempt: error => {
console.log("Attempt ${error.attemptNumber} failed. There are ${error.retriesLeft} retries left.");
}
});
const transformedCode = "export default ${JSON.stringify(data)};";
return { code: transformedCode, map: null };
} catch (error) {
this.error("Failed to fetch data for ${id} after multiple retries: ${error.message}");
}
}
return null;
},
};
}
"""
## 5. Security Considerations
Secure API integration is paramount to preventing data breaches and unauthorized access.
**Standards:**
* **Do This:** Store API keys and other sensitive credentials using environment variables.
* **Don't Do This:** Hardcode API keys directly in the source code or configuration files.
* **Do This:** Use HTTPS for all API communication to encrypt data in transit.
* **Don't Do This:** Use HTTP for API communication, which exposes data to eavesdropping.
* **Do This:** Validate and sanitize data received from APIs to prevent injection attacks.
* **Don't Do This:** Directly use API data in code without proper validation or sanitization.
* **Do This:** Implement proper authorization and authentication mechanisms for accessing APIs. Use bearer tokens, API keys, or OAuth 2.0 where possible.
* **Don't Do This:** Rely on weak or non-existent authentication methods.
* **Do This:** Implement rate limiting to protect against denial-of-service (DoS) attacks. Consider using libraries like "rate-limiter-flexible".
* **Don't Do This:** Allow unlimited API requests, which can overload the API server.
## 6. Testing API Integrations
Thoroughly testing API integrations is important for ensuring that your Rollup plugins function correctly and reliably.
**Standards:**
* **Do This:** Write unit tests for your API client modules and service adapters. Mock the API requests using libraries like "nock" or "jest.mock()" to avoid making actual API calls during testing.
* **Don't Do This:** Skip unit testing of API integration logic.
* **Do This:** Write integration tests that verify the end-to-end behavior of your Rollup plugins with real API endpoints in a test environment.
* **Don't Do This:** Rely solely on manual testing to verify API integrations.
* **Do This:** Use environment variables to configure API endpoints and credentials for testing purposes.
* **Don't Do This:** Hardcode test API endpoints or credentials in the test code.
* **Do This:** Create dedicated test data sets that are representative of the data that you expect to receive from the API.
* **Don't Do This:** Use arbitrary or unrealistic data in your tests.
## 7. Modern Rollup Plugins and API's
Consider the latest Rollup Plugin architecture and the implications for consuming data from external APIs.
**Standards:**
* **Do This:** Use the "this.emitFile" hook to output data as files, and then have Rollup process these as assets.
* **Don't Do This:** Avoid complex "transform" functions that manipulate code strings directly, favoring instead small data files for Rollup to manipulate.
* **Do This:** Use Rollup's "watch" capabilities (through "this.addWatchFile") to automatically refresh data from the API when relevant configuration files change. This ensures the build automatically re-runs.
* **Don't Do This:** Ignore watch capabilities, and rely on users manually re-running the build.
danielsogl
Created Mar 6, 2025
This guide explains how to effectively use .clinerules
with Cline, the AI-powered coding assistant.
The .clinerules
file is a powerful configuration file that helps Cline understand your project's requirements, coding standards, and constraints. When placed in your project's root directory, it automatically guides Cline's behavior and ensures consistency across your codebase.
Place the .clinerules
file in your project's root directory. Cline automatically detects and follows these rules for all files within the project.
# Project Overview project: name: 'Your Project Name' description: 'Brief project description' stack: - technology: 'Framework/Language' version: 'X.Y.Z' - technology: 'Database' version: 'X.Y.Z'
# Code Standards standards: style: - 'Use consistent indentation (2 spaces)' - 'Follow language-specific naming conventions' documentation: - 'Include JSDoc comments for all functions' - 'Maintain up-to-date README files' testing: - 'Write unit tests for all new features' - 'Maintain minimum 80% code coverage'
# Security Guidelines security: authentication: - 'Implement proper token validation' - 'Use environment variables for secrets' dataProtection: - 'Sanitize all user inputs' - 'Implement proper error handling'
Be Specific
Maintain Organization
Regular Updates
# Common Patterns Example patterns: components: - pattern: 'Use functional components by default' - pattern: 'Implement error boundaries for component trees' stateManagement: - pattern: 'Use React Query for server state' - pattern: 'Implement proper loading states'
Commit the Rules
.clinerules
in version controlTeam Collaboration
Rules Not Being Applied
Conflicting Rules
Performance Considerations
# Basic .clinerules Example project: name: 'Web Application' type: 'Next.js Frontend' standards: - 'Use TypeScript for all new code' - 'Follow React best practices' - 'Implement proper error handling' testing: unit: - 'Jest for unit tests' - 'React Testing Library for components' e2e: - 'Cypress for end-to-end testing' documentation: required: - 'README.md in each major directory' - 'JSDoc comments for public APIs' - 'Changelog updates for all changes'
# Advanced .clinerules Example project: name: 'Enterprise Application' compliance: - 'GDPR requirements' - 'WCAG 2.1 AA accessibility' architecture: patterns: - 'Clean Architecture principles' - 'Domain-Driven Design concepts' security: requirements: - 'OAuth 2.0 authentication' - 'Rate limiting on all APIs' - 'Input validation with Zod'
# Performance Optimization Standards for Rollup This document outlines coding standards focused specifically on performance optimization when using Rollup. Adhering to these guidelines will help improve application speed, responsiveness, and resource usage. We will focus on modern approaches and patterns relevant to the latest versions of Rollup. ## General Principles * **Minimize Bundle Size:** Smaller bundles download and parse faster, leading to quicker application startup times. * **Optimize Dependency Graph:** Efficient module resolution and tree-shaking are crucial for removing dead code and minimizing dependencies. * **Leverage Code Splitting:** Divide your application into smaller, on-demand chunks to reduce initial load time. * **Use Efficient Plugins & Configurations:** Choose plugins and configurations that are optimized for performance. * **Profile and Analyze:** Regularly profile your builds to identify performance bottlenecks and areas for improvement. ## 1. Tree-Shaking and Dead Code Elimination ### 1.1 Strict Mode and ES Modules **Standard:** Always use strict mode (""use strict";") and ES modules ("import"/"export") for optimal tree-shaking. **Why:** Rollup relies on static analysis enabled by ES modules to determine which parts of your code are used and which can be safely removed. Strict mode helps prevent unintended side effects that can hinder accurate analysis. **Do This:** """javascript // my-module.js "use strict"; export function add(a, b) { return a + b; } export function subtract(a, b) { return a - b; } """ """javascript // main.js import { add } from './my-module.js'; console.log(add(5, 3)); // 8 """ **Don't Do This:** (Common Anti-Pattern: CommonJS) """javascript // my-module.js (Avoid CommonJS) module.exports = { add: function(a, b) { return a + b; }, subtract: function(a, b) { return a - b; } }; """ **Explanation:** Using CommonJS ("module.exports" and "require") makes it very difficult for Rollup to figure out which exports are *actually* used, potentially preventing it from eliminating dead code. ES modules provide static structure which enables precise tree-shaking. ### 1.2 Side Effects Configuration **Standard:** Explicitly declare side effects in your "package.json". **Why:** By default, Rollup assumes that any module might have side effects (e.g., modifying global state). Declaring modules as side-effect-free allows Rollup to safely remove entire modules or parts of modules if they aren't directly used, further reducing the bundle size. **Do This:** (Example in "package.json") """json { "name": "my-library", "version": "1.0.0", "sideEffects": false, "main":"dist/index.cjs", "module": "dist/index.mjs", "exports": { ".": { "import": "./dist/index.mjs", "require": "./dist/index.cjs" } }, "files": [ "dist" ], "devDependencies": { "rollup": "^4.0.0" }, "scripts": { "build": "rollup -c" } } """ """javascript // rollup.config.js import { terser } from 'rollup-plugin-terser'; export default { input: 'src/index.js', output: [ { file: 'dist/index.mjs', format: 'es', sourcemap: true }, { file: 'dist/index.cjs', format: 'cjs', sourcemap: true } ], plugins: [ terser() // Minify the output with Terser ] }; """ If only specific files in your package have side effects, list them specifically: """json { "sideEffects": [ "./src/styles.css", "./src/global-init.js" ] } """ **Don't Do This:** Omit the "sideEffects" property, especially for libraries. This unnecessarily increases bundle sizes. **Explanation:** The "sideEffects" property tells Rollup whether a module execution causes any observable change outside of its scope. When set to "false", Rollup can safely remove the module if none of its exports are used. It provides critical hints to optimization tools. ### 1.3 Ensure Pure Functions **Standard:** Write functions that are "pure," meaning they produce the same output for the same input and have no side effects. **Why:** Pure functions are much easier for Rollup (and Terser via plugin) to optimize because their results can be memoized or inlined, and calls to unused pure functions can be safely removed, as they don't cause any unexpected behavior. **Do This:** """javascript // Pure function export function calculateArea(width, height) { return width * height; } """ **Don't Do This:** """javascript // Impure function (modifies external state) let totalArea = 0; export function addToTotalArea(width, height) { totalArea += width * height; return totalArea; } """ **Explanation:** Impure functions (like "addToTotalArea") are difficult to optimize because their behavior depends on external state and can have side effects. Rollup may be forced to include them even if their return value isn't directly used. ## 2. Code Splitting ### 2.1 Dynamic Imports **Standard:** Use dynamic imports ("import()") to split your code into smaller chunks loaded on demand. **Why:** Dynamic imports allow you to load modules asynchronously only when needed, which significantly reduces the initial load time of your application. **Do This:** """javascript // main.js async function loadComponent() { const { default: MyComponent } = await import('./my-component.js'); const component = new MyComponent(); document.body.appendChild(component.render()); } document.getElementById('load-button').addEventListener('click', loadComponent); """ """javascript // my-component.js export default class MyComponent { render() { const element = document.createElement('div'); element.textContent = 'This is my component!'; return element; } } """ **Don't Do This:** Load all modules upfront, even those only needed under specific conditions. This defeats the purpose of code splitting. **Explanation:** When Rollup encounters a dynamic "import()", it creates a separate chunk for the imported module. This chunk is only loaded when the "import()" statement is executed. Helps user experience by loading parts needed immediately and fetching the rest in the background. ### 2.2 Manual Chunks **Standard:** Use the "manualChunks" option in your Rollup configuration to define custom chunks. **Why:** The "manualChunks" option provides fine-grained control over how your code is split into chunks, allowing you to optimize for specific use cases (e.g., vendor libraries, common utility functions). **Do This:** (Example in "rollup.config.js") """javascript import { terser } from 'rollup-plugin-terser'; export default { input: 'src/index.js', output: { dir: 'dist', format: 'es', // Ensure ES Modules output sourcemap: true, chunkFileNames: 'chunks/[name]-[hash].js', manualChunks: { vendor: ['lodash', 'moment'], // create a vendor chunk utils : ['./src/utils/helper.js'] }, }, plugins: [ terser() ] }; """ """javascript // main.js import _ from 'lodash'; import moment from 'moment'; import { helperFunction } from './utils/helper.js'; console.log(_.chunk([1, 2, 3, 4, 5], 2)); console.log(moment().format('MMMM Do YYYY, h:mm:ss a')); console.log(helperFunction()); """ **Don't Do This:** Rely solely on Rollup's default chunking behavior. Manual chunking can dramatically improve performance in many situations by avoiding unnecessary duplication of code. **Explanation:** The "manualChunks" configuration option uses a function or an object to define custom chunks. The example consolidates "lodash" and "moment" into a "vendor" chunk, which can be cached separately by the browser and reused across multiple pages. ### 2.3 Entry Points for Separate Pages **Standard:** Create distinct entry points for different pages or sections of your application to facilitate code splitting. **Why:** This ensures that only the code required for a specific page is loaded initially, improving page load times and reducing resource consumption. **Do This:** """ src/ index.js (Entry point for the main app) about.js (Entry point for the about page) contact.js (Entry point for the contact page) components/ (Shared components) """ """javascript // rollup.config.js export default { input: { index: 'src/index.js', about: 'src/about.js', contact: 'src/contact.js' }, output: { dir: 'dist', format: 'es', chunkFileNames: 'chunks/[name]-[hash].js', // Important for clarity } }; """ **Explanation:** Each input file ("index.js", "about.js", "contact.js") becomes a separate entry point, which directs Rollup to generate dedicated chunks for main app, about page, and contact page. ## 3. Plugin Optimization ### 3.1 Minimize Plugin Usage **Standard:** Use only the plugins that are strictly necessary for your build process and evaluate their performance impact. **Why:** Each plugin adds overhead to the build process. Unnecessary or inefficient plugins can significantly slow down your builds. **Do This:** Analyze plugin impact and remove any unused plugins. Benchmark your builds with and without specific plugins. If you find a plugin causing performance issues, investigate alternatives or create a custom solution. **Don't Do This:** Add plugins without thoroughly understanding their impact on build performance. **Explanation:** Plugin performance varies. Some plugins might perform computationally intensive tasks, like code transformations or file system operations. Only use the essential ones to keep build times down. ### 3.2 Optimize Plugin Configuration **Standard:** Configure your plugins to be as efficient as possible. **Why:** Many plugins offer configuration options that can significantly affect their performance. By carefully adjusting these options, you can optimize plugin behavior for your specific needs. **Do This:** (Example using "rollup-plugin-terser" and "rollup-plugin-esbuild") """javascript // rollup.config.js import { terser } from 'rollup-plugin-terser'; import esbuild from 'rollup-plugin-esbuild'; export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'es', sourcemap: true }, plugins: [ esbuild({ minify: process.env.NODE_ENV === 'production', // Only minify in production target: 'es2020', // Specify target environment jsxFactory: 'React.createElement', // Configure JSX if needed jsxFragment: 'React.Fragment', }), terser({ compress: { //Optimized settings for maximum compression while maintaining runtime compatibility passes: 3, //Increased passes for better compression results unsafe: true, //Enables potentially unsafe transformations unsafe_comps: true, //Enables unsafe comparisons/optimizations }, mangle: true, //Enable name mangling }) ] }; """ **Don't Do This:** Use default plugin configurations without considering their performance implications. **Explanation:** The "terser" plugin's compression options can be tweaked to control the level of code minification, with tradeoffs between build time and output size. Similarly, "rollup-plugin-esbuild" provides excellent performance out of the box, but can be further tuned based on your app's specific JSX or ES version requirements. Setting the "minify" option appropriately ensures terser does not run in development mode. Consider using environment variables to switch between production and development settings. ### 3.3 Consider Alternative Plugins **Standard:** Explore alternative plugins that provide similar functionality with better performance. **Why:** The Rollup ecosystem is constantly evolving, and new plugins emerge that may offer significant performance improvements over existing ones. **Do This:** Evaluate new plugins based on benchmarks comparing their speed and output size against existing plugins on your project. **Example:** Consider using "rollup-plugin-esbuild" or swc instead of Babel for faster transpilation. Esbuild and SWC are written in Go and Rust, respectively, making them significantly faster at transpilation and minification than Babel, which is written in JavaScript. ## 4. Output Format ### 4.1 ES Modules Format **Standard:** Prefer the ES module format ("format: 'es'") for modern browsers and bundlers. **Why:** ES modules are the standard format for modern JavaScript development and offer the best tree-shaking and code-splitting capabilities. **Do This:** """javascript // rollup.config.js export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'es', sourcemap: true } }; """ **Don't Do This:** Use legacy formats like UMD or CommonJS unless you need to support older environments with no ES module support. **Explanation:** While UMD and CommonJS have their uses, ES modules provide a structured way for module resolution and optimization that is well-suited for modern web development workflows. Outputting multiple formats can increase build time and complexity when it's not necessary. ### 4.2 Optimize Sourcemaps **Standard:** Use sourcemaps strategically. **Why:** Sourcemaps are essential for debugging, but generating them can add overhead to the build process and increase the size of output files. **Do This:** * Enable sourcemaps in development for easier debugging. * Consider disabling or using inline sourcemaps in production to reduce file size. * Test the performance differences to ensure you can debug efficiently. **Example:** """javascript // rollup.config.js export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'es', sourcemap: process.env.NODE_ENV === 'development' ? 'inline' : false } }; """ **Explanation:** Using "'inline'" sourcemaps embeds the sourcemap directly in the JavaScript file, reducing the number of requests required by the browser, but increasing the file size; disable sourcemaps entirely in production if minimizing file size is the top priority, accepting potential debugging difficulties. ### 4.3 Minification and Compression **Standard:** Minify your code in production environments to reduce bundle size. **Why:** Minification removes whitespace, comments, and other unnecessary characters from your code, which can significantly reduce the size of your JavaScript files. **Do This:** * Use a minification plugin like "rollup-plugin-terser". * Configure the minification plugin to use aggressive compression settings in production. * Use Brotli or Gzip Compression on your server """javascript // rollup.config.js import { terser } from 'rollup-plugin-terser'; export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'es', sourcemap: true }, plugins: [ terser({ compress: { drop_console: true // Remove console.log statements } }) ] }; """ **Explanation:** The "terser" plugin can be configured to remove "console.log" statements, minify variable names, and perform other optimizations to reduce the size of your code. Employing Brotli (if available) or Gzip compression on your web server further reduces the file size of served assets. ## 5. Asynchronous Operations and Lazy Loading ### 5.1 Prioritize Asynchronous Operations **Standard:** Favor asynchronous operations (e.g., "async/await", "Promise.all") to avoid blocking the main thread. **Why:** Synchronous operations can block the main thread, causing the user interface to freeze. Asynchronous operations allow the browser to perform other tasks while waiting for the operation to complete, improving responsiveness. **Do This:** """javascript async function fetchData() { const response = await fetch('/api/data'); const data = await response.json(); return data; } """ **Don't Do This:** Perform long-running synchronous operations on the main thread. **Explanation:** Using "async/await" makes asynchronous code easier to read and understand. The "fetch" API is inherently asynchronous, preventing the main thread from blocking. ### 5.2 Lazy Loading of Resources **Standard:** Lazy load images, videos, and other resources that are not initially visible on the page. **Why:** Lazy loading prevents the browser from downloading resources that are not immediately needed, reducing initial load time and bandwidth consumption. **Do This:** * Use the "loading="lazy"" attribute on "<img>" elements. * Use a JavaScript library like "lozad.js" for more advanced lazy loading techniques. """html <img src="my-image.jpg" loading="lazy" alt="My Image"> """ **Explanation:** The "loading="lazy"" attribute tells the browser to only load the image when it is about to enter the viewport. ## 6. Profiling and Analysis ### 6.1 Analyze Bundle Contents **Standard:** Use tools like "rollup-plugin-visualizer" to analyze the contents of your bundles. **Why:** Bundle analysis tools provide insights into the size and composition of your bundles, helping you identify large dependencies and potential areas for optimization. **Do This:** """javascript // rollup.config.js import { visualizer } from 'rollup-plugin-visualizer'; export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'es', sourcemap: true }, plugins: [ visualizer({ template: 'treemap', // Use a treemap visualization open: true, // Automatically open the visualization in the browser filename: 'dist/stats.html' // Output file name }) ] }; """ **Explanation:** The "rollup-plugin-visualizer" generates an interactive treemap visualization that shows the size of each module in your bundle. You can identify large dependencies and consider alternatives or code splitting strategies. ### 6.2 Measure Performance **Standard:** Use browser developer tools and performance monitoring tools (e.g., Lighthouse, WebPageTest) to measure the performance of your application. **Why:** Profiling your application in real-world conditions is crucial for identifying performance bottlenecks and validating the effectiveness of your optimization efforts.
# Core Architecture Standards for Rollup This document outlines the core architectural standards for developing Rollup plugins, core functionalities, and related tools. Adhering to these standards ensures maintainability, performance, security, and consistency across the project. It targets both human developers and AI coding assistants. ## 1. Fundamental Architectural Patterns ### 1.1 Modular Design **Standard:** Implement modular design principles to promote code reusability, testability, and maintainability. Rollup's core and plugins should be composed of independent, well-defined modules with minimal dependencies. **Why:** * **Maintainability:** Easier to understand, modify, and debug individual modules. * **Testability:** Modules can be tested in isolation, leading to more robust and reliable code. * **Reusability:** Modules can be reused across different parts of the codebase or in other projects. **Do This:** * Break down large functionalities into smaller, focused modules. * Use dependency injection to decouple modules. * Ensure each module has a clear and well-defined responsibility. **Don't Do This:** * Create monolithic components with tightly coupled dependencies. * Overload modules with multiple responsibilities. **Example:** """javascript // src/moduleA.js export function doSomething(input) { // ... complex logic return result; } // src/moduleB.js import { doSomething } from './moduleA'; export function useModuleA(data) { const processedData = doSomething(data); // ... further processing return finalResult; } """ ### 1.2 Event-Driven Architecture **Standard:** Leverage Rollup's plugin hooks (e.g., "buildStart", "resolveId", "transform", "generateBundle") to create an event-driven architecture for extending Rollup's functionality. **Why:** * **Extensibility:** Plugins can tap into Rollup's build process without modifying the core. * **Decoupling:** Plugins operate independently, reducing the risk of conflicts or unexpected side effects. * **Flexibility:** Allows developers to customize Rollup's behavior to suit specific project needs. **Do This:** * Use appropriate plugin hooks to intercept and modify Rollup's build process. * Ensure event handlers are efficient and avoid blocking the main thread. * Provide clear and consistent plugin options for customization. **Don't Do This:** * Directly modify Rollup's core code to add new features. * Create plugin hooks that conflict with existing ones. **Example:** """javascript // rollup-plugin-example.js export default function examplePlugin(options = {}) { return { name: 'example-plugin', transform(code, id) { if (options.applyTo && !options.applyTo.test(id)) { return null; } const transformedCode = code.replace(/foo/g, 'bar'); return { code: transformedCode, map: null // If you create a sourcemap, return it here }; }, generateBundle(options, bundle, isWrite) { // Access and manipulate the generated bundle for (const fileName in bundle) { const chunk = bundle[fileName]; if (chunk.type === 'chunk') { chunk.code = chunk.code.replace(/console\.log/g, '//console.log'); //Example: Remove console.log statements } } } }; } """ ### 1.3 Data Flow and Immutability **Standard:** Maintain a clear and predictable data flow throughout the build process. Emphasize immutability to prevent unexpected state changes and simplify debugging. **Why:** * **Predictability:** Easier to reason about the behavior of the code. * **Debuggability:** Immutability simplifies tracking down errors. * **Performance (potentially):** Although immutability can introduce overhead, it can also enable optimizations (e.g., memoization, structural sharing). **Do This:** * Use immutable data structures where appropriate (e.g., "Object.freeze", libraries like Immutable.js or Immer if needed for complex scenarios - but only when justified by performance bottlenecks). * Avoid modifying input parameters directly within functions. * Return new objects/arrays instead of mutating existing ones. **Don't Do This:** * Rely on mutable state to track changes across different phases of the build. * Modify input parameters directly within functions without creating copies. **Example:** """javascript function processData(data) { // Create a new object instead of modifying the original const newData = { ...data, processed: true }; return newData; } const originalData = { value: 'hello' }; const processedData = processData(originalData); console.log(originalData); // { value: 'hello' } Original remains unchanged console.log(processedData); // { value: 'hello', processed: true } New object with modification """ ## 2. Project Structure and Organization ### 2.1 Directory Structure **Standard:** Follow a consistent and well-defined directory structure for all Rollup projects, plugins, and related tools. **Example:** """ rollup-project/ ├── src/ # Source code │ ├── core/ # Core modules │ │ ├── index.js # Entry point for core module │ │ └── utils.js # Utility functions │ ├── plugins/ # Plugin-related modules │ │ ├── pluginA.js # Plugin A implementation │ │ └── pluginB.js # Plugin B implementation │ └── index.js # Main entry point for the project ├── test/ # Unit Tests │ ├── core/ # Core module tests │ │ └── utils.test.js # Tests for utils.js │ ├── plugins/ │ │ └── pluginA.test.js # Tests for pluginA.js ├── dist/ # Output files (generated by Rollup) ├── rollup.config.js # Rollup configuration file ├── package.json # Project dependencies and metadata └── README.md # Project documentation """ **Why:** * **Discoverability:** Easily locate specific files and functionalities. * **Maintainability:** Consistent structure simplifies navigation and understanding of the project. * **Scalability:** Well-organized structure facilitates adding new features and modules. **Do This:** * Separate source code, tests, and build artifacts into dedicated directories. * Use descriptive names for directories and files. * Organize modules within "src" based on their functionality. **Don't Do This:** * Mix source code, tests, and build artifacts in the same directory. * Use cryptic or ambiguous names for directories and files. * Create a deeply nested or overly complex directory structure. ### 2.2 Naming Conventions **Standard:** Use clear, descriptive, and consistent naming conventions for all variables, functions, classes, and files. Follow a consistent naming style (e.g., camelCase for variables and functions, PascalCase for classes). **Why:** * **Readability:** Easy to understand the purpose and functionality of code elements. * **Maintainability:** Consistent naming simplifies code modification and debugging. * **Collaboration:** Improves communication and understanding among developers. **Do This:** * Use descriptive names that accurately reflect the purpose of the code element. * Follow the camelCase convention for variables and functions (e.g., "myVariable", "calculateSum"). * Follow the PascalCase convention for classes and components (e.g., "MyComponent", "DataProcessor"). * Use UPPER_SNAKE_CASE for constants (e.g., "MAX_VALUE", "DEFAULT_SETTINGS"). **Don't Do This:** * Use single-character or ambiguous names (e.g., "x", "y", "data"). * Violate naming conventions (e.g., use PascalCase for variables). * Use inconsistent naming throughout the codebase. **Example:** """javascript // Variable names const userAge = 30; // Good: Descriptive and camelCase const a = 30; // Bad: Ambiguous // Function names function calculateTotal(price, quantity) { // Good: Descriptive and camelCase // ... } // Class names class UserProfile { // Good: PascalCase // ... } """ ### 2.3 Module Exports **Standard:** Use named exports for better code organization and tree-shaking capabilities unless a module has a clear and singular primary function. **Why:** * **Tree-shaking:** Enables Rollup to remove unused code during the build process. * **Readability:** Clearer identification of exported functions and variables. * **Maintainability:** Easier to refactor and update code without breaking dependencies. **Do This:** * Use named exports for most modules. * Use a default export only when a module has a single, primary function or component. **Don't Do This:** * Rely solely on default exports, especially for modules with multiple functions. * Mix named and default exports inconsistently. **Example:** """javascript // src/utils.js (Named exports) export function add(a, b) { return a + b; } export function subtract(a, b) { return a - b; } // src/MyComponent.js (Default export, assuming it's the main component) import React from 'react'; function MyComponent() { // ... return ( <div>My Component</div> ); } export default MyComponent; """ ## 3. Coding Style and Best Practices ### 3.1 ECMAScript Standards **Standard:** Adhere to the latest ECMAScript standards (ES2023 and beyond) for writing modern, efficient, and readable JavaScript code. **Why:** * **Modernity:** Leveraging the latest language features improves code quality and performance. * **Compatibility:** Ensures compatibility with modern browsers and environments. * **Readability:** Modern syntax often results in more concise and expressive code. **Do This:** * Use "const" and "let" for variable declarations instead of "var". * Use arrow functions for concise function definitions. * Use template literals for string interpolation. * Use destructuring for extracting values from objects and arrays. * Utilize modern features of the JS language. **Don't Do This:** * Use "var" for variable declarations. * Use traditional function expressions when arrow functions are more appropriate. * Use string concatenation instead of template literals. * Avoid destructuring when it can improve code readability. **Example:** """javascript // Variable declaration const name = 'John'; let age = 30; // Arrow function const multiply = (x, y) => x * y; // Template literal const message = "Hello, ${name}! You are ${age} years old."; // Destructuring const user = { firstName: 'John', lastName: 'Doe' }; const { firstName, lastName } = user; console.log(firstName, lastName); // John Doe """ ### 3.2 Asynchronous Programming **Standard:** Use "async/await" for handling asynchronous operations to improve code readability and error handling. Avoid deeply nested callbacks (callback hell). **Why:** * **Readability:** "async/await" makes asynchronous code look and behave like synchronous code. * **Error Handling:** "try/catch" blocks can be used to handle errors in asynchronous operations. * **Maintainability:** Easier to reason about and debug asynchronous code. **Do This:** * Use "async" keyword to define asynchronous functions. * Use "await" keyword to wait for the completion of asynchronous operations. * Use "try/catch" blocks to handle errors gracefully. **Don't Do This:** * Rely on deeply nested callbacks for handling asynchronous operations. * Ignore errors in asynchronous operations. **Example:** """javascript async function fetchData() { try { const response = await fetch('https://example.com/data'); const data = await response.json(); return data; } catch (error) { console.error('Error fetching data:', error); throw error; // Re-throw the error to be handled by the caller } } async function processData() { try { const data = await fetchData(); // ... process data ... } catch(error) { //Error already logged in FetchData, maybe do something more. } } """ ### 3.3 Error Handling **Standard:** Implement robust error handling mechanisms to prevent unexpected crashes and provide informative error messages. **Why:** * **Stability:** Ensures the application continues to function even when errors occur. * **Debuggability:** Provides valuable information for diagnosing and resolving errors. * **User Experience:** Prevents the application from crashing and provides user-friendly error messages. **Do This:** * Use "try/catch" blocks to handle exceptions. * Log errors with descriptive messages, including relevant context. * Provide meaningful error messages to the user when appropriate (without exposing sensitive information). * Consider using error tracking tools for monitoring and analyzing errors in production. **Don't Do This:** * Ignore exceptions or swallow errors silently. * Expose sensitive information in error messages. * Rely on generic error messages that provide no useful information. **Example:** """javascript try { // ... code that may throw an error ... if (x === 0) { throw new Error('Division by zero is not allowed.'); } const result = 10 / x; return result; } catch (error) { console.error('An error occurred:', error.message); // Log the error with context // Perhaps throw a custom error for caller to handle or retry. throw new CustomError("Division failed") } """ ### 3.4 Comments and Documentation **Standard:** Write clear, concise, and up-to-date comments and documentation to explain the purpose, functionality, and usage of code elements. Use JSDoc-style comments for documenting functions, classes, and modules. **Why:** * **Readability:** Comments and documentation make it easier to understand the code. * **Maintainability:** Helps developers quickly grasp the functionality of code elements. * **Collaboration:** Facilitates communication and knowledge sharing among developers. **Do This:** * Write comments to explain complex logic or non-obvious code. * Use JSDoc-style comments to document functions, classes, and modules. * Keep comments and documentation up-to-date with the latest code changes. * Add documentation to public facing portions of the software. **Don't Do This:** * Write redundant comments that simply repeat what the code already says. * Leave outdated or inaccurate comments. * Neglect to document important code elements. **Example:** """javascript /** * Calculates the sum of two numbers. * * @param {number} a - The first number. * @param {number} b - The second number. * @returns {number} The sum of the two numbers. */ function add(a, b) { // This function adds two numbers together return a + b; } """ ## 4. Performance Optimization ### 4.1 Code Splitting **Standard:** Utilize Rollup's code splitting capabilities to create smaller bundles and improve initial load times. **Why:** * **Improved Load Times:** Smaller bundles download and parse faster, resulting in a better user experience. * **Caching:** Browsers can cache individual chunks, so only the changed code needs to be re-downloaded. * **Reduced Bandwidth Consumption:** Users only download the code they need. **Do This:** * Use dynamic imports ("import()") to create split points in your code. * Configure Rollup to generate multiple output chunks. **Don't Do This:** * Bundle the entire application into a single large chunk. * Over-split the code into too many small chunks, which can lead to increased request overhead. **Example:** """javascript // Dynamically import a module async function loadModule() { const module = await import('./myModule'); module.default(); } loadModule(); """ ### 4.2 Tree-Shaking **Standard:** Take advantage of Rollup's tree-shaking feature to eliminate unused code and reduce bundle size. **Why:** * **Smaller Bundles:** Removes dead code, resulting in smaller and more efficient bundles. * **Improved Performance:** Reduces the amount of code that needs to be downloaded and parsed. **Do This:** * Use ES modules (named exports) to enable tree-shaking. * Avoid side effects in your code. * Use "sideEffects: false" in your "package.json" to indicate that your code has no side effects (if applicable). **Don't Do This:** * Rely on CommonJS modules, which are not as effectively tree-shakable. * Introduce side effects that prevent Rollup from removing unused code. **Example:** """javascript // package.json { "name": "my-module", "version": "1.0.0", "sideEffects": false // Indicate no side effects } """ ### 4.3 Minimization and Compression **Standard:** Use a minifier (e.g., Terser) to reduce the size of the generated code. Enable Gzip or Brotli compression on the server to further reduce the file sizes transmitted to the browser. **Why:** * **Smaller File Sizes:** Minimization removes whitespace and shortens variable names, reducing file sizes. * **Improved Load Times:** Smaller files download and parse faster. * **Reduced Bandwidth Consumption:** Users download less data. **Do This:** * Configure Rollup to use a minifier plugin (e.g., "@rollup/plugin-terser"). * Enable Gzip or Brotli compression on the server. **Don't Do This:** * Deploy unminified or uncompressed code to production. **Example:** """javascript // rollup.config.js import terser from '@rollup/plugin-terser'; export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'iife' }, plugins: [ terser() // Minify the code ] }; """ ## 5. Security Best Practices ### 5.1 Dependency Management **Standard:** Use a dependency management tool (e.g., npm, yarn, pnpm) to manage project dependencies. Regularly update dependencies to patch security vulnerabilities. **Why:** * **Security:** Outdated dependencies may contain security vulnerabilities. * **Stability:** Dependency management ensures consistent versions across different environments. * **Reproducibility:** Makes it easier to recreate the project environment. **Do This:** * Use "npm install", "yarn install", or "pnpm install" to install dependencies. * Use "npm audit", "yarn audit", or "pnpm audit" to identify security vulnerabilities. * Regularly update dependencies to the latest versions. * Use "package-lock.json" or "yarn.lock" to lock dependency versions. **Don't Do This:** * Manually download and install dependencies. * Ignore security vulnerabilities reported by the dependency management tool. * Use outdated or unmaintained dependencies. ### 5.2 Input Validation **Standard:** Validate all external inputs to prevent injection attacks and other security vulnerabilities. **Why:** * **Security:** Prevents attackers from injecting malicious code or data into the application. * **Stability:** Ensures that the application behaves predictably even with invalid inputs. **Do This:** * Validate all user inputs, including form data, query parameters, and API requests. * Sanitize inputs to remove potentially harmful characters or code. * Use appropriate validation libraries or functions for specific data types. **Don't Do This:** * Trust external inputs without validation. * Store or process sensitive data without proper sanitization. ### 5.3 Secure Configuration **Standard:** Store sensitive configuration data (e.g., API keys, database passwords) securely using environment variables or dedicated configuration management tools. Avoid hardcoding sensitive data in the codebase. **Why:** * **Security:** Prevents sensitive data from being exposed in the codebase or version control system. * **Flexibility:** Allows you to easily change configuration settings without modifying the code. * **Environment Isolation:** Enables you to use different configuration settings for different environments (e.g., development, testing, production). **Do This:** * Store sensitive configuration data in environment variables. * Use a configuration management tool (e.g., "dotenv", "config") to manage configuration settings. * Avoid hardcoding sensitive data in the codebase. **Don't Do This:** * Store sensitive data in plain text files or in the codebase. * Commit sensitive data to version control. * Expose sensitive data in client-side code. **Example:** """javascript // .env file API_KEY=your_secret_api_key DATABASE_URL=your_database_connection_string // src/config.js import dotenv from 'dotenv'; dotenv.config(); const apiKey = process.env.API_KEY; // Access the API key from the environment variable const databaseUrl = process.env.DATABASE_URL; export { apiKey, databaseUrl }; """ These standards aim to improve the quality, consistency, and security of Rollup projects and provide a solid foundation for both human developers and AI coding assistants. Regular reviews and updates of these standards should be conducted to keep pace with advancements in the Rollup ecosystem and evolving security threats.
# Component Design Standards for Rollup This document outlines the coding standards for component design in Rollup projects. It's intended to guide developers in writing reusable, maintainable, and performant code, specifically within the Rollup ecosystem. These standards are tailored to reflect the latest best practices and features of Rollup projects. ## 1. Principles of Component Design in Rollup ### 1.1. Single Responsibility Principle (SRP) * **Standard:** Each Rollup plugin or transform should have a single, well-defined purpose. * **Do This:** Create separate plugins for different concerns like code minification, adding banners, and handling specific file types. * **Don't Do This:** Bundle multiple unrelated functionalities into a single, monolithic plugin. * **Why:** Promotes modularity, testability, and easier maintenance. Changes to one aspect don't inadvertently affect other parts of the system. """javascript // Do This: Separate plugins for different tasks // rollup.config.js import minify from 'rollup-plugin-terser'; import banner from 'rollup-plugin-banner'; export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'umd', name: 'MyModule' }, plugins: [ banner({ banner: '/* My Awesome Library */' }), minify() ] }; """ ### 1.2. Abstraction and Encapsulation * **Standard:** Abstract away complex implementation details within modules and plugins. Expose clear, well-defined interfaces. * **Do This:** Create a plugin that hides the complexity of a specific transformation process and provides simple options for customization. * **Don't Do This:** Expose internal workings or rely on undocumented behavior within the Rollup configuration. * **Why:** Simplifies usage, protects against accidental breakage caused by internal changes, and makes it easier to swap out implementations. """javascript // Do This: Encapsulate complex logic in a plugin // my-custom-plugin.js export default function myCustomPlugin(options = {}) { const { pattern, replacement } = options; return { name: 'my-custom-plugin', transform(code, id) { if (id.endsWith('.svelte')) return null; // don't run on svelte files if (!pattern || !replacement) return code; return code.replace(pattern, replacement); } }; } // rollup.config.js import myCustomPlugin from './my-custom-plugin.js'; export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'es' }, plugins: [ myCustomPlugin({ pattern: /__VERSION__/g, replacement: '1.2.3' }) ] }; """ ### 1.3. Don't Repeat Yourself (DRY) * **Standard:** Avoid duplicating code. Extract common functionality into reusable modules or helper functions. * **Do This:** If multiple plugins need to parse similar configuration options, create a shared utility function to handle the parsing. * **Don't Do This:** Copy and paste the same parsing logic into each plugin that needs it. * **Why:** Reduces redundancy, making code easier to update and less prone to errors. """javascript // Do This: Share utility functions // utils.js export function parseOptions(options) { // Logic to parse and validate options const parsedOptions = { ...options }; // basic example return parsedOptions; } // plugin1.js import { parseOptions } from './utils.js'; export default function plugin1(options = {}) { const parsed = parseOptions(options); // ... } // plugin2.js import { parseOptions } from './utils.js'; export default function plugin2(options = {}) { const parsed = parseOptions(options); // ... } """ ### 1.4. Composition over Inheritance * **Standard:** Favor composing plugin functionalities from smaller, independent plugins over creating complex inheritance hierarchies. * **Do This:** Create individual plugins for specific transformations and combine them in the Rollup configuration. * **Don't Do This:** Create a base plugin class with a complex inheritance structure for different transformation types. * **Why:** Promotes flexibility and reduces coupling between plugins. It's easier to mix and match functionality as needed. """javascript // Do This: Compose plugins // rollup.config.js import pluginA from './plugin-a.js'; import pluginB from './plugin-b.js'; import pluginC from './plugin-c.js'; export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'es' }, plugins: [ pluginA(), pluginB(), pluginC() ] }; """ ## 2. Creating Reusable Components (Plugins) ### 2.1. Plugin Structure * **Standard:** Follow the standard Rollup plugin structure, including a name and relevant lifecycle hooks. * **Do This:** Ensure your plugin exports a function that returns an object with a "name" property and appropriate lifecycle hooks (e.g., "transform", "renderChunk"). * **Don't Do This:** Export a simple object or directly modify the Rollup configuration. """javascript // Do This: Standard plugin structure export default function myPlugin(options = {}) { return { name: 'my-plugin', // Required: The name of the plugin transform(code, id) { // Optional: Transform code here }, renderChunk(code, chunk, options, meta) { // Optional: Alter the final chunk } }; } """ ### 2.2. Configuration Options * **Standard:** Design configuration options that are intuitive, well-documented, and validated. * **Do This:** Use descriptive option names, provide default values, and validate the types and values of received options. Utilize a schema validator library if necessary. * **Don't Do This:** Use obscure option names, assume default values, or fail to validate configuration options. * **Why:** Improves usability and prevents unexpected behavior due to invalid configurations. """javascript // Do This: Validate and provide defaults for options import { isString } from 'lodash-es'; // Or any other utility library export default function myPlugin(options = {}) { const { message = 'Hello', include } = options; if (!isString(message)) { throw new Error('message option must be a string'); } return { name: 'my-plugin', transform(code, id) { if (include && !id.includes(include)) { return null; } return "console.log("${message}");\n${code}"; } }; } // rollup.config.js import myPlugin from './my-plugin.js'; export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'es' }, plugins: [ myPlugin({ message: 'Custom Message', // Valid string include: 'src/' }) ] }; """ ### 2.3. Handling Dependencies * **Standard:** Declare and manage plugin dependencies explicitly. * **Do This:** Specify peer dependencies to avoid version conflicts with the consuming projects. Consider using "rollup-plugin-node-resolve" to resolve external dependencies of your plugin if required. * **Don't Do This:** Bundle dependencies directly into the plugin if they are also likely to be used in the consuming project. * **Why:** Avoids dependency conflicts and ensures predictable behavior. """json // Do This: Package.json with peer dependencies { "name": "my-rollup-plugin", "version": "1.0.0", "peerDependencies": { "lodash-es": "^4.0.0", // example "rollup": "^4.0.0" // Specify minimum supported Rollup version }, "devDependencies": { "rollup": "^4.0.0", "lodash-es": "^4.0.0" } } // rollup.config.js (in the consuming project) import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import myPlugin from 'my-rollup-plugin'; // Assumes installed from npm export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'es' }, plugins: [ resolve(), // Resolve node_modules commonjs(), // Convert CommonJS to ES modules myPlugin() // Use the plugin ] }; """ It's generally best practice to include "@rollup/plugin-node-resolve" and "@rollup/plugin-commonjs" in the *consumer's* "rollup.config.js" file, NOT bundled within the plugin itself. This provides the consumer with full control over versions and configuration. ### 2.4. Error Handling and Logging * **Standard:** Implement robust error handling and provide informative logging for debugging. * **Do This:** Use "this.error()" and "this.warn()" provided by Rollup to report errors and warnings. Provide context-specific messages and, when possible, include code snippets related to the error. * **Don't Do This:** Throw generic errors or rely on "console.log" for debugging in a production environment. Avoid verbose logs unless specifically enabled through an option. * **Why:** Aids in debugging and provides users with actionable information about potential issues. """javascript // Do This: Use this.error() and this.warn() export default function myPlugin(options = {}) { return { name: 'my-plugin', transform(code, id) { try { // Some potentially error-prone operation if (code.includes('invalid code')) { this.error({ message: 'Invalid code detected.', id }); // Add file ID // Or use this.error(new Error('Detailed error information')) } // ... } catch (error) { this.warn("Problem during transform: ${error.message}"); } return code; } }; } """ ### 2.5. Testing * **Standard:** Write comprehensive unit and integration tests for plugins. * **Do This:** Use a testing framework like Jest or Mocha and a Rollup testing utility to verify plugin functionality. Automate the tests using CI/CD. * **Don't Do This:** Rely on manual testing or skip testing altogether. * **Why:** Ensures reliability and prevents regressions. """javascript // Do This: Example test (using Jest) // my-plugin.test.js import { rollup } from 'rollup'; import myPlugin from './my-plugin.js'; import fs from 'node:fs/promises'; async function buildAndRun(options) { const bundle = await rollup({ input: 'test/fixtures/input.js', // Create a simple input file plugins: [myPlugin(options)] }); const outputOptions = { format: 'es' }; const { output } = await bundle.generate(outputOptions); return output[0].code; } it('should transform code correctly', async () => { const result = await buildAndRun({ message: 'Testing!' }); expect(result).toContain('Testing!'); }); it('should not transform files if include option is specified', async () => { await fs.writeFile('test/fixtures/input.js', 'console.log("hello");') const result = await buildAndRun({ message: 'Testing!', include: 'other' }); expect(result).toContain('console.log("hello");'); expect(result).not.toContain('Testing!'); }); """ Create testable output artifacts. Use "fs.writeFile("testfile.txt", code)" to save generated files into a "test" directory, then use node's "fs/promises" to read those files back in for comparison. This allows more accurate testing of different output configurations. ### 2.6. Documentation * **Standard:** Provide clear and comprehensive documentation for each plugin. * **Do This:** Include a README file with a description of the plugin, installation instructions, configuration options, usage examples, and contribution guidelines. Document public interfaces with JSDoc-style comments. * **Don't Do This:** Omit documentation or provide incomplete or outdated information. * **Why:** Makes the plugin easier to understand and use. ## 3. Modern Approaches and Patterns ### 3.1. ES Modules * **Standard:** Use ES modules for plugin development. * **Do This:** Use "export default function myPlugin() {}" for plugin definitions and "import" statements for dependencies. * **Don't Do This:** Use CommonJS modules ("module.exports", "require") unless absolutely necessary. * **Why:** ES modules are the standard for modern JavaScript development and provide better static analysis and tree-shaking capabilities. ### 3.2. Async/Await * **Standard:** Use "async/await" for asynchronous operations. * **Do This:** Use "async" functions with "await" to handle asynchronous tasks like file I/O or network requests. * **Don't Do This:** Use callbacks or promises directly unless you have a specific reason to do so, which is rare. * **Why:** Improves code readability and simplifies asynchronous control flow. """javascript // Do This: Async/await for asset loading import { readFile } from 'node:fs/promises'; export default function myPlugin(options = {}) { return { name: 'my-plugin', async load(id) { if (id.endsWith('template.html')) { try { const template = await readFile(id, 'utf-8'); return "export default ${JSON.stringify(template)};"; } catch (error) { this.error("Failed to load template: ${error.message}"); } } } }; } """ ### 3.3. Virtual Modules * **Standard:** Utilize "this.emitFile" for creating virtual modules within Rollup. * **Do This:** For dynamically generated code (e.g., from templates or schemas), use "this.emitFile" to inject them as virtual modules into the bundle. Specify "type: 'asset'" if it needs to be preserved as a standalone file. * **Don't Do This:** Directly manipulate the file system. * **Why:** Keeps intermediate files in memory, improving performance and cleanliness. """javascript // Do This: Create virtual modules export default function myPlugin() { return { name: 'my-plugin', buildStart() { const generatedCode = "export const value = ${Math.random()};"; this.emitFile({ type: 'chunk', // Or 'asset' if you want a file id: 'generated-module', name: 'generated', fileName: 'generated.js', code: generatedCode }); }, resolveId(source) { if (source === 'generated-module') { return 'generated-module'; // Resolve to the virtual module ID } return null; } }; } // In your .js files import { value } from 'generated-module' // will load it in """ ### 3.4 Source Maps * **Standard**: Ensure source maps are properly generated and handled by plugins. * **Do This**: When doing transformations, update the associated sourcemap using libraries like "magic-string" or similar utility. Rollup automatically chains sourcemaps from different plugins, so ensure your modifications preserve this chain. * **Don't do This**: Modifying code without adjusting the sourcemap as this will make debugging very hard. This is especially important for code generation plugins which create new files. * **Why**: Proper source map handling makes debugging transformed code much easier. """javascript import MagicString from 'magic-string'; export default function sourcemapPlugin() { return { name: "sourcemap-plugin", transform(code, id) { const magicString = new MagicString(code); magicString.prepend('/* This code was modified by sourcemap-plugin */\n'); magicString.append('\n/* End of modification by sourcemap-plugin */'); const map = magicString.generateMap({ source: id, includeContent:true }); //true is important return { code: magicString.toString(), map: map }; } } } """ ## 4. Security Considerations ### 4.1. Malicious Code Injection * **Standard:** Sanitize and validate any user-provided input that is used in code generation or transformations. * **Do This:** Use secure coding practices to prevent code injection vulnerabilities, especially when handling user-provided configuration options. When building output, escape strings properly. * **Don't Do This:** Directly insert user input into code without validation or sanitization. * **Why:** Prevents malicious code from being injected into the final bundle. ### 4.2. Dependency Vulnerabilities * **Standard:** Regularly audit and update dependencies to address known vulnerabilities. * **Do This:** Use tools like "npm audit" or "yarn audit" to identify and fix dependency vulnerabilities. Keep Rollup and its plugins updated. * **Don't Do This:** Ignore security warnings or use outdated dependencies. * **Why:** Reduces the risk of security exploits. ## 5. Performance Optimization ### 5.1. Minimize Plugin Overhead * **Standard:** Only use necessary transformations. * **Do This:** Be aware of the performance cost of unnecessary operations. Aim to create plugins performant and only use the ones that add proper value. * **Don't Do This:** Apply a kitchen sink of transformations without thinking if they actually add value. * **Why:** Avoid useless operation and spend CPU cycles unnecesarily ### 5.2. Leverage asynchronous operations * **Standard:** Parallelise long operations. * **Do This:** Whenever suitable, use "Promise.all" to parallelise operations happening over multiple files. Ensure that processing of the different files is independent or otherwise apply proper synchronisation with mutexes or other appropriate mechanisms. * **Don't Do This:** Perform intensive tasks in synchronised manner that slows down build process. * **Why:** Avoid bottlenecks and improve build times of the project ### 5.3 Code Splitting * **Standard**: Use code splitting to reduce bundle sizes. * **Do This**: Use the 'dynamic import' syntax and configure Rollup to create separate chunks for different parts of the application. This strategy can significantly improve the initial load time by only delivering necessary code when the application starts with deferred loading. * **Don't Do This**: Include all code into one large bundle. * **Why**: Improves initial load time and overall performance, especially for large applications. """javascript // Example: Dynamic import for code splitting async function loadComponent() { const { default: component } = await import('./my-component.js'); // Use the dynamically loaded component document.body.appendChild(component); } loadComponent(); """ ### 5.4 Cache Results * **Standard:** Cache intermediate results within plugins to reduce unnecessary recomputation. * **Do This:** Implement caching strategies, especially for operations that depend on external resources or computationally intensive calculations. Use the "this.cache" API to store the value. * **Don't Do This:** Recompute results unnecessarily on every build. * **Why:** Drastically improves performance of incremental builds. """javascript export default function cachePlugin(options = {}) { return { name: 'cache-plugin', transform(code, id) { const cachedResult = this.cache.get(id); if (cachedResult) { return cachedResult; } // Perform transformation const transformedCode = code + "// Modified by cache plugin" // very simple example; this.cache.set(id, transformedCode); return transformedCode; } }; } """ These component design standards provide a strong foundation for developing high-quality Rollup plugins and applications, which are important for maintainability, performance, and security. Following these practices will result in code that is easier to understand, debug and reuse, leading to more productive development workflows.
# State Management Standards for Rollup This document outlines the coding standards for state management within Rollup projects. It aims to provide a comprehensive guide for developers to write maintainable, performant, and secure code when handling application state in Rollup modules and plugins. It focuses on modern JavaScript and Rollup conventions, using up-to-date examples and best practices. This guide will help teams ensure consistent architecture, data flow, and reactivity across their Rollup ecosystem. ## 1. Principles of State Management in Rollup State management in Rollup differs from traditional frontend frameworks like React or Vue. Rollup is a *module bundler*, meaning it *combines* your code and *doesn't* directly manage runtime application state. However, the modules Rollup bundles *do*, and so how your modules are structured to manage their internal state is important for overall application design. We need to consider: * **Module Scoping:** Rollup encourages modularity. Modules should manage their own state, minimizing global state. * **Data Flow:** Understand how data flows through your application components and how this affects the bundling process. * **Side Effects**: Rollup enables side effects within modules, so handle them with care. ### 1.1. Standard: Prefer Module-Level Scoping * **Do This:** Encapsulate state within individual modules to promote reusability and prevent naming conflicts. * **Don't Do This:** Rely heavily on global variables or singletons for state, as these can lead to unintended side effects and make debugging difficult. **Why?** Module-level scope reduces the risk of variable collisions and improves code maintainability by encapsulating logic and state. Global variables make code harder to reason about because any part of the application can modify them unexpectedly. """javascript // Good: module using internal state let counter = 0; export function increment() { counter++; return counter; } export function decrement() { counter--; return counter; } """ """javascript // Bad: using a global variable window.globalCounter = 0; // Avoid! export function increment() { window.globalCounter++; return window.globalCounter; } """ ### 1.2. Standard: Be Explicit About Data Flow * **Do This:** Clearly define how data enters and exits your modules using function parameters and return values of the imported functions. * **Don't Do This:** Depend on implicit or magical state updates, which can make the application logic difficult to follow. **Why?** Explicit data flow increases code predictability, making it easier to trace and debug data-related issues. Predictable data flow is crucial for maintainability. """javascript // Good: Explicit data flow export function updateState(state, changes) { return { ...state, ...changes }; } // Usage: import { updateState } from './state-management'; let currentState = { name: 'Initial', value: 0 }; const newState = updateState(currentState, { value: 1 }); console.log(newState); // { name: 'Initial', value: 1 } """ """javascript // Bad: Implicit state update let internalState = { name: 'Initial', value: 0 }; export function updateValue(newValue) { internalState.value = newValue; // Avoid direct mutation! } """ ### 1.3. Standard: Control Side Effects * **Do This:** Isolate side effects (such as modifying external state) to specific functions and modules. * **Don't Do This:** Allow side effects to occur randomly throughout your code, as this makes debugging and testing far more difficult. **Why?** Controlling side effects helps in debugging and testing. By isolating side effects, you can quickly identify and fix issues when unexpected behavior occurs. Testing becomes more reliable and predictable. """javascript // Good: Controlled side effect within a module import { log } from './logger'; export function processData(data) { // Perform some calculations const result = data * 2; // Log the result (side effect) log("Processed data: ${result}"); return result; } """ """javascript // Bad: Random side effect - using console.log calls directly export function processData(data) { const result = data * 2; console.log("Processing data: ${result}"); // Side effect interspersed. Avoid! return result; } """ ## 2. Implementing State Management Patterns The specific approach to state management depends heavily on the complexity of your application and how your individual modules will interact. Rollup itself doesn't enforce a specific pattern, so you choose the one that best fits your needs. ### 2.1. Standard: Plain JavaScript Objects * **Do This:** Use standard JavaScript objects to store and manage simple state within modules. This approach works well for small to medium-sized applications. Focus on immutability where feasible. * **Don't Do This:** Overcomplicate with state management libraries if basic objects suffice. **Why?** Using plain JavaScript objects reduces dependencies and keeps the code lightweight. It is perfect for scenarios where external libraries would just add unnecessary overhead. """javascript // Example: Managing state with JavaScript objects (immutably) let initialState = { count: 0, name: 'Example' }; function reducer(state, action) { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; case 'DECREMENT': return { ...state, count: state.count - 1 }; case 'UPDATE_NAME': return { ...state, name: action.payload }; default: return state; } } // Usage let currentState = initialState; currentState = reducer(currentState, { type: 'INCREMENT' }); currentState = reducer(currentState, { type: 'UPDATE_NAME', payload: 'New Name' }); console.log(currentState); // { count: 1, name: 'New Name' } """ ### 2.2. Standard: Event Emitters * **Do This:** Implement an event emitter pattern to create reactive state using custom events. This is useful for decoupling modules and handling asynchronous updates. * **Don't Do This:** Overuse event emitters for synchronous state updates; consider direct function calls for simpler cases. **Why?** Event emitters allow different modules to react to state changes without direct dependencies, promoting looser coupling. This decoupling makes code easier to modify and extend. """javascript // Example: Event emitter implementation import { EventEmitter } from 'events'; class Store extends EventEmitter { constructor(initialState) { super(); this.state = initialState; } getState() { return this.state; } update(newState) { this.state = { ...this.state, ...newState }; this.emit('stateChanged', this.state); } } const store = new Store({ count: 0 }); // Subscribe to state changes store.on('stateChanged', (newState) => { console.log('State changed:', newState); }); // Update the state store.update({ count: 1 }); """ ### 2.3. Standard: RxJS Observables * **Do This:** Use RxJS Observables for managing complex asynchronous state and data streams. * **Don't Do This:** Implement complex custom solutions when RxJS can handle the scenarios more efficiently. This is particularly useful for real-time data processing. **Why?** RxJS provides a powerful and flexible way to handle asynchronous data streams. Observables simplify complex logic with operators for filtering, mapping, and combining data, making your code more reactive and efficient. """javascript // Example: RxJS implementation import { BehaviorSubject } from 'rxjs'; class Store { constructor(initialState) { this.state$ = new BehaviorSubject(initialState); } getState() { return this.state$.getValue(); } update(newState) { this.state$.next({ ...this.state$.getValue(), ...newState }); } subscribe(callback) { return this.state$.subscribe(callback); } } const store = new Store({ count: 0 }); // Subscribe to state changes const subscription = store.subscribe((newState) => { console.log('RxJS State changed:', newState); }); // Update the state store.update({ count: 1 }); // Unsubscribe when not needed. subscription.unsubscribe(); """ ## 3. State Management Within Rollup Plugins Rollup plugins can maintain their internal state, which can be helpful for caching or managing plugin-specific settings. ### 3.1. Standard: Encapsulate Plugin State * **Do This:** Use closures or classes to encapsulate the plugin's state. * **Don't Do This:** Rely on global variables to store plugin state, which could clash with other plugins or application code. **Why?** Encapsulation ensures that the plugin's internal state does not interfere with other parts of the application or other plugins, preventing unexpected side effects. """javascript // Example: Plugin state using closures function myPlugin() { let internalState = {}; // Encapsulated state return { name: 'my-plugin', transform(code, id) { // Access and/or modify the internal state internalState[id] = code.length; // Example of side effect (logging plugin activity) console.log("File ${id} transformed. Code length: ${internalState[id]}"); return code; }, buildEnd() { console.log('Plugin build completed. Processed file sizes:', internalState); } }; } export default myPlugin; """ ### 3.2. Standard: Use Plugin Context * **Do This:** Leverage the "this" context within plugin lifecycle hooks to share state and methods. Rollup plugins that use ES Module syntax bind "this" to a plugin context object. * **Don't Do This:** Directly modify the configuration object, as it could lead to unexpected behavior. **Why?** The plugin context facilitates sharing data and utility functions between different hooks within the plugin, promoting a consistent and maintainable structure. """javascript // Example: Sharing state using plugin context function myPlugin() { return { name: 'my-plugin', options(options) { this.sharedState = { count: 0 }; return options; }, transform(code, id) { this.sharedState.count++; console.log("Transforming ${id}. Count: ${this.sharedState.count}"); return code; }, buildEnd() { console.log("Total files transformed: ${this.sharedState.count}"); } }; } export default myPlugin; """ ### 3.3. Standard: Be Mindful of Persistent State * **Do This:** Ensure that any persistent state within the plugin (e.g., cached data) is properly managed and does not lead to memory leaks. * **Don't Do This:** Accumulate state indefinitely without clearing or updating it, especially when dealing with large datasets. Always clean up resources appropriately. **Why?** Properly managing persistent state inside a plugin prevents memory leaks and ensures that the plugin behaves efficiently, especially in long-running or watch-mode builds. """javascript // Example: Caching data within a Rollup plugin import { createHash } from 'crypto'; function cachingPlugin() { const cache = new Map(); return { name: 'caching-plugin', transform(code, id) { const hash = createHash('sha256').update(code).digest('hex'); if (cache.has(id) && cache.get(id).hash === hash) { console.log("[cache] Returning cached version of ${id}"); return cache.get(id).code; } // Process the code (in this example, just converting to uppercase) const transformedCode = code.toUpperCase(); // Store the transformed code in the cache cache.set(id, { code: transformedCode, hash: hash }); console.log("[cache] Caching transformed version of ${id}"); return transformedCode; } }; } export default cachingPlugin; """ ## 4. Asynchronous State Management Rollup plugins often perform asynchronous operations, such as reading files or making network requests. Proper state management is crucial to handle these operations correctly. ### 4.1. Standard: Use Async/Await * **Do This:** Utilize "async" and "await" to manage asynchronous state updates in a readable and maintainable way. * **Don't Do This:** Rely on callbacks or promises without proper error handling, which can lead to difficult-to-debug issues. **Why?** "async/await" makes asynchronous code look and behave a bit more like synchronous code, which improves readability. It also provides easier error handling with "try/catch" blocks. """javascript // Example: Asynchronous plugin using async/await import { readFile } from 'fs/promises'; function asyncPlugin() { let state = { filesRead: 0 }; return { name: 'async-plugin', async load(id) { try { const content = await readFile(id, 'utf-8'); state.filesRead++; console.log("[async plugin] Read file ${id}. Total files read: ${state.filesRead}"); return content; } catch (error) { this.error("Failed to read file ${id}: ${error.message}"); return null; } } }; } export default asyncPlugin; """ ### 4.2. Standard: Handle Errors Robustly * **Do This:** Implement error-handling mechanisms (e.g., "try/catch" blocks) to catch and manage exceptions that may occur during asynchronous operations. Specifically using the plugin context's "this.warn" and "this.error" methods. * **Don't Do This:** Ignore possible errors, as this can cause your plugin to fail silently or produce unexpected results. **Why?** Robust error handling prevents the plugin from crashing or producing incorrect output, improving the overall stability and reliability of the build process. """javascript // Example: Robust error handling in an async plugin import { readFile } from 'fs/promises'; function errorHandlingPlugin() { return { name: 'error-handling-plugin', async load(id) { try { const content = await readFile(id, 'utf-8'); return content; } catch (error) { this.error("[error-handling-plugin] Failed to read file ${id}: ${error.message}"); return null; // Important to return null to halt processing of this file. } } }; } export default errorHandlingPlugin; """ ## 5. Security Considerations When managing state, especially in plugins that handle user-provided data, security is paramount. ### 5.1. Standard: Validate and Sanitize Data * **Do This:** Validate and sanitize all external data to prevent common security vulnerabilities such as cross-site scripting (XSS) or injection attacks. * **Don't Do This:** Directly use external data without proper validation, as this can expose your plugin and application to security risks. **Why?** Validating and sanitizing data ensures that only safe and expected data is processed, mitigating potential security threats and ensuring the integrity of the build process. """javascript // Example: Data validation and sanitization import { createHash } from 'crypto'; function securePlugin() { return { name: 'secure-plugin', transform(code, id) { // Validate the file ID if (!isValidFileId(id)) { this.warn("[secure-plugin] Invalid file ID: ${id}"); return null; } // Sanitize the code content const sanitizedCode = sanitize(code); // Generate a hash of the sanitized code const hash = createHash('sha256').update(sanitizedCode).digest('hex'); console.log("[secure-plugin] Processed and secured file ${id}, hash: ${hash}"); return sanitizedCode; } }; function isValidFileId(id) { // Implement your validation logic here return typeof id === 'string' && id.length > 0; } function sanitize(code) { // Implement your code sanitization logic here (e.g., escaping HTML entities) return code.replace(/</g, '<').replace(/>/g, '>'); } } export default securePlugin; """ ### 5.2. Standard: Avoid Storing Sensitive Information * **Do This:** Avoid storing sensitive information (e.g., API keys, passwords) directly in the plugin's state. If you must store such data, encrypt it and manage access carefully. Consider environment variables. * **Don't Do This:** Hardcode sensitive information or store it in plain text within the plugin's codebase or state. **Why?** Preventing the storage of sensitive information minimizes the risk of data breaches and security compromises. Use secure configuration management practices, such as environment variables or secure credential stores. ### 5.3. Standard: Use Secure Dependencies * **Do This:** Regularly update your dependencies to patch security vulnerabilities. Scan your dependencies for known security issues using tools like "npm audit" or "yarn audit". * **Don't Do This:** Use outdated or unmaintained dependencies, as they may contain known security vulnerabilities that can be exploited. **Why?** Keeping dependencies up to date and actively scanning for vulnerabilities reduces the risk of introducing security flaws into your plugin and application. Regularly audit and update dependencies to maintain a secure environment. ## 6. Conclusion Adhering to these coding standards ensures clean, maintainable, and secure code for state management in Rollup. By embracing modularity, explicit data flow, and robust error handling, developers can build plugins and applications that are manageable and reliable.
# Testing Methodologies Standards for Rollup This document outlines the recommended testing methodologies for Rollup plugins and configurations. Adhering to these standards ensures code quality, maintainability, and reduces the risk of regressions and vulnerabilities. ## Unit Testing ### Standards * **Do This:** Write focused unit tests that isolate individual functions and modules. * **Why:** Unit tests provide fast feedback on code changes and help pinpoint the source of errors quickly. This approach decreases debug time and increases confidence in the reliability of individual components. * **Don't Do This:** Create overly broad unit tests that test multiple functionalities simultaneously. These tests become brittle, difficult to maintain, and often fail to accurately identify the broken component. ### Implementation * **Frameworks:** Use a testing framework like Jest, Mocha, or Ava, combined with an assertion library like Chai or expect. Jest is generally favored due to its built-in features like mocking and code coverage. * **Mocking:** Use mocking libraries (e.g., Jest's "jest.mock()") to isolate the unit under test from its dependencies. Avoid mocking the internals of Rollup itself, unless absolutely necessary for testing edge cases. Focus on mocking dependencies *used* by your plugin. * **Test Coverage:** Strive for high test coverage (80% or higher). Use tools like Istanbul (integrated into Jest) to measure coverage and identify gaps. Coverage shouldn't be the sole metric, but a good indicator. ### Code Example (Jest) """javascript // src/my-plugin.js import { transform } from './transformer'; // Hypothetical transformer function export default function myPlugin() { return { name: 'my-plugin', transform(code, id) { if (id.endsWith('.special.js')) { return transform(code); } return null; } }; } // src/transformer.js export function transform(code) { // Complex transformation logic here return code.toUpperCase(); // Simple example } // test/my-plugin.test.js import myPlugin from '../src/my-plugin'; import { transform } from '../src/transformer'; // Import the actual transform function jest.mock('../src/transformer', () => ({ // Mock the transformer transform: jest.fn(code => "MOCKED_${code}") })); describe('myPlugin', () => { it('should transform .special.js files', () => { const plugin = myPlugin(); const code = 'some code'; const id = 'file.special.js'; const result = plugin.transform(code, id); expect(transform).toHaveBeenCalledWith(code); // Check mock was invoked expect(result).toBe("MOCKED_${code}"); //Check mock return }); it('should not transform other files', () => { const plugin = myPlugin(); const code = 'some code'; const id = 'file.js'; const result = plugin.transform(code, id); expect(result).toBeNull(); }); }); """ * **Anti-pattern:** Directly depending on the file system or external APIs within a unit test *without mocking*. This introduces external dependencies, making tests slow, unreliable, and harder to reason about. Always mock these dependencies to isolate the unit. ## Integration Testing ### Standards * **Do This:** Verify that different parts of your Rollup plugin work correctly together. Specifically, test the interaction between your plugin, Rollup's internal APIs, and other plugins that might be used in a typical build. * **Why:** Integration tests catch bugs that arise from interactions between modules that individually pass unit tests. This is crucial for Rollup, where plugins frequently modify Rollup's internal state and interact with the module graph. * **Don't Do This:** Neglect integration testing in favor of relying solely on unit tests. This can lead to overlooked issues related to plugin interoperability and Rollup's build process. Also, don't make integration tests *too* broad – keep them focused on specific interactions. ### Implementation * **Rollup API:** Leverage Rollup's programmatic API ("rollup.rollup()", "bundle.generate()") to simulate real-world build scenarios. * **Configuration Files:** Create small, representative "rollup.config.js" files for integration tests. * **Assertions on Output:** Assert on the generated bundle's code, file structure, and emitted assets. * **Plugin Interoperability:** Test your plugin alongside other commonly used plugins (e.g., "@rollup/plugin-commonjs", "@rollup/plugin-node-resolve"). ### Code Example """javascript // test/integration.test.js import { rollup } from 'rollup'; import myPlugin from '../src/my-plugin'; import commonjs from '@rollup/plugin-commonjs'; import resolve from '@rollup/plugin-node-resolve'; import * as fs from 'fs/promises'; describe('Integration Tests', () => { it('should integrate with commonjs and resolve plugins', async () => { const bundle = await rollup({ input: 'test/fixtures/input.js', plugins: [ myPlugin(), commonjs(), resolve({ // Resolve options can be set specifically for testing browser: true // Mock Node context, simulate browser }) ] }); const { output } = await bundle.generate({ format: 'es' }); const generatedCode = output[0].code; expect(generatedCode).toContain('// Generated by my-plugin'); // Check for plugin modification expect(generatedCode).toContain('console.log'); // Validate CommonJS and resolve worked // Optionally write the generated code to a file for debugging failing tests // await fs.writeFile('test/output.js', generatedCode); }, 30000); it('should handle errors gracefully', async () => { // Example config that causes an error in myPlugin const shouldThrow = async () => { await rollup({ // Await the rollup call directly here input: 'test/fixtures/input.js', plugins: [ myPlugin({ shouldFail: true }), // Pass options to simulate error ] }); } await expect(shouldThrow).rejects.toThrowError('Simulated Error'); }); }); """ * **Anti-pattern:** Running integration tests against a *real* production environment or staging server. Integration tests should be self-contained and reproducible, relying only on local files and mocked services or APIs. Relying on external systems introduces volatility and makes debugging nearly impossible. Also avoid complex file system operations within tests unless they are part of the specific functionality you're testing. ## End-to-End (E2E) Testing ### Standards * **Do This:** Simulate real user workflows using a browser environment. Test the *entire* build process, from input files to output. Only necessary for plugins that heavily interact with the browser. * **Why:** E2E tests ensure that the built application functions as expected in a production-like environment. This catches issues arising from complex build configurations, browser-specific behavior, and interactions between different parts of the application. * **Don't Do This:** Use E2E tests as a substitute for unit or integration tests. E2E tests are slower and more complex to set up and maintain, making them unsuited for testing individual components or interactions. ### Implementation * **Frameworks:** Use frameworks like Cypress, Playwright, or Puppeteer. Each framework has a different set of trade-offs in terms of performance, ease of use, and browser support. Playwright is generally preferred for modern projects. * **Sample Application:** Create a small sample application that uses your Rollup plugin and demonstrates typical use cases. * **Build Process:** Integrate the Rollup build process into your E2E test suite. Run Rollup programmatically or via a shell command before executing your browser tests. * **Assertions via Browser:** Use the E2E testing framework's API to interact with the application in the browser, assert on the rendered output, and verify expected behavior. ### Code Example (Playwright) """javascript // playwright.config.js module.exports = { webServer: { command: 'npm run build && npm run serve', // Build and serve your test app port: 3000, timeout: 120 * 1000, // Extend timeout for build reuseExistingServer: !process.env.CI, }, use: { baseURL: 'http://localhost:3000', browserName: 'chromium', }, testMatch: 'test/e2e/*.test.js', }; // test/e2e/my-plugin.test.js const { test, expect } = require('@playwright/test'); test('My Plugin modifies page content', async ({ page }) => { await page.goto('/'); // Add a selector to isolate content myPlugin modifies const title = await page.locator('#test-area'); // Assume test area with id await expect(title).toHaveText("Plugin Applied"); // Expect text based on plugin output }); """ * **Anti-pattern:** Writing overly complex or flaky E2E tests that are difficult to debug. Keep E2E tests focused on verifying critical user flows and minimize dependencies on external services or data. Use "test.describe.configure({ mode: 'serial' })" for E2E tests to ensure they are run sequentially, to avoid race conditions and interference between tests if they share state. ## Additional Considerations * **CI/CD Integration:** Integrate your test suite into your CI/CD pipeline to automatically run tests on every commit. * **Performance Testing:** Use tools like Lighthouse or WebPageTest to measure the impact of your Rollup plugin on the performance of the generated bundle. Especially relevant if your plugin performs complex transformations. * **Security Testing:** Use linters and static analysis tools (e.g., ESLint with security-related rules, SonarQube) to identify potential security vulnerabilities in your code. * **Regression Testing:** Maintain a comprehensive suite of regression tests to catch bugs introduced by new code changes. When fixing a bug, *always* write a test that reproduces the bug to prevent future regressions. * **Snapshot Testing:** Consider snapshot testing for complex UI components or configurations. Use with caution, as snapshots can become brittle and require frequent updates. * **Property-Based Testing (Fuzzing):** Property-based testing (using libraries like fast-check) can generate a wide range of inputs to uncover edge cases and unexpected behavior. * **Documentation:** Always provide comprehensive documentation for your tests, including clear descriptions of the test cases, setup instructions, and expected results. Aim for full transparency for anyone that has to work with the tests. * **Code Review:** Code review is a vital practice. Another developer reviewing your code may suggest improvements to your testing methodology. By following these standards, you can ensure the quality, maintainability, and reliability of your Rollup plugins and configurations. Remember to adapt these standards to your specific project needs and coding style.