Files
stack/packages/mosaic/framework/skills/tsdown/references/advanced-hooks.md
T
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05:00

6.9 KiB

Lifecycle Hooks

Extend the build process with lifecycle hooks.

Overview

Hooks provide a way to inject custom logic at specific stages of the build lifecycle. Inspired by unbuild.

Recommendation: Use plugins for most extensions. Use hooks for simple custom tasks or Rolldown plugin injection.

Usage Patterns

Object Syntax

export default defineConfig({
  entry: ['src/index.ts'],
  hooks: {
    'build:prepare': async (context) => {
      console.log('Build starting...');
    },
    'build:done': async (context) => {
      console.log('Build complete!');
    },
  },
});

Function Syntax

export default defineConfig({
  entry: ['src/index.ts'],
  hooks(hooks) {
    hooks.hook('build:prepare', () => {
      console.log('Preparing build...');
    });

    hooks.hook('build:before', (context) => {
      console.log(`Building format: ${context.format}`);
    });
  },
});

Available Hooks

build:prepare

Called before the build process starts.

When: Once per build session

Context:

{
  options: ResolvedConfig,
  hooks: Hookable
}

Use cases:

  • Setup tasks
  • Validation
  • Environment preparation

Example:

hooks: {
  'build:prepare': async (context) => {
    console.log('Starting build for:', context.options.entry)
    await cleanOldFiles()
  },
}

build:before

Called before each Rolldown build.

When: Once per format (ESM, CJS, etc.)

Context:

{
  options: ResolvedConfig,
  buildOptions: BuildOptions,
  hooks: Hookable
}

Use cases:

  • Modify build options per format
  • Inject plugins dynamically
  • Format-specific setup

Example:

hooks: {
  'build:before': async (context) => {
    console.log(`Building ${context.buildOptions.format} format...`)

    // Add format-specific plugin
    if (context.buildOptions.format === 'iife') {
      context.buildOptions.plugins.push(browserPlugin())
    }
  },
}

build:done

Called after the build completes.

When: Once per build session

Context:

{
  options: ResolvedConfig,
  chunks: RolldownChunk[],
  hooks: Hookable
}

Use cases:

  • Post-processing
  • Asset copying
  • Notifications
  • Deployment

Example:

hooks: {
  'build:done': async (context) => {
    console.log(`Built ${context.chunks.length} chunks`)

    // Copy additional files
    await copyAssets()

    // Send notification
    notifyBuildComplete()
  },
}

Common Patterns

Build Notifications

export default defineConfig({
  hooks: {
    'build:prepare': () => {
      console.log('🚀 Starting build...');
    },
    'build:done': (context) => {
      const size = context.chunks.reduce((sum, c) => sum + c.code.length, 0);
      console.log(`✅ Build complete! Total size: ${size} bytes`);
    },
  },
});

Conditional Plugin Injection

export default defineConfig({
  hooks(hooks) {
    hooks.hook('build:before', (context) => {
      // Add minification only for production
      if (process.env.NODE_ENV === 'production') {
        context.buildOptions.plugins.push(minifyPlugin());
      }
    });
  },
});

Custom File Copy

import { copyFile } from 'fs/promises';

export default defineConfig({
  hooks: {
    'build:done': async (context) => {
      // Copy README to dist
      await copyFile('README.md', `${context.options.outDir}/README.md`);
    },
  },
});

Build Metrics

export default defineConfig({
  hooks: {
    'build:prepare': (context) => {
      context.startTime = Date.now();
    },
    'build:done': (context) => {
      const duration = Date.now() - context.startTime;
      console.log(`Build took ${duration}ms`);

      // Log chunk sizes
      context.chunks.forEach((chunk) => {
        console.log(`${chunk.fileName}: ${chunk.code.length} bytes`);
      });
    },
  },
});

Format-Specific Logic

export default defineConfig({
  format: ['esm', 'cjs', 'iife'],
  hooks: {
    'build:before': (context) => {
      const format = context.buildOptions.format;

      if (format === 'iife') {
        // Browser-specific setup
        context.buildOptions.globalName = 'MyLib';
      } else if (format === 'cjs') {
        // Node-specific setup
        context.buildOptions.platform = 'node';
      }
    },
  },
});

Deployment Hook

export default defineConfig({
  hooks: {
    'build:done': async (context) => {
      if (process.env.DEPLOY === 'true') {
        console.log('Deploying to CDN...');
        await deployToCDN(context.options.outDir);
      }
    },
  },
});

Advanced Usage

Multiple Hooks

export default defineConfig({
  hooks(hooks) {
    // Register multiple hooks
    hooks.hook('build:prepare', setupEnvironment);
    hooks.hook('build:prepare', validateConfig);

    hooks.hook('build:before', injectPlugins);
    hooks.hook('build:before', logFormat);

    hooks.hook('build:done', generateManifest);
    hooks.hook('build:done', notifyComplete);
  },
});

Async Hooks

export default defineConfig({
  hooks: {
    'build:prepare': async (context) => {
      await fetchRemoteConfig();
      await initializeDatabase();
    },
    'build:done': async (context) => {
      await uploadToS3(context.chunks);
      await invalidateCDN();
    },
  },
});

Error Handling

export default defineConfig({
  hooks: {
    'build:done': async (context) => {
      try {
        await riskyOperation();
      } catch (error) {
        console.error('Hook failed:', error);
        // Don't throw - allow build to complete
      }
    },
  },
});

Hookable API

tsdown uses hookable for hooks. Additional methods:

export default defineConfig({
  hooks(hooks) {
    // Register hook
    hooks.hook('build:done', handler);

    // Register hook once
    hooks.hookOnce('build:prepare', handler);

    // Remove hook
    hooks.removeHook('build:done', handler);

    // Clear all hooks for event
    hooks.removeHooks('build:done');

    // Call hooks manually
    await hooks.callHook('build:done', context);
  },
});

Tips

  1. Use plugins for most extensions
  2. Hooks for simple tasks like notifications or file copying
  3. Async hooks supported for I/O operations
  4. Don't throw errors unless you want to fail the build
  5. Context is mutable in build:before for advanced use cases
  6. Multiple hooks allowed for the same event

Troubleshooting

Hook Not Called

  • Verify hook name is correct
  • Check hook is registered in config
  • Ensure async hooks are awaited

Build Fails in Hook

  • Add try/catch for error handling
  • Don't throw unless intentional
  • Log errors for debugging

Context Undefined

  • Check which hook you're using
  • Verify context properties available for that hook