Most WordPress enqueue script examples hardcode the dependency list and a version string. That breaks when you build with @wordpress/scripts. Add import { useState } from '@wordpress/element' and the bundle now needs wp-element. Change one line and browsers keep serving the cached file. Forget to run npm run build on a fresh clone and the site loads a 404, or a require of a missing file causes a fatal error.
wp-scripts build already writes the answer next to your bundle: build/index.asset.php, which returns the exact dependencies and a content hash. The helper below reads that file and enqueues the JS with the right dependencies and cache-busting version. It also loads index.css and style-index.css, switches to the -rtl.css files on RTL sites and hooks up JSON translations. If the build folder is missing, it does nothing instead of crashing.
The snippet
Put it in your theme’s functions.php, a small plugin or a Scripts Organizer PHP block. The second function is the call for a theme with a build/ folder.
/**
* Enqueue JS + CSS produced by @wordpress/scripts (wp-scripts build).
*/
function dpc_enqueue_build( string $handle, string $dir, string $url, string $entry = 'index', array $args = array() ): bool {
$dir = untrailingslashit( $dir );
$url = untrailingslashit( $url );
$entry = sanitize_file_name( $entry );
$asset_file = "{$dir}/{$entry}.asset.php";
if ( ! is_readable( $asset_file ) ) {
do_action( 'dpc_build_missing', $handle, $asset_file );
return false;
}
$asset = require $asset_file;
if ( ! is_array( $asset ) ) {
return false;
}
$deps = array_values( array_unique( array_merge(
(array) ( $asset['dependencies'] ?? array() ),
(array) ( $args['deps'] ?? array() )
) ) );
$version = (string) ( $asset['version'] ?? '' );
if ( '' === $version ) {
$version = (string) filemtime( $asset_file );
}
$found = false;
if ( is_readable( "{$dir}/{$entry}.js" ) ) {
$script_args = array( 'in_footer' => (bool) ( $args['in_footer'] ?? true ) );
if ( in_array( $args['strategy'] ?? '', array( 'defer', 'async' ), true ) ) {
$script_args['strategy'] = $args['strategy'];
}
wp_enqueue_script( $handle, "{$url}/{$entry}.js", $deps, $version, $script_args );
if ( ! empty( $args['textdomain'] ) && in_array( 'wp-i18n', $deps, true ) ) {
wp_set_script_translations( $handle, $args['textdomain'], $args['languages_path'] ?? '' );
}
$found = true;
}
// index.css = CSS imported by the entry, style-index.css = style.(s)css files.
$styles = array(
$entry => $handle,
"style-{$entry}" => "{$handle}-style",
);
foreach ( $styles as $file => $style_handle ) {
if ( ! is_readable( "{$dir}/{$file}.css" ) ) {
continue;
}
wp_enqueue_style( $style_handle, "{$url}/{$file}.css", (array) ( $args['style_deps'] ?? array() ), $version );
if ( is_readable( "{$dir}/{$file}-rtl.css" ) ) {
wp_style_add_data( $style_handle, 'rtl', 'replace' );
}
$found = true;
}
return $found;
}
add_action( 'wp_enqueue_scripts', 'dpc_enqueue_theme_build' );
function dpc_enqueue_theme_build(): void {
dpc_enqueue_build(
'dpc-theme',
get_theme_file_path( 'build' ),
get_theme_file_uri( 'build' ),
'index',
array(
'textdomain' => wp_get_theme()->get( 'TextDomain' ),
'languages_path' => get_theme_file_path( 'languages' ),
)
);
}
The build side is the standard setup. Install the package once:
npm install --save-dev @wordpress/scripts
npm run build
Then add the scripts to package.json. The two i18n scripts need WP-CLI:
{
"scripts": {
"start": "wp-scripts start",
"build": "wp-scripts build",
"i18n:pot": "wp i18n make-pot . languages/my-theme.pot --exclude=node_modules,vendor,src",
"i18n:json": "wp i18n make-json languages --no-purge"
}
}
How it works
index.asset.phpreturnsdependenciesandversion. They go straight intowp_enqueue_script(), so a new@wordpress/*import adds its handle automatically and every build changes the version.- Extra handles you pass in
depsare merged in, for libraries the bundler can’t see. - Scripts load in the footer by default. Pass
strategyset todeferorasyncto use the WordPress 6.3+ loading strategies. Any other value is ignored. wp_set_script_translations()only runs when the bundle really depends onwp-i18nand you passed a text domain.- Both CSS outputs are enqueued if they exist. When wp-scripts also wrote an
-rtl.cssfile,wp_style_add_data( ..., 'rtl', 'replace' )lets WordPress swap it in on RTL sites. - A missing build returns
falseand firesdpc_build_missing, so you can log it or show a notice. No fatal error, no 404 in the page source.
Customise it
For an editor-only bundle, add a second entry (for example src/editor.js builds to build/editor.js) and load it on the editor hook:
add_action( 'enqueue_block_editor_assets', 'dpc_enqueue_editor_build' );
function dpc_enqueue_editor_build(): void {
dpc_enqueue_build( 'dpc-editor', get_theme_file_path( 'build' ), get_theme_file_uri( 'build' ), 'editor' );
}
In a plugin, pass plugin_dir_path( __FILE__ ) . 'build' and plugins_url( 'build', __FILE__ ) from your main plugin file instead of the theme paths. If you need several entry points or a custom webpack setup, see WP Scripts Custom Config.
Gotchas
- JSON translation files are named after an md5 of the script’s path relative to the theme or plugin root (
build/index.js). That’s whymake-potabove scansbuild/and excludessrc/. Scansrc/and the hashes won’t match, so no translations load. - Don’t commit
build/to the repo and also ignore it in your release zip. Pick one. If you build in CI, runnpm ci && npm run buildbefore zipping, as in this GitHub Action to create a release. - Don’t add
wp-elementorreacttodepsby hand: the asset file already lists what the bundle imports, and adding them yourself is how lists go stale. - Running
wp-scripts startwrites the same files, so this helper also works in development. The version changes on every rebuild.
Snippet poll. Should this snippet be in the Scripts Organizer library for one-click import? 10 votes put it there.
0 of 10 votes