# Deployment and DevOps Standards for esbuild
This document outlines coding standards for Deployment and DevOps practices when using esbuild. It provides guidelines for build processes, CI/CD integration, and production considerations to ensure maintainability, performance, and security. These standards are designed to be used by developers and AI coding assistants to produce consistent and reliable esbuild-based applications.
## 1. Build Processes and Automation
### 1.1. Using "esbuild" CLI and Configuration Files
**Standard:** Employ "esbuild"'s CLI and configuration files ("esbuild.config.js" or "esbuild.config.ts") to manage build processes.
**Why:** Configuration files provide a centralized, declarative way to define build options, improving readability and maintainability. CLI commands are suitable for CI/CD integration.
**Do This:**
"""javascript
// esbuild.config.js
import { build } from 'esbuild';
build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
minify: true,
sourcemap: true,
platform: 'browser',
format: 'esm',
plugins: [], // Add plugins here
}).catch(() => process.exit(1));
"""
**Don't Do This:**
* Hardcoding build configurations directly within scripts without utilizing "esbuild"'s configuration system.
* Manually running "esbuild" commands without automation.
**Code Example:** Including plugins for custom transformations:
"""javascript
// esbuild.config.js
import { build } from 'esbuild';
import { someEsbuildPlugin } from 'some-esbuild-plugin';
build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
minify: true,
sourcemap: true,
platform: 'browser',
format: 'esm',
plugins: [someEsbuildPlugin()],
}).catch(() => process.exit(1));
"""
### 1.2. Scripting Build Commands
**Standard:** Use "npm" scripts or similar to wrap "esbuild" CLI commands for consistency and ease of use.
**Why:** Scripts provide a command alias and encapsulate complex commands.
**Do This:**
"""json
// package.json
{
"scripts": {
"build": "node esbuild.config.js",
"build:prod": "NODE_ENV=production node esbuild.config.js",
"dev": "esbuild src/index.ts --bundle --outfile=dist/bundle.js --sourcemap --watch"
}
}
"""
"""bash
npm run build
npm run build:prod
npm run dev
"""
**Don't Do This:**
* Executing long "esbuild" commands directly in the terminal.
* Skipping the use of scripts for common operations.
**Code Example:** Using environment variables to conditionally adjust build configurations:
"""javascript
// esbuild.config.js
import { build } from 'esbuild';
const production = process.env.NODE_ENV === 'production';
build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
minify: production,
sourcemap: !production,
platform: 'browser',
format: 'esm',
}).catch(() => process.exit(1));
"""
### 1.3. Handling Assets
**Standard:** Integrate asset processing (e.g., images, fonts) directly into the "esbuild" pipeline or use dedicated asset management tools that complement "esbuild".
**Why:** Seamless asset inclusion enhances build efficiency and dependency management.
**Do This:**
* Use "esbuild" file loaders for simple asset handling:
"""javascript
// esbuild.config.js
import { build } from 'esbuild';
build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
minify: true,
loader: {
'.svg': 'file',
'.png': 'file',
},
}).catch(() => process.exit(1));
"""
* Use a dedicated plugin for more comprehensive asset processing:
"""javascript
// Example using a hypothetical asset plugin:
import { build } from 'esbuild';
import { assetPlugin } from 'esbuild-asset-plugin'; // Hypothetical
build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
minify: true,
plugins: [assetPlugin({ publicPath: '/assets' })],
}).catch(() => process.exit(1));
"""
**Don't Do This:**
* Manually copying assets after the build process.
* Ignoring asset optimization during the build.
### 1.4. Code Splitting
**Standard:** Leverage "esbuild"'s built-in code splitting capabilities for improved initial load times and caching.
**Why:** Code splitting reduces the size of the initial bundle, improving performance.
**Do This:**
"""javascript
// esbuild.config.js
import { build } from 'esbuild';
build({
entryPoints: ['src/index.ts', 'src/other-module.ts'],
bundle: true,
outdir: 'dist',
format: 'esm',
splitting: true, // Enable code splitting
chunkNames: '[name]-[hash]',
}).catch(() => process.exit(1));
"""
**Don't Do This:**
* Bundling the entire application into a single large file unnecessarily.
* Ignoring the performance benefits of code splitting, especially for complex apps.
**Code Example:** Dynamic imports
"""javascript
// src/index.ts
async function loadModule() {
const module = await import('./lazy-loaded-module');
module.doSomething();
}
loadModule();
"""
"""javascript
// src/lazy-loaded-module.ts
export function doSomething() {
console.log('Doing something in the lazy loaded module!');
}
"""
## 2. CI/CD Integration
### 2.1. Continuous Integration Workflow
**Standard:** Integrate "esbuild" build processes into CI/CD pipelines (e.g., GitHub Actions, GitLab CI, Jenkins).
**Why:** Automates build, test, and deployment processes.
**Do This:**
"""yaml
# .github/workflows/build.yml (GitHub Actions example)
name: Build
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Build
run: npm run build:prod
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist
"""
**Don't Do This:**
* Manually building and deploying code.
* Skipping automated testing during the CI/CD process.
### 2.2. Caching Build Artifacts
**Standard:** Leverage caching mechanisms provided by CI/CD tools to cache "node_modules" and build outputs to accelerate subsequent builds.
**Why:** Reduces build times and CI/CD pipeline execution.
**Do This:**
"""yaml
# .github/workflows/build.yml (GitHub Actions example with caching)
name: Build
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Cache dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install dependencies
run: npm install
- name: Build
run: npm run build:prod
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist
"""
**Don't Do This:**
* Ignoring caching opportunities for dependencies and build artifacts.
* Using overly broad cache keys that invalidate the cache unnecessarily.
### 2.3. Environment Variables
**Standard:** Manage environment-specific configurations using environment variables in CI/CD pipelines and deployment environments.
**Why:** Separates configuration from code, simplifying deployments and environment management.
**Do This:**
* Set environment variables in CI/CD configuration (e.g., GitHub Actions secrets, GitLab CI variables).
* Access environment variables in your "esbuild" configuration:
"""javascript
// esbuild.config.js
import { build } from 'esbuild';
const apiBaseUrl = process.env.API_BASE_URL || 'https://default-api.com';
build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
define: {
'process.env.API_BASE_URL': JSON.stringify(apiBaseUrl),
},
}).catch(() => process.exit(1));
"""
**Don't Do This:**
* Hardcoding environment-specific configurations in the codebase.
* Storing sensitive information directly in code or configuration files.
### 2.4. Automated Testing
**Standard:** Integrate automated unit, integration, and end-to-end tests into CI/CD pipelines to ensure code quality and prevent regressions.
**Why:** Automated testing is crucial for catching errors early and maintaining code reliability.
**Do This:**
* Include test execution commands in your CI/CD pipeline.
* Use a testing framework like Jest, Mocha, or Vitest.
"""yaml
# .github/workflows/build.yml (GitHub Actions example with testing)
name: Build
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test # Assuming 'npm test' runs your tests
- name: Build
run: npm run build:prod
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist
"""
**Don't Do This:**
* Skipping automated testing in the CI/CD pipeline.
* Deploying code without sufficient test coverage.
## 3. Production Considerations
### 3.1. Minification and Optimization
**Standard:** Enable minification and tree shaking in production builds to reduce bundle size and improve performance.
**Why:** Minification removes unnecessary characters, and tree shaking eliminates dead code, both reducing bundle size.
**Do This:**
"""javascript
// esbuild.config.js
import { build } from 'esbuild';
build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
minify: true, // Enable minification
treeShaking: true, // Explicitly enable tree shaking if needed. Esbuild performs this by default if sideEffects are correctly set in package.json
platform: 'browser',
format: 'esm',
}).catch(() => process.exit(1));
"""
**Don't Do This:**
* Deploying unminified code to production.
* Ignoring tree shaking opportunities. Make sure to set "sideEffects: false" where applicable in the "package.json".
### 3.2. Source Maps
**Standard:** Generate source maps for production builds to facilitate debugging in production environments. Configure your server and error tracking tools to correctly use source maps.
**Why:** Source maps allow debugging minified code by mapping it back to the original source.
**Do This:**
"""javascript
// esbuild.config.js
import { build } from 'esbuild';
build({
entryPoints: ['src/index.ts'],
bundle: true,
outfile: 'dist/bundle.js',
minify: true,
sourcemap: true, // Generate sourcemap
}).catch(() => process.exit(1));
"""
* Configure your web server (e.g., Nginx, Apache) to serve source maps correctly.
**Don't Do This:**
* Omitting source maps in production builds. However, manage access to them securely.
* Exposing source maps publicly without proper security measures.
### 3.3. Content Delivery Network (CDN)
**Standard:** Utilize a CDN to serve static assets (JavaScript, CSS, images) from geographically distributed servers.
**Why:** CDNs improve load times for users around the world.
**Do This:**
* Upload build artifacts to a CDN (e.g., AWS CloudFront, Cloudflare CDN, Azure CDN).
* Configure your application to load assets from the CDN.
**Don't Do This:**
* Serving static assets directly from the application server, especially without caching.
* Ignoring the performance benefits of CDNs for global users.
### 3.4. Caching Strategies
**Standard:** Implement effective caching strategies to leverage browser and CDN caching. Use cache busting techniques to ensure users receive the latest version of your assets.
**Why:** Efficient caching reduces server load and improves performance.
**Do This:**
* Use content hashes in filenames for long-term caching.
* Configure appropriate cache headers on your web server and CDN. "esbuild" can help manage this with entry point output naming.
"""javascript
// esbuild.config.js
import { build } from 'esbuild';
build({
entryPoints: ['src/index.ts'],
bundle: true,
outdir: 'dist',
entryNames: '[dir]/[name]-[hash]', // Include hash in the filename
minify: true,
sourcemap: true,
}).catch(() => process.exit(1));
"""
* Invalidate the cache when deploying new versions.
* Leverage service workers for advanced caching strategies (optional).
**Don't Do This:**
* Using overly aggressive caching that prevents users from getting the latest updates.
* Failing to invalidate the cache when deploying new versions.
### 3.5 Monitoring and Logging
**Standard:** Implement comprehensive monitoring and logging to track application health, performance, and errors in production.
**Why:** Proactive monitoring helps identify and resolve issues quickly.
**Do This:**
* Use error tracking services (e.g., Sentry, Rollbar) to capture JavaScript errors in production.
* Implement logging for critical events and performance metrics.
* Set up alerts for errors and performance degradation.
**Don't Do This:**
* Ignoring errors and performance issues in production.
* Failing to monitor application health and performance.
* Logging sensitive information.
### 3.6. Security Best Practices
**Standard:** Follow security best practices to protect your application from vulnerabilities.
**Why:** Security is paramount, especially in production environments.
**Do This:**
* Keep dependencies up-to-date to address security vulnerabilities.
* Use a tool to scan your dependencies for vulnerabilities (e.g., "npm audit", "yarn audit").
* Implement security headers (e.g., Content Security Policy, X-Frame-Options) on your web server.
* Sanitize user inputs to prevent cross-site scripting (XSS) attacks.
**Don't Do This:**
* Using vulnerable dependencies.
* Exposing sensitive information.
* Implementing insecure coding practices.
By adhering to these standards, developers can effectively leverage "esbuild" for building and deploying high-quality, efficient, and secure applications. This guideline must be updated frequently with "esbuild" new features, version upgrades, and community best practices.
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'
# Tooling and Ecosystem Standards for esbuild This document outlines the coding standards and best practices for developing with esbuild, specifically focusing on tooling and ecosystem considerations. Adhering to these standards ensures maintainability, performance, and security of esbuild-based projects. ## 1. Core Tooling ### 1.1. Package Managers **Standard:** Use npm or yarn for package management. pnpm is highly encouraged for optimized space and performance. **Do This:** * Use "npm install", "yarn add", or "pnpm add" to install dependencies. * Prefer "pnpm install" for deduplication and speed. **Don't Do This:** * Manually download and manage dependencies. * Mix package managers within a single project. **Why:** Package managers automate dependency resolution, versioning, and installation. They greatly simplify project setup and ensure consistency across development environments. pnpm optimizes storage and installation speed through symlinking. **Example (using pnpm):** """bash pnpm add react react-dom """ ### 1.2. Editor Configuration **Standard:** Utilize editor configuration files (".editorconfig") to maintain consistent code formatting across different editors and IDEs. **Do This:** * Include an ".editorconfig" file in the project root. * Use editor plugins that support ".editorconfig". **Don't Do This:** * Rely solely on individual editor settings. **Why:** ".editorconfig" enforces consistent coding styles, such as indentation and line endings, regardless of the editor used. This improves code readability and collaboration. **Example (".editorconfig"):** """ini root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.md] trim_trailing_whitespace = false """ ### 1.3. Linting and Formatting **Standard:** Employ ESLint and Prettier for linting and formatting JavaScript and TypeScript code. **Do This:** * Configure ESLint with recommended rules for JavaScript/TypeScript and React (if applicable). * Use Prettier for code formatting. * Integrate ESLint and Prettier into your development workflow (e.g., via VS Code extensions or Git hooks). * Use the "--fix" flag when running ESLint to automatically correct fixable issues. **Don't Do This:** * Ignore linting or formatting errors. * Rely solely on manual code reviews for style consistency. **Why:** Linters identify potential errors and enforce coding standards. Formatters automatically format code to a consistent style. This reduces bugs, improves readability, and saves time during code reviews. **Example (ESLint configuration ".eslintrc.js"):** """javascript module.exports = { env: { browser: true, es2021: true, node: true }, extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', 'plugin:react/recommended', 'plugin:react-hooks/recommended', 'prettier' // Ensure Prettier is last in the list ], parser: '@typescript-eslint/parser', parserOptions: { ecmaFeatures: { jsx: true }, ecmaVersion: 12, sourceType: 'module' }, plugins: [ '@typescript-eslint', 'react', 'react-hooks' ], rules: { 'react/react-in-jsx-scope': 'off', // Required for older React versions 'no-unused-vars': 'warn', // Or 'error' in stricter environments '@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-module-boundary-types': 'off', 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks 'react-hooks/exhaustive-deps': 'warn' // Checks effect dependencies }, settings: { react: { version: 'detect' // Automatically detect the React version } } }; """ **Example (Prettier configuration ".prettierrc.js"):** """javascript module.exports = { semi: true, trailingComma: 'all', singleQuote: true, printWidth: 120, tabWidth: 2, }; """ **Example (integration with ESLint):** """json // package.json { "scripts": { "lint": "eslint . --ext .js,.jsx,.ts,.tsx", "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix", "format": "prettier --write ." } } """ ### 1.4. Type Checking **Standard:** Use TypeScript for type checking. **Do This:** * Configure TypeScript with strict mode enabled (""strict": true" in "tsconfig.json"). * Address all type errors and warnings. **Don't Do This:** * Use "any" excessively. * Ignore TypeScript errors in the build process. **Why:** TypeScript provides static typing, which helps catch errors early, improves code maintainability, and enables better tooling (e.g., autocompletion and refactoring). **Example ("tsconfig.json"):** """json { "compilerOptions": { "target": "esnext", "module": "esnext", "moduleResolution": "node", "jsx": "react-jsx", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "dist" }, "include": ["src"], "exclude": ["node_modules"] } """ ### 1.5. Bundling and Minification **Standard:** Use esbuild for bundling and minification. **Do This:** * Configure esbuild with appropriate settings for your target environment (e.g., ""target": "esnext"" for modern browsers, ""format": "esm"" for ES modules). * Use esbuild's built-in minification ("minify"), tree-shaking, and dead code elimination features. * Consider using esbuild's code splitting feature for larger projects. **Don't Do This:** * Rely on older bundlers (e.g., Webpack, Parcel) if esbuild can meet your needs. * Disable essential esbuild optimizations. **Why:** esbuild is significantly faster than other bundlers while still offering essential features like minification, tree-shaking, and code splitting. This leads to faster build times and smaller bundle sizes. **Example (esbuild configuration):** """javascript // build.js const esbuild = require('esbuild'); esbuild.build({ entryPoints: ['src/index.tsx'], bundle: true, outfile: 'dist/bundle.js', minify: true, sourcemap: true, format: 'esm', target: 'esnext', jsxFactory: 'React.createElement', jsxFragment: 'React.Fragment', }).catch(() => process.exit(1)); """ ### 1.6. Testing **Standard:** Implement unit and integration tests using Jest, Vitest, or similar frameworks. **Do This:** * Write unit tests to verify individual components and functions. * Write integration tests to verify interactions between different parts of the application. * Use mocking and stubbing techniques to isolate units of code during testing. * Run tests automatically as part of your CI/CD pipeline. * Prefer Vitest in scenarios where esbuild is also used as it can leverage the same transforms. **Don't Do This:** * Skip writing tests. * Write tests that are too tightly coupled to implementation details. **Why:** Tests help prevent regressions, improve code quality, and facilitate refactoring. Running tests automatically ensures that changes don't introduce new bugs. **Example (Jest configuration "jest.config.js"):** """javascript module.exports = { preset: 'ts-jest', testEnvironment: 'jsdom', setupFilesAfterEnv: ['<rootDir>/src/setupTests.ts'], // Optional setup file moduleNameMapper: { '\\.(css|less|scss|sass)$': 'identity-obj-proxy', // Mock CSS modules }, }; """ **Example (Vitest configuration "vitest.config.ts"):** """typescript import { defineConfig } from 'vitest/config' import react from '@vitejs/plugin-react' // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], test: { environment: 'jsdom', setupFiles: ['./src/setupTests.ts'], globals: true, css: true, }, }) """ ## 2. Ecosystem Integrations ### 2.1. React **Standard:** If using React, follow React's best practices and use modern patterns such as functional components and hooks. **Do This:** * Use functional components with hooks for managing state and side effects. * Consider using a state management library like Redux or Zustand for complex application state. * Use React context for passing data down the component tree. * Optimize performance with techniques like memoization ("React.memo") and code splitting. **Don't Do This:** * Rely on class components for new code. * Mutate state directly. **Why:** Functional components and hooks promote code reuse, improve readability, and are generally easier to test. Proper state management helps keep your application predictable and maintainable. **Example (functional component with hooks):** """typescript import React, { useState, useEffect } from 'react'; function Counter() { const [count, setCount] = useState(0); useEffect(() => { document.title = "Count: ${count}"; }, [count]); return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>Increment</button> </div> ); } export default Counter; """ ### 2.2. Frameworks (Next.js, Remix) **Standard:** If using a framework like Next.js or Remix, leverage framework-specific features and conventions. **Do This:** * Use Next.js for server-side rendering, static site generation, and API routes. * Use Remix for progressive enhancement, web standards, and server-side data loading. * Follow the framework's recommended project structure and file naming conventions. **Don't Do This:** * Try to circumvent framework conventions. * Implement features that are already provided by the framework. **Why:** Frameworks provide structure, conventions, and pre-built components that greatly simplify development. Following framework guidelines ensures maintainability and allows you to leverage the framework's optimizations. **Example (Next.js API route):** """javascript // pages/api/hello.js export default function handler(req, res) { res.status(200).json({ name: 'John Doe' }); } """ ### 2.3. CSS Preprocessors and Postprocessors **Standard:** Use CSS preprocessors (Sass, Less, Stylus) and postprocessors (PostCSS) to enhance CSS development. **Do This:** * Use Sass for features like variables, mixins, and nesting. * Use PostCSS for vendor prefixing, linting, and other transformations. * Configure esbuild to handle CSS processing. **Don't Do This:** * Write raw CSS when a preprocessor can simplify the task. * Manually manage vendor prefixes. **Why:** CSS preprocessors and postprocessors automate common CSS tasks, improve maintainability, and enable more advanced styling techniques. **Example (esbuild PostCSS Plugin):** """javascript // build.js const esbuild = require('esbuild'); const postcss = require('postcss'); const autoprefixer = require('autoprefixer'); esbuild.build({ entryPoints: ['src/index.tsx'], bundle: true, outfile: 'dist/bundle.js', minify: true, sourcemap: true, format: 'esm', target: 'esnext', jsxFactory: 'React.createElement', jsxFragment: 'React.Fragment', plugins: [{ name: 'postcss', setup(build) { build.onLoad({ filter: /\.css$/ }, async (args) => { const { css } = await postcss([autoprefixer]).process('@import "'+args.path+'"', { from : args.path }) return { contents: css, loader: 'css', }; }); }, }], }).catch(() => process.exit(1)); """ ### 2.4. State Management **Standard:** Use a state management solution (Redux, Zustand, Recoil, Jotai) appropriate for your application's complexity. **Do This:** * Use Redux for complex, global state management needs (emphasizing predictability and debugging). Use Redux Toolkit for simplified Redux usage. * Use Zustand or Jotai for simpler state management in smaller to medium-sized applications (emphasizing ease of use and minimal boilerplate). * Use React Context for localized state management. **Don't Do This:** * Overuse global state (put state where it's needed). * Mutate state directly (always create new state objects). **Why:** State management libraries provide a structured way to manage application state, making it easier to reason about and debug. They also improve performance by preventing unnecessary re-renders. **Example (Zustand):** """typescript import create from 'zustand'; interface BearState { bears: number; increase: (by: number) => void; } const useBearStore = create<BearState>((set) => ({ bears: 0, increase: (by) => set((state) => ({ bears: state.bears + by })), })); export default useBearStore; // In a component: import useBearStore from './store'; function MyComponent() { const bears = useBearStore((state) => state.bears); const increase = useBearStore((state) => state.increase); return ( <div> Bears: {bears} <button onClick={() => increase(1)}>Add Bear</button> </div> ); } """ ## 3. Security Practices ### 3.1. Dependency Management **Standard:** Regularly audit and update dependencies. **Do This:** * Use "npm audit", "yarn audit", or "pnpm audit" to identify vulnerabilities in your dependencies. * Update dependencies regularly to patch vulnerabilities. * Use tools like Dependabot to automate dependency updates. **Don't Do This:** * Ignore security vulnerabilities in dependencies. * Use outdated dependencies. **Why:** Dependencies can contain security vulnerabilities that can be exploited by attackers. Regularly auditing and updating dependencies helps mitigate these risks. **Example (npm audit):** """bash npm audit """ ### 3.2. Input Validation and Sanitization **Standard:** Validate and sanitize all user input. **Do This:** * Validate input on both the client and server side. * Sanitize input to prevent cross-site scripting (XSS) attacks. **Don't Do This:** * Trust user input implicitly. **Why:** User input can be malicious and can be used to inject code or compromise the application. Validating and sanitizing input helps prevent these attacks. ### 3.3. Secure Configuration **Standard:** Store sensitive information securely. **Do This:** * Store API keys, passwords, and other sensitive information in environment variables or a secrets management system. * Avoid hardcoding sensitive information in your code. * Rotate API keys and passwords regularly. **Don't Do This:** * Commit sensitive information to version control. **Why:** Hardcoding sensitive information in your code or committing it to version control can expose it to attackers. Storing this information securely helps protect it. ## 4. Performance Optimization ### 4.1. Code Splitting **Standard:** Use esbuild's code splitting feature to reduce initial load times. **Do This:** * Split your application into smaller modules that can be loaded on demand. * Use dynamic imports ("import()") to load modules asynchronously. **Don't Do This:** * Create large, monolithic bundles. **Why:** Code splitting reduces the amount of JavaScript that needs to be downloaded and parsed on initial page load, leading to faster load times and improved user experience. **Example (dynamic import):** """javascript async function loadComponent() { const { MyComponent } = await import('./MyComponent'); // Use MyComponent } """ ### 4.2. Tree Shaking **Standard:** Ensure that esbuild can effectively tree shake your code. **Do This:** * Use ES modules ("import" and "export") for all JavaScript code. * Avoid side effects in your modules. * Use ""sideEffects": false" property in "package.json" if your package contains no side effects. **Don't Do This:** * Use CommonJS modules ("require" and "module.exports"). * Introduce unnecessary dependencies. **Why:** Tree shaking removes unused code from your bundles, reducing their size and improving performance. ### 4.3. Image Optimization **Standard:** Optimize images for the web. **Do This:** * Use compressed image formats (e.g., WebP, JPEG). * Resize images to the appropriate dimensions. * Use lazy loading for images below the fold. **Don't Do This:** * Use large, unoptimized images. **Why:** Large images can significantly slow down page load times. Optimizing images helps improve performance and reduce bandwidth consumption. ### 4.4. Caching **Standard:** Implement caching strategies. **Do This:** * Use browser caching to cache static assets. * Use a service worker to cache application shell. * Use server-side caching to cache API responses. * Utilize esbuild's incremental builds leveraging its cache. **Don't Do This:** * Disable caching. * Cache sensitive data. **Why:** Caching reduces the number of requests that need to be made to the server, leading to faster load times and improved user experience. ## 5. Deprecation Considerations Given the rapid evolution of esbuild and its ecosystem, awareness of deprecated features and tools is crucial. **Standard:** Stay informed about deprecated features and migration paths. **Do This:** * Consult the official esbuild documentation and release notes regularly. * Monitor esbuild's issue tracker for discussions about deprecations and alternatives. * Replace deprecated features and integrations with their modern equivalents promptly. **Don't Do This:** * Continue using deprecated features without considering the implications. * Ignore warnings about deprecations in your build logs. **Why:** Using deprecated features may lead to subtle bugs, performance issues, or potential security risks as the ecosystem evolves. Promptly adopting modern alternatives ensures the long-term maintainability and stability of your esbuild-based projects. ## 6. Community and Collaboration **Standard:** Contribute to the esbuild community and follow standards to make collaboration easy. **Do This:** * Share your tools and plugins. * Contribute to open source projects in the esbuild space. * Ask questions and discuss best practices on the forums **Don't Do This:** * Assume you know the best way to write code. * Avoid getting feedback. **Why:** Active participation in the wider esbuild community helps innovation.
# 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.
# 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.
# Security Best Practices Standards for esbuild This document outlines security best practices for developing with esbuild. Adhering to these guidelines will help prevent common vulnerabilities and promote secure, robust applications. ## 1. Input Validation and Sanitization ### 1.1 General Principles * **Do This:** Validate all external inputs, whether from command-line arguments, environment variables, configuration files, or network requests, *before* using them in esbuild. This is a PRIMARY defense against injection attacks and incorrect behavior. * **Don't Do This:** Assume that external inputs are safe or well-formed without validation. This is a VERY COMMON and MAJOR security flaw. * **Why:** Untrusted input can lead to various security vulnerabilities: code injection, path traversal, denial-of-service, etc. Validation ensures the input conforms to the expected format and constraints. ### 1.2 Specific esbuild Scenarios #### 1.2.1 "entryPoints" Validation * **Do This:** Ensure that "entryPoints" passed to esbuild are validated, especially if they originate from user input or dynamic configuration. Use path canonicalization and allow-listing to control the files that can be bundled. * **Don't Do This:** Directly pass unvalidated user-provided file paths as entry points. This can allow attackers to bundle arbitrary files into the output. * **Example:** """javascript const esbuild = require('esbuild'); const path = require('path'); const fs = require('fs'); async function build(userInput) { const allowedEntryPoints = ['./src/index.js', './src/main.ts']; // Define allowed starting files if (!allowedEntryPoints.includes(userInput)) { //Validate user's input against whitelisted entries console.error("Invalid entry point."); return; } try { await esbuild.build({ entryPoints: [userInput], //Use validated input bundle: true, outfile: 'dist/bundle.js', format: 'esm'//Output format, using 'esm' for modern browsers }); console.log('Build complete.'); } catch (e) { console.error('Build failed:', e); } } // Simulate user input (dangerous if unvalidated) build('./src/index.js'); """ #### 1.2.2 Plugin Configuration * **Do This:** Carefully validate configurations for esbuild plugins. Plugins often interact with the file system or external processes. Ensure all input is sanitized. * **Don't Do This:** Allow unvalidated user input to configure plugin parameters that control file system access or execution behavior. * **Example:** Suppose you have a plugin that reads a config file path from an environment variable: """javascript const esbuild = require('esbuild'); const path = require('path'); const fs = require('fs'); function myPlugin(options) { return { name: 'my-plugin', setup(build) { build.onLoad({ filter: /\.config\.json$/ }, async (args) => { if (!options || !options.configFilePath) { throw new Error('configFilePath option is required for my-plugin'); } const filePath = path.resolve(options.configFilePath); //Validate the file path before reading it if (!filePath.startsWith(path.resolve(__dirname))) { //Check if path starts with your directory! throw new Error('Invalid config file path: it must be within project directory.'); } const contents = await fs.promises.readFile(filePath, 'utf8'); return { contents, loader: 'json' }; }); }, }; } async function buildWithPlugin() { // Securely get configuration from environment variable with validation const configFilePath = process.env.MY_CONFIG_PATH; if (!configFilePath) { console.log("Path not set, falling back to default"); } const validatedPath = configFilePath === undefined ? "./config.json" : configFilePath; try { await esbuild.build({ entryPoints: ['./src/index.js'], bundle: true, outfile: 'dist/bundle.js', plugins: [myPlugin({configFilePath: validatedPath})], format: 'esm' }); console.log('Build with plugin complete.'); } catch (e) { console.error('Build with plugin failed:', e); } } buildWithPlugin(); """ #### 1.2.3 "define" Option Handling * **Do This:** Carefully control the values you inject into your code using the "define" option, especially if those values depend on environment variables or other external sources. Ensure the injected values are properly escaped and that they do not introduce vulnerabilities. * **Don't Do This:** Directly inject unvalidated strings or code snippets into your application using the "define" option. This can create code injection vulnerabilities. * **Example:** """javascript const esbuild = require('esbuild'); async function build() { const apiEndpoint = process.env.API_ENDPOINT || 'https://default-api.example.com'; // Validate the API endpoint (simple example, needs to be more robust in real use) if (!apiEndpoint.startsWith('https://') && !apiEndpoint.startsWith('http://')) { console.error('Invalid API endpoint.'); return; // Or throw an error. } try { await esbuild.build({ entryPoints: ['./src/index.js'], bundle: true, outfile: 'dist/bundle.js', define: { 'process.env.API_ENDPOINT': JSON.stringify(apiEndpoint), //Stringify the validated value }, format: 'esm' }); console.log('Build complete.'); } catch (e) { console.error('Build failed:', e); } } build(); """ ## 2. Secure File Handling ### 2.1 Principles * **Do This:** Restrict file system access to the minimum necessary scope. Use absolute paths or resolve relative paths carefully to prevent path traversal issues. * **Don't Do This:** Allow esbuild plugins or build scripts to access arbitrary files based on user input or poorly validated configuration. * **Why:** Unrestricted file access allows attackers to read sensitive data or overwrite critical files, leading to information disclosure, code execution, or denial-of-service. ### 2.2 Implementation #### 2.2.1 Restricting File Access in Plugins * **Do This:** Implement strict checks within esbuild plugins to ensure that any file I/O operations are confined to a well-defined directory or list of allowed files. Use "path.resolve" and "path.normalize" to sanitize file paths and prevent traversal. * **Don't Do This:** Trust relative paths from user input without proper sanitization. This is a classic path traversal vulnerability. * **Example:** """javascript const esbuild = require('esbuild'); const path = require('path'); const fs = require('fs'); function secureFileReadPlugin(baseDir) { return { name: 'secure-file-read', setup(build) { build.onLoad({ filter: /\.(txt|json)$/ }, async (args) => { const resolvedPath = path.resolve(args.path); // Check if the resolved path is within the allowed base directory if (!resolvedPath.startsWith(path.resolve(baseDir))) { throw new Error("Access denied: ${args.path} is outside the allowed directory."); } try { const contents = await fs.promises.readFile(resolvedPath, 'utf8'); return { contents, loader: 'text' }; } catch (e) { console.error("Error reading file: ${args.path}", e); return null; // Indicate failure to load. } }); }, }; } async function buildFiles() { try { await esbuild.build({ entryPoints: ['./src/index.js'], bundle: true, outfile: 'dist/bundle.js', plugins: [secureFileReadPlugin('./data')], // Restrict access to the ./data directory format: 'esm' }); console.log('Build complete.'); } catch (e) { console.error('Build failed:', e); } } buildFiles(); """ #### 2.2.2 Avoiding Symlink Vulnerabilities * **Do This:** When dealing with file paths, especially in plugins, be aware of symlink (symbolic link) vulnerabilities. Symlinks can be used to redirect file operations outside the intended directory. Use "fs.realpathSync" (with caution, see below) or similar mechanisms to resolve symlinks and ensure you're operating on the actual file. * **Don't Do This:** Assume that a file path provided by the user or configuration actually points where it says. * **Caveat:** "fs.realpathSync", especially when resolving paths provided by potential attackers introduce performance issues, which can lead to local resource exhaustion attacks. If you are dealing with web requests, consider adding timeouts to prevent requests from hanging for undue periods. ## 3. Dependency Management ### 3.1 Principle * **Do This:** Keep your esbuild and all its dependencies up to date with the latest security patches. Use a dependency management tool like npm or yarn to track and update dependencies. Regularly audit your dependencies for known vulnerabilities using tools like "npm audit" or "yarn audit". * **Don't Do This:** Ignore security vulnerability reports from your dependency management tools. Failing to update vulnerable dependencies is a major security risk. * **Why:** Outdated dependencies often contain known security vulnerabilities that can be exploited by attackers. Regular updates and audits mitigate this risk. ### 3.2 Implementation #### 3.2.1 Regular Dependency Audits * **Do This:** Include "npm audit" or "yarn audit" as part of your continuous integration (CI) process. Fail the build if vulnerabilities are found and require developers to address them. * **Don't Do This:** Rely solely on manual audits. Automated audits ensure consistent and timely vulnerability detection. * **Example:** """bash # In your CI script: npm audit --production # or yarn audit --production """ #### 3.2.2 Dependency Pinning * **Do This:** Use explicit versioning (e.g., specifying exact versions in "package.json" or using "package-lock.json" or "yarn.lock") to ensure consistent builds and prevent unexpected breaking changes or vulnerability introductions from dependency updates. Consider Renovate Bot to manage dependency updates automatically. * **Don't Do This:** Use wide version ranges (e.g., "^1.0.0" or "~1.0.0") in production environments without careful testing of updates. This can lead to unpredictable behavior and security regressions. * **Example:** """json // package.json { "dependencies": { "esbuild": "latest", "lodash": "4.17.21" // Pinned to a specific version } } """ ## 4. Secure Plugin Development ### 4.1 Principles * **Do This:** Follow the principle of least privilege when developing esbuild plugins. Only request the necessary permissions and access to files and resources required for the plugin's functionality. Sanitize all input! * **Don't Do This:** Create plugins that require excessive permissions or access to sensitive data without a clear and justifiable reason. * **Why:** Plugins with unnecessary privileges can be exploited to perform malicious actions or leak sensitive information. ### 4.2 Implementation #### 4.2.1 Minimizing Plugin Permissions * **Do This:** Carefully review the esbuild API and only use the features and hooks that are absolutely necessary for your plugin. Avoid using features that grant broad access to the file system or build process unless required. * **Don't Do This:** Assume broad permissions are harmless. * **Example:** Only use "build.onLoad" if you need to customize the loading process. If not, rely on esbuild's default loaders. Avoid "build.onResolve" if you don't need to modify module resolution. #### 4.2.2 Data Sanitization * **Do This:** If your plugin processes or transforms data, sanitize the data to prevent injection attacks or unexpected behavior. Escape HTML, SQL, or other special characters as appropriate for the context in which the data will be used. ### 4.3 Error Handling and Logging * **Do This:** Implement robust error handling in your esbuild plugins. Log errors and warnings to provide useful debugging information, but avoid logging sensitive data that could expose vulnerabilities. * **Don't Do This:** Silently ignore errors or log sensitive information such as API keys or passwords. """javascript const esbuild = require('esbuild'); function errorLoggingPlugin() { return { name: 'error-logging', setup(build) { build.onEnd(result => { if (result.errors.length > 0) { console.error('Build failed with errors:'); result.errors.forEach(error => { // Important: Sanitize error message before logging, as it may contain // user-controlled data if esbuild is configured dynamically. const safeMessage = error.text.replace(/[^a-zA-Z0-9\s.:]/g, ''); // Example sanitization console.error(" - ${safeMessage}"); }); } else { console.log('Build succeeded.'); } }); }, }; } """ ## 5. Limiting Code Execution ### 5.1 Principles * **Do This**: Avoid using "eval()" or similar dynamic code execution features within esbuild or its plugins. If you must use them, carefully validate and sanitize any inputs to prevent code injection vulnerabilities. * **Don't Do This:** Dynamically generate and execute code based on user input without strict validation. * **Why:** Dynamic code execution allows attackers to inject arbitrary code into your application, leading to severe security breaches. ### 5.2 Implementation #### 5.2.1 Alternatives to "eval()" * **Do This:** Use safer alternatives to "eval()", such as "JSON.parse()" for parsing JSON data or template literals for string interpolation. Modern JavaScript provides many alternatives to dynamic code execution. * **Example:** """javascript const userInput = '{"name": "John Doe", "age": 30}'; // Instead of: // const data = eval("(${userInput})"); // DANGEROUS! // Use: try { const data = JSON.parse(userInput); // Safer and more efficient console.log(data.name); } catch (e) { console.error('Invalid JSON input:', e); } """ #### 5.2.2 Strict CSP * **Do This:** Implement a strict Content Security Policy (CSP) in your web application to prevent the execution of inline scripts and other dynamic code. This can help mitigate the impact of code injection vulnerabilities. * **Don't Do This:** Use a permissive CSP or no CSP at all. ## 6. Preventing Denial-of-Service (DoS) Attacks ### 6.1 Principles * **Do This:** Implement rate limiting and resource quotas to prevent attackers from overwhelming your esbuild process with excessive requests or large input files. * **Don't Do This:** Allow unlimited resource consumption, which can lead to denial-of-service attacks. * **Why:** DoS attacks can disrupt your application's availability and performance. Rate limiting and resource quotas help mitigate this risk. ### 6.2 Implementation #### 6.2.1 File Size Limits * **Do This:** Set maximum file size limits for input files processed by esbuild. Reject files that exceed the limit. * **Don't Do This:** Process arbitrarily large files, which can consume excessive memory and CPU resources. * **Example:** (In a plugin that processes files): """javascript const esbuild = require('esbuild'); const fs = require('fs'); function fileSizeLimitPlugin(maxSize) { return { name: 'file-size-limit', setup(build) { build.onLoad({ filter: /.*/ }, async (args) => { // Apply to ALL files. Adjust filter as needed try { const stats = fs.statSync(args.path); //Synchronous call ok since is running on build and not web requests if (stats.size > maxSize) { throw new Error("File size exceeds limit (${maxSize} bytes): ${args.path}"); } return undefined; // Continue with default loading } catch (error) { console.error(error.message); return { errors: [{ text: error.message }], }; } }); }, }; } async function buildWithLimits() { try { await esbuild.build({ entryPoints: ['./src/index.js'], bundle: true, outfile: 'dist/bundle.js', plugins: [fileSizeLimitPlugin(1024 * 1024)], // Limit file size to 1MB, configure per your need format: 'esm' }); console.log('Build complete.'); } catch (e) { console.error('Build failed:', e); } } buildWithLimits(); """ #### 6.2.2 Request Rate Limiting * **Do This:** Implement rate limiting on requests to your esbuild service to prevent abuse. This can be done using middleware or reverse proxies. * **Don't Do This:** Allow unlimited requests. A naive implementation can permit attackers to overload your server, making it unavailable to legitimate users. ## 7. Configuration Management ### 7.1 Principles * **Do This:** Store configuration settings in environment variables or dedicated configuration files and avoid hardcoding sensitive information directly in your code. Ensure that configuration settings are properly secured and access-controlled. * **Don't Do This:** Embed API keys, passwords, or other sensitive data directly in your esbuild configuration or source code. * **Why:** Hardcoded secrets can be accidentally exposed in version control systems or logs, leading to unauthorized access to your resources. ### 7.2 Implementation #### 7.2.1 Environment Variables * **Do This:** Use environment variables to store configuration settings, especially for sensitive information. Retrieve the environment variables at runtime when needed. """javascript const esbuild = require('esbuild'); async function build() { try { await esbuild.build({ entryPoints: ['./src/index.js'], bundle: true, outfile: 'dist/bundle.js', define: { 'process.env.API_KEY': JSON.stringify(process.env.API_KEY || 'default_key'), }, format: 'esm' }); console.log('Build complete.'); } catch (e) { console.error('Build failed:', e); } } build(); """ #### 7.2.2 Secure Storage * **Do This:** Use secure storage mechanisms, such as HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault, to store and manage sensitive configuration information. These tools provide encryption, access controls, and audit logging. * **Don't Do This:** Store sensitive information in plain text configuration files without proper protection. ## 8. Using SRI (Subresource Integrity) ### 8.1 Principles * **Do This:** When serving assets from CDNs, use Subresource Integrity (SRI) to ensure that browsers only execute files that match a known hash. This protects against CDN compromises or accidental modifications of assets. * **Don't Do This:** Neglect using SRI, making your application vulnerable to malicious code injection from compromised CDNs. * **Why:** SRI provides a critical layer of defense against supply chain attacks. ### 8.2 Implementation #### 8.2.1 Build Process Integration * **Do This:** Integrate SRI hash generation into your esbuild build process. Plugins can calculate the SHA hashes of your assets, and the resulting HTML can include the integrity attributes. Tools like "esbuild-plugin-sri" automate this process. """javascript // esbuild.config.js const esbuild = require('esbuild'); const { sriPlugin } = require('esbuild-plugin-sri'); esbuild.build({ entryPoints: ['src/index.js'], outfile: 'dist/bundle.js', format: 'esm', plugins: [sriPlugin({ algorithms: ['sha256', 'sha384'] })] }).catch(() => process.exit(1)); """ #### 8.2.2 HTML Integration * **Do This:** Ensure your build system automatically injects the SRI hashes into the "<script>" and "<link>" tags in your HTML. * **Example:** """html <script src="https://cdn.example.com/bundle.js" integrity="sha384-EXAMPLE_SHA384_HASH" crossorigin="anonymous"></script> """ ## 9. General Security Practices * **Principle of Least Privilege:** Apply the principle of least privilege to all aspects of your esbuild development process. Only grant the necessary permissions and access to resources required for each task. * **Regular Code Reviews:** Conduct regular code reviews to identify potential security vulnerabilities and ensure adherence to secure coding practices. * **Security Training:** Provide security training to developers to raise awareness of common security risks and best practices. * **Stay Updated:** Stay informed about the latest security vulnerabilities and best practices related to esbuild and its dependencies. Regularly review security advisories and updates. By adhering to these security best practices, you can significantly reduce the risk of security vulnerabilities in your esbuild projects and build more secure and reliable applications.
# 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.