# Core Architecture Standards for esbuild
This document outlines the core architectural standards for contributing to and maintaining esbuild. It emphasizes clarity, performance, maintainability, and security to ensure the long-term health and evolution of the project.
## 1. Fundamental Architectural Principles
esbuild's architecture centers around the concept of a fast, parallelizable, and correct bundler. Therefore, these architectural principles are of utmost importance:
* **Performance-Oriented Design:** Every design decision should prioritize performance. Minimize memory allocation, favor efficient data structures, and exploit parallelism wherever possible.
* **Correctness Above All:** The bundler must produce correct results. Implement thorough testing, perform rigorous code reviews, and prioritize correctness over premature optimizations.
* **Platform Independence:** esbuild should be easily portable to different operating systems and architectures. Abstract away platform-specific details and use standard libraries where possible.
* **Modularity and Extensibility:** The codebase should be modular and well-organized to facilitate maintenance and future extensions. Design components with clear interfaces and minimize dependencies.
## 2. Project Structure and Organization
esbuild's source code is organized into several key directories. Adhering to this structure is essential for consistent development.
* "internal/": This directory contains most of the core logic of esbuild, including parsing, linking, transforms, and AST manipulation. It’s further subdivided by functionality:
* "loaders/": Contains code for parsing different file types (e.g., ".js", ".ts", ".css").
* "linker/": Handles linking modules together into a single bundle.
* "parser/": Implements the JavaScript, TypeScript, and CSS parsers.
* "printer": Contains the code generator that prints the final output.
* "pkg/": Exposes the public API of esbuild. It defines the interfaces used by external clients.
* "cmd/esbuild/": Contains the entry point for the command-line tool.
* "test/": Contains the test suite, which includes unit tests, integration tests, and end-to-end tests.
**Standards:**
* **Do This:** Place new features or bug fixes within the appropriate directory based on their functionality.
* **Don't Do This:** Introduce new top-level directories unless absolutely necessary. Consult with core maintainers before making major structural changes.
* **Why:** Maintaining a consistent project structure makes it easier for developers to navigate the codebase and understand the location of specific features.
**Example:**
If you're implementing a new feature for TypeScript parsing, the code should reside within the "internal/parser/" directory or a subdirectory thereof.
## 3. Code Style and Conventions
Following a consistent code style is crucial for readability and maintainability. esbuild primarily uses Go with a focus on simplicity and explicitness.
* **Formatting:** Use "go fmt" to automatically format your code. This ensures consistent indentation, spacing, and line breaks.
* **Naming:**
* Use descriptive names for variables, functions, and types.
* Follow Go's naming conventions (e.g., "CamelCase" for exported identifiers, "camelCase" for unexported identifiers).
* Avoid abbreviations unless they are widely understood.
* **Comments:**
* Write clear and concise comments to explain complex logic.
* Document exported functions and types with godoc-style comments.
* Keep comments up to date with the code they describe.
* **Error Handling:** Use explicit error handling in Go. Return errors as the last return value of a function and check for errors at the call site.
**Standards:**
* **Do This:** Run "go fmt" on your code before submitting a pull request.
* **Do This:** Write godoc-style comments for all exported functions and types.
* **Do This:** Handle errors explicitly and provide informative error messages.
* **Don't Do This:** Use cryptic variable names or excessively short comments.
* **Don't Do This:** Ignore errors or use "_" (blank identifier) to discard them silently.
* **Why:** Consistent formatting and naming conventions improve code readability. Comments help other developers understand the code's purpose and functionality. Explicit error handling prevents unexpected behavior and makes debugging easier.
**Example:**
"""go
// ParseJavaScript parses the given JavaScript code and returns an AST.
// It returns an error if the code is invalid.
func ParseJavaScript(code string) (*ast.Program, error) {
program, err := parser.Parse(code)
if err != nil {
return nil, fmt.Errorf("failed to parse JavaScript code: %w", err)
}
return program, nil
}
"""
## 4. Memory Management
esbuild manipulates large amounts of data, particularly abstract syntax trees (ASTs) and source code. Efficient memory management is critical for performance.
* **Object Pooling:** Use object pools to reuse frequently allocated objects. This reduces the overhead of memory allocation and garbage collection. The "sync.Pool" type in Go's standard library is useful for this.
* **Avoid Unnecessary Copies:** Pass data by pointer or slice whenever possible to avoid unnecessary memory copies.
* **Data Structures:** Choose data structures carefully to minimize memory usage and access time. Consider using techniques like:
* Bitfields for compact representation of boolean flags.
* Custom hash tables for efficient lookups.
* Arrays and slices instead of linked lists when appropriate.
**Standards:**
* **Do This:** Use object pools for frequently allocated objects.
* **Do This:** Pass data by pointer or slice to avoid unnecessary copies.
* **Do This:** Choose data structures that are appropriate for the specific use case.
* **Don't Do This:** Allocate large amounts of memory without considering the impact on performance.
* **Don't Do This:** Create unnecessary copies of data.
* **Why:** Efficient memory management reduces memory consumption, improves performance, and lowers garbage collection overhead.
**Example:**
"""go
var astNodePool = sync.Pool{
New: func() interface{} {
return new(ast.Node)
},
}
func acquireASTNode() *ast.Node {
return astNodePool.Get().(*ast.Node)
}
func releaseASTNode(node *ast.Node) {
// Reset the node's fields to their zero values before returning it to the pool.
node.Type = ast.NodeTypeInvalid
node.Data = nil
astNodePool.Put(node)
}
// Usage:
node := acquireASTNode()
// ... use the node ...
releaseASTNode(node)
"""
## 5. Concurrency and Parallelism
esbuild leverages concurrency and parallelism to improve build performance. The core bundler is designed to be highly parallelizable.
* **Worker Pools:** Use worker pools to distribute tasks across multiple goroutines. This allows esbuild to take advantage of multi-core processors. The "golang.org/x/sync/errgroup" package can be helpful for managing groups of goroutines.
* **Data Partitioning:** Divide large tasks into smaller subtasks that can be processed independently in parallel.
* **Synchronization:** Use appropriate synchronization primitives (e.g., mutexes, channels, atomic operations) to protect shared data and prevent race conditions. Be mindful of potential deadlocks.
* **Avoid Global State:** Minimize the use of global state, as it can introduce contention and make it difficult to reason about concurrency.
**Standards:**
* **Do This:** Use worker pools to parallelize tasks.
* **Do This:** Partition data into smaller chunks that can be processed independently.
* **Do This:** Use appropriate synchronization primitives when accessing shared data.
* **Don't Do This:** Share mutable global state without proper synchronization.
* **Don't Do This:** Create unnecessary goroutines. Launch goroutines only when there is a significant amount of work to be done in parallel.
* **Why:** Concurrency and parallelism can significantly improve build performance. However, incorrect use of concurrency can lead to race conditions, deadlocks, and other problems.
**Example:**
"""go
import (
"fmt"
"sync"
"golang.org/x/sync/errgroup"
)
func processFile(filename string) error {
// Simulate file processing
fmt.Println("Processing file:", filename)
// ... actual processing logic ...
return nil
}
func processFilesParallel(filenames []string) error {
var eg errgroup.Group
const numWorkers = 4 // Adjust based on available CPU cores
jobChan := make(chan string, len(filenames)) // Buffered channel
for _, filename := range filenames {
jobChan <- filename
}
close(jobChan)
var worker = func(jobChan <-chan string) func() error {
return func() error {
for filename := range jobChan {
if err := processFile(filename); err != nil {
return err
}
}
return nil
}
}
for i := 0; i < numWorkers; i++ {
eg.Go(worker(jobChan))
}
return eg.Wait()
}
"""
## 6. Testing
A comprehensive test suite is essential for ensuring the correctness and stability of esbuild.
* **Unit Tests:** Write unit tests to verify the behavior of individual functions and components.
* **Integration Tests:** Write integration tests to verify the interaction between different components.
* **End-to-End Tests:** Write end-to-end tests to verify the behavior of the entire bundler. These tests should simulate real-world build scenarios.
* **Regression Tests:** Write regression tests to prevent previously fixed bugs from reoccurring.
* **Fuzzing:** Use fuzzing techniques to discover unexpected behavior and potential vulnerabilities.
**Standards:**
* **Do This:** Write unit tests for all new features and bug fixes.
* **Do This:** Write integration tests to verify the interaction between different components.
* **Do This:** Write end-to-end tests to simulate real-world build scenarios.
* **Do This:** Add a regression test when fixing a bug.
* **Do This:** Run the test suite regularly to ensure that the code is working correctly.
* **Don't Do This:** Skip writing tests.
* **Don't Do This:** Submit code without running the test suite.
* **Why:** A comprehensive test suite helps to ensure the correctness and stability of the code. It also makes it easier to refactor the code without introducing bugs.
## 7. Security
Security best practices are of high importance, especially when dealing with user-provided code.
* **Input Validation:** Validate all user-provided input and escape it appropriately.
* **Avoid Code Injection:** Be careful when generating code from strings, as this can open the door to code injection vulnerabilities. Use parameterized code generation techniques instead.
* **Dependency Management:** Keep dependencies up to date to avoid known vulnerabilities. Use a dependency management tool (e.g., "go mod") to manage dependencies and ensure that they are reproducible.
* **Sandboxing:** Consider using sandboxing techniques to isolate esbuild from the rest of the system.
* **Regular Audits:** Perform regular security audits to identify potential vulnerabilities.
**Standards:**
* **Do This:** Validate and escape all user-provided input.
* **Do This:** Use parameterized code generation techniques instead of generating code from strings.
* **Do This:** Keep dependencies up to date.
* **Do This:** Perform regular security audits.
* **Don't Do This:** Trust user-provided input without validation.
* **Don't Do This:** Generate code from strings without proper escaping.
* **Why:** Security vulnerabilities can have serious consequences. Following security best practices helps to protect esbuild and its users from attacks.
## 8. Error Reporting and Logging
Clear and informative error reporting and logging are crucial for debugging and troubleshooting.
* **Error Messages:** Provide clear and informative error messages that help the user understand the problem and how to fix it. Include relevant context in the error message.
* **Logging:** Use logging to record important events and debug information. Log levels should be used appropriately (e.g., "debug", "info", "warning", "error").
* **Stack Traces:** Include stack traces in error messages to help pinpoint the source of the error.
* **Structured Logging:** Consider using structured logging to make it easier to analyze logs programmatically. JSON or other structured formats are preferred over plain text.
**Standards:**
* **Do This:** Provide clear and informative error messages.
* **Do This:** Use logging to record important events and debug information.
* **Do This:** Include stack traces in error messages.
* **Do This:** Consider using structured logging.
* **Don't Do This:** Provide cryptic or unhelpful error messages.
* **Don't Do This:** Log sensitive information.
* **Why:** Effective error reporting and logging make it easier to debug and troubleshoot problems.
## 9. API Design
When designing new APIs, consider the following guidelines:
* **Simplicity:** Keep APIs simple and easy to use. Avoid unnecessary complexity.
* **Consistency:** Follow existing API conventions.
* **Discoverability:** Make APIs discoverable by using clear and descriptive names.
* **Extensibility:** Design APIs to be extensible so that they can be adapted to future needs.
* **Documentation:** Document APIs thoroughly with godoc-style comments.
**Standards:**
* **Do This:** Keep APIs simple and easy to use.
* **Do This:** Follow existing API conventions.
* **Do This:** Make APIs discoverable by using clear and descriptive names.
* **Do This:** Design APIs to be extensible.
* **Do This:** Document APIs thoroughly with godoc-style comments.
* **Don't Do This:** Create APIs that are overly complex or difficult to use.
* **Don't Do This:** Deviate from existing API conventions without a good reason.
* **Why:** Well-designed APIs are easier to use, maintain, and extend.
## 10. Dependency Management
esbuild uses "go mod" for dependency management.
* **Vendoring:** Vendor dependencies to ensure that builds are reproducible.
* **Minimal Dependencies:** Keep the number of dependencies to a minimum to reduce the risk of vulnerabilities and improve build times.
* **Up-to-Date Dependencies:** Keep dependencies up to date to benefit from bug fixes and security patches.
**Standards:**
* **Do This:** Vendor dependencies.
* **Do This:** Keep the number of dependencies to a minimum.
* **Do This:** Keep dependencies up to date.
* **Don't Do This:** Add unnecessary dependencies.
* **Don't Do This:** Use outdated dependencies.
* **Why:** Proper dependency management ensures that builds are reproducible and secure.
This document provides a comprehensive overview of the core architectural standards for esbuild. By adhering to these standards, developers can contribute to a high-quality, performant, and maintainable project.
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 esbuild This document outlines the coding standards for component design within the esbuild project. These standards are intended to promote the creation of reusable, maintainable, performant, and secure code, targeting the latest version of esbuild. These standards will be incorporated into AI coding assistants to ensure code consistency and quality across the project. ## 1. Component Definition and Purpose ### 1.1. Standard: Clear Component Responsibility **Do This:** * Define each component with a single, clearly defined responsibility. * Use descriptive names that accurately reflect the component's purpose. **Don't Do This:** * Create "god components" that handle multiple unrelated tasks. * Use vague names that offer no insight into the component's functionality. **Why:** Single responsibility improves modularity, testability, and maintainability. A component that does one thing well is easier to understand, debug, and reuse. **Example:** """typescript // Good - Specific task: resolving import paths interface Resolver { resolvePath(modulePath: string, importerPath: string): Promise<string | null>; } class DefaultResolver implements Resolver { async resolvePath(modulePath: string, importerPath: string): Promise<string | null> { // Logic to resolve the module path return null; // Placeholder } } // Bad - Multiple tasks (resolving and transforming) class ModuleHandler { // Avoid async resolvePath(modulePath: string, importerPath: string): Promise<string | null> { // ... } async transform(code: string): Promise<string> { // ... } } """ ### 1.2. Standard: Explicit Component Interface **Do This:** * Clearly define the input and output of each component using TypeScript interfaces or types. * Use interfaces or abstract classes to define contracts for abstract operations. TypeScript "type" is also acceptable for simple components. **Don't Do This:** * Rely on implicit assumptions about the data types or formats being processed. * Expose internal state directly without proper encapsulation. **Why:** Explicit interfaces provide a contract that ensures components interact predictably. This improves code robustness and simplifies debugging. Type safety prevents unexpected runtime errors. **Example:** """typescript // Good: Explicit interface interface TransformContext { options: { sourcemap: boolean; }; target: string; } interface Transformer { transform(code: string, context: TransformContext): Promise<string>; } class BabelTransformer implements Transformer { constructor(private babelConfig: any) {} async transform(code: string, context: TransformContext): Promise<string> { // ... Babel transformation logic using context and config ... return code; // placeholder } } // Bad: Implicit assumptions and direct property access class LegacyTransformer { // Avoid async transform(code: string, options: any): Promise<string> { if (options.sourcemap) { // Implicit dependency on options structure // ... } return code; // placeholder } } """ ## 2. Component Intercommunication ### 2.1. Standard: Loose Coupling **Do This:** * Minimize dependencies between components. Components should interact through well-defined interfaces, not direct access to internals. * Consider using dependency injection or other design patterns to manage dependencies. **Don't Do This:** * Create tight coupling where components are highly dependent on specific implementations of other components. * Directly instantiate components within other components unless absolutely necessary. **Why:** Loose coupling increases code flexibility and allows for easier modification and testing. It promotes code reuse, which leads to faster development cycles. **Example:** """typescript // Good: Dependency Injection interface BundlerPlugin { apply(compiler: Compiler): void; } class MyCustomPlugin implements BundlerPlugin { constructor(private options: any) {} apply(compiler: Compiler): void { compiler.hooks.beforeBuild.tap('MyCustomPlugin', () => { console.log('Custom plugin running'); }); } } interface Compiler { hooks: { beforeBuild: { tap: (name: string, callback: () => void) => void; }; }; // ... other compiler API } class EsbuildCompiler implements Compiler { hooks = { beforeBuild: { tap: (name: string, callback: () => void) => void } }; } // Placeholder implementation const compiler = new EsbuildCompiler(); const myPlugin = new MyCustomPlugin({ setting: true }); myPlugin.apply(compiler); // Injects the compiler // Bad: Tight Coupling class AnotherPlugin { // Avoid constructor() { // The plugin directly relies on the EsbuildCompiler Implementation. this.compiler = new EsbuildCompiler(); // Tight coupling } compiler: EsbuildCompiler; // Tightly coupled to EsbuildCompiler. } """ ### 2.2. Standard: Asynchronous Communication **Do This:** * Utilize asynchronous operations (Promises, async/await) for long-running or I/O-bound tasks to avoid blocking the main thread. * Implement appropriate error handling for asynchronous operations using "try...catch" blocks or ".catch()" methods. **Don't Do This:** * Perform synchronous operations that can potentially block the main thread, leading to performance issues. * Ignore potential errors in asynchronous operations, which can lead to unhandled exceptions or unexpected behavior. **Why:** Asynchronous operations are essential for maintaining a responsive user interface and preventing performance bottlenecks, especially during build processes in esbuild. Proper error handling prevents silent failures and improves system stability. **Example:** """typescript // Good: Asynchronous processing async function loadModule(file: string): Promise<string> { try { // Simulate asynchronous file reading return new Promise((resolve) => { setTimeout(() => { resolve("Content of ${file}"); }, 50); }); } catch (error) { console.error("Error loading module ${file}:", error); throw error; // Re-throw to propagate the error } } // Bad: Synchronous processing (Avoid) function loadModuleSync(file: string): string { // Simulate synchronous file reading(Avoid) // In a real scenario, this might be blocking I/O on a drive return "Content of ${file}"; } """ ### 2.3. Standard: Event-Driven Architecture (Where Appropriate) **Do This:** * Consider leveraging an event-driven architecture for inter-component communication when components need to react to events happening in other components without direct dependencies. * Use esbuild's (or a compatible library's) mechanisms or standard JavaScript events for event handling. **Don't Do This:** * Overuse events when a direct function call or a simpler communication mechanism would suffice. * Create complex event chains that are difficult to trace and debug. **Why:** Event-driven architectures promote loose coupling and allow for flexible and extensible systems. Components can subscribe to events of interest without knowing the details of the event publishers. **Example** """typescript // Good: Event-driven approach interface BuildEvent { type: 'buildStart' | 'buildEnd' | 'buildError'; payload?: any; } class Emitter { listeners: {[key:string]:((event:BuildEvent)=> void)[]} = {}; on(event: string, listener: (event: BuildEvent) => void) { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event].push(listener); } emit(event: BuildEvent) { const eventName = event.type if(this.listeners[eventName]){ this.listeners[eventName].forEach(listener => listener(event)); } } } const buildEvents = new Emitter(); // Example Listener buildEvents.on('buildStart', (event) => { console.log('Build started', event); }); // Within Esbuild, emit the event buildEvents.emit({ type: 'buildStart', payload: { timestamp: Date.now() } }); // Bad: Polling (Avoid unless absolutely necessary) class PollingComponent { // Avoid private lastBuildTime: number | null = null; async checkBuildStatus() { setInterval(async () => { const currentBuildTime = await this.getLatestBuildTime(); // Hypothetical function if (currentBuildTime !== this.lastBuildTime) { console.log('Build status changed!'); this.lastBuildTime = currentBuildTime; } }, 1000); } async getLatestBuildTime(): Promise<number | null> { // Check the file system to see if the build has completed return Date.now(); // Placeholder } } """ ## 3. Component Implementation Details ### 3.1. Standard: Immutability where appropriate **Do This:** * Favor immutable data structures where possible. This simplifies reasoning about component state and prevents unexpected side effects. * Use immutable libraries to guarantee immutability. **Don't Do This:** * Mutate data structures directly when immutability can be achieved. * Sharemutable state between components without careful synchronization. **Why:** Immutability makes state predictable and easier to reason about. This simplifies debugging, testing, and concurrency. **Example:** """typescript // Good: Immutable data import { produce } from "immer"; interface BuildOptions { entryPoints: string[]; outfile: string; } function updateOptions(baseOptions: BuildOptions, newEntryPoint: string): BuildOptions { return produce(baseOptions, (draft) => { draft.entryPoints.push(newEntryPoint); }); } const initialOptions: BuildOptions = { entryPoints: ['src/index.js'], outfile: 'dist/bundle.js' }; const updatedOptions = updateOptions(initialOptions, 'src/another.js'); console.log('Initial options', initialOptions); console.log('Updated options', updatedOptions); // Initial options and updatedOptions are distinct objects. // Bad: Mutable data interface MutableBuildOptions { // Avoid entryPoints: string[]; outfile: string; } function dangerouslyUpdateOptions(options: MutableBuildOptions, newEntryPoint: string): void { options.entryPoints.push(newEntryPoint); // Mutates the original object } const mutableOptions: MutableBuildOptions = { entryPoints: ['src/index.js'], outfile: 'dist/bundle.js' }; dangerouslyUpdateOptions(mutableOptions, 'src/another.js'); console.log('Options after mutation', mutableOptions); // The original object is modified. """ ### 3.2. Standard: Error Handling **Do This:** * Implement robust error handling within each component. Use "try...catch" blocks to handle potential exceptions. * Log errors with sufficient context to aid in debugging. * Provide informative error messages to the user or calling component. **Don't Do This:** * Swallow exceptions without logging or handling them. * Rely on generic error messages that provide no useful information. * Expose sensitive information in error messages. **Why:** Proper error handling prevents unexpected crashes and makes it easier to diagnose and resolve issues. Informative error messages improve the user experience and reduce debugging time. **Example:** """typescript // Good: Error handling async function compileModule(file: string): Promise<string> { try { // Simulate compilation that may throw an exception if (file.includes('error')) { throw new Error("Compilation failed for ${file}"); } return "Compiled code for ${file}"; } catch (error: any) { console.error("Error compiling ${file}:", error.message); throw new Error("Failed to compile ${file}. See logs for details."); } } // Bad: Missing error handling async function unsafeCompileModule(file: string): Promise<string> { // Avoid // Simulate compilation that may throw an exception, without try...catch if (file.includes('error')) { throw new Error("Compilation failed for ${file}"); } return "Compiled code for ${file}"; } """ ### 3.3 Standard: Component Configuration **Do This:** * Pass configuration values (especially environment-specific ones) to components during construction instead of hardcoding them. * Use the "esbuild.context()" API for complex use cases where you need to rebuild based on file changes or manual triggers. * Consider using environment variables, command-line arguments, or configuration files to manage configuration. **Don't Do This:** * Embed credentials or API keys directly within the code. * Use global state to store configuration values. **Why:** Properly configuring components will limit errors and make it trivial to move components to more environments. **Example:** """typescript // Good: Configurable plugins interface PluginConfig { apiURL: string; apiKey: string; } class AnalyticsPlugin implements BundlerPlugin { constructor(private config: PluginConfig) {} apply(compiler: Compiler): void { compiler.hooks.beforeBuild.tap('AnalyticsPlugin', () => { this.sendAnalyticsData(); }); } private sendAnalyticsData() { // Send data to configurable API endpoint console.log("API URL: ${this.config.apiURL}"); console.log("API Key: ${this.config.apiKey}"); // ... } } // Usage const analyticsConfig: PluginConfig = { apiURL: process.env.ANALYTICS_API_URL || 'http://default-analytics-api.com', apiKey: process.env.ANALYTICS_API_KEY || 'default-key', }; const analyticsPlugin = new AnalyticsPlugin(analyticsConfig); """ ## 4. Esbuild-Specific Considerations ### 4.1. Standard: Plugin Interoperability **Do This:** * Design plugins to be compatible with other esbuild plugins. Avoid conflicting with common plugin patterns. * Use unique namespaces or prefixes for any global variables or functions introduced by your plugin to prevent name collisions. **Don't Do This:** * Assume exclusive control over the build process. * Modify esbuild's internal state directly. **Why:** esbuild's plugin ecosystem relies on interoperability. Well-behaved plugins ensure a seamless build environment. **Example:** """typescript // Good: Namespaced Plugin const MyPluginNamespace = 'my-custom-plugin'; interface MyPluginOptions { // ... } class MyEsbuildPlugin implements BundlerPlugin { constructor(private options: MyPluginOptions) {} apply(compiler: Compiler): void { compiler.hooks.beforeBuild.tap("${MyPluginNamespace}:beforeBuild", () => { // Add custom logic here console.log('Running my plugin'); }); } } // Bad: Global conflicts class ConflictingPlugin implements BundlerPlugin { // Avoid apply(compiler: Compiler): void { compiler.hooks.beforeBuild.tap('beforeBuild', () => { //Avoid using generic names like 'beforeBuild' as they can conflict with other plugins console.log('Running my conflicting plugin'); }); } } """ ### 4.2. Standard: Utilizing Esbuild Transforms **Do This:** * Utilize esbuild's built-in transform capabilities for common tasks such as JSX compilation, TypeScript transpilation, and minification. * Use custom transforms via plugins only when necessary. **Don't Do This:** * Reimplement functionality that is already provided by esbuild. * Introduce unnecessary dependencies or complexity. **Why:** Leveraging esbuild's built-in transforms improves performance and reduces the need for external dependencies. **Example:** """typescript // Good: Using esbuild's built-in JSX transform const options = { entryPoints: ['src/index.jsx'], bundle: true, outfile: 'dist/bundle.js', jsxFactory: 'h', // Customize JSX factory function, if needed jsxFragment: 'Fragment', // Customize JSX fragment function, if needed }; // In esbuild, configure the 'jsxFactory' and 'jsxFragment' options directly: require('esbuild').build(options).catch((e: any) => console.error(e.message)); // Bad: Using an external transform for JSX *without a good reason* // (e.g., Babel just for JSX when Esbuild can handle it natively) """ ### 4.3. Standard: Optimization for Esbuild **Do This:** * Use esbuild's specific optimization flags (minify, tree shaking, etc.) to maximize build performance and minimize bundle size. * Profile build times and bundle sizes to identify potential bottlenecks. **Don't Do This:** * Assume default settings are always optimal. * Ignore performance implications when designing complex plugins. **Why:** Esbuild is designed for speed and efficiency. Utilizing its optimization features unlocks its full potential. ## 5. Testing and Documentation ### 5.1. Standard: Unit and Integration Tests **Do This:** * Write unit tests for individual components to verify their functionality in isolation. * Write integration tests to ensure that components work together correctly and that the system as a whole behaves as expected. **Don't Do This:** * Skip writing tests altogether. Relying on manual testing is insufficient for large projects. * Write tests that are too brittle or tightly coupled to implementation details. Tests should focus on behavior, not implementation. **Why:** Rigorous testing is crucial for maintaining code quality and preventing regressions. Tests provide confidence when making changes and refactoring code. ### 5.2. Standard: Clear Documentation **Do This:** * Document each component's purpose, inputs, outputs, and dependencies with clear and concise language. Use JSDoc or similar tools. * Provide examples of how to use the component in common scenarios. **Don't Do This:** * Leave components undocumented, making it difficult for others to understand and use them. * Write documentation that is outdated or inaccurate. **Why:** Clear documentation is essential for maintainability and collaboration. It empowers developers to understand the system quickly and make changes with confidence. By adhering to these component design standards, the esbuild project can maintain a high level of code quality and ensure its long-term success.
# State Management Standards for esbuild This document outlines coding standards for state management within esbuild projects. These standards aim to promote maintainability, performance, and predictability. While esbuild primarily focuses on bundling, its configuration and plugin ecosystem often require managing state effectively. This document specifically addresses state-related challenges within the esbuild context, not general application state management (e.g., React Context, Redux). ## 1. General Principles * **Explicitness:** State changes should be explicit and predictable. Avoid implicit state mutations that can lead to unexpected behavior. * **Immutability (Preferred):** Favor immutable data structures, especially for configuration and plugin options. Changes should generate new objects, not modify existing ones. This prevents unintended side effects and simplifies debugging. * **Single Source of Truth:** Ensure each piece of state has a single, well-defined source. Avoid duplicating state or deriving it unnecessarily, as this increases the risk of inconsistencies. * **Minimization:** Only store essential data in state. Derived values should be computed on demand rather than stored, if computationally feasible. * **Scope Awareness:** State should be scoped appropriately. Module-level, function-level, and block-level scoping should be deliberately chosen based on the state's purpose and lifespan. Avoid global state unless absolutely necessary. * **Performance Consciousness:** For performance-critical pathways, be mindful of the overhead of state management techniques. Immutable operations (e.g. spreading objects) can be expensive if overused. Consider mutable updates with careful usage when necessary. * **Asynchronous Awareness:** Be acutely aware of asynchronicity. esbuild's plugin API is inherently asynchronous, requiring best practices for concurrently modifying state. * **Error Handling:** State management code should include robust error handling to prevent application crashes or data corruption. ## 2. Configuration State Management esbuild relies heavily on configuration objects passed to the "build" and "transform" functions. Effective management of these configurations is crucial. ### 2.1. Standard: Immutable Configuration * **Do This:** Create new configuration on modification instead of altering the original one. * **Don't Do This:** Mutate configuration objects directly after they've been passed to esbuild. * **Why:** esbuild might internally cache or rely on the immutability of certain configuration options. Mutable configuration can cause unexpected side effects or cache invalidation issues. """javascript // Correct: Creating a new configuration by spreading const originalConfig = { entryPoints: ["src/index.js"], bundle: true, outfile: "dist/bundle.js", }; async function updateConfig(extraPlugins) { const newConfig = { ...originalConfig, plugins: [...(originalConfig.plugins || []), ...extraPlugins], }; await esbuild.build(newConfig); } // Incorrect: Modifying the original configuration const originalConfig = { entryPoints: ["src/index.js"], bundle: true, outfile: "dist/bundle.js", }; async function mutateConfig(extraPlugins) { originalConfig.plugins = [...(originalConfig.plugins || []), ...extraPlugins]; //BAD: Mutating original config await esbuild.build(originalConfig); // risk of side effects } """ * **Anti-patterns:** Passing the same mutable config object to "esbuild.build" multiple times and expecting consistent outcomes. ### 2.2. Standard: Configuration Validation * **Do This:** Validate configuration objects before passing them to "esbuild.build". * **Don't Do This:** Assume the configuration is always in the correct format. * **Why:** esbuild might throw cryptic errors if the config isn't valid. Valiation prevents this. """typescript import * as esbuild from 'esbuild'; import Ajv from "ajv"; const configSchema = { type: "object", properties: { entryPoints: { type: "array", items: { type: "string" } }, bundle: { type: "boolean" }, outfile: { type: "string" }, }, required: ["entryPoints", "bundle", "outfile"], additionalProperties: false, }; const ajv = new Ajv(); const validate = ajv.compile(configSchema); async function buildWithValidation(config: any) { const valid = validate(config); if (!valid) { console.error("Invalid esbuild config:", validate.errors); throw new Error("Invalid esbuild configuration."); } await esbuild.build(config); } const invalidConfig = { entryPoints: ["src/index.js"], bundle: "yes", outfile: "dist/bundle.js"}; //bundle should be boolean buildWithValidation(invalidConfig).catch(e => console.error(e)); const validConfig = { entryPoints: ["src/index.js"], bundle: true, outfile: "dist/bundle.js"}; buildWithValidation(validConfig); """ * **Tools:** AJV (JSON Schema validator) is popular. TypeScript's type system and validation tooling can also be used. Typebox is another good alternative. ### 2.3. Standard: Centralized Configuration * **Do This:** Define configuration in a central module and import / extend it where needed. * **Don't Do This:** Scatter configuration settings across your codebase. * **Why:** Centralization promotes consistency and allows easy updates. """javascript // config/esbuild.config.js export const baseConfig = { entryPoints: ["src/index.js"], bundle: true, outfile: "dist/bundle.js", minify: process.env.NODE_ENV === "production", sourcemap: true, }; // build.js import * as esbuild from 'esbuild'; import { baseConfig } from "./config/esbuild.config.js"; esbuild.build({ ...baseConfig, plugins: [], }).catch(() => process.exit(1)); // anotherBuild.js import * as esbuild from 'esbuild'; import { baseConfig } from "./config/esbuild.config.js"; esbuild.build({ ...baseConfig, outfile: 'anotherDist/anotherBundle.js' }).catch(() => process.exit(1)); """ ## 3. Plugin State Management Plugins often need to maintain internal state related to transform results, caching, or external resource tracking. ### 3.1. Standard: Encapsulated Plugin State * **Do This:** Use closures scope variables within a plugin factory function to encapsulate plugin state. * **Don't Do This:** Use global variables or properties outside the plugin's scope to store state. This creates a "spooky action at a distance", and leaks/pollutes the environment. * **Why:** Proper encapsulation prevents naming conflicts and ensures that the plugin's state is isolated from the rest of the application. It is a key principle of modularity. """typescript import * as esbuild from 'esbuild'; function MyPlugin(options: { cacheDir: string }) { // Plugin state is encapsulated within the closure let cache = new Map<string, string>(); let callCount = 0; return { name: "my-plugin", setup(build: esbuild.PluginBuild) { build.onLoad({ filter: /.*/ }, async (args) => { callCount++; const cachedValue = cache.get(args.path); if(cachedValue) { console.log("[my-plugin]: CACHE HIT!"); return { contents: cachedValue, loader: 'js' } } // Simulate reading from a file system const contents = "// File: ${args.path} Called: ${callCount}"; cache.set(args.path, contents) return { contents, loader: 'js' }; }); build.onEnd( (result) => { console.log("[my-plugin]: Build ended"); } ) }, }; } esbuild.build({ entryPoints: ["src/index.js"], bundle: true, outfile: "dist/bundle.js", plugins: [MyPlugin({ cacheDir: ".cache" })], }).catch(() => process.exit(1)); """ ### 3.2. Standard: Immutability for Plugin Options * **Do This:** Treat plugin options (passed to the plugin factory) as immutable. * **Don't Do This:** Change the options object passed to your plugin after the plugin factory is called. * **Why:** Similar to configuration, esbuild might rely on the options provided to the plugin being stable. """javascript // Plugin Definition function MyPlugin(options) { // Treat options as read-only // ... use options but don't modify return { name: "my-plugin", setup(build) { // Access options here, but don't modify console.log("My Plugin options:", options); } } } // Usage const pluginOptions = { debug: true, verbose: false }; esbuild.build({ entryPoints: ["src/index.js"], bundle: true, outfile: "dist/bundle.js", plugins: [MyPlugin(pluginOptions)], }).then(() => { // DO NOT MUTATE pluginOptions here! // pluginOptions.debug = false; // BAD }).catch(() => process.exit(1)); """ ### 3.3. Standard: Asynchronous State Updates * **Do This:** When multiple "onLoad", "onResolve", or "onTransform" hooks access and modify shared plugin state, use appropriate concurrency control mechanisms (e.g., locks, atomic operations) to prevent race conditions. * **Don't Do This:** Assume that these hooks execute sequentially or atomically when updating shared state. * **Why:** esbuild runs these hooks concurrently which canlead to race conditions if your state has dependencies between operations. """typescript import * as esbuild from 'esbuild'; function CounterPlugin() { let counter = 0; const mutex = new Mutex(); // From the "async-mutex" package return { name: "counter-plugin", setup(build) { build.onLoad({ filter: /.*/ }, async (args) => { const release = await mutex.acquire(); try { counter++; console.log("[counter-plugin]: File ${args.path} loaded. Counter: ${counter}"); return { contents: "// Counter: ${counter}", loader: 'js' }; // Example: return content with counter } finally { release(); } }); }, }; } import { Mutex } from "async-mutex"; esbuild.build({ entryPoints: ["src/index.js", "src/another.js"], // Multiple entry points to trigger concurrency bundle: true, outfile: "dist/bundle.js", plugins: [CounterPlugin()], }).catch(() => process.exit(1)); """ * **Libraries:** "async-mutex" provides a simple mutex implementation. Atomic operations may be relevant for simpler cases. ### 3.4 Standard: Avoid direct filesystem writes within onLoad/onResolve/onTransform hooks. * **Do This:** Accumulate changes in memory and write them to disk in the "onEnd" hook. * **Don't Do This:** Write directly to the file system within the "onLoad", "onResolve", or "onTransform" hooks. * **Why:** Writing to the filesystem in these hooks can be inefficient and slow down the build process. esbuild's parallel execution model makes it prone to race conditions and file system contention. """typescript import * as esbuild from 'esbuild'; import * as fs from 'fs/promises'; interface FileChange { path: string; content: string; } function FileUpdatePlugin() { let fileChanges: FileChange[] = []; return { name: 'file-update-plugin', setup(build) { build.onLoad({ filter: /.*/ }, async (args) => { // Simulate modifying file content; accumulate the change fileChanges.push({ path: args.path, content: "// Modified ${args.path}" }); return { contents: "// Placeholder for ${args.path}", loader: 'js' }; }); build.onEnd(async (result) => { // Write all accumulated changes to the file system in onEnd for (const change of fileChanges) { await fs.writeFile(change.path, change.content); console.log("Updated file: ${change.path}"); } fileChanges = []; // Reset for the next build }); }, }; } esbuild.build({ entryPoints: ["src/index.js", "src/another.js"], bundle: true, outfile: "dist/bundle.js", plugins: [FileUpdatePlugin()], }).catch(() => process.exit(1)); """ ### 3.5. Standard: Caching Strategies * **Do This:** If your plugin performs computationally intensive tasks, implement a caching mechanism to avoid recomputation on subsequent builds. Use esbuild's "initialOptions" to persist cache directories. * **Don't Do This:** Rely on global variables or other non-persistent storage for caching, as this will not work across builds or in different processes. * **Why:** Caching can drastically improve build performance, especially for large projects or when using complex transformations. """typescript import * as esbuild from 'esbuild'; import * as fs from 'fs/promises'; import * as crypto from 'crypto'; import * as path from 'path'; interface MyPluginOptions { cacheDir: string; } function generateCacheKey(content: string): string { return crypto.createHash('sha256').update(content).digest('hex'); } function MyCachingPlugin(options: MyPluginOptions) { const cache = new Map<string, string>(); const { cacheDir } = options; return { name: "my-caching-plugin", async setup(build: esbuild.PluginBuild) { // Ensure cache directory exists await fs.mkdir(cacheDir, { recursive: true }); build.onLoad({ filter: /.*/ }, async (args) => { const fileContent = await fs.readFile(args.path, 'utf-8'); const cacheKey = generateCacheKey(fileContent) const cachedPath = path.join(cacheDir, "${cacheKey}.js"); try { await fs.access(cachedPath); console.log("[my-caching-plugin]: Cache hit for ${args.path}"); const cachedContents = await fs.readFile(cachedPath, 'utf-8'); return { contents: cachedContents, loader: 'js' }; } catch (e) { console.log("[my-caching-plugin]: Cache miss for ${args.path}"); // Simulate expensive computation const transformedContents = "// Transformed content: ${fileContent}"; //Write to cache directory await fs.writeFile(cachedPath, transformedContents) return { contents: transformedContents, loader: 'js' }; } }); }, }; } const cacheDirectory = ".esbuild-cache"; esbuild.build({ entryPoints: ["src/index.js"], bundle: true, outfile: "dist/bundle.js", plugins: [MyCachingPlugin({cacheDir: cacheDirectory})], // Make sure the cache directory is persistent by supplying an empty cache: incremental: true, // Enable incremental builds for better caching metafile: true, write: true }).catch(() => process.exit(1)); """ * **Storage:** Use the filesystem for persistent caching. Consider using a dedicated caching library. Libraries might handle cache eviction policies, invalidation strategies, and other advanced features. ### 3.6 Standard: Properly dispose of resources. * **Do This:** Release any resources allocated by your plugin (e.g., file handles, network connections) in the "onEnd" hook or when the esbuild process exits. * **Don't Do This:** Leak resources, as this can lead to memory leaks or other issues. * **Why:** Failing to release resources can lead to performance degradation or application instability. """typescript import * as esbuild from 'esbuild'; import * as fs from 'fs/promises'; function FileWatchingPlugin() { let watcher: fs.FSWatcher | null = null; let watchedFiles = new Set<string>(); return { name: 'file-watching-plugin', setup(build) { build.onStart(async () => { // Initialize watcher watcher = fs.watch('.', {recursive: true}); for await (const event of watcher) { console.log("File changed: ${event.filename} (${event.eventType})"); } }); build.onEnd(async (result) => { // Close the watcher to release resources console.log("Closing watcher"); //@ts-ignore watcher.close(); //Properly dispose of watcher. }); }, }; } esbuild.build({ entryPoints: ["src/index.js"], bundle: true, outfile: "dist/bundle.js", plugins: [FileWatchingPlugin()], watch: true }).catch(() => process.exit(1)); """ ## 4. Metafile and Analysis State esbuild's metafile provides a wealth of information that can be used for optimization, analysis, and visualization. ### 4.1. Standard: Analyzing the Metafile * **Do This:** Use the "metafile" option to generate a JSON file containing build metadata, then analyze it. * **Don't Do This:** Ignore the metafile, especially in complex projects where it can help identify optimization opportunities. * **Why:** The metafile provides insights into module sizes, dependencies, and other build characteristics that can be used to improve performance. """javascript import * as esbuild from 'esbuild'; import * as fs from 'fs/promises'; async function analyzeMetafile(metafilePath) { try { const metafileContent = await fs.readFile(metafilePath, 'utf-8'); const metafile = JSON.parse(metafileContent); let totalBytes = 0; // Iterate through all outputs for (const outputFile in metafile.outputs) { const output = metafile.outputs[outputFile]; totalBytes += output.bytes; } console.log("Total size of all outputs is:", totalBytes); // Example: Print all inputs and their respective bytes used in the build console.log("Inputs and their sizes:"); for (const inputFile in metafile.inputs) { const input = metafile.inputs[inputFile]; console.log("- ${inputFile}: ${input.bytes} bytes"); } } catch (error) { console.error("Error analyzing metafile:", error); } } esbuild.build({ entryPoints: ["src/index.js"], bundle: true, outfile: "dist/bundle.js", metafile: true, write: true, }).then(async result => { if (result.metafile) { const metafilePath = 'meta.json'; await fs.writeFile(metafilePath, JSON.stringify(result.metafile, null, 2)); await analyzeMetafile(metafilePath); // Analyze the metafile after writing } }).catch(() => process.exit(1)); """ ### 4.2. Standard: Automated Optimization * **Do This:** Use the metafile data to automate optimization tasks, such as identifying large dependencies or unused code. * **Don't Do This:** Manually inspect and optimize the build output without using the metafile data. Automate for efficiency. * **Why:** Automation ensures consistent and repeatable optimization processes that are not prone to human error. Example: (Pseudo-code - requires complex implementation). """javascript // (Conceptual example - implementation not provided) async function pruneLargeDependencies(metafilePath) { // 1. Analyze metafile to identifty large deps // 2. Dynamically rewrite code to lazy-load these dependencies // 3. Trigger a rebuild with updated code. } """ These standards provide a framework for consistent and maintainable state management in esbuild projects. Each project will present unique state management challenges. Ensure that you understand the underlying principles and adapt your implementation accordingly, and focus on the specific challenges within the esbuild context, like the plugin API, configuration, and metafile analysis.
# Performance Optimization Standards for esbuild This document outlines coding standards focused on performance optimization within the esbuild ecosystem. These standards are designed to improve application speed, responsiveness, and resource utilization. It takes into account the latest features and best practices for achieving optimal performance with esbuild. ## 1. Bundle Size Optimization ### 1.1 Code Splitting **Standard:** Implement code splitting to reduce the initial bundle size and improve page load times. Only load the code necessary for the initial view. **Why:** Loading less code initially speeds up the time to interactive (TTI). Code splitting allows browsers to parallelize downloads and cache specific parts of the application independently. **Do This:** * Use dynamic imports ("import()") to create split points. * Configure esbuild's "splitting: true" option. * Consider route-based splitting for single-page applications (SPAs). **Don't Do This:** * Load the entire application in one monolithic bundle. * Neglect analyzing the bundle to identify splitting opportunities. **Example:** """javascript // app.js async function loadModule() { const module = await import('./my-module.js'); module.default(); } loadModule(); """ """javascript // esbuild config (esbuild.config.js or similar) require('esbuild').build({ entryPoints: ['app.js'], bundle: true, outfile: 'dist/bundle.js', format: 'esm', splitting: true, chunkNames: '[name]-[hash]', // useful for cache busting }).catch(() => process.exit(1)); """ **Anti-Pattern:** Incorrectly configuring "splitting" or forgetting to set the "format: 'esm'" can lead to unexpected bundling behavior. Ensure your target environment supports ES modules. ### 1.2 Tree Shaking (Dead Code Elimination) **Standard:** Utilize tree shaking to remove unused code from the final bundle. **Why:** Reduces the bundle size by excluding code that is never executed, leading to faster download and parsing times. **Do This:** * Write pure ES modules with explicit exports. * Avoid side effects in top-level code (e.g., modifying global variables). * Use ""sideEffects": false" in "package.json" if your package contains no side effects. Otherwise, specify files with side effects **Don't Do This:** * Rely on CommonJS modules (tree shaking is less effective). * Import entire libraries when only specific functions are needed. **Example:** """javascript // my-module.js export function usedFunction() { return "This function is used."; } export function unusedFunction() { return "This function is never used."; } """ """javascript // app.js import { usedFunction } from './my-module.js'; console.log(usedFunction()); """ esbuild, with its default settings, will automatically remove "unusedFunction" during bundling. **package.json:** """json { "name": "my-package", "version": "1.0.0", "sideEffects": false // or ["./some-file-with-side-effects.js"] } """ **Anti-Pattern:** Accidental side effects (e.g., modifying a global variable during module initialization) can prevent tree shaking. Always ensure your modules are pure and side-effect free, or meticulously document any side effects when using ""sideEffects": [...]" in "package.json". ### 1.3 Minimization and Compression **Standard:** Minify and compress the bundled code to further reduce its size. **Why:** Minification removes unnecessary characters (whitespace, comments) and shortens variable names. Compression (e.g., using Gzip or Brotli) significantly reduces the file size transferred over the network. **Do This:** * Use esbuild's built-in minification ("minify: true"). * Configure a web server to compress assets using Gzip or Brotli. * Consider using a dedicated compression tool like "brotli" for pre-compressing assets. **Don't Do This:** * Deploy unminified and uncompressed code to production. * Rely solely on client-side compression (server-side compression is more efficient). **Example:** """javascript // esbuild config require('esbuild').build({ entryPoints: ['app.js'], bundle: true, outfile: 'dist/bundle.js', minify: true, // Enable minification }).catch(() => process.exit(1)); """ **Web Server Configuration (Example Nginx):** """nginx gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/rss+xml application/atom+xml image/svg+xml; gzip_vary on; brotli on; brotli_static on; # serve pre-compressed files if they exist brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/rss+xml application/atom+xml image/svg+xml; """ **Anti-Pattern:** Neglecting compression on the server-side. Even minified code can benefit greatly from Gzip or Brotli compression. ### 1.4 Dependency Optimization **Standard:** Analyze and optimize your project dependencies. **Why:** Over-reliance on large or unnecessary dependencies can significantly increase bundle size. **Do This:** * Use tools like "npm audit" or "yarn audit" to identify vulnerabilities and outdated dependencies. Upgrading can sometimes reduce size. * Consider alternatives to large libraries. Can a smaller, more focused library accomplish the same task? * Use "esbuild --analyze" to understand your bundle composition and identify large dependencies. * Import modules directly from source using specific file paths, when appropriate, to reduce the overhead that some package entry points introduce. **Don't Do This:** * Install dependencies without understanding their impact on bundle size. * Keep unused or outdated dependencies in your project. **Example:** Using "--analyze" """bash esbuild app.js --bundle --outfile=dist/bundle.js --analyze """ This generates a detailed report in your terminal (or can be saved to a file) showing the size of each module in your bundle. This helps to pinpoint the largest contributors to your bundle's overall size. **Anti-Pattern:** Blindly adding dependencies without considering their size or alternatives. Regularly audit your dependencies. ## 2. Build Performance ### 2.1 Caching **Standard:** Leverage caching to speed up subsequent builds. **Why:** Caching reduces the amount of work esbuild needs to do on each build, especially for large projects. **Do This:** * Use esbuild's incremental build feature, "incremental: true", for faster rebuilds during development. * Utilize a persistent cache directory by setting the "cache" property in the esbuild config. * Consider using a dedicated caching solution for more complex build pipelines. **Don't Do This:** * Disable caching in development environments. * Ignore cache invalidation strategies when files change. **Example:** """javascript // esbuild config let ctx = await require('esbuild').context({ entryPoints: ['app.js'], bundle: true, outfile: 'dist/bundle.js', incremental: true, // Enable incremental builds cache: 'esbuild-cache', // Specify a cache directory }); await ctx.watch() // Rebuild when files change """ **Anti-Pattern:** Not utilizing esbuild's built-in caching mechanisms results in significantly slower builds, especially as projects grow. ### 2.2 Parallelism **Standard:** Utilize esbuild's inherent parallelism to improve build speed, or consider using task runners like "npm-run-all" or "concurrently" to run multiple esbuild instances in parallel (e.g., for different bundles or environments). **Why:** esbuild is designed to utilize all available CPU cores, improving build performance significantly. In some cases, further parallelism can be achieved by running multiple esbuild commands concurrently. **Do This:** * Ensure your development environment has sufficient CPU cores for optimal performance. * If building multiple independent bundles, consider using a task runner to parallelize the builds. * Monitor CPU utilization during builds to identify potential bottlenecks. **Don't Do This:** * Artificially limit esbuild's parallelism (unless there is a specific reason to do so). * Overload the system with too many concurrent builds, which can lead to performance degradation. **Example (using "npm-run-all"):** """json // package.json { "scripts": { "build:app": "esbuild app.js --bundle --outfile=dist/app.js", "build:admin": "esbuild admin.js --bundle --outfile=dist/admin.js", "build": "npm-run-all --parallel build:app build:admin" }, "devDependencies": { "npm-run-all": "*" } } """ This example shows how to build two independent bundles ("app.js" and "admin.js") in parallel using "npm-run-all", significantly reducing the overall build time. **Anti-Pattern:** Neglecting to take advantage of parallelism. Many developers simply run a single esbuild command, missing out on substantial performance gains. ### 2.3 Target Selection **Standard:** Specify the appropriate "target" for your build to minimize polyfills and transformations. **Why:** Targeting a modern environment avoids unnecessary code transformations and polyfills, leading to smaller and faster bundles. **Do This:** * Set the "target" option in esbuild's configuration to match the browsers you need to support. Use modern targets ("esnext", "es2020", etc.) when possible. * Consider using a tool like "browserslist" to manage browser compatibility and automatically configure the target. **Don't Do This:** * Target older browsers unnecessarily, which forces esbuild to include more polyfills and transformations. * Leave the "target" unspecified, which often results in a default target that is too broad. **Example:** """javascript // esbuild config require('esbuild').build({ entryPoints: ['app.js'], bundle: true, outfile: 'dist/bundle.js', target: 'es2020', // Target modern browsers }).catch(() => process.exit(1)); """ Or utilizing browserslist: **package.json** """json { "browserslist": [ "> 0.5%", "last 2 versions", "not dead" ] } """ esbuild will automatically infer the correct target from your browserslist configuration. **Anti-Pattern:** Over-transpiling code. Targeting modern browsers drastically reduces the need for polyfills and transforms, leading to huge performance improvements. ### 2.4 Avoid Expensive Operations During Build **Standard:** Minimize or eliminate expensive operations during the build process. **Why:** Complex transformations, image processing, or network requests during the build can significantly increase build times. **Do This:** * Pre-process images or other assets *before* the build. * Use caching mechanisms to avoid re-running expensive tasks on every build. * Move non-critical operations to runtime (e.g., lazy-loading images). **Don't Do This:** * Perform complex transformations within the esbuild pipeline unless absolutely necessary. * Make network requests or access the file system excessively during the build. **Example:** Instead of resizing images during the esbuild process using a plugin, pre-resize them and include the resized images in your project. **Anti-Pattern:** Embedding complex operations within the build. Keeps builds fast and manageable. ## 3. Runtime Performance ### 3.1 Lazy Loading **Standard:** Implement lazy loading for non-critical resources to improve initial page load time. **Why:** Loading resources on demand reduces the amount of data transferred initially, improving perceived performance. **Do This:** * Use "import()" for lazy-loading JavaScript modules. * Implement lazy-loading for images and other media using the "loading="lazy"" attribute. **Don't Do This:** * Load all resources upfront, even if they are not immediately needed. **Example:** """html <img src="my-image.jpg" loading="lazy" alt="My Image"> """ ### 3.2 Optimize Dynamic Imports **Standard**: Use named chunks and prefetch/preload when using dynamic imports heavily. **Why**: Naming the code-split chunks generated by dynamic imports helps keep bundles consistent across builds for better caching. Using "link rel=prefetch" or "link rel=preload" causes the browser to download the module in the background, so it is available when actually imported. **Do This**: * Use the "chunkNames" option in your esbuild config. * Insert "<link rel=prefetch ...>" or "<link rel=preload ...>" tags for frequently used dynamic imports. **Don't Do This**: * Leave chunk naming to the defaults. The hashes change on every rebuild, breaking caching. **Example**: """javascript // esbuild config require('esbuild').build({ entryPoints: ['app.js'], bundle: true, outfile: 'dist/bundle.js', format: 'esm', splitting: true, chunkNames: '[name]-[hash]', // meaningful chunk names }).catch(() => process.exit(1)); """ """html <link rel="prefetch" href="/dist/my-module-XXXX.js" as="script"> """ **Anti-Pattern**: Not using a chunkName and allowing default hashes in dynamic imports ### 3.3 Efficient Code **Standard**: Write performant algorithms and data structures to minimize CPU usage. **Why**: Inefficient code can lead to slow rendering, poor responsiveness, and increased battery consumption. **Do This**: * Profile your code to identify performance bottlenecks. * Use appropriate data structures for the task (e.g., "Map" vs. "Object", "Set" vs. "Array"). * Avoid unnecessary loops and computations. **Don't Do This**: * Write code without considering its performance implications. * Ignore profiling results and blindly optimize code. **Example:** Using more efficient built-in Javascript methods Instead of: """javascript const arr = [1, 2, 3, 4, 5]; const newArr = []; for (let i = 0; i < arr.length; i++) { newArr.push(arr[i] * 2); } """ Do this: """javascript const arr = [1, 2, 3, 4, 5]; const newArr = arr.map(x => x * 2); """ **Anti-Pattern**: Writing code sloppily without thinking about the performance implications will bog down the application. ### 3.4 Memory Management **Standard**: Strive for efficient memory management, and reduce memory leaks. **Why**: Improper memory management can lead to crashes, and sluggish performance **Do This**: * Avoid constantly allocating and deallocating objects **Don't Do This**: * Create memory leaks! **Example:** Bad: """javascript function createEventListener() { const element = document.getElementById('myElement'); element.addEventListener('click', function() { // This creates a memory leak because the listener isn't removed // when the element is removed }); } """ Good: """javascript function createEventListener() { const element = document.getElementById('myElement'); const listener = function() { // Click handler }; element.addEventListener('click', listener); // remove the event listener when the element is no longer needed. element.removeEventListener('click', listener); } """ **Anti-Pattern**: Forgetting to unregister event listeners will cause memory leaks ## 4. Plugin Optimization ### 4.1 Minimize Number of Plugins **Standard:** Use as few esbuild plugins as possible. **Why:** Each plugin adds overhead to the build process because it must parse and process every file. **Do This:** * Evaluate whether a plugin is truly necessary. Can the same functionality be achieved with built-in esbuild features or a simple build script? * Combine the functionality of multiple plugins into a single plugin if possible. **Don't Do This:** * Add plugins without considering their performance impact. **Anti-Pattern:** Having a cluttered esbuild config with many overly specific plugins. ### 4.2 Optimize Plugin Performance **Standard:** Ensure that plugin logic is performant. **Why:** Slow plugin logic can significantly increase build times. **Do This:** * Profile your plugin code to identify performance bottlenecks. * Use efficient algorithms and data structures within your plugins. * Cache results where appropriate. **Don't Do This:** * Perform expensive operations unnecessarily within a plugin. **Example:** Within an esbuild plugin, use caching to avoid re-processing files that have not changed. ### 4.3 Avoid Blocking Plugins **Standard:** Avoid using synchronous operations in esbuild plugins. **Why:** Synchronous operations block the main thread, preventing esbuild from processing other files in parallel. **Do This:** * Use asynchronous APIs instead of synchronous ones (e.g., "fs.readFile" instead of "fs.readFileSync"). **Don't Do This:** * Perform synchronous file I/O or other blocking operations within a plugin. **Example:** Using asynchronous file reading """javascript const fs = require('fs').promises; // Import asynchronous fs methods const myPlugin = { name: 'my-plugin', setup(build) { build.onLoad({ filter: /\.txt$/ }, async (args) => { // Use async function const contents = await fs.readFile(args.path, 'utf8'); // Asynchronous file read return { contents, loader: 'text' }; }); }, }; module.exports = myPlugin; """ **Anti-Pattern:** Synchronously reading and processing files inside of an esbuild plugin will slow down builds.
# Testing Methodologies Standards for esbuild This document outlines the standards for testing methodologies within the esbuild project. It serves as a guide for developers and a context for AI coding assistants, ensuring consistent and high-quality testing practices. These standards aim to improve maintainability, reliability, and performance of esbuild. ## 1. General Testing Philosophy ### 1.1. Test Driven Development (TDD) **Do This:** Embrace TDD by writing tests *before* implementing the code. This helps clarify requirements, ensures testability, and promotes a more modular design. **Don't Do This:** Implement features and then add tests as an afterthought. This often results in incomplete test coverage and tightly coupled code. **Why This Matters:** TDD encourages designing for testability from the start, leading to more maintainable and robust code. It also provides immediate feedback on code changes. **Specific to esbuild:** Given esbuild's performance-critical nature, TDD helps ensure that new features and optimizations don't introduce regressions. Write performance tests alongside functional tests. """javascript // Example: Test before implementation. Assume we're creating a new plugin that supports JSX automatic runtime. // file: tests/jsx-automatic-runtime.test.js const esbuild = require('esbuild'); const path = require('path'); const fs = require('fs'); describe('JSX Automatic Runtime Plugin', () => { it('should transform JSX using automatic runtime', async () => { const entryPoint = path.join(__dirname, 'fixtures/jsx-input.jsx'); const outfile = path.join(__dirname, 'dist/output.js'); // Before the plugin is implemented, this will throw because esbuild doesn't inherently support JSX automatic runtime // The goal is to make this test pass when the plugin *is* implemented. await esbuild.build({ entryPoints: [entryPoint], outfile: outfile, bundle: true, format: 'esm', plugins: [/* new JSXAutomaticRuntimePlugin() Implement this to pass test! */], //Placeholder for plugin jsx: 'automatic', // Enables automatic JSX runtime }); const output = fs.readFileSync(outfile, 'utf8'); expect(output).toContain('React.createElement'); // Verify output contains the transformed JSX }); }); // Initial placeholder test """ ### 1.2. Continuous Integration (CI) **Do This:** Integrate tests with a CI/CD pipeline to automatically run tests on every commit. This ensures fast feedback and prevents regressions. **Don't Do This:** Rely on manual testing only. This is error-prone and slows down development. **Why This Matters:** CI provides early warning of errors and integrates testing directly into the development workflow, promoting code quality. **Specific to esbuild:** Configure CI to run tests across different platforms (Linux, macOS, Windows) and Node.js versions to ensure cross-platform compatibility, which is absolutely key for a tool like esbuild. Performance tests should also be part of the CI pipeline. ### 1.3. Code Coverage **Do This:** Track code coverage metrics to identify untested areas. Aim for high coverage, but remember that coverage is a tool, not an end in itself. **Don't Do This:** Obsess over achieving 100% coverage without considering the quality of the tests. Meaningful tests are more important than high numbers. **Why This Matters:** Code coverage helps identify gaps in testing, reducing the risk of undiscovered bugs. **Specific to esbuild:** Pay close attention to code coverage in performance-sensitive areas such as the parser, printer, and module resolver. Utilize tools like "c8" or "nyc" for generating coverage reports. """javascript // Example: Using c8 for code coverage // In package.json: // "scripts": { // "test": "c8 mocha" // } // Run tests with coverage: // npm test """ ## 2. Types of Tests ### 2.1. Unit Tests **Do This:** Write focused unit tests that isolate individual functions, classes, or modules. Use mocks and stubs to control dependencies. **Don't Do This:** Write unit tests that are too broad or that depend on external systems. These tests are slow and brittle. **Why This Matters:** Unit tests provide fast feedback and help identify the source of bugs quickly. They also encourage modular design. **Specific to esbuild:** Unit test core components like the parser, scanner, and AST transformers. """javascript // Example: Unit test for a simple function const { transform } = require('../src/transform'); // hypothetical function const assert = require('assert'); describe('transform', () => { it('should correctly transform a simple expression', () => { const input = '1 + 1'; const expectedOutput = '2'; // Assuming simple evaluation const result = transform(input); assert.strictEqual(result, expectedOutput, 'Transformation failed'); }); it('should handle errors gracefully', () => { assert.throws(() => transform('invalid code'), Error, 'Expected error to be thrown'); }); }); """ Anti-pattern: Testing "transform" with external file dependencies would make this a bad unit test. Only internal logic should be triggered and tested. ### 2.2. Integration Tests **Do This:** Write integration tests to verify the interaction between multiple modules or components. Focus on testing the "seams" between different parts of the system. **Don't Do This:** Write integration tests that are too similar to unit tests. They should test higher-level interactions. Also avoid testing entire end-to-end flows in integration tests – that is the role of E2E tests. **Why This Matters:** Integration tests catch bugs that arise from the interaction between components, which unit tests might miss. **Specific to esbuild:** Test the integration of the parser with the module resolver or the transformer with the printer. """javascript // Example: Integration test for module resolution with a mock file system const esbuild = require('esbuild'); const assert = require('assert'); const path = require('path'); // Mock file system (for example, using 'memfs' or similar) const mockFs = require('memfs'); const vol = mockFs.Volume.fromJSON({ '/entry.js': 'import { foo } from "./module"; console.log(foo);', '/module.js': 'export const foo = "bar";', }); describe('Module Resolution Integration Test', () => { it('should correctly resolve modules with mock file system', async () => { // Override the default file system with the mock. mockFs.patch(); // Patch the real fs module try { vol.mkdirSync(path.dirname('/.test-tmp'), { recursive: true }); vol.writeFileSync('/.test-tmp/out.js', ''); await esbuild.build({ entryPoints: ['/entry.js'], bundle: true, outfile: '/.test-tmp/out.js', write: true, //This writes to the filesystem, so we need the mock absWorkingDir: '/', }); const output = vol.readFileSync('/.test-tmp/out.js', 'utf8'); assert.ok(output.includes('console.log("bar")')); // Check that "bar" is bundled correctly } finally { mockFs.restore(); // Restore the real fs module } }); }); """ ### 2.3. End-to-End (E2E) Tests **Do This:** Write E2E tests to verify the entire system works correctly from the user's perspective. Simulate real user interactions and validate the final output. **Don't Do This:** Over-rely on E2E tests for everything. They are slow and difficult to maintain. Focus them on critical user flows. **Why This Matters:** E2E tests ensure that all parts of the system work together correctly and that the system meets the user's needs. **Specific to esbuild:** Test complex build configurations, plugin interactions, and command-line interface behavior. Use tools like Playwright or Puppeteer for browser-based testing if testing browser targets. Command-line interface testing is crucial. Also test "stdin" functionality. """javascript //Example: End-to-end CLI test using node's child_process and asserting the output file content. const { execSync } = require('child_process'); const fs = require('fs'); const path = require('path'); const assert = require('assert'); describe('CLI End-to-End Tests', () => { it('should successfully bundle a simple project via CLI', () => { const inputPath = path.join(__dirname, 'fixtures/e2e-input.js'); const outputPath = path.join(__dirname, 'dist/e2e-output.js'); //Clean up, if file exists if (fs.existsSync(outputPath)) { fs.unlinkSync(outputPath); } // Construct the esbuild command line call (adjust path to your esbuild executable) const command = "node ./esbuild.js ${inputPath} --bundle --outfile=${outputPath} --format=cjs"; // Execute the command synchronously (for test purposes) execSync(command, { stdio: 'inherit' });// Use inherit to see the output in console // Assert that the output file exists. assert.ok(fs.existsSync(outputPath), 'Output file was not created'); // Validate content of output file const outputFileContent = fs.readFileSync(outputPath, 'utf8'); assert.ok(outputFileContent.includes('// Expected Content'), 'Output file does not contain expected content'); // cleanup fs.unlinkSync(outputPath); }); it('should handle stdin input', ()=>{ const input = "console.log('hello from stdin')"; const result = execSync("echo "${input}" | node ./esbuild.js --minify", {stdio: 'pipe'}).toString(); assert.ok(result.includes("console.log(\"hello from stdin\")")); }) }); """ ### 2.4. Performance Tests **Do This:** Regularly run performance tests to track the speed of esbuild's core operations. Use representative benchmark suites and track metrics over time. Implement statistical significance tests for perf changes. **Don't Do This:** Ignore performance testing or rely on anecdotal observations. Performance regressions can have a significant impact on users. **Why This Matters:** Performance is a key feature of esbuild. Performance tests help ensure that optimizations are effective and that regressions are detected early. **Specific to esbuild:** Benchmark bundling speed, minification speed, and code generation speed. Use tools like "benchmark.js" or "autocannon" to measure performance. Track the 95th and 99th percentile latencies. """javascript // Example: Performance test using benchmark.js const Benchmark = require('benchmark'); const esbuild = require('esbuild'); const path = require('path'); const suite = new Benchmark.Suite(); // Define a simple build task const entryPoint = path.join(__dirname, 'fixtures/benchmark-input.js'); // Larger file for perf tests const outfile = path.join(__dirname, 'dist/benchmark-output.js'); suite.add('esbuild.build', { defer: true, // Allows asynchronous setup and teardown fn: function(deferred) { esbuild.build({ entryPoints: [entryPoint], outfile: outfile, bundle: true, minify: true, format: 'esm', }).then(() => { deferred.resolve(); }).catch(e => { console.error("Build failed:", e); deferred.reject(); }); }, 'setup': function() { if (fs.existsSync(outfile)) { fs.unlinkSync(outfile); // Ensure a fresh start for each test } }, 'teardown': function() { //Optional: Clean up created file if needed but usually you want to inspect it if the benchmark failed. } }) .on('cycle', function(event) { console.log(String(event.target)); }) .on('complete', function() { console.log('Fastest is ' + this.filter('fastest').map('name')); }) .run({ 'async': true }); """ It's very important to add representative code into "benchmark-input.js". Having a variety of code patterns will provide stability in these types of benchmarks. ### 2.5. Regression Tests **Do This:** Create targeted regression tests when bugs are fixed. These tests should specifically reproduce the bug and ensure that it doesn't reappear. **Don't Do This:** Rely on existing tests to catch regressions. Specific regression tests are more reliable. **Why This Matters:** Regression tests prevent bugs from being reintroduced after they have been fixed, ensuring long-term stability. **Specific to esbuild:** Whenever a bug is reported and fixed, write a regression test that covers the specific scenario that caused the bug. Tag issues with the 'regression' label. """javascript // Example: Regression test based on a reported bug const esbuild = require('esbuild'); const assert = require('assert'); describe('Regression Test: Issue #1234 (Incorrect handling of dynamic imports)', () => { it('should correctly handle dynamic imports in specific scenario', async () => { const result = await esbuild.build({ entryPoints: [path.join(__dirname, 'fixtures/regression-input.js')], bundle: true, write: false, format: 'esm' }); const contents = result.outputFiles[0].contents; assert.ok(contents.includes('dynamic import correctly handled'), 'Dynamic import was not handled correctly'); }); }); """ ### 2.6 Fuzz Testing **Do This:** Utilize fuzz testing to generate large amounts of random input to find unexpected crashes, asserts failures, or security vulnerabilities. **Don't Do This:** Assume that human-generated examples cover all possible edge cases. **Why This Matters:** Fuzzing can automatically discover bugs that are easily missed during manual code review or unit testing. **Specific to esbuild:** Fuzz the parser with random JavaScript code, or fuzz plugin input with random data. """ javascript // Example Fuzz testing for the parser component // NOTE: this example uses simplified and is only intended as a demonstration. // Effective fuzzing requires integration with a fuzzing framework like AFL or libFuzzer. const fuzz = require('fuzzaldrin-plus'); // Example library - replace with a proper fuzzing harness const esbuild = require('esbuild'); describe('Fuzz Testing - Parser ', () => { it('should not crash when processing invalid JavaScript code', () => { const iterations = 100; for (let i = 0; i < iterations; i++) { const randomCode = fuzz.filter(['1', 'a', '+', '-', '*', '/', '(', ')', '[', ']', '{', '}', ';', '=', '.'], Math.floor(Math.random() * 15)); try { esbuild.transformSync(randomCode.join('')); } catch (e) { // Expecting to throw on invalid cases, allow unhandled rejections from testing framework - // Real implementation should catch specific esbuild error types. } } }); }); """ ## 3. Test Structure and Style ### 3.1. Test File Organization **Do This:** Organize test files in a directory structure that mirrors the source code. Create separate test files for each module or component. **Don't Do This:** Put all tests in a single file or mix test files with source code. **Why This Matters:** Clear file organization makes it easier to find, understand, and maintain tests. **Specific to esbuild:** Maintain a "tests" directory at the root of the repository, with subdirectories that mirror the "src" directory. ### 3.2. Test Naming Conventions **Do This:** Use descriptive names for test files, test suites, and test cases. Follow a consistent naming convention. **Don't Do This:** Use unclear or ambiguous names. **Why This Matters:** Clear naming improves the readability and maintainability of the tests. **Specific to esbuild:** Use filenames like "module-name.test.js" or "component-name.spec.js". Use descriptive test case names that explain what is being tested. ### 3.3. Test Assertions **Do This:** Use clear and specific assertions. Provide helpful error messages when assertions fail. **Don't Do This:** Use vague or generic assertions. **Why This Matters:** Clear assertions make it easier to understand why a test failed and to diagnose the underlying problem. **Specific to esbuild:** Use "assert.strictEqual", "assert.deepStrictEqual", and other specific assertion methods from Node.js' "assert" module. Avoid using truthy/falsy assertions unless appropriate. When possible, extract utilities for comparing AST nodes. """javascript // Example: Good and bad assertions const assert = require('assert'); // Good: Specific assertion with a helpful error message assert.strictEqual(result, expected, 'The transform function should return the expected value'); // Bad: Vague assertion assert.ok(result); // Doesn't explain what "result" should be // Bad: Type coercion assert.equal(1, "1"); // Avoid this, and use strictEqual """ ### 3.4. Test Setup and Teardown **Do This:** Use "beforeEach" and "afterEach" hooks to set up and tear down test fixtures. This ensures that each test runs in a clean environment. **Don't Do This:** Leave test fixtures in a dirty state after a test runs. **Why This Matters:** Proper setup and teardown prevent tests from interfering with each other and ensure reproducible results. **Specific to esbuild:** Use "before" and "after" especially for setting up mock environments. Use "beforeEach" and "afterEach" for manipulating these shared resources. ### 3.5 Mocking **Do This:** Use mocking to isolate the component under test and control its dependencies. Avoid over-mocking. Mock only external dependencies or components that are difficult to test directly. **Don't Do This:** Mock internal implementation details. This makes tests brittle and tightly coupled to the implementation. **Why This Matters:** Mocking makes tests faster, more predictable, and easier to maintain. **Specific to esbuild:** Mock file system access, network requests, or calls to external tools. Consider using libraries like "proxyquire" or "mock-fs". ## 4. Tooling and Frameworks ### 4.1. Test Runner **Do This:** Use a popular test runner like Mocha or Jest. These runners provide features like test discovery, parallel execution, and code coverage reporting. **Don't Do This:** Write your own test runner or use an unsupported runner. **Why This Matters:** Established test runners provide a rich set of features and are well-supported by the community. **Specific to esbuild:** Both Mocha and Jest are acceptable. Mocha is likely preferable due to ease of integration. Jest support requires additional configuration. ### 4.2. Assertion Library **Do This:** Use the built-in "assert" module with its strict assertion functions. **Don't Do This:** Rely solely on truthiness. **Why This Matters:** Built-in assertion modules ensure consistency across the test suite. ### 4.3. Mocking Library **Do This:** Use a mocking library like "proxyquire" or "mock-fs" to create mock objects and replace dependencies. **Don't Do This:** Roll your own mocking framework. **Why This Matters:** Mocking libraries provide a convenient and reliable way to isolate components under test. ## 5. Security Testing **Do This:** Integrate security testing into the development process, including static analysis, dynamic analysis, and penetration testing if necessary. **Don't Do This:** Neglect security testing or treat it as an afterthought. **Why This Matters:** Security testing helps identify and mitigate vulnerabilities before they can be exploited. **Specific to esbuild:** Perform static analysis to detect potential security issues in the code. Fuzz test the parser and other critical components with malformed input. """javascript // Example Use a vulnerability scanner against esbuild's dependencies // npm audit // OR using yarn // yarn audit """ This document provides a comprehensive overview of the testing methodologies standards for esbuild. Adhering to these standards will help ensure the quality, reliability, and maintainability of the project. Remember that this document should be updated as esbuild evolves and new testing techniques emerge. Regularly review and refine these standards to stay current with best practices.
# API Integration Standards for esbuild This document outlines the coding standards and best practices for integrating esbuild with backend services and external APIs. It focuses on modern approaches, maintainability, performance, and security, adhering to the latest esbuild version and ecosystem. These standards aim to guide developers and provide context for AI coding assistants in generating high-quality esbuild code. ## 1. Architectural Considerations ### 1.1 Decoupling and Abstraction **Standard:** Decouple esbuild build processes from specific API implementations. Use abstraction layers to interact with backend services. **Why:** * **Maintainability:** Allows for changes in API endpoints, authentication methods, or data structures without affecting the core build process. * **Testability:** Enables easier unit testing by mocking the abstraction layer. * **Flexibility:** Supports different environments (development, staging, production) with varied API configurations. **Do This:** Define interfaces or abstract classes for API interactions. **Don't Do This:** Directly embed API calls within esbuild build scripts. **Example:** """typescript // api-client.ts (Abstraction Layer) export interface APIClient { fetchData(endpoint: string): Promise<any>; postData(endpoint: string, data: any): Promise<any>; } export class ProductionAPIClient implements APIClient { private apiUrl = 'https://api.example.com'; async fetchData(endpoint: string): Promise<any> { const response = await fetch("${this.apiUrl}/${endpoint}"); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } async postData(endpoint: string, data: any): Promise<any> { const response = await fetch("${this.apiUrl}/${endpoint}", { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(data), }); if (!response.ok) { throw new Error("HTTP error! status: ${response.status}"); } return await response.json(); } } // Mock API Client for testing export class MockAPIClient implements APIClient { async fetchData(endpoint: string): Promise<any> { return Promise.resolve({ data: "Mock data for ${endpoint}" }); } async postData(endpoint: string, data: any): Promise<any> { return new Promise((resolve) => { resolve({status:200, post_response:data}) }) } } """ """typescript // esbuild-plugin.ts (esbuild plugin using the abstraction) import * as esbuild from 'esbuild'; import { APIClient, ProductionAPIClient, MockAPIClient } from './api-client'; interface MyPluginOptions { apiClient: APIClient; } const myPlugin = (options: MyPluginOptions): esbuild.Plugin => { return { name: 'my-plugin', setup(build) { build.onStart(async () => { try { const data = await options.apiClient.fetchData('config'); console.log('Fetched data:', data); // Process API data here } catch (error) { console.error('Error fetching data:', error); } }); }, }; }; //Example usage: const useMockAPI = true; esbuild.build({ entryPoints: ['src/index.ts'], bundle: true, outfile: 'dist/bundle.js', plugins: [myPlugin({ apiClient: useMockAPI ? new MockAPIClient() : new ProductionAPIClient() })], }).catch(() => process.exit(1)); """ ### 1.2 Configuration Management **Standard:** Externalize API configurations (endpoints, authentication keys) using environment variables or configuration files. **Why:** * **Security:** Prevents hardcoding sensitive information in the code. * **Environment Awareness:** Allows easy switching between development, staging, and production environments. * **Deployment:** Facilitates deployment using CI/CD pipelines and immutable infrastructure. **Do This:** Use libraries like "dotenv" or "cross-env" to manage environment variables. **Don't Do This:** Hardcode API URLs or keys directly into your source code. **Example:** """typescript // .env file API_URL=https://api.staging.example.com API_KEY=your_staging_api_key """ """typescript // Use dotenv in your esbuild plugin import * as esbuild from 'esbuild'; import * as dotenv from 'dotenv'; import * as path from 'path'; dotenv.config({ path: path.resolve(__dirname, '.env') }); // Load .env file const apiConfigPlugin = (): esbuild.Plugin => ({ name: 'api-config', setup(build) { build.onDefine(() => { const apiUrl = process.env.API_URL || 'https://api.default.example.com'; const apiKey = process.env.API_KEY || 'default_api_key'; return { 'process.env.API_URL': JSON.stringify(apiUrl), 'process.env.API_KEY': JSON.stringify(apiKey), }; }); }, }); esbuild.build({ entryPoints: ['src/index.ts'], bundle: true, outfile: 'dist/bundle.js', plugins: [apiConfigPlugin()], }).catch(() => process.exit(1)); """ """typescript // src/index.ts const apiUrl = process.env.API_URL; const apiKey = process.env.API_KEY; console.log("API URL:", apiUrl); console.log("API Key:", apiKey); """ ### 1.3 Error Handling and Resilience **Standard:** Implement robust error handling for API requests. Handle network errors, timeouts, and invalid responses gracefully. **Why:** * **User Experience:** Prevents the application from crashing or displaying cryptic error messages. * **Reliability:** Improves the overall resilience of the build process. * **Debugging:** Provides helpful error messages and logging for troubleshooting. **Do This:** Use "try...catch" blocks and implement retry mechanisms for transient errors. **Don't Do This:** Ignore errors or let unhandled exceptions bubble up. **Example:** """typescript // api-client.ts import fetch from 'node-fetch'; // Ensure fetch is available in your environment export async function fetchDataWithRetry(url: string, maxRetries = 3): Promise<any> { let retryCount = 0; while (retryCount < maxRetries) { try { const response = await fetch(url); if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } return await response.json(); } catch (error) { console.error("Attempt ${retryCount + 1} failed: ${error}"); retryCount++; // Wait before retrying (exponential backoff) await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 100)); } } throw new Error("Failed to fetch data after ${maxRetries} retries."); } // Usage within an esbuild plugin: import * as esbuild from 'esbuild'; import { fetchDataWithRetry } from './api-client'; const retryPlugin = (): esbuild.Plugin => ({ name: 'retry-plugin', setup(build) { build.onStart(async () => { try { const data = await fetchDataWithRetry('https://api.example.com/data'); console.log('Successfully fetched data:', data); } catch (error) { console.error('Failed to fetch data after retries:', error); } }); }, }); esbuild.build({ entryPoints: ['src/index.ts'], bundle: true, outfile: 'dist/bundle.js', plugins: [retryPlugin()], }).catch(() => process.exit(1)); """ ## 2. Authentication and Authorization ### 2.1 Secure Key Management **Standard:** Protect API keys and secrets using secure storage mechanisms (e.g., environment variables, secrets management tools). **Why:** * **Security:** Prevents unauthorized access to your APIs. * **Compliance:** Meets regulatory requirements for data protection. * **Integrity:** Ensures that only authorized users and services can access your resources. **Do This:** Use environment variables or vault-like services like HashiCorp Vault or AWS Secrets Manager to store secrets. **Don't Do This:** Store API keys in source code or configuration files committed to version control. **Example:** Retrieve API key from environment variable """typescript // api-client.ts const apiKey = process.env.API_KEY; if (!apiKey) { console.error('API_KEY environment variable is not set!'); } export async function fetchData(url: string): Promise<any> { const headers = { 'Authorization': "Bearer ${apiKey}", 'Content-Type': 'application/json', }; const response = await fetch(url, { headers }); if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } return response.json(); } """ ### 2.2 Token-Based Authentication (JWT) **Standard:** Implement token-based authentication (e.g., JWT) when integrating with APIs that require user authentication. **Why:** * **Security:** Avoids storing user credentials directly in the application. * **Scalability:** Allows for stateless authentication, which is easier to scale. * **Flexibility:** Supports different authentication flows (e.g., username/password, social login). **Do This:** Obtain JWT tokens from the backend and use them in API requests. Handle token expiration and renewal. **Don't Do This:** Implement custom authentication schemes unless absolutely necessary. **Example:** """typescript // api-client.ts - example JWT storage/retrieval (Browser example) async function getAccessToken(): Promise<string | null> { return localStorage.getItem('accessToken'); } async function setAccessToken(token: string): Promise<void> { localStorage.setItem('accessToken', token); } async function refreshToken(): Promise<string | null> { try { const refreshToken = localStorage.getItem('refreshToken'); //retrieve the refresh token const response = await fetch('/refresh-token', { // Replace with your refresh token endpoint method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: "Bearer ${refreshToken}", }, }); if (!response.ok) { throw new Error('Failed to refresh token'); } const data = await response.json(); const newAccessToken = data.accessToken; const newRefreshToken = data.refreshToken; //get the new refresh token await setAccessToken(newAccessToken); localStorage.setItem('refreshToken', newRefreshToken); // store the new refresh token return newAccessToken; } catch (error) { console.error('Error refreshing token:', error); //Redirect to login in real app return null; } } export async function fetchWithAuth(url: string): Promise<any> { let accessToken = await getAccessToken(); if (!accessToken) { console.warn('No access token found. Redirect to login.'); //Force log out or redirect here in a real app return null; } const headers = { 'Authorization': "Bearer ${accessToken}", 'Content-Type': 'application/json', }; let response = await fetch(url, { headers }); //Handle token expiry if (response.status === 401) { console.log('Access token expired. Refreshing token...'); accessToken = await refreshToken(); //Retry with the refreshed token if (accessToken) { headers.Authorization = "Bearer ${accessToken}"; response = await fetch(url, { headers }); } else { //Redirection or log out again console.error('Failed to refresh token. Redirect to login.'); return null; } } if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } return response.json(); } """ ## 3. Data Handling and Transformation ### 3.1 Data Validation **Standard:** Validate API responses to ensure data integrity and prevent unexpected errors. **Why:** * **Robustness:** Protects your application from malformed or unexpected data. * **Data Integrity:** Ensures that the data used by your application is accurate and reliable. * **Debugging:** Provides clear error messages when data is invalid. **Do This:** Use schema validation libraries like Zod, Yup, or Joi to define data schemas. **Don't Do This:** Trust that API responses will always conform to the expected format. **Example:** """typescript // Using Zod for data validation import { z } from 'zod'; const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), }); type User = z.infer<typeof UserSchema>; async function fetchUser(id: number): Promise<User> { const response = await fetch("https://api.example.com/users/${id}"); if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } const data = await response.json(); try { //Parse data with the schema, throwing errors on fail return UserSchema.parse(data); } catch (error) { if (error instanceof z.ZodError) { console.error("Validation error for user data:", error.errors); throw new Error("Data validation failed: ${error.message}"); } else { console.error("Unexpected error during validation:", error); throw error; // re-throw if it's not a ZodError } } } """ ### 3.2 Data Transformation **Standard:** Transform API data into the format expected by your application. **Why:** * **Decoupling:** Isolates your application from changes in the API data structure. * **Flexibility:** Allows you to adapt the data to your specific needs. * **Performance:** Can be optimized depending on the amount and structure of incoming data **Do This:** Use mapping functions or data transformation libraries (e.g., Lodash, Ramda) to convert API data. **Don't Do This:** Directly use API data without any transformation or adaptation. **Example:** """typescript // Example transformation using a mapping function interface APIRepository { id: number; full_name: string; description: string | null; stargazers_count: number; } interface Repository { id: number; fullName: string; description: string; stars: number; } function transformRepository(apiRepo: APIRepository): Repository { return { id: apiRepo.id, fullName: apiRepo.full_name, description: apiRepo.description || 'No description provided.', stars: apiRepo.stargazers_count, }; } async function fetchRepositories(): Promise<Repository[]> { const response = await fetch('https://api.github.com/users/octocat/repos'); if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } const apiRepos: APIRepository[] = await response.json(); return apiRepos.map(transformRepository); } //Example Usage fetchRepositories().then(repositories => { console.log(repositories); }); """ ## 4. Performance Optimization ### 4.1 Caching **Standard:** Implement caching mechanisms to reduce the number of API requests. **Why:** * **Performance:** Improves the responsiveness of your application. * **Cost Savings:** Reduces the load on your backend services and lowers API usage costs. * **Rate Limiting:** Helps avoid rate limits imposed by APIs. **Do This:** Use browser caching, service worker caching, or in-memory caching strategies. **Don't Do This:** Cache sensitive data or cache data for too long. **Example:** """typescript // Basic in-memory cache const cache = new Map<string, { data: any; expiry: number }>(); async function fetchDataWithCache(url: string, ttl: number = 60): Promise<any> { const cachedData = cache.get(url); if (cachedData && cachedData.expiry > Date.now()) { console.log("Serving data from cache for ${url}"); return cachedData.data; } const response = await fetch(url); if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } const data = await response.json(); cache.set(url, { data, expiry: Date.now() + ttl * 1000 }); console.log("Fetched data for ${url} and cached it."); return data; } //Usage fetchDataWithCache('https://api.example.com/data') .then(data => console.log(data)); """ ### 4.2 Request Batching and Throttling **Standard:** Batch multiple API requests into a single request or throttle the rate of API calls to avoid overwhelming the server. **Why:** * **Performance:** Reduces the overhead of multiple HTTP requests. * **Resource Management:** Prevents the application from consuming excessive resources. * **API Limits:** Avoids exceeding API rate limits. **Do This:** Use libraries such as "lodash.chunk" for batching, and "lodash.throttle" or "p-throttle" for rate limiting. **Don't Do This:** Send an excessive number of API requests in a short period of time. **Example (Throttling):** """typescript import throttle from 'lodash.throttle'; async function makeApiCall(data: any): Promise<any> { // Simulate an API call return new Promise((resolve) => { setTimeout(() => { console.log("API called with data:", data); resolve({ status: 'success', data: data }); }, 100); }); } const throttledApiCall = throttle(makeApiCall, 500); // Throttle to once per 500ms async function simulateApiCalls() { for (let i = 1; i <= 5; i++) { console.log("Calling API ${i}"); await throttledApiCall({ id: i }); } } simulateApiCalls(); """ ## 5. Tooling and Libraries ### 5.1 Fetch API **Standard:** Use the native "fetch" API or a polyfill for making HTTP requests. **Why:** * **Modernity:** "fetch" is the standard API for making HTTP requests in modern browsers and Node.js. * **Simplicity:** Provides a cleaner and more intuitive API compared to older libraries like "XMLHttpRequest". * **ES Modules:** It works very well with ES Modules **Do This:** Use "fetch" API **Don't Do This:** Use deprecated XHR implementations. **Example:** """typescript async function fetchData(url: string): Promise<any> { const response = await fetch(url); if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } return await response.json(); } """ ### 5.2 Axios Axios remains a popular choice due to its extensive features, automatic transforms with JSON data, and broad browser compatibility, making it a strong choice for complex applications requiring rich functionality. Though "fetch" is powerful, it offers minimal built-in features. **When to use Axios instead of fetch:** * **Automatic JSON Transformation:** Axios automatically serializes request data to JSON and parses JSON responses, streamlining development. * **Request Cancellation:** If you need to cancel requests (e.g., when a user navigates away), Axios provides this functionality out-of-the-box. * **Wide Browser Support:** Axios supports older browsers out of the box whereas fetch calls for a polyfill. * **Error Handling:** Axios provides better error handling capabilities, including detailed error responses and the ability to intercept requests and responses globally. """typescript import axios from 'axios'; async function fetchData(url: string) { try { const response = await axios.get(url); console.log('Response data:', response.data); return response.data; } catch (error: any) { console.error('Error fetching data:', error.message); if (error.response) { console.log('Response status:', error.response.status); console.log('Response headers:', error.response.headers); } else if (error.request) { console.log('Request:', error.request); } else { console.log('Error config:', error.config); } } } fetchData('https://api.example.com/data'); """ ## 6. Security Considerations ### 6.1 Input Sanitization **Standard:** Sanitize all data received from APIs to prevent cross-site scripting (XSS) and other injection attacks. **Why:** * **Security:** Protects your application and users from malicious code. * **Data Integrity:** Ensures that the data displayed by your application is safe and reliable. * **Compliance:** Adheres to security best practices and regulatory requirements. **Do This:** Use libraries like DOMPurify or sanitize-html to sanitize HTML content. Validate and escape other types of data. **Don't Do This:** Display API data directly without any sanitization or validation. **Example:** """typescript import DOMPurify from 'dompurify'; function sanitizeHTML(html: string): string { return DOMPurify.sanitize(html); } async function fetchArticle(): Promise<string> { const response = await fetch('https://api.example.com/article'); if (!response.ok) { throw new Error("HTTP error! Status: ${response.status}"); } const articleHTML = await response.text(); return sanitizeHTML(articleHTML); } //Example usage fetchArticle().then(safeHTML => { document.getElementById('article-container').innerHTML = safeHTML; }); """ ### 6.2 CORS Handling **Standard:** Configure Cross-Origin Resource Sharing (CORS) properly to allow requests from your application's domain. **Why:** * **Security:** Prevents unauthorized access to your APIs from other domains. * **Functionality:** Ensures that your application can make requests to the APIs it needs to access. * **Browser Compatibility:** Adheres to browser security policies. **Do This:** Set the "Access-Control-Allow-Origin" header to the appropriate domain or use wildcards for development environments (with caution). **Don't Do This:** Use overly permissive CORS configurations in production environments. Make sure to set proper header limits. This document serves as a comprehensive guide to API integration standards within esbuild projects, emphasizing maintainability, performance, and security. By adhering to these guidelines, developers can create robust and reliable integrations with backend services and external APIs.