Files
stack/v1/packages/mosaic/framework/skills/tsdown/references/advanced-hooks.md
T

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