or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

build-context.mdcore-build-api.mdfile-transformation.mdindex.mdplugin-system.mdutility-functions.md
tile.json

plugin-system.mddocs/

Plugin System

Extensible plugin architecture for custom build transformations, file resolution, and code generation. The plugin system allows deep customization of the build process through lifecycle hooks and custom resolution logic.

Capabilities

Plugin Interface

Core structure for creating ESBuild plugins.

interface Plugin {
  /**
   * Name of the plugin for debugging and error reporting
   */
  name: string;
  
  /**
   * Setup function called when the plugin is registered
   * @param build - Build context providing plugin APIs
   */
  setup(build: PluginBuild): void;
}

Basic Plugin Example:

import * as esbuild from "esbuild";

const examplePlugin: esbuild.Plugin = {
  name: "example-plugin",
  setup(build) {
    // Plugin logic goes here
    console.log("Plugin initialized");
  },
};

await esbuild.build({
  entryPoints: ["src/app.js"],
  bundle: true,
  outfile: "dist/app.js",
  plugins: [examplePlugin],
});

Plugin Build Context

The build context provided to plugin setup functions, offering access to build hooks and utilities.

interface PluginBuild {
  /**
   * Initial build options passed to esbuild
   */
  initialOptions: BuildOptions;
  
  /**
   * Register callback for build start
   * @param callback - Function called when build starts
   */
  onStart(callback: () => void | Promise<OnStartResult>): void;
  
  /**
   * Register callback for build end
   * @param callback - Function called when build completes
   */ 
  onEnd(callback: (result: BuildResult) => void | Promise<OnEndResult>): void;
  
  /**
   * Register callback for module resolution
   * @param options - Resolution filter and configuration
   * @param callback - Function to handle resolution
   */
  onResolve(
    options: OnResolveOptions,
    callback: (args: OnResolveArgs) => OnResolveResult | Promise<OnResolveResult>
  ): void;
  
  /**
   * Register callback for file loading
   * @param options - Loading filter and configuration
   * @param callback - Function to handle file loading
   */
  onLoad(
    options: OnLoadOptions,
    callback: (args: OnLoadArgs) => OnLoadResult | Promise<OnLoadResult>
  ): void;
  
  /**
   * Register cleanup callback for plugin disposal
   * @param callback - Function called when plugin is disposed
   */
  onDispose(callback: () => void): void;
  
  /**
   * Resolve a module path using esbuild's resolution logic
   * @param path - Module path to resolve
   * @param options - Resolution options
   * @returns Promise resolving to resolution result
   */
  resolve(
    path: string,
    options?: ResolveOptions
  ): Promise<ResolveResult>;
  
  /**
   * Access to esbuild APIs for advanced plugin functionality
   */
  esbuild: {
    build: typeof build;
    transform: typeof transform;
    formatMessages: typeof formatMessages;
    analyzeMetafile: typeof analyzeMetafile;
  };
}

Build Lifecycle Hooks

onStart Hook

Called when a build operation begins.

/**
 * Register callback for build start
 * @param callback - Function called when build starts
 */
onStart(callback: () => void | Promise<OnStartResult>): void;

interface OnStartResult {
  errors?: PartialMessage[];
  warnings?: PartialMessage[];
}

Usage Example:

const buildStartPlugin: esbuild.Plugin = {
  name: "build-start",
  setup(build) {
    build.onStart(() => {
      console.log("Build started at", new Date().toISOString());
    });
  },
};

onEnd Hook

Called when a build operation completes.

/**
 * Register callback for build end
 * @param callback - Function called when build completes
 */
onEnd(callback: (result: BuildResult) => void | Promise<OnEndResult>): void;

interface OnEndResult {
  errors?: PartialMessage[];
  warnings?: PartialMessage[];
}

Usage Example:

const buildEndPlugin: esbuild.Plugin = {
  name: "build-end",
  setup(build) {
    build.onEnd((result) => {
      console.log(`Build completed with ${result.errors.length} errors`);
      
      if (result.metafile) {
        // Analyze the build
        return esbuild.analyzeMetafile(result.metafile).then(analysis => {
          console.log("Bundle analysis:", analysis);
        });
      }
    });
  },
};

Module Resolution Hooks

onResolve Hook

Intercept and customize module resolution logic.

/**
 * Register callback for module resolution
 * @param options - Resolution filter and configuration
 * @param callback - Function to handle resolution
 */
onResolve(
  options: OnResolveOptions,
  callback: (args: OnResolveArgs) => OnResolveResult | Promise<OnResolveResult>
): void;

interface OnResolveOptions {
  filter: RegExp;
  namespace?: string;
}

interface OnResolveArgs {
  path: string;
  importer: string;
  namespace: string;
  resolveDir: string;
  kind: ImportKind;
  pluginData: any;
}

interface OnResolveResult {
  errors?: PartialMessage[];
  warnings?: PartialMessage[];
  path?: string;
  external?: boolean;
  sideEffects?: boolean;
  namespace?: string;
  suffix?: string;
  pluginData?: any;
  watchFiles?: string[];
  watchDirs?: string[];
}

type ImportKind = 
  | "entry-point"
  | "import-statement" 
  | "require-call"
  | "dynamic-import"
  | "require-resolve"
  | "import-rule"
  | "composes-from"
  | "url-token";

Usage Examples:

// Redirect imports to different files
const redirectPlugin: esbuild.Plugin = {
  name: "redirect",
  setup(build) {
    build.onResolve({ filter: /^old-package$/ }, (args) => {
      return { path: "new-package", external: true };
    });
  },
};

