Files
agent-skills/skills/tsdown/references/advanced-hooks.md
Jason Woltje f5792c40be feat: Complete fleet — 94 skills across 10+ domains
Pulled ALL skills from 15 source repositories:
- anthropics/skills: 16 (docs, design, MCP, testing)
- obra/superpowers: 14 (TDD, debugging, agents, planning)
- coreyhaines31/marketingskills: 25 (marketing, CRO, SEO, growth)
- better-auth/skills: 5 (auth patterns)
- vercel-labs/agent-skills: 5 (React, design, Vercel)
- antfu/skills: 16 (Vue, Vite, Vitest, pnpm, Turborepo)
- Plus 13 individual skills from various repos

Mosaic Stack is not limited to coding — the Orchestrator and
subagents serve coding, business, design, marketing, writing,
logistics, analysis, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-16 16:27:42 -06: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