WordPress Enqueue Script Example for wp-scripts Builds

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.php returns dependencies and version. They go straight into wp_enqueue_script(), so a new @wordpress/* import adds its handle automatically and every build changes the version.
  • Extra handles you pass in deps are merged in, for libraries the bundler can’t see.
  • Scripts load in the footer by default. Pass strategy set to defer or async to use the WordPress 6.3+ loading strategies. Any other value is ignored.
  • wp_set_script_translations() only runs when the bundle really depends on wp-i18n and you passed a text domain.
  • Both CSS outputs are enqueued if they exist. When wp-scripts also wrote an -rtl.css file, wp_style_add_data( ..., 'rtl', 'replace' ) lets WordPress swap it in on RTL sites.
  • A missing build returns false and fires dpc_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 why make-pot above scans build/ and excludes src/. Scan src/ 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, run npm ci && npm run build before zipping, as in this GitHub Action to create a release.
  • Don’t add wp-element or react to deps by hand: the asset file already lists what the bundle imports, and adding them yourself is how lists go stale.
  • Running wp-scripts start writes 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

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You’ll also get DPlugins product updates. No spam, and you can unsubscribe anytime.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You'll also get DPlugins product updates. No spam, and you can unsubscribe anytime.