We use cookies to make your viewing experience better. By accepting you consent, you agree to our Cookie policy

Improve your Craft CMS skills

Plugin Development: How To Put A Build Step Into Craft CMS 3

10 min read
Craft CMS Plugin Development

Craft CMS plugins are powerful but developing them brings challenges. Slow build times and disorganized code can cripple productivity. Implementing an automated build process solves these pain points for smooth plugin workflows. This guide dives into build setup for next-level Craft CMS development - optimizing performance, enabling caching, integrating testing, and more using Webpack and modern tools. Take your plugin dev further with these advanced techniques.

Use Webpack and NPM scripts to implement automated production builds in Craft plugins. This enables transpiling, bundling, minification, versioning, and splitting to optimize performance. Integrate frameworks like Jest and PHPUnit to run tests during builds for quality and confidence. Leverage events and file hashes to trigger incremental rebuilds on changes.

Choosing a Build Tool for Craft CMS Plugins

Comparing Build Tools for Usage with Craft CMS

As a guide to learning Craft CMS, when developing a plugin for Craft CMS, one of the first decisions is which build tool to use.

There are several popular options like Webpack, Parcel, Rollup and Gulp that each have their own advantages. Here is an overview comparing the key factors:

Webpack is one of the most full-featured bundlers available. It can handle complex builds and provides code splitting for lazy loading. The main downsides are longer build times and a steeper learning curve for configuration. Webpack is best for large, complex plugins.

Parcel takes a simpler approach than Webpack with zero config out of the box. It's very fast, using multicore processing and caching for speed. The main limitation is less flexibility for customisation. Parcel works well for smaller plugins.

Rollup focuses on bundling JavaScript through tree shaking and optimisation. It's much faster than Webpack with simpler configuration. However, Rollup requires plugins to match Webpack's functionality. It's ideal for JS-centric plugins.

Gulp is more of a task runner than a bundler. It handles workflows like Sass compiling, minification and linting. Gulp can work alongside a bundler like Webpack for automation. It's great for optimising assets in a plugin.

When choosing Craft CMS, lean towards Parcel and Rollup for their speed and simplicity. For complex requirements, Webpack offers the most customisation. Gulp accelerates build tasks in any project. Evaluate your plugin's size and needs to select the best fit.

Evaluating Bundling vs Task Running

There is an important distinction between bundlers like Webpack and task runners like Gulp. Bundlers focus on compiling assets like Sass and bundling JavaScript modules into optimized files. Task runners automate repetitive development workflows.

Bundling is required to package a plugin's front-end code and assets into distributable files. The bundling process handles dependency management, minification and code splitting. This prepares a plugin's assets for release.

Task running complements the bundling process by automating workflows around it. For example, Gulp can run Sass compilation, then pass the CSS to Webpack for bundling. Gulp also helps with linting, minification and file copying.

For Craft plugins, a bundler is essential to bundle the JavaScript and CSS. A task runner like Gulp is recommended too for workflow automation. This combination provides robust bundling and task automation for smooth plugin development.

Selecting Build Tool Based on Project

With an understanding of the popular build tools, here are some guidelines for selecting one for a Craft CMS plugin:

  • Small plugin with mostly CSS/Sass and simple JS – Use Parcel for its zero config approach.

  • Medium complexity plugin with some JS bundling needed – Rollup provides fast optimized bundling.

  • Large complex plugin with code splitting required – Webpack has the most advanced features.

  • Need to automate asset processing and workflows – Gulp is ideal for task running.

Also consider that Parcel and Rollup are easier to configure than Webpack, making them great starting choices. For advanced builds, Webpack can handle the most complex requirements.

Gulp works well in any sized project for workflow automation. Using Gulp alongside a bundler provides robust asset processing and bundling.

Prioritize simplicity and speed at first over advanced features. Evaluate if your plugin's specific requirements need Webpack's customizations or if a simpler bundler will suffice. With these criteria in mind, you can select the best build tool match for your Craft CMS plugin.

Setting Up a Craft CMS Plugin

Creating New Plugin with Craft CLI

The Craft CLI provides a quick way to generate starter code and scaffolding for a new Craft plugin, eliminating the need to manually setup all the files and boilerplate.

To get started, first install the Craft CLI globally using Composer. Then by running the craft plugin/create command and specifying a plugin handle and name, the CLI will generate a folder containing all the necessary files and structure to build out the plugin's functionality.

This includes essential elements like the source code location in src/, Composer dependencies in composer.json, versioning history in CHANGELOG.md, and documentation in README.md. The src folder itself contains the controllers, variables, services and templates that form the core of the plugin's capabilities. With this foundation set up through the CLI initialization, developers can jump right into fleshing out the unique aspects of their plugin.