// Resolve virtual modules
const virtualPlugin: esbuild.Plugin = {
  name: "virtual",
  setup(build) {
    build.onResolve({ filter: /^virtual:/ }, (args) => {
      return {
        path: args.path,
        namespace: "virtual",
      };
    });
  },
};

File Loading Hooks

onLoad Hook

Intercept and customize file loading logic.

/**
 * Register callback for file loading
 * @param options - Loading filter and configuration
 * @param callback - Function to handle file loading
 */
onLoad(
  options: OnLoadOptions,
  callback: (args: OnLoadArgs) => OnLoadResult | Promise<OnLoadResult>
): void;

interface OnLoadOptions {
  filter: RegExp;
  namespace?: string;
}

interface OnLoadArgs {
  path: string;
  namespace: string;
  suffix: string;
  pluginData: any;
}

interface OnLoadResult {
  errors?: PartialMessage[];
  warnings?: PartialMessage[];
  contents?: string | Uint8Array;
  loader?: Loader;
  resolveDir?: string;
  pluginData?: any;
  watchFiles?: string[];
  watchDirs?: string[];
}

Usage Examples:

// Load virtual modules
const virtualLoaderPlugin: esbuild.Plugin = {
  name: "virtual-loader",
  setup(build) {
    build.onLoad({ filter: /.*/, namespace: "virtual" }, (args) => {
      if (args.path === "virtual:config") {
        return {
          contents: `export const config = ${JSON.stringify({
            apiUrl: process.env.API_URL || "http://localhost:3000",
            version: "1.0.0",
          })};`,
          loader: "js",
        };
      }
    });
  },
};

// Transform file contents
const preprocessPlugin: esbuild.Plugin = {
  name: "preprocess",
  setup(build) {
    build.onLoad({ filter: /\.special$/ }, async (args) => {
      const fs = require("fs");
      const contents = await fs.promises.readFile(args.path, "utf8");
      
      // Custom preprocessing
      const processed = contents
        .replace(/%%VERSION%%/g, "1.0.0")
        .replace(/%%BUILD_TIME%%/g, new Date().toISOString());
      
      return {
        contents: processed,
        loader: "js",
      };
    });
  },
};

Advanced Plugin Features

Resolve Method

Use esbuild's resolution logic within plugins.

/**
 * Resolve a module path using esbuild's resolution logic
 * @param path - Module path to resolve
 * @param options - Resolution options
 * @returns Promise resolving to resolution result
 */
resolve(path: string, options?: ResolveOptions): Promise<ResolveResult>;

interface ResolveOptions {
  importer?: string;
  namespace?: string;
  resolveDir?: string;
  kind?: ImportKind;
  pluginData?: any;
}

interface ResolveResult {
  errors: Message[];
  warnings: Message[];
  path: string;
  external: boolean;
  sideEffects: boolean;
  namespace: string;
  suffix: string;
  pluginData: any;
}

Usage Example:

const resolverPlugin: esbuild.Plugin = {
  name: "resolver",
  setup(build) {
    build.onLoad({ filter: /\.resolve-test$/ }, async (args) => {
      // Use esbuild's resolver to find a dependency
      const result = await build.resolve("react", {
        resolveDir: path.dirname(args.path),
        kind: "import-statement",
      });
      
      if (result.errors.length > 0) {
        return { errors: result.errors };
      }
      
      return {
        contents: `export const reactPath = ${JSON.stringify(result.path)};`,
        loader: "js",
      };
    });
  },
};

Plugin Data Flow

Pass data between plugin hooks using pluginData.

const dataFlowPlugin: esbuild.Plugin = {
  name: "data-flow",
  setup(build) {
    build.onResolve({ filter: /\.tracked$/ }, (args) => {
      return {
        path: args.path,
        pluginData: {
          originalImporter: args.importer,
          resolvedAt: Date.now(),
        },
      };
    });
    
    build.onLoad({ filter: /\.tracked$/ }, (args) => {
      console.log("Loading file with data:", args.pluginData);
      
      // Access data from onResolve
      const { originalImporter, resolvedAt } = args.pluginData;
      
      return {
        contents: `// Resolved from ${originalImporter} at ${resolvedAt}`,
        loader: "js",
      };
    });
  },
};

Complete Plugin Examples

Environment Variable Replacement

const envPlugin: esbuild.Plugin = {
  name: "env",
  setup(build) {
    const options = build.initialOptions;
    options.define = options.define || {};
    
    // Add environment variables to define
    for (const [key, value] of Object.entries(process.env)) {
      if (key.startsWith("PUBLIC_")) {
        options.define[`process.env.${key}`] = JSON.stringify(value);
      }
    }
  },
};

Asset Processing Plugin

const assetPlugin: esbuild.Plugin = {
  name: "assets",
  setup(build) {
    const fs = require("fs");
    const path = require("path");
    
    build.onResolve({ filter: /\.(png|jpg|svg)$/ }, (args) => {
      return {
        path: path.resolve(args.resolveDir, args.path),
        namespace: "asset",
      };
    });
    
    build.onLoad({ filter: /.*/, namespace: "asset" }, async (args) => {
      const contents = await fs.promises.readFile(args.path);
      const base64 = contents.toString("base64");
      const ext = path.extname(args.path).slice(1);
      const mimeType = {
        png: "image/png",
        jpg: "image/jpeg", 
        svg: "image/svg+xml",
      }[ext] || "application/octet-stream";
      
      return {
        contents: `export default "data:${mimeType};base64,${base64}";`,
        loader: "js",
      };
    });
  },
};