# Security Best Practices Standards for Rollup
This document outlines security best practices for developing Rollup plugins, configurations, and build processes. Adhering to these standards will help mitigate potential vulnerabilities and ensure the integrity of bundled code.
## 1. Input Validation and Sanitization
### 1.1. Validate Plugin Options
**Standard:** Always validate and sanitize options passed to Rollup plugins to prevent injection attacks and unexpected behavior.
**Why:** Unvalidated options can be exploited to execute arbitrary code or manipulate the build process. This is particularly crucial when options involve file paths or external resources.
**Do This:**
"""javascript
// rollup-plugin-example.js
import { createFilter } from '@rollup/pluginutils';
import { resolve, isAbsolute } from 'path';
function examplePlugin(options = {}) {
const { include, exclude, customOption } = options;
// Validate 'include' and 'exclude' using @rollup/pluginutils createFilter
const filter = createFilter(include, exclude);
// Validate 'customOption'
if (typeof customOption !== 'string') {
throw new Error('customOption must be a string.');
}
if (customOption.length > 100) { // Arbitrary length limit
throw new Error('customOption is too long.');
}
// Sanitize 'customOption' against Regular Expression Denial of Service (ReDoS).
const safeCustomOption = customOption.replace(/([.*+?^${}()|[\]\\])/g, '\\$1');
return {
name: 'example',
transform(code, id) {
if (!filter(id)) return null;
// Use safeCustomOption instead of customOption here
// Avoid using eval or Function constructor with user-supplied data
// e.g., const result = new Function('return ' + safeCustomOption)(); // AVOID!
// Instead rely on known function calls with limited scope
const transformedCode = code + "\n// Transformed with safeCustomOption: ${safeCustomOption}";
return transformedCode;
}
};
}
export default examplePlugin;
"""
**Don't Do This:**
"""javascript
// BAD: Unvalidated option usage
function examplePlugin(options = {}) {
return {
name: 'example',
transform(code, id) {
// Potential vulnerability: using options.customCode directly
return code + options.customCode;
}
};
}
"""
**Anti-Pattern:** Directly using plugin options without any validation or sanitization. Relying solely on user-provided data can lead to catastrophic vulnerabilities.
### 1.2. Validate Resolved File Paths
**Standard:** Validate and normalize file paths before using them to access the file system. Use "path.resolve()" and "path.normalize()" to mitigate path traversal vulnerabilities.
**Why:** Path traversal vulnerabilities can allow attackers to read or write arbitrary files on the system.
**Do This:**
"""javascript
import { createFilter } from '@rollup/pluginutils';
import { resolve, normalize, isAbsolute } from 'path';
import { readFileSync } from 'fs';
function securePlugin(options = {}) {
const { filePath } = options;
if (!filePath) {
throw new Error('filePath is required');
}
// Ensure the path is absolute and normalized
const absolutePath = resolve(filePath);
const normalizedPath = normalize(absolutePath);
// Check if the resolved path is within the expected directory
const baseDir = process.cwd(); // Or a more specific expected directory
if (!normalizedPath.startsWith(baseDir)) {
throw new Error('filePath must be within the project directory');
}
return {
name: 'secure-plugin',
load() {
try {
const content = readFileSync(normalizedPath, 'utf-8');
return content;
} catch (error) {
this.error("Failed to read file: ${error.message}");
}
}
};
}
export default securePlugin;
"""
**Don't Do This:**
"""javascript
// BAD: Unvalidated file path
import { readFileSync } from 'fs';
function insecurePlugin(options = {}) {
return {
name: 'insecure-plugin',
load() {
try {
// Potential path traversal vulnerability
const content = readFileSync(options.filePath, 'utf-8');
return content;
} catch (error) {
this.error("Failed to read file: ${error.message}");
}
}
};
}
"""
**Anti-Pattern:** Directly using user-supplied file paths without validation, particularly in functions like "fs.readFileSync" or dynamic imports.
## 2. Code Injection Prevention
### 2.1. Avoid "eval()" and "Function()" Constructor
**Standard:** Never use "eval()" or the "Function()" constructor with user-supplied data.
**Why:** These can execute arbitrary code, leading to severe security vulnerabilities.
**Do This:** Rely on alternative methods such as lookup tables, switch statements, or specialized libraries for dynamic code execution.
"""javascript
// Instead of using eval, use object lookup:
const operations = {
add: (a, b) => a + b,
subtract: (a, b) => a - b,
multiply: (a, b) => a * b,
};
function calculate(operationName, a, b) {
const operation = operations[operationName];
if (!operation) {
throw new Error("Invalid operation: ${operationName}");
}
return operation(a, b);
}
console.log(calculate('add', 5, 3)); // Output: 8
"""
**Don't Do This:**
"""javascript
// BAD: Using eval()
function executeCode(userInput) {
// Extremely dangerous
eval(userInput);
}
"""
**Anti-Pattern:** Using "eval()" or "Function()" to execute code derived from user input or external sources.
### 2.2. Template Literals and Dynamic Strings
**Standard:** When constructing dynamic strings, particularly for code generation, use careful escaping and validation to prevent code injection. Consider using templating engines with automatic escaping.
**Why:** Incorrectly constructed dynamic strings can allow attackers to inject malicious code snippets.
**Do This:**
"""javascript
function createSafeString(input) {
// Escape potentially harmful characters
const escapedInput = input.replace(/["${}]/g, '\\$&');
return "console.log(\"${escapedInput}\");";
}
const userInput = 'Hello ${() => alert("XSS")}"';
const safeCode = createSafeString(userInput);
console.log(safeCode); // Output: console.log("Hello \${() => alert("XSS")}\"");
// This output is safe to include in generated code.
"""
**Don't Do This:**
"""javascript
// BAD: Unescaped template literals
function createInsecureString(input) {
return "console.log(\"${input}\");";
}
const userInput = 'Hello ${() => alert("XSS")}"';
const insecureCode = createInsecureString(userInput);
// BAD OUTPUT: console.log("Hello ${() => alert("XSS")}")"
"""
**Anti-Pattern:** Directly embedding user input or external data into template literals without proper escaping.
## 3. Dependency Management
### 3.1. Use Lockfiles
**Standard:** Always use lockfiles (e.g., "package-lock.json", "yarn.lock", "pnpm-lock.yaml") to ensure consistent dependency versions across environments.
**Why:** Lockfiles prevent unexpected version upgrades that could introduce vulnerabilities.
**Do This:** Regularly commit and update your lockfiles. Use "npm ci" or "yarn install --frozen-lockfile" in CI/CD environments to enforce the lockfile.
### 3.2. Regularly Audit Dependencies
**Standard:** Regularly audit your project's dependencies for known vulnerabilities using tools like "npm audit", "yarn audit", or "snyk".
**Why:** Dependencies can contain security flaws that can compromise your application.
**Do This:**
"""bash
npm audit
# or
yarn audit
# or
snyk test
"""
Address any identified vulnerabilities by updating dependencies or applying patches.
### 3.3. Avoid Unnecessary Dependencies
**Standard:** Keep your project's dependency count to a minimum. Only include dependencies that are truly necessary.
**Why:** Each dependency increases the attack surface of your application.
**Do This:** Regularly review your dependencies and remove any that are no longer needed. Consider alternatives that provide similar functionality without adding dependencies.
**Anti-Pattern:** Adding dependencies without thoroughly evaluating their necessity and security implications.
### 3.4. Pin Dependency Versions
**Standard:** Pin the specific versions of your core dependencies in "package.json". This helps prevent unexpected breaking changes or the introduction of vulnerabilities from patch or minor version updates.
**Why:** Using version ranges like "^1.0.0" can introduce unexpected changes from minor or patch updates that might contain security flaws. Pinning versions provides predictability.
**Do This:**
"""json
// package.json
{
"dependencies": {
"lodash": "4.17.21",
"axios": "0.27.2"
}
}
"""
This ensures you always use the exact versions you've tested. Security updates should be applied by consciously updating the version number and retesting. Consider using tools like "npm-check-updates" to help manage updates.
**Don't Do This:**
"""json
// BAD - Using version ranges
{
"dependencies": {
"lodash": "^4.0.0",
"axios": "~0.20.0"
}
}
"""
**Anti-Pattern:** Using wide version ranges for dependencies without careful oversight and testing of updates. This exposes your project to potential vulnerabilities from upstream changes.
## 4. Secure Build Process
### 4.1. Environment Variables for Secrets
**Standard:** Store sensitive information such as API keys and database passwords in environment variables, not directly in your code or configuration files.
**Why:** Environment variables are less likely to be accidentally committed to version control.
**Do This:**
"""javascript
// rollup.config.js
import replace from '@rollup/plugin-replace';
export default {
// ... other configurations
plugins: [
replace({
preventAssignment: true,
values: {
'process.env.API_KEY': JSON.stringify(process.env.API_KEY),
},
}),
],
};
"""
Set the "API_KEY" environment variable in your deployment environment.
**Don't Do This:**
"""javascript
// BAD: Hardcoding API key
const apiKey = 'YOUR_API_KEY';
"""
**Anti-Pattern:** Hardcoding sensitive information directly in your code or configuration files.
### 4.2. Secure Third-Party Plugins
**Standard:** Thoroughly vet any third-party Rollup plugins before using them in your project. Ensure they are actively maintained, have a good reputation, and do not exhibit any suspicious behavior.
**Why:** Malicious or poorly written plugins can introduce vulnerabilities into your build process and bundled code.
**Do This:**
* Check the plugin's source code on GitHub or npm.
* Read the plugin's documentation and issue tracker.
* Look for any security audits or vulnerability reports.
* Test the plugin in a sandboxed environment before using it in production.
### 4.3. Minimize Build Dependencies
**Standard:** Reduce the number of dependencies required during the build process to minimize the risk of introducing vulnerabilities.
**Why:** Build dependencies can introduce security flaws and increase the attack surface.
**Do This:** Use tools like "esbuild" or "swc" for faster and more secure builds. Use tools such as "webpack-stats-duplicates" to check for package duplication that can be safely removed.
### 4.4. CI/CD Security
**Standard:** Implement robust CI/CD pipelines that include security scanning, vulnerability checks, and automated testing.
**Why:** Automating security checks helps identify and prevent vulnerabilities from being introduced into your codebase.
**Do This:**
* Integrate tools like "npm audit", "yarn audit", or "snyk" into your CI/CD pipeline.
* Run static analysis tools to identify potential security flaws in your code.
* Perform penetration testing on your application before deploying to production.
## 5. Output Sanitization
### 5.1. Prevent Cross-Site Scripting (XSS)
**Standard:** Sanitize any data that is dynamically inserted into HTML to prevent XSS attacks.
**Why:** XSS attacks can allow attackers to inject malicious scripts into your application.
**Do This:**
* Use a templating engine (e.g., Handlebars, Mustache) with automatic escaping.
* Use a library specifically designed for sanitizing HTML (e.g., DOMPurify).
* Apply output encoding to prevent the interpretation of HTML tags.
**Don't Do This:**
"""javascript
// BAD: Directly inserting data into HTML
element.innerHTML = userInput;
"""
**Anti-Pattern:** Directly inserting user input into HTML without sanitization.
### 5.2. Content Security Policy (CSP)
**Standard:** Implement a Content Security Policy (CSP) to restrict the sources from which the browser can load resources.
**Why:** CSP helps mitigate XSS attacks by preventing the execution of untrusted scripts.
**Do This:**
Configure your server to send the "Content-Security-Policy" header with appropriate directives.
Example:
"""
Content-Security-Policy: default-src 'self'; script-src 'self' https://example.com; style-src 'self' https://example.com
"""
This policy allows resources to be loaded only from the same origin and from "https://example.com".
## 6. Secure Plugin Development
### 6.1. Principle of Least Privilege
**Standard:** Plugins should request only the minimum permissions or access they need to perform their intended function.
**Why:** Reduces the potential damage if a plugin is compromised.
**Do This:** Carefully define the scope of a plugin and avoid requesting unnecessary access to the file system, network, or other resources.
### 6.2. Data Encryption
**Standard:** If a plugin handles sensitive data (e.g., API keys, passwords), encrypt the data at rest and in transit.
**Why:** Protects sensitive data from unauthorized access.
**Do This:**
Use established encryption libraries (e.g., "crypto" in Node.js) and follow industry best practices for key management.
### 6.3. Secure Communication
**Standard:** If a plugin communicates with external services, use HTTPS and validate the server's certificate.
**Why:** Prevents man-in-the-middle attacks and ensures the integrity of data transmitted over the network.
**Do This:** Use the "https" module in Node.js and configure it to verify the server's certificate.
### 6.4 Handling Plugin Errors
**Standard:** Plugins must gracefully handle errors and avoid exposing sensitive information in error messages.
**Why:** Error messages can inadvertently reveal details about the system or application that can be exploited by attackers.
**Do This:** Log errors to a secure location, but avoid displaying them directly to users. Use generic error messages that do not reveal sensitive information.
"""javascript
// Secure error handling example
try {
// Dangerous operation
} catch (error) {
console.error('An error occurred during processing.'); // Generic error message
// Securely log the full error details to a file or monitoring system
// Important: DO NOT expose sensitive details to the user.
}
"""
**Don't Do This:**
"""javascript
// Insecure error handling
try {
// Dangerous operation
} catch (error) {
console.error("Operation failed: ${error.message}"); // Exposes internal details
// May expose file paths, secrets, etc.
}
"""
**Anti-Pattern:** Leaking sensitive information in error messages or logs.
## 7. Regular Security Updates
**Standard:** Keep Rollup, your plugins, and all dependencies up to date with the latest security patches.
**Why:** Security vulnerabilities are constantly being discovered, and updates often contain fixes for these vulnerabilities.
**Do This:** Regularly update your dependencies using "npm update", "yarn upgrade", or "pnpm update". Subscribe to security advisories for Rollup and your dependencies to stay informed of any new vulnerabilities.
## 8. Testing and Auditing
### 8.1 Unit Tests
**Standard:** Implement thorough unit tests to verify the security of your Rollup plugins and configurations. These tests should specifically target potential vulnerabilities such as input validation, code injection, and path traversal.
**Why:** Unit tests help identify and prevent security flaws early in the development process.
**Do This:** Use a testing framework like Jest or Mocha to write unit tests that cover all critical security aspects of your code.
### 8.2 Static Analysis
**Standard:** Use static analysis tools to automatically identify potential security vulnerabilities in your codebase.
**Why:** Static analysis can detect common security flaws that might be missed during manual code reviews.
**Do This:** Integrate static analysis tools like ESLint with security plugins (e.g., "eslint-plugin-security") into your development workflow.
### 8.3 Security Audits
**Standard:** Conduct regular security audits of your Rollup plugins and configurations. These audits should be performed by experienced security professionals.
**Why:** Security audits can identify vulnerabilities that might not be apparent through automated testing or static analysis.
**Do This:** Engage a reputable security firm to perform a comprehensive audit of your code. Address any findings promptly and thoroughly.
## 9. Handling Secrets
### 9.1 Secure Storage
**Standard:** Secrets should be stored securely and accessed only when necessary.
**Why:** Exposed secrets can lead to unauthorized access and data breaches.
**Do This:** Use a dedicated secrets management solution like HashiCorp Vault or AWS Secrets Manager to store and manage secrets. Use environment variables for local development and ensure that these are not checked into version control.
### 9.2 Least Privilege
**Standard:** Access to secrets should be granted based on the principle of least privilege.
**Why:** Limiting access to secrets reduces the risk of unauthorized disclosure.
**Do This:** Grant only the necessary access to secrets to individual developers, applications, and services. Use fine-grained access controls to restrict access to specific secrets based on need.
### 9.3 Rotation
**Standard:** Secrets should be regularly rotated to limit the impact of a potential compromise.
**Why:** Rotating secrets reduces the window of opportunity for attackers to use compromised credentials.
**Do This:** Implement a process for regularly rotating secrets on a predefined schedule. Automate the rotation process where possible.
By adhering to these security best practices, you can significantly reduce the risk of vulnerabilities in your Rollup projects and ensure the integrity of your bundled code. Always stay informed of the latest security threats and update your practices accordingly.
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'
# 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.
# Code Style and Conventions Standards for Rollup This document outlines the code style and conventions to be followed when contributing to Rollup or developing Rollup-based projects. Adhering to these standards ensures consistency, readability, maintainability, and performance across the codebase. ## 1. General Principles * **Consistency:** Maintain a consistent style throughout the codebase. Use automated tools like ESLint and Prettier to enforce these standards. * **Readability:** Write code that is easy to understand and follow. Use meaningful names, comments, and proper indentation. * **Maintainability:** Design code that is easy to modify, extend, and debug. Follow modular design principles and avoid unnecessary complexity. * **Performance:** Optimize code for performance. Consider the impact of your code on bundle size and execution speed. * **Security:** Write secure code. Be aware of potential vulnerabilities and take steps to mitigate them. Use secure coding practices and follow security best practices. ## 2. Formatting ### 2.1. Whitespace * **Indentation:** Use 2 spaces for indentation. Avoid tabs. """javascript // Do This function foo() { console.log('Hello'); } // Don't Do This function bar() { console.log('Hello'); } """ *Why:* Consistent indentation enhances readability and reduces visual clutter. * **Line Length:** Limit lines to a maximum of 120 characters. *Why:* Longer lines are harder to read and can cause horizontal scrolling in some editors. * **Blank Lines:** Use blank lines to separate logical sections of code, such as function definitions, loops, and conditional statements. """javascript // Do This function processData(data) { // Process the data const processedData = data.map(item => item * 2); // Return the processed data return processedData; } // Don't Do This function processData(data){const processedData=data.map(item=>item*2);return processedData;} """ *Why:* Blank lines improve readability by visually separating different parts of the code. * **Trailing Whitespace:** Remove trailing whitespace at the end of lines. *Why:* Trailing whitespace is invisible but can cause unnecessary changes in diffs. * **Whitespace Around Operators:** Use spaces around operators. """javascript // Do This const x = 1 + 2; const y = a * (b - c); // Don't Do This const x=1+2; const y=a*(b-c); """ *Why:* Improves readability by visually separating operators from operands. ### 2.2. Curly Braces * **Placement:** Place opening curly braces on the same line as the statement. """javascript // Do This function foo() { console.log('Hello'); } if (condition) { console.log('True'); } // Don't Do This function foo() { console.log('Hello'); } if (condition) { console.log('True'); } """ *Why:* This style is more compact and easier to read. * **Single-Line Blocks:** For single-line blocks, curly braces are optional but **strongly encouraged** for clarity and to prevent errors when modifying the code later. """javascript // Do This if (condition) { console.log('Condition is true'); } // Discouraged (but allowed if very simple): if (condition) console.log('Condition is true'); """ *Why:* Adding curly braces makes the code more explicit and reduces the risk of introducing bugs. ### 2.3. Quotes * **Strings:** Use single quotes ("'") for strings. """javascript // Do This const name = 'John Doe'; // Don't Do This const name = "John Doe"; """ *Why:* Single quotes are generally preferred for simple strings as they are more readable and consistent. However, template literals should be used where appropriate for string interpolation or multi-line strings. ### 2.4. Semicolons * **Always Use Semicolons:** Always include semicolons at the end of statements. """javascript // Do This const message = 'Hello, world!'; console.log(message); // Don't Do This (relying on ASI) const message = 'Hello, world!' console.log(message) """ *Why:* While JavaScript has Automatic Semicolon Insertion (ASI), relying on it can lead to unexpected behavior and errors. Explicit semicolons make the code more predictable and less prone to bugs. ## 3. Naming Conventions ### 3.1. General * **Descriptive Names:** Use descriptive and meaningful names for variables, functions, and classes. """javascript // Do This const userAge = 30; function calculateArea(width, height) { return width * height; } // Don't Do This const a = 30; function calc(w, h) { return w * h; } """ *Why:* Descriptive names make the code easier to understand and maintain. * **Avoid Abbreviations:** Avoid abbreviations unless they are widely understood in the specific context. """javascript // Do This const maximumFileSize = 1024; // KB // Avoid const maxFS = 1024; """ *Why:* Abbreviations can make the code harder to understand, especially for newcomers. ### 3.2. Variables * **"const" and "let":** Use "const" for variables that should not be reassigned. Use "let" for variables that need to be reassigned. Avoid "var". """javascript // Do This const apiKey = 'YOUR_API_KEY'; let counter = 0; counter++; // Don't Do This var apiKey = 'YOUR_API_KEY'; var counter = 0; """ *Why:* "const" and "let" provide better scoping and help prevent accidental reassignment, leading to more reliable code. "var" has function scoping, which can lead to confusion and bugs. * **Camel Case:** Use camel case for variable names. """javascript // Do This const firstName = 'John'; const lastName = 'Doe'; // Don't Do This const first_name = 'John'; const LastName = 'Doe'; """ *Why:* Camel case is the standard convention for variable names in JavaScript and improves readability. ### 3.3. Functions * **Camel Case:** Use camel case for function names. """javascript // Do This function calculateSum(a, b) { return a + b; } // Don't Do This function CalculateSum(a, b) { return a + b; } """ *Why:* Camel case improves readability and consistency with other variable names. * **Descriptive Verbs:** Use descriptive verbs for function names. """javascript // Do This function getUserById(id) { // ... } function validateInput(input) { // ... } // Don't Do This function user(id) { // ... } function check(input) { // ... } """ *Why:* Clear verb-based names make the function's purpose immediately apparent. ### 3.4. Classes * **Pascal Case:** Use Pascal case for class names. """javascript // Do This class UserAccount { constructor(name, email) { this.name = name; this.email = email; } } // Don't Do This class userAccount { constructor(name, email) { this.name = name; this.email = email; } } """ *Why:* Pascal case is the standard convention for class names in JavaScript. ### 3.5. Constants * **Upper Case with Underscores:** Use upper case with underscores for constant names. """javascript // Do This const API_URL = 'https://example.com/api'; const MAX_RETRIES = 3; // Don't Do This const apiUrl = 'https://example.com/api'; const maxRetries = 3; """ *Why:* This convention clearly distinguishes constants from variables and improves readability. Constants should be declared with "const", making them immutable. ## 4. Code Style Specific to Rollup ### 4.1. Module Imports and Exports * **Explicit Imports:** Use explicit imports to specify which modules or members are being imported. """javascript // Do This import { rollup } from 'rollup'; import commonjs from '@rollup/plugin-commonjs'; // Don't Do This import * as rollup from 'rollup'; // Avoid wildcard imports """ *Why:* Explicit imports make the code more readable and help avoid potential naming conflicts. They also allow for better tree-shaking by only importing the necessary code. * **Named Exports:** Prefer named exports over default exports. """javascript // Do This export function myFunction() { // ... } export const myConstant = 42; // Don't Do This (generally) export default function() { // ... } """ *Why:* Named exports make it clear what is being exported from a module. Also, they prevent naming collisions and enable the TypeScript compiler to provide more accurate type checking. *When Default Exports are Acceptable:* Default exports can be suitable when a module primarily exports a single function, class, or value (e.g., a React component). Consider readability on a case-by-case basis, but consistency within a project is key. * **Consistent Use of File Extensions:** When importing local modules, consistently use file extensions (e.g., ".js", ".ts"). This improves clarity and can avoid resolution issues, particularly in environments with differing module resolution configurations. """javascript // Do This import { something } from './my-module.js'; // Okay (if consistent across the project and module resolution configured correctly) import { something } from './my-module'; """ *Why:* While omitting the extension may work in some configurations, explicitly including it enhances clarity and reduces ambiguity. This is especially helpful when working with different module systems or bundlers beyond Rollup. ### 4.2 Rollup Plugin Development * **Plugin Structure:** Rollup plugins should be structured as functions that return an object with lifecycle hooks. """javascript // Do This import { createFilter } from '@rollup/pluginutils'; function myPlugin(options = {}) { const filter = createFilter(options.include, options.exclude); return { name: 'my-plugin', transform(code, id) { if (!filter(id)) return null; // Modify the code const modifiedCode = code.replace('foo', 'bar'); return { code: modifiedCode, map: null // Sourcemap (optional) }; } }; } export default myPlugin; """ *Why:* This structure is the standard for Rollup plugins and allows Rollup to properly manage the plugin's lifecycle. Using "@rollup/pluginutils" for filtering is also recommended. * **Name Property:** Every Rollup plugin should have a "name" property. """javascript // Do This return { name: 'my-plugin', // ... }; // Don't Do This return { // ... }; """ *Why:* The "name" property is used for logging and debugging purposes, helping identify the plugin responsible for specific transformations. * **Asynchronous Operations:** Use "async/await" for asynchronous operations in plugin hooks. """javascript // Do This async transform(code, id) { const result = await someAsyncOperation(code); return { code: result, map: null }; } // Don't Do This (using Promises directly without async/await) transform(code, id) { return someAsyncOperation(code).then(result => ({ code: result, map: null })); } """ *Why:* "async/await" makes asynchronous code easier to read and reason about, improving maintainability. * **Error Handling:** Implement proper error handling in plugin hooks. """javascript // Do This async transform(code, id) { try { const result = await someAsyncOperation(code); return { code: result, map: null }; } catch (error) { this.error("Error processing ${id}: ${error.message}"); } } // Don't Do This (ignoring potential errors) async transform(code, id) { const result = await someAsyncOperation(code); return { code: result, map: null }; } """ *Why:* Proper error handling prevents unexpected crashes and provides informative error messages, making debugging easier. Use "this.error" (from the plugin context) to report build errors to Rollup. * **Sourcemaps:** Generate sourcemaps when modifying code in plugin hooks. """javascript //With existing sourcemap import { SourceMap } from 'rollup'; async transform(code, id) { try { const result = await someTransformationLibrary.transform(code, {sourceMaps: true}); return { code: result.code, map: result.map //Existing sourcemap }; } catch (error) { this.error("Error processing ${id}: ${error.message}"); } } //Generating new Sourcemap import { SourceMap } from 'rollup'; async transform(code, id) { try { const result = await someTransformationLibrary.transform(code); const map = new SourceMap({ file: id }); map.addFile({ content: code }); return { code: result.code, map: map.toString() //New sourcemap as string }; } catch (error) { this.error("Error processing ${id}: ${error.message}"); } } """ *Why:* Sourcemaps allow developers to debug the original source code even after it has been transformed by Rollup. This greatly enhances the developer experience, especially for complex projects. ### 4.3. Configuration Files (rollup.config.js/ts) * **Clear and Concise Configuration:** Keep Rollup configuration files clean and easy to understand. """javascript // Do This import commonjs from '@rollup/plugin-commonjs'; import { nodeResolve } from '@rollup/plugin-node-resolve'; import typescript from '@rollup/plugin-typescript'; import { defineConfig } from 'rollup'; // Recommended export default defineConfig({ input: 'src/index.ts', output: { file: 'dist/bundle.js', format: 'es', sourcemap: true }, plugins: [ nodeResolve(), commonjs(), typescript() ] }); // Less Preferred (but allowed): Complex logic inline export default { //... very complicated configuration logic } """ *Why:* A well-structured configuration file improves maintainability and reduces the risk of errors. Using "defineConfig" (if using Rollup's TypeScript support) provides type safety and better editor support. * **Environment Variables:** Use environment variables for configuration options that may vary between environments. """javascript // rollup.config.js import replace from '@rollup/plugin-replace'; const production = process.env.NODE_ENV === 'production'; export default { // ... plugins: [ replace({ 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') }) ], // ... }; """ *Why:* Environment variables allow you to configure Rollup builds without modifying the configuration file, making it easier to deploy to different environments. * **Multiple Configurations:** Export an array of configurations for multiple builds. """javascript // rollup.config.js import { defineConfig } from 'rollup'; import typescript from '@rollup/plugin-typescript'; export default [ defineConfig({ input: 'src/index.ts', output: [ { file: 'dist/bundle.cjs', format: 'cjs' }, { file: 'dist/bundle.js', format: 'es' } ], plugins: [typescript()] }), defineConfig({ input: 'src/cli.ts', output: { file: 'dist/cli.js', format: 'cjs' }, plugins: [typescript()] }) ]; """ *Why:* This is particularly useful for libraries that want to output multiple formats (CJS, ESM, UMD) or for projects that have separate entry points (e.g., a library and a CLI tool). ### 4.4. TypeScript Usage * **Strict Mode:** Enable strict mode in TypeScript (""strict": true" in "tsconfig.json"). """json // tsconfig.json { "compilerOptions": { "strict": true, // ... other options }, "include": ["src/**/*"], "exclude": ["node_modules"] } """ *Why:* Strict mode enables a set of stricter type checking rules, which can help catch errors early and improve code quality. This includes "noImplicitAny", "noImplicitThis", "strictNullChecks", "strictFunctionTypes", and "strictPropertyInitialization". * **Explicit Types:** Use explicit types for function parameters, return values, and variables when the type cannot be inferred. """typescript // Do This function add(a: number, b: number): number { return a + b; } const message: string = 'Hello, world!'; // Avoid: Relying solely on type inference when clarity improves with explicitness. function add(a, b) { // Implied 'any' type – BAD! return a + b; } """ *Why:* Explicit types make the code more readable and help prevent type-related errors. They also improve the developer experience by providing better code completion and error messages in IDEs. * **Interfaces and Types:** Use interfaces and types to define clear data structures. """typescript // Do This interface User { id: number; name: string; email: string; } type Result<T> = { success: true; data: T; } | { success: false; error: string; }; //Example usage function getUser(id: number): Result<User> { // ... implementation } // Don't Do This (using inline type definitions repeatedly) function processUser(user: { id: number; name: string; email: string }) { // ... } """ *Why:* Interfaces and types provide a clear and reusable way to define data structures, improving code organization and maintainability. Using discriminated unions (like "Result<T>") provides type safety for representing different outcomes of a function. * **Null and Undefined Checks:** Use strict null checks ("strictNullChecks") in TypeScript and handle null and undefined values properly. """typescript // Do This function greet(name?: string) { if (name) { console.log("Hello, ${name}!"); } else { console.log('Hello, guest!'); } } // Less Safe (if strictNullChecks is enabled) function greet(name: string) { // Potential error if name is undefined console.log("Hello, ${name}!"); } """ *Why:* TypeScript's "strictNullChecks" feature helps prevent errors caused by unexpected null or undefined values. Always check for these values before using them. ## 5. Documentation * **JSDoc Comments:** Use JSDoc comments to document functions, classes, and variables. """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 calculateSum(a, b) { return a + b; } """ *Why:* JSDoc comments provide valuable information about the code and can be used to generate documentation automatically. * **Meaningful Comments:** Write comments that explain the intent and purpose of the code, not just what the code does. """javascript // Do This // Cache the result to improve performance const cachedResult = calculateExpensiveOperation(); // Don't Do This (obvious and unhelpful) // Calculate the result const result = calculateExpensiveOperation(); """ *Why:* Meaningful comments help other developers understand the code and make it possible to maintain it more effectively. ## 6. Testing * **Unit Tests:** Write comprehensive unit tests to ensure that the code works as expected. * **Integration Tests:** Write integration tests to ensure that different parts of the system work together correctly. * **Test Coverage:** Aim for high test coverage to minimize the risk of bugs. * **Descriptive Test Names:** Use descriptive names for tests to make it clear what each test is verifying. ## 7. Security * **Input Validation:** Validate all user input to prevent security vulnerabilities. * **Output Encoding:** Encode output to prevent cross-site scripting (XSS) attacks. * **Secure Dependencies:** Use secure dependencies and keep them up to date. * **Avoid Hardcoded Secrets:** Avoid hardcoding secrets (e.g., API keys, passwords) in the code. Use environment variables or configuration files to store sensitive information. ## 8. Tooling * **ESLint:** Use ESLint to enforce code style and detect potential errors. * **Prettier:** Use Prettier to format the code automatically. * **TypeScript:** Use TypeScript for type checking and to improve code quality. * **Rollup Plugins:** Utilize appropriate Rollup plugins to optimize and transform the code. * **Git:** Utilize Git for version control, following standard branching and commit message conventions. ## 9. Updating this document This document is a living document and should be updated as needed to reflect new best practices and changes in the Rollup ecosystem. All contributions and suggestions are welcome.
# Deployment and DevOps Standards for Rollup This document outlines the Deployment and DevOps standards for Rollup-based projects, focusing on build processes, CI/CD pipelines, and production considerations. These standards aim to ensure maintainable, performant, and secure deployments of Rollup bundles. ## 1. Build Processes and Production Considerations ### 1.1. Configurable Build Scripts **Standard:** Utilize configurable build scripts for different environments (development, staging, production). **Why:** Encapsulating build logic in scripts enables automation and environment-specific optimizations. Configuration allows adjusting output and optimization levels depending on where the build is being deployed. **Do This:** * Use "npm scripts" or similar to define build commands. * Leverage environment variables to modify Rollup's configuration. **Don't Do This:** * Hardcode environment-specific settings directly into your Rollup configuration file. * Manually run Rollup builds without utilizing scripts. **Example:** """json // package.json { "scripts": { "build:dev": "rollup -c rollup.config.js --environment NODE_ENV:development", "build:prod": "rollup -c rollup.config.js --environment NODE_ENV:production" } } """ """javascript // rollup.config.js import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import terser from '@rollup/plugin-terser'; const production = process.env.NODE_ENV === 'production'; export default { input: 'src/main.js', output: { file: 'dist/bundle.js', format: 'iife', sourcemap: true }, plugins: [ nodeResolve(), commonjs(), production && terser() // Only minify in production ] }; """ **Anti-Pattern:** Including sensitive information (API keys, database credentials) in your Rollup configuration or build scripts. Use secure environment variables. ### 1.2. Code Splitting for Optimized Delivery **Standard:** Implement code splitting to reduce initial load times. **Why:** Chunking your code allows browsers to download only the necessary code for a particular page or feature, significantly improving performance, especially for large applications. **Do This:** * Use dynamic "import()" statements for modules that are not needed upfront. * Configure Rollup to create separate chunks for these modules. **Don't Do This:** * Bundle all code into a single, monolithic file. * Neglect to analyze the generated chunks and adjust splitting strategy as needed. **Example:** """javascript // src/main.js async function loadDashboard() { const { renderDashboard } = await import('./dashboard'); renderDashboard(); } loadDashboard(); // src/dashboard.js export function renderDashboard() { console.log('Rendering the dashboard!'); } """ """javascript // rollup.config.js import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; export default { input: 'src/main.js', output: { dir: 'dist', format: 'es', sourcemap: true, chunkFileNames: 'chunks/[name]-[hash].js' // Explicitly name chunks }, plugins: [ nodeResolve(), commonjs() ] }; """ **Explanation:** The dynamic import in "src/main.js" tells Rollup to create a separate chunk for "src/dashboard.js". The "chunkFileNames" output option helps manage the naming of these split chunks. ### 1.3. Asset Management and Optimization **Standard:** Integrate asset management and optimization during the build process. **Why:** Handling assets correctly ensures that images, fonts, and other resources are optimized for performance and delivered efficiently. **Do This:** * Use Rollup plugins like "@rollup/plugin-image" or community plugins for processing images, fonts, and other assets. * Implement techniques like image compression, lazy loading, and resource inlining. **Don't Do This:** * Include large, unoptimized assets directly in your bundle. * Serve assets without appropriate caching headers. **Example:** """javascript // rollup.config.js import { nodeResolve } from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import image from '@rollup/plugin-image'; export default { input: 'src/main.js', output: { file: 'dist/bundle.js', format: 'iife', sourcemap: true }, plugins: [ nodeResolve(), commonjs(), image() // Handles image imports. Configure options within its plugin config if needed. ] }; """ """javascript // src/main.js import logo from './logo.png'; const img = document.createElement('img'); img.src = logo; document.body.appendChild(img); """ **Advanced Asset Handling:** For more advanced asset handling consider using a dedicated asset pipeline tool alongside Rollup, communicating via emitted files. Rollup can then bundle the generated JavaScript files that reference those optimized assets. This approach provides more control over transformations and optimizations. ### 1.4. Sourcemaps for Debugging **Standard:** Generate and properly configure sourcemaps for production debugging. **Why:** Sourcemaps allow you to debug your minified code in the browser using the original source files. This is crucial for diagnosing and fixing production issues. **Do This:** * Enable sourcemap generation in your Rollup configuration ("output.sourcemap: true"). * Ensure your server is configured to serve sourcemaps. * Use a service (like Sentry or Bugsnag) to automatically upload sourcemaps for error tracking. **Don't Do This:** * Deploy code to production without sourcemaps. * Expose sourcemaps publicly if they contain sensitive information. **Example:** """javascript // rollup.config.js export default { input: 'src/main.js', output: { file: 'dist/bundle.js', format: 'iife', sourcemap: true // Enable sourcemap generation } }; """ **Best Practice:** Store sourcemaps in a secure location accessible only to your error tracking service. Update your deployment process to automatically upload the sourcemaps after a successful build and deployment. ## 2. CI/CD Pipelines ### 2.1. Automated Builds and Testing **Standard:** Automate the build and testing process as part of your CI/CD pipeline. **Why:** Automated builds and tests ensure consistent and reliable deployments, reducing the risk of introducing errors into production. **Do This:** * Use a CI/CD platform like Jenkins, GitLab CI, GitHub Actions, CircleCI, or similar. * Configure the pipeline to run tests, linting, and Rollup builds. * Fail the build if any of these steps fail. **Don't Do This:** * Manually build and deploy code to production. * Skip testing or linting in the CI/CD pipeline. **Example (GitHub Actions):** """yaml # .github/workflows/ci.yml name: CI on: push: branches: [ "main" ] pull_request: branches: [ "main" ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Node.js uses: actions/setup-node@v3 with: node-version: '18.x' # Use a supported Node.js version - name: Install dependencies run: npm install - name: Run tests run: npm test - name: Build run: npm run build:prod """ **Explanation:** This workflow automates the installation of dependencies, running tests, and creating a production build using "npm run build:prod" (defined in "package.json"). Any failure in these steps will prevent the workflow from completing, thereby preventing a deployment. ### 2.2. Versioning and Tagging **Standard:** Implement proper versioning and tagging in your CI/CD pipeline. **Why:** Versioning and tagging provide a clear history of releases and allow for easy rollback to previous versions if necessary. **Do This:** * Use semantic versioning (SemVer). * Automatically tag commits with version numbers during the release process. * Store build artifacts with versioned names. **Don't Do This:** * Deploy code without versioning or tagging. * Use arbitrary or inconsistent versioning schemes. **Example (using "npm version" and git tagging):** """bash # In CI/CD pipeline after successful build: npm version patch # or minor/major based on the changes. This increments the version in package.json git add package.json git commit -m "Bump version to $(npm pkg get version)" git tag $(npm pkg get version) git push origin $(npm pkg get version) """ **Important:** Ensure your CI/CD system has the permissions to push tags to your repository. Consider using a dedicated "release" branch to trigger the versioning and tagging process. ### 2.3. Environment-Specific Configuration **Standard:** Manage environment-specific configuration using environment variables or configuration files. **Why:** Environment-specific configuration allows you to deploy the same code to different environments (development, staging, production) without modifying the code itself. **Do This:** * Use environment variables to store sensitive configuration data. * Load environment variables into your application at runtime. * Use ".env" files for development environments. **Don't Do This:** * Hardcode environment-specific settings directly into your code. * Commit sensitive configuration data to your version control system. **Example (using "dotenv" and environment variables):** """javascript // .env (for development) API_URL=http://localhost:3000 """ """javascript // rollup.config.js import dotenv from 'rollup-plugin-dotenv'; export default { // ...other config plugins: [ dotenv() ] } """ """javascript // src/api.js const apiUrl = process.env.API_URL; export async function fetchData() { const response = await fetch(apiUrl + '/data'); return response.json(); } """ **Note:** In production environments, environment variables should be set directly in the deployment environment (e.g., using the cloud provider's configuration settings) and not through ".env" files. ### 2.4. Rollback Strategy **Standard:** Define and test a rollback strategy. **Why:** A rollback strategy allows you to quickly revert to a previous version of your application in case of critical errors or deployment failures. **Do This:** * Keep previous build artifacts available. * Implement a mechanism to easily switch between versions. * Test the rollback process regularly. **Don't Do This:** * Deploy updates without a tested rollback plan. * Rely on manual intervention for rollbacks. **Implementation:** Rollback strategies depend heavily on the specifics of your deployment platform. Consider using feature flags to quickly disable problematic features. Containerization technologies (like Docker) and orchestration systems (like Kubernetes) significantly simplify rollback procedures. ## 3. Monitoring and Alerting ### 3.1. Application Performance Monitoring (APM) **Standard:** Integrate Application Performance Monitoring (APM) tools to monitor the performance of your Rollup-based application. **Why:** APM tools provide insights into the performance of your application, allowing you to identify and resolve performance bottlenecks. **Do This:** * Use tools like New Relic, Datadog, Sentry, or similar. * Track key metrics such as load times, API response times, and error rates. * Set up alerts to notify you when performance metrics exceed predefined thresholds. **Don't Do This:** * Deploy applications without performance monitoring. * Ignore performance alerts. **Implementation:** APM tools often require specific instrumentation within the application code. This might involve wrapping key functions or adding custom event tracking. ### 3.2. Error Tracking and Reporting **Standard:** Implement robust error tracking and reporting to quickly identify and address issues in production. **Why:** Error tracking allows you to proactively identify and fix errors before they impact users. **Do This:** * Use tools like Sentry, Bugsnag, or Rollbar. * Capture detailed error reports, including stack traces and user context. * Set up alerts to notify you of new or recurring errors. **Don't Do This:** * Rely solely on user reports for error detection. * Ignore error reports. **Example (Sentry):** """javascript import * as Sentry from "@sentry/browser"; import { Integrations } from "@sentry/tracing"; Sentry.init({ dsn: "YOUR_SENTRY_DSN", integrations: [new Integrations.BrowserTracing()], // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring. // We recommend adjusting this value in production tracesSampleRate: 0.1, release: "my-app@" + process.env.VERSION, // Include version information environment: process.env.NODE_ENV }); try { // Your application code } catch (e) { Sentry.captureException(e); } """ **Best Practice:** Include version information "(process.env.VERSION)" and the environment "(process.env.NODE_ENV)" with your error reports. This greatly aids in diagnosing the root cause of errors, especially in complex deployment environments. Set the "release" value in Sentry's configuration. ### 3.3. Logging **Standard:** Implement comprehensive logging to capture important events and debug issues. **Why:** Logging provides a record of application activity, allowing you to diagnose problems and track user behavior. **Do This:** * Use a logging library like "winston" or "pino". * Log important events, such as user logins, API calls, and errors. * Use different log levels (e.g., debug, info, warn, error) to indicate the severity of the event. * Rotate log files to prevent them from growing too large. * Centralize log storage for easier analysis. **Don't Do This:** * Log sensitive information (e.g., passwords, API keys). * Log excessively, which can impact performance. * Store logs on the same server as your application indefinitely. **Technology Specifics** * **ES Modules and Dynamic Imports:** Rollup is optimized for ES modules and dynamic "import()" statements. When building libraries, prefer ES module outputs, but also consider providing CommonJS or UMD builds for maximum compatibility. * **Tree Shaking:** Rollup's tree shaking algorithm is highly effective. Structure your code to maximize its ability to remove dead code and thus only include what's actually used. Avoid side-effecting code in module scope. * **Plugin Ecosystem:** Leverage the extensive Rollup plugin ecosystem to handle various tasks like TypeScript compilation, CSS processing, asset management, and code minification. * **Configuration:** Rollup configuration files can be complex. Use JavaScript (instead of JSON) for your configuration file allowing for dynamic configurations and better reusability. By adhering to these deployment and DevOps standards, you can ensure that your Rollup-based projects are deployed reliably, performant, and securely. Regularly review and update these standards to align with the latest best practices and Rollup features.
# 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.
# 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<any>; } """ """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<any> { 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.