Compiling and Enqueuing Assets in WordPress with @wordpress/scripts
TL;DR:npm install @wordpress/scripts --save-dev, addbuild/watch/watch:hotscripts topackage.json, then auto-register the compiled output in PHP using the.asset.phpfile it generates. No webpack config required.
Dealing with webpack directly is no picnic, and it only gets worse the bigger or more complicated a project gets. Add SCSS compiling, TypeScript, SVG imports in your CSS, or CSS modules, and now you're configuring all of that and managing a pile of npm dependencies to make it work. @wordpress/scripts does all of it for you, with no webpack config needed, unless you want to extend the built-in config later. Here's how.
Install @wordpress/scripts
In your plugin or theme directory:
npm init -y
npm install @wordpress/scripts --save-devConfigure package.json
Add custom scripts:
"scripts": {
"build": "wp-scripts build",
"watch": "wp-scripts start",
"watch:hot": "wp-scripts start --hot"
}Optionally, specify exact JavaScript files to compile, or a custom output path:
"scripts": {
"build": "wp-scripts build assets/src/one.js assets/src/two.js --output-path=assets/dist/",
"watch": "wp-scripts start assets/src/one.js assets/src/two.js --output-path=assets/dist/",
"watch:hot": "wp-scripts start assets/src/one.js assets/src/two.js --output-path=assets/dist/ --hot"
}Script Command When to use it
build wp-scripts build Production build, one-time compile
watch wp-scripts start Development, recompiles on every save
watch:hot wp-scripts start --hot Development, hot module replacement without a full reloadSet up your project structure
assets/
src/
index.js (or index.ts)
style.scss
build/
index.js
index.asset.php
style.cssInside index.js, import styles directly. This lets wp-scripts compile them without any separate configuration:
import './style.scss';Compile your assets
Build for production:
npm run buildFor development, with automatic recompiling:
npm run watchFor hot module replacement:
npm run watch:hotAny of these outputs the following into build/:
build/index.js
build/index.asset.php
build/style.cssRegister and enqueue in PHP
That build/index.asset.php file is the key piece: it lets you auto-register your compiled scripts and styles instead of hand-maintaining version numbers and dependency arrays. Here's a generalized function that handles auto-registration. Customize the my-plugin parts to match your actual plugin or theme slug, so it doesn't collide with anyone else's:
function register_assets() {
$asset_root = plugin_dir_path( __FILE__ ) . 'assets/build/';
$asset_uri = plugin_dir_url( __FILE__ ) . 'assets/build/';
$asset_files = glob( $asset_root . '*.asset.php' );
// Load runtime if present (used by webpack for chunking).
if ( true === is_readable( $asset_root . 'runtime.js' ) ) {
enqueue_script(
'my-plugin/runtime',
$asset_uri . 'runtime.js',
array(),
filemtime( $asset_root . 'runtime.js' )
);
}
foreach ( $asset_files as $file ) {
$script_meta = require $file;
$slug = basename( $file, '.asset.php' );
$handle = "my-plugin/{$slug}";
$js_path = $asset_root . "{$slug}.js";
$js_uri = $asset_uri . "{$slug}.js";
$css_path = $asset_root . "{$slug}.css";
$css_uri = $asset_uri . "{$slug}.css";
if ( true === is_readable( $css_path ) ) {
wp_register_style(
$handle,
$css_uri,
array_filter( $script_meta['dependencies'], 'wp_style_is' ),
$script_meta['version']
);
}
if ( true === is_readable( $js_path ) ) {
wp_register_script(
$handle,
$js_uri,
array_filter( $script_meta['dependencies'], 'wp_script_is' ),
$script_meta['version'],
array( 'in_footer' => true )
);
}
}
}
add_action( 'init', 'register_assets' );Now enqueue the registered scripts wherever you actually need them:
function enqueue_my_scripts() {
wp_enqueue_script( 'my-plugin/index' ); // 'index' comes from index.asset.php.
wp_enqueue_style( 'my-plugin/index' );
}
add_action( 'wp_enqueue_scripts', 'enqueue_my_scripts' );$script_meta['dependencies'] and $script_meta['version'] come straight out of the generated .asset.php file, which wp-scripts keeps in sync with whatever WordPress packages your code actually imports. You never hand-maintain that dependency list yourself.
Bonus: keep your build directory out of version control
Add this to .gitignore so the compiled output doesn't end up in your source repo, unless you specifically want to commit built assets for deployment:
assets/build/Build before deployment, or commit the built assets as part of your deploy process, whichever fits your workflow.
See the @wordpress/scripts package on npm for the full command reference, and WordPress's developer documentation on wp_enqueue_script for the enqueueing functions used above.
Where to go next
This is one piece of bridging modern JavaScript tooling with WordPress. If you're building blocks or larger plugin features on top of this setup, WordPress's Block Editor Handbook is the next logical reference.
FAQ
Do I need to write my own webpack config to use @wordpress/scripts?
No. @wordpress/scripts ships a working webpack configuration out of the box that handles SCSS, TypeScript, and SVG imports in CSS. You only need a custom config if you want to extend or override its defaults.
What's the difference between build, watch, and watch:hot?
build compiles once for production. watch recompiles automatically every time you save a file, useful during development. watch:hot adds hot module replacement, updating the running page without a full reload.
What does the .asset.php file actually do?
It's generated automatically during compilation and contains the script's dependency list and a version string based on the file's contents. The PHP registration function reads it so you never have to hand-maintain dependencies or cache-busting versions yourself.
Should I commit the build folder to git?
Usually not. Add it to .gitignore and either build during deployment or as a separate step in your CI/CD process. Commit built assets only if your deployment process specifically expects them already compiled.
Why does my enqueued script or style not show up?
Check that the handle you're enqueuing matches exactly what was registered, that the file actually exists at the expected path, and that wp_enqueue_script/wp_enqueue_style are called on a hook that runs after registration, like wp_enqueue_scripts.