Adding Plugin Code and Assets

After initializing the starter files, the next phase is filling in the plugin's source code and assets. This involves adding the controllers for frontend and admin sections, services for business logic, variables for passing data to templates, the Twig templates themselves, and any CSS, JavaScript or images in assetbundles.

For example, a "Contact Form" plugin might include a ContactFormController.php to handle submissions, a ContactService.php service for validation and emails, a ContactVariable.php variable to expose form fields in Twig, a contact-form.twig frontend template, and a contact-index.twig admin template.

Any stylesheets, scripts or images would get organized into assetbundles based on the default src/assetbundles structure. With the foundation provided by the CLI scaffolding, developers can build out all the controllers, services, variables and templates that bring the plugin to life.

Git Repository Initialization

Once the initial plugin code is in place, it's good practice to initialize a Git repository for version control and commit the code. After navigating to the plugin's root folder in the terminal, running git init will create the required .git subfolder to start tracking changes.

Before making the first commit, a .gitignore file should be added to exclude non-essential files like dependencies and environments. With the ignore file created, the initial plugin code can be committed with git add . to stage the files, followed by git commit -m "Initial plugin commit" to capture the starting point.

This first commit establishes the foundation for ongoing version control as development progresses. Additionally, using a remote Git hosting service like GitHub helps enable collaboration and offsite backup of the codebase. By following these version control best practices, Craft CMS plugin developers can ensure a streamlined and reproducible environment.

Configuring Webpack for Craft Plugins

Installing Webpack Packages

To use Webpack for a Craft CMS plugin, the first step is installing the Webpack packages locally via npm.

In the plugin directory, initialise a node project:

npm init -y

Then install the webpack and webpack CLI packages:

npm install --save-dev webpack webpack-cli

This will add the webpack and webpack-cli modules to package.json as dev dependencies. They provide the bundling functionality and command line interface for Webpack respectively.

With the packages installed, Webpack can now be configured for the project.

Creating a Webpack Config File

The main Webpack configuration goes in a webpack.config.js file in the root of the project.

First import the node path module and webpack:

const path = require('path');

const webpack = require('webpack');

Then export a configuration object:

module.exports = {

// Webpack options


Some key initial settings are:

  • entry - Entry point JS file

  • output - Output bundle location

  • mode - development or production

For example:

module.exports = {

entry: './src/js/app.js',

output: {

path: path.resolve(__dirname, 'dist'),

filename: 'app.bundle.js'


mode: 'development'


This provides the foundation to build on as more functionality is needed.

Setting Up Entry Points, Output, Loaders

Some other key Webpack configuration areas for Craft plugins are:

Entry Points - In addition to the main JS entry point, also add CSS and image entry points:

entry: {

app: './src/js/app.js',

styles: './src/css/styles.css',

images: './src/images/'


Output - Update output options for CSS and images:

output: {

filename: '[name].bundle.[contenthash].js',

path: path.resolve(__dirname, 'dist')


Loaders - Handle JS transpiling with Babel and asset loading:

module: {

rules: [


test: /\.js$/,

use: 'babel-loader'



test: /\.css$/,

use: ['style-loader', 'css-loader']



test: /\.(png|jpe?g|gif)$/,

use: 'file-loader'




This configures key aspects like transpiling ES6 via Babel, loading CSS files, and handling images.

With these core configurations in place, the webpack.config.js file provides a solid foundation for bundling a Craft plugin's assets.

Configuring Babel for Craft Plugins

Adding Babel Presets and Plugins

Babel provides transpiling and polyfilling to support new JavaScript features in older browsers.

To configure it for a Craft CMS plugin:

First install the Babel packages:

npm install --save-dev @babel/core @babel/cli @babel/preset-env

This provides the core transpiler, CLI tool, and baseline transpiling with the preset-env.

Then create a .babelrc config file:


"presets": ["@babel/preset-env"]


This specifies the env preset to be used.

Additional presets or plugins can be added like:


"presets": ["@babel/preset-env"],

"plugins": ["@babel/plugin-proposal-class-properties"]


This enables support for class properties.

In the Webpack config, add the babel-loader rule:


test: /\.js$/,

use: 'babel-loader'


Now Babel will transpile the JavaScript during bundling.

Targeting Browsers

The env preset can be configured to target specific browsers to support.

For example, to support last 2 versions of major browsers:


"presets": [

["@babel/preset-env", {

"targets": {

"chrome": "58",

"ie": "11"





Or for > 1% usage globally:


"presets": [

["@babel/preset-env", {

"targets": "> 1%, not dead"




This optimizes the transpiled output for the specified browsers. Polyfills and transpiling are only added if needed for those target environments.

The browser targets should be based on the plugin's audience and Craft CMS's own browser support policy.

Transpiling and Polyfilling

When Babel encounters new JavaScript syntax like classes, arrow functions or async/await, it will transform or "transpile" that code into older equivalent code.

For example:

// Input

class MyPlugin {

hello = () => {

return 'Hello';



// Output

var MyPlugin = function MyPlugin() {

_classCallCheck(this, MyPlugin);

this.hello = function () {

return 'Hello';



This transpiles the class syntax down to constructor functions.

In addition to transpiling, Babel will also add polyfills for new APIs like Promises that may be missing in older browsers. This provides full compatibility.

The combination of transpiling and polyfilling allows developers to use the latest JavaScript features while still supporting older browsers needed for Craft CMS plugins.

Development Builds for Craft Plugins

Local Development Server

Webpack's development server provides a local build of a Craft plugin with live reloading to accelerate development.

To enable it, install webpack-dev-server:

npm install --save-dev webpack-dev-server

Then add a devServer config:

devServer: {

contentBase: path.join(__dirname, 'dist'),

compress: true,

port: 3000,


This will serve the files from dist on port 3000.

In package.json add a script:

"scripts": {

"dev": "webpack serve"


Now run npm run dev to start the dev server and open the browser.

Any changes made to source files will instantly rebuild and reload in the browser. This live reloading makes rapid iteration and debugging much faster.

Fast Incremental Builds

In development mode, Webpack builds extremely fast by caching and reusing output between builds.

For example, when a small CSS change is made, only the CSS is rebuilt - JavaScript and other assets are cached.

Webpack tracks dependencies between files, so only code affected by changes is rebuilt.

Combined with the dev server's live reloading, developers can test changes in real-time with lightning fast incremental builds.

This accelerates the modify, build, test loop to be instantaneous rather than slow full rebuilds.

Sourcemaps and Debugging

Sourcemaps help map compiled code like bundles back to the original source code. This improves debugging in development.

Enable sourcemaps in Webpack config:

devtool: 'inline-source-map',

And in Babel config:

"sourceMaps": "inline"

Now when debugging, the original source will be shown rather than the transpiled output.

Sourcemaps, fast incremental builds, and live reloading combine to create an optimal debugging environment for rapid Craft CMS plugin development.

The dev server and tools maximize productivity when building a plugin. For production bundles, development optimizations are disabled for optimal output.

Optimized Production Builds

Minification and Tree Shaking

For production builds, the output needs to be fully optimized and minimized for performance.

Webpack's minification mode will minify and mangle JavaScript and CSS code for smaller file sizes.

Tree shaking analyzes code to eliminate any unused exports that aren't imported elsewhere.

This removes dead code.

Scope hoisting combines many small modules into a few larger modules to improve runtime performance.

In Webpack config:

mode: 'production',

optimization: {

usedExports: true,

concatenateModules: true


This enables these optimizations to create highly optimized production code.

The result is lean and efficient bundles without any wasted code or cruft.

Code Splitting

Webpack splits bundles via "code splitting" so users only load code as needed.

For example, page-specific JavaScript can be split into separate bundles and loaded on demand.

Common dependencies are split into shared chunks for caching.

In config:

optimization: {

splitChunks: {

chunks: 'all'



This automatically splits code based on reused dependencies.

Code splitting improves performance by only loading necessary code for each page view.

CSS Optimization

For CSS, extract it into dedicated files with the MiniCssExtractPlugin:

const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module: {

rules: [


test: /\.css$/,

use: [MiniCssExtractPlugin.loader, 'css-loader']




plugins: [

new MiniCssExtractPlugin()


This extracts CSS into standalone files.

The optimize-css-assets-webpack-plugin minifies the output:

const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');

optimization: {

minimizer: [new OptimizeCssAssetsPlugin()]


Unused CSS can be removed with purgecss-webpack-plugin.

Together these produce optimized CSS for production.

Following these best practices ensures Craft CMS plugin builds are as lean, efficient and high performance as possible for end users. The optimizations shave kilobytes off JS and CSS files to accelerate load times.

Build Scripts for Craft

NPM Scripts

Defining NPM scripts in package.json provides a straightforward way to automate builds for Craft plugins. Rather than complex shell scripts, tasks like running Webpack can simply be scripts like "build" and "dev". Additional flags pass along options like config files to customize the builds.

Following best practices like modularizing scripts into reusable parts enables assembling more complex workflows from simple building blocks.

Beyond just development and production builds, scripts can integrate other tasks like linting, testing and deployment into the process. With this scripting approach, complicated tooling pipelines are simplified into intuitive commands for managing Craft plugin builds.

Cross-Platform Compatibility

For optimal developer experience, NPM build scripts should work consistently across operating systems like Windows, Linux and macOS. Node conventions like platform-independent path separators, environment variables, and command execution utilities enable writing cross-compatible scripts.

Avoiding shell-specific syntax that might not translate across operating systems is also important. Testing scripts locally on different environments ensures they do not make OS-specific assumptions. When crafted carefully for portability, the automation around Craft plugin builds can remain identical across developer machines

Parameterizing Configs

In addition to scripts themselves, the configurations those scripts rely on can also be parameterized based on the environment.

For example, setting the Webpack mode or enabling sourcemaps can be done by passing options to scripts rather than directly modifying config files. This allows switching between modes like development and production by simply providing different parameters to the same underlying script. Build configurations remain flexible and consistent without needing separate config files to be maintained.

Following these patterns enables Craft plugin developers to have full control over their build environment with minimal overhead.

Versioning and Caching

Version Hashing

Adding unique hash strings to asset filenames enables effective long-term caching by busting cached versions when file contents change.

Hashes are automatically generated based on content by Webpack using the [contenthash] placeholder. This results in bundle names like app.8e0d1fa5.js with unique fingerprints. Now when code is updated and rebuilt, the hash also changes, forcing clients to re-download the new file rather than serving stale cached code.

Webpack manages the hashing seamlessly, enabling assets to be cached indefinitely with no worries.

Extracting Manifests

In addition to adding hashes, Webpack generates manifests that map the hashed filenames back to their original equivalents. This provides a lookup to reference the correct hashed asset from unhashed locations like HTML.

The Webpack ManifestPlugin handles extracting the manifests during the build process to create the mapping between hashed and unhashed file names. Using the manifests ensures hashed assets can be correctly referenced for caching benefits without breaking other code.

Handling Versioning

Some best practices for versioning hashed filenames include retaining the original file extension like .js or .css for easier debugging, using descriptive chunk names like main or vendors, and letting Webpack handle unique [contenthash] fingerprints automatically.

An example set of hashed files would look like main.a871cff2.js, vendors.78563df6.js, styles.bf186d0d.css. This maintains readability while enabling effective caching and asset management. By incorporating content hashing, manifest generation, and thoughtful naming conventions, versioning unlocks the full performance potential of long-term caching for Craft CMS plugins.

Triggering Builds in Craft

Event Hooks

Craft provides event hooks that can automatically trigger builds as plugins are updated. Hooking into events like BEFORE_SAVE_PLUGIN and AFTER_SAVE_PLUGIN allows builds to run at the optimal times right before and after plugin changes are saved.

Other useful hooks are BEFORE_INSTALL_PLUGIN to do an initial build when a plugin is first installed. With this event-driven approach, the build process integrates seamlessly into the plugin development workflow in Craft without requiring any extra steps to manually trigger builds.

Checking File Hashes

Before unnecessarily re-running builds, the current file hashes can be checked against the hashes from the previous build to see if anything actually changed.

By comparing the hash sums of the current build with those stored as config or plugin settings, the system can detect if bundles need to be rebuilt or if sources remain unchanged. This skip build optimization avoids wasted time rebuilding unmodified assets.

Rebuilding on Changes

When file hashes differ, indicating source changes, Webpack's smart incremental rebuilding can be leveraged to only process and output the bundles affected by those changes. For example if CSS assets changed, only the CSS would be rebuilt, while unchanged JavaScript would not be re-processed. After selectively rebuilding only updated bundles, the new file hashes can be stored to become the basis for comparison on the next build.

By combining Craft's powerful event hooks, intelligent change detection, and Webpack's partial rebuilds, the plugin build process can become a seamless automated part of the development workflow, firing instantly on relevant changes without any unnecessary work.

Troubleshooting Build Errors

Fixing Webpack Errors

Webpack can produce cryptic errors that require some decoding to resolve. Here are some common issues and solutions:

Module not found - Check the module is installed and import path is correct. Pay attention to case sensitivity.

Can't resolve 'file' - Double check file extensions and locations match config. Add file extensions to imports.

Broken CSS/images - Confirm loaders are configured correctly for each asset type. Update loader packages if needed.

Multiple versions - A dependency version mismatch. Run npm update and reinstall to sync.

Outdated packages - Update Webpack, Babel and other build packages to latest compatible versions.

Following the error details closely and checking config values can usually uncover the problem cause.

Debugging Babel Issues

For Babel issues:

Syntax not supported - Need to add or update a Babel plugin/preset for that syntax.

Old code generated - Target environments need updating in Babel config to support newer features.

Polyfills missing - Add @babel/polyfill import or use transform-runtime plugin.

Sourcemaps not working - Ensure devtool and sourceMap options are enabled.

Babel errors originate from incorrect configuration for the desired syntax and environment.

Adjusting plugins, presets and targets resolves most problems.

Handling Cross-Platform Differences

If builds work on one OS but fail on another:

  • Standardize path handling with path.join() and path.resolve().

  • Use cross-env for environment variables.

  • Avoid shell-specific commands like && or line continuations.

  • Test locally in different environments to catch differences.

Writing cross-platform code from the start prevents obscure OS-specific failures.

With careful inspection of errors, checking configs, and an awareness of portability, most build issues can be identified and corrected efficiently. The solutions provide a smoother development experience across environments.

Optimizing Build Performance

Code Splitting

Intelligently splitting code into smaller chunks optimizes loading performance compared to large, monolithic bundles. Webpack automatically splits shared dependencies into separate cached chunks via splitChunks configuration. Further manual splitting can happen by designating separate entry points for page-specific bundles, using require.ensure() for on-demand async chunks, and with getComponent() for splitting React components. By granularly splitting code into logical parts needed for each route or page, only the necessary portion needs to be loaded initially while the rest can be lazy loaded later.

Dynamic Importing

In addition to upfront splitting, dynamic import() statements allow asynchronously loading modules at run-time on demand. By inline importing modules only when specifically needed, the corresponding chunk splits out and loads just-in-time. Lazy loading with dynamic imports ensures no unnecessary code is loaded prematurely. This technique provides fine-grained control over splitting points in the code for optimal delivery.

Tree Shaking

Finally, tree shaking analyzes the full dependency graph to detect and eliminate modules that are imported but never actually used. By pruning true dead code that isn't imported elsewhere, tree shaking removes unnecessary bloat and results in leaner final bundles. Since typical applications utilize only a subset of imported libraries, removing unused code has an amplifying impact on overall bundle size and page load performance.

Combining intelligent preload splitting, on-demand dynamic importing, and unused code removal via tree shaking gives fine-grained control over delivering the minimal necessary code for each page. Craft CMS plugins can leverage these techniques to optimize build output and deliver blazing-fast experiences.

Automated Testing for Craft

Configuring Testing Frameworks

Setting up a testing framework is essential for consistently verifying code quality in Craft plugins. Popular options like Jest, Mocha, and PHPUnit can be configured for automation.

For JavaScript tests, Jest provides an excellent combination of features and speed right out of the box. Its snapshot testing quickly validates UI code changes. Mocha offers more customization with different assert styles like Chai.

For PHP, PHPUnit is the standard for unit testing Craft plugin services and classes. It integrates cleanly with Composer for dependency management.

The test runner should output human-readable results indicating passing, failing, and skipped tests. Code coverage analysis is also highly recommended.

With the framework emitting clear test reporting, CI systems can programmatically detect and prevent regressions.

Running Tests During Builds

Once configured, the testing framework can be triggered to run automatically during builds for quality assurance.

For example, add a test NPM script:

"scripts": {

"test": "jest"


Then modify the main build script to also run tests:

"build": "npm test && webpack ..."

Now tests will execute on every build, failing the process if regressions occur.

Similarly for PHP, Composer scripts can integrate PHPUnit testing into the workflow.

Automating tests this way ensures code changes don't introduce bugs.

Code Coverage Metrics

Code coverage analysis provides visibility into untested areas of the codebase during test runs.

Both Jest and PHPUnit can generate coverage reports in formats like JSON, HTML, XML and text.

These identify untested files, functions, lines, and branches in the code.

Using coverage data, developers can strategically add tests for under-tested modules and maximize coverage.

With testing frameworks rigorously inspecting code quality on each build, Craft plugins remain robust and risk-free as they evolve. Automated testing provides confidence for continuous integration and release.

Shape April 2022 HR 202
Andy Golpys
- Author

Andy has scaled multiple businesses and is a big believer in Craft CMS as a tool that benefits both Designer, Developer and Client. 

Show us some love
Email Us
We usually reply within 72 hours
Agency Directory
Submit your agency
Affiliate Partners
Let's chat