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:optionshook) 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 myHookHooks 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.
| Hook | Arguments | When 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(),
],
},
})