Hooks

Use build lifecycle hooks to extend Packem's functionality

Hooks

Hooks allow you to tap into Packem's build lifecycle and execute custom logic at specific points during the build process. This enables advanced customization and integration with external tools.

Hooks are registered through the hooks option and are powered by hookable. Every hook receives the active BuildContext as its first argument; some hooks receive an additional argument (for example the Rollup build, the resolved Rollup options, or the watcher).

import { defineConfig } from '@visulima/packem/config'
import transformer from '@visulima/packem/transformer/esbuild'

export default defineConfig({
  transformer,
  hooks: {
    'build:before': async (context) => {
      context.logger.info('Build starting...')
    },
    'build:done': async (context) => {
      context.logger.info('Build completed!')
    },
  },
})

Hook Merging

When using presets, hooks from both the preset and your configuration are automatically merged. This means:

  • Hooks from presets are preserved - Preset hooks (like the Solid preset's rollup:options hook) are automatically included
  • User config hooks take precedence - If both preset and user config define the same hook, your hook will be used
  • Multiple hooks can coexist - Different hooks from preset and user config are all registered
// Preset defines: hooks: { 'rollup:options': presetHook }
// Your config defines: hooks: { 'build:before': myHook }

// Result: Both hooks are registered
// - 'rollup:options' uses presetHook
// - 'build:before' uses myHook

Hooks are merged automatically when using presets. You don't need to manually combine them.

Available Hooks

Packem exposes the following hooks (defined by the BuildHooks type). Each callback may be synchronous or return a Promise.

HookArgumentsWhen it runs
build:prepare(context)Before the build context is fully set up.
build:before(context)Right before the build starts.
build:done(context)After the build finishes.
builder:before(name, context)Before an individual builder runs.
builder:done(name, context)After an individual builder finishes.
rollup:options(context, options)After the Rollup options are assembled, before bundling.
rollup:build(context, build)After Rollup produces the build.
rollup:done(context)After the Rollup build completes.
rollup:watch(context, watcher)After the Rollup watcher is created.
rollup:dts:options(context, options)After the declaration-build Rollup options are assembled.
rollup:dts:build(context, build)After the declaration build is produced.
rollup:dts:done(context)After the declaration build completes.
validate:before(context)Before validation runs.
validate:done(context)After validation runs.

typedoc:before and typedoc:done still exist but are deprecated. Use builder:before and builder:done instead.

Build Lifecycle

export default defineConfig({
  hooks: {
    'build:prepare': async (context) => {
      context.logger.info('Preparing build context...')
    },

    'build:before': async (context) => {
      context.logger.info('Build starting...')
    },

    'build:done': async (context) => {
      context.logger.info('Build completed!')
    },
  },
})

Rollup Hooks

export default defineConfig({
  hooks: {
    'rollup:options': async (context, options) => {
      // Inspect or mutate the resolved Rollup options
      context.logger.debug('Rollup input:', options.input)
    },

    'rollup:build': async (context, build) => {
      context.logger.info('Rollup build produced')
    },

    'rollup:done': async (context) => {
      context.logger.info('Rollup finished')
    },
  },
})

Declaration (DTS) Hooks

export default defineConfig({
  hooks: {
    'rollup:dts:options': async (context, options) => {
      // Adjust the declaration-build Rollup options
    },

    'rollup:dts:build': async (context, build) => {
      context.logger.info('Declaration build produced')
    },

    'rollup:dts:done': async (context) => {
      context.logger.info('Declaration build finished')
    },
  },
})

Validation Hooks

export default defineConfig({
  hooks: {
    'validate:before': async (context) => {
      context.logger.info('Running validation...')
    },

    'validate:done': async (context) => {
      context.logger.info('Validation finished')
    },
  },
})

Common Use Cases

Code Generation

export default defineConfig({
  hooks: {
    'build:before': async () => {
      // Generate types from schema
      const { generateTypes } = await import('./scripts/generate-types')
      await generateTypes()
    },
  },
})

Asset Processing

export default defineConfig({
  hooks: {
    'build:done': async () => {
      // Optimize images
      const { optimizeImages } = await import('./scripts/optimize-images')
      await optimizeImages('dist/assets')
    },
  },
})

Version Management

export default defineConfig({
  hooks: {
    'build:before': async () => {
      // Update version in files
      const { writeFile } = await import('node:fs/promises')
      const version = process.env.npm_package_version

      await writeFile(
        'src/version.ts',
        `export const VERSION = '${version}'`
      )
    },
  },
})

Bundle Analysis

export default defineConfig({
  hooks: {
    'build:done': async (context) => {
      // Inspect the entries Packem produced
      for (const entry of context.buildEntries) {
        context.logger.info(`${entry.path}: ${entry.size?.bytes ?? 0} bytes`)
      }
    },
  },
})

Advanced Patterns

Conditional Hooks

export default defineConfig({
  hooks: {
    'build:done': async () => {
      if (process.env.NODE_ENV === 'production') {
        // Production-only tasks
        await deployToS3()
      }

      if (process.env.ANALYZE_BUNDLE) {
        // Optional bundle analysis
        await generateBundleReport()
      }
    },
  },
})

Async Hook Chains

A hook value may be a single function or an array of functions. All registered functions run in order.

export default defineConfig({
  hooks: {
    'build:before': [
      async () => {
        console.log('Step 1: Clean dist')
        await cleanDist()
      },

      async () => {
        console.log('Step 2: Generate types')
        await generateTypes()
      },

      async () => {
        console.log('Step 3: Copy assets')
        await copyAssets()
      },
    ],
  },
})

Error Handling

export default defineConfig({
  hooks: {
    'build:before': async () => {
      try {
        await riskyOperation()
      } catch (error) {
        console.warn('Optional operation failed:', error.message)
        // Continue build despite error
      }
    },
  },
})

Validation Integration

Reacting to Validation

Use the validate:before and validate:done hooks to run custom checks around Packem's built-in validation (such as attw, bundleLimit, dependencies and packageJson).

export default defineConfig({
  validation: {
    bundleLimit: {
      limit: '50KB',
    },
  },
  hooks: {
    'validate:before': async (context) => {
      context.logger.info('Starting validation pass')
    },

    'validate:done': async (context) => {
      context.logger.info('Validation pass complete')
    },
  },
})

Testing Integration

Test Hooks

export default defineConfig({
  hooks: {
    'build:done': async () => {
      if (process.env.RUN_TESTS) {
        const { exec } = await import('node:child_process')
        const { promisify } = await import('node:util')
        const execAsync = promisify(exec)

        try {
          await execAsync('npm test')
          console.log('Tests passed!')
        } catch (error) {
          console.error('Tests failed:', error.message)
          process.exit(1)
        }
      }
    },
  },
})

Coverage Reports

export default defineConfig({
  hooks: {
    'build:done': async () => {
      if (process.env.GENERATE_COVERAGE) {
        await generateCoverageReport()
        await uploadCoverageToService()
      }
    },
  },
})

Deployment Hooks

Automatic Deployment

export default defineConfig({
  hooks: {
    'build:done': async () => {
      if (process.env.AUTO_DEPLOY === 'true') {
        console.log('Deploying to production...')

        // Upload to CDN
        await uploadToCDN('dist')

        // Update service
        await updateService()

        // Send notification
        await sendDeploymentNotification()
      }
    },
  },
})

Environment-Specific Deployments

export default defineConfig({
  hooks: {
    'build:done': async () => {
      const env = process.env.DEPLOY_ENV

      switch (env) {
        case 'staging':
          await deployToStaging()
          break
        case 'production':
          await deployToProduction()
          break
        default:
          console.log('No deployment configured')
      }
    },
  },
})

Performance Monitoring

Build Metrics

export default defineConfig({
  hooks: {
    'build:before': async () => {
      // Record build start time
      global.buildStartTime = Date.now()
    },

    'build:done': async () => {
      // Calculate build duration
      const duration = Date.now() - global.buildStartTime
      console.log(`Build completed in ${duration}ms`)

      // Send metrics to monitoring service
      await sendMetrics({
        buildDuration: duration,
        timestamp: Date.now(),
      })
    },
  },
})

Bundle Size Tracking

export default defineConfig({
  hooks: {
    'build:done': async (context) => {
      const bundleSizes = Object.fromEntries(
        context.buildEntries.map((entry) => [entry.path, entry.size?.bytes ?? 0])
      )

      // Track bundle size changes
      await trackBundleSizes(bundleSizes)

      // Alert if bundle size increased significantly
      await checkBundleSizeThresholds(bundleSizes)
    },
  },
})

Debugging Hooks

Hook Logging

export default defineConfig({
  hooks: {
    'build:before': async (context) => {
      context.logger.info('Starting build process')
    },

    'rollup:done': async (context) => {
      context.logger.info('Rollup bundling finished')
    },

    'build:done': async (context) => {
      context.logger.info('Build completed successfully')
    },
  },
})

Hook Performance

function withTiming(hookName: string, fn: Function) {
  return async (...args: any[]) => {
    const start = Date.now()
    await fn(...args)
    const duration = Date.now() - start
    console.log(`Hook ${hookName} took ${duration}ms`)
  }
}

export default defineConfig({
  hooks: {
    'build:before': withTiming('build:before', async () => {
      await heavyOperation()
    }),
  },
})

Troubleshooting

Hook Errors

Hook errors can interrupt the build process. Use proper error handling.

export default defineConfig({
  hooks: {
    'build:before': async () => {
      try {
        await riskyOperation()
      } catch (error) {
        console.error('Hook error:', error)
        // Decide whether to continue or fail
        if (error.critical) {
          throw error // Fail the build
        }
        // Otherwise continue
      }
    },
  },
})

Async Hook Issues

export default defineConfig({
  hooks: {
    'build:before': async () => {
      // Ensure all async operations complete
      await Promise.all([
        operation1(),
        operation2(),
        operation3(),
      ])
    },
  },
})

Hook Order Dependencies

export default defineConfig({
  hooks: {
    'build:before': [
      // Order matters - dependencies first
      async () => await setupDependencies(),
      async () => await configureEnvironment(),
      async () => await startServices(),
    ],
  },
})

Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues