Skip to content

Bundling with wp-scripts – WordPress Plugin Boilerplate

The project uses @wordpress/scripts (wp-scripts) for bundling and compiling frontend assets like JavaScript and CSS/SCSS. This tool simplifies the build process by providing a set of pre-configured webpack settings tailored for WordPress development.

CommandDescription
npm run startWatch mode for development. Compiles assets on change.
npm run buildProduction build. Compiles and minifies assets.
npm run lint:jsLint JavaScript files in resources/
npm run lint:styleLint CSS/SCSS files in resources/
npm run lint:fix:jsLint and auto-fix JavaScript files
npm run lint:fix:styleLint and auto-fix CSS/SCSS files
npm run formatFormat JavaScript files using Prettier
npm run packages-updateUpdate WordPress packages to latest versions

start and build pass --experimental-modules, which is required for the Interactivity API. With this flag, wp-scripts emits two webpack configs — the classic script build plus an ESM module build that compiles block viewScriptModule (view.js) modules. Without it, those modules are never built and interactive blocks silently ship without their frontend behavior.

The extendConfig helper in webpack.config.js augments only the classic script build (jQuery shim + custom entries) and passes the ESM module config through untouched, so the module build stays clean. The flag is harmless when the plugin has no interactive blocks.

The resources/ folder contains source assets and is excluded from distribution. The build/ folder contains compiled assets.

  • Directoryresources
    • Directoryadmin
      • Directoryjs
        • app.js
      • Directoryscss
        • app.scss
      • Directoryimages/
        • …
    • Directoryfrontend
      • Directoryjs
        • app.js
      • Directoryscss
        • app.scss
      • Directoryimages/
        • …
  • Directorybuild
    • demo-plugin-admin.js
    • demo-plugin-admin.css
    • demo-plugin-frontend.js
    • demo-plugin-frontend.css

Entrypoints define what files webpack compiles. Custom entries are defined in the customEntries object in webpack.config.js and merged with any auto-discovered block entries from @wordpress/scripts:

webpack.config.js
const customEntries = {
'demo-plugin-frontend': [
path.resolve(__dirname, 'resources/frontend/js/app.js'),
path.resolve(__dirname, 'resources/frontend/scss/app.scss'),
],
'demo-plugin-admin': [
path.resolve(__dirname, 'resources/admin/js/app.js'),
path.resolve(__dirname, 'resources/admin/scss/app.scss'),
],
};

Each entrypoint bundles its JS and SCSS into the build/ directory:

  • build/demo-plugin-frontend.js + build/demo-plugin-frontend.css
  • build/demo-plugin-admin.js + build/demo-plugin-admin.css

To add a new entrypoint, add it to the customEntries object in webpack.config.js:

webpack.config.js
const customEntries = {
// ... existing entries
'my-new-feature': [
path.resolve(__dirname, 'resources/frontend/js/my-feature.js'),
path.resolve(__dirname, 'resources/frontend/scss/my-feature.scss'),
],
};

This creates build/my-new-feature.js and build/my-new-feature.css.

Assets referenced via url() in SCSS files are automatically processed and copied to the build/ directory. For example:

.hero {
background-image: url('../images/hero.jpg');
}

Webpack resolves the path, copies the image to build/, and updates the URL in the compiled CSS. This is the default behavior and requires no additional configuration.

For files that need to be copied as-is without being referenced in SCSS or JS (e.g., fonts loaded via @font-face with external URLs, pre-compiled libraries), you can optionally add the CopyPlugin:

Terminal
npm install copy-webpack-plugin --save-dev

Then add it to the plugins array inside the extendConfig function in webpack.config.js:

webpack.config.js
const CopyPlugin = require('copy-webpack-plugin');
const extendConfig = (config) => ({
...config,
plugins: [
...config.plugins,
// ... existing plugins
new CopyPlugin({
patterns: [
{ from: 'resources/static', to: 'static', noErrorOnMissing: true },
],
}),
],
// ...
});

This copies files from resources/static/ to build/static/ without processing. Note: This plugin is not included by default and must be installed via npm install copy-webpack-plugin --save-dev.

The main plugin class provides enqueue_entrypoint() for loading bundled assets. It handles both JS and CSS from a single call and can be used in any context.

src/Demo_Plugin.php
private function enqueue_entrypoint( string $entry, array $localize_data = array() ): void

The plugin includes two preset entrypoints for admin and frontend. You can use enqueue_entrypoint() anywhere you need to load assets:

// Enqueue on a specific admin page
add_action('admin_enqueue_scripts', function($hook) {
if ($hook !== 'settings_page_my-plugin') {
return;
}
$this->enqueue_entrypoint('demo-plugin-admin');
});
// Enqueue for a shortcode
public function render_shortcode(): string {
$this->enqueue_entrypoint('demo-plugin-frontend');
return '<div class="my-shortcode">...</div>';
}

Pass PHP data to JavaScript using the second parameter:

$this->enqueue_entrypoint('demo-plugin-frontend', [
'ajax_url' => admin_url('admin-ajax.php'),
'nonce' => wp_create_nonce('demo_plugin_nonce'),
'user_id' => get_current_user_id(),
]);

The data is available in JavaScript as a global object named after the plugin (hyphens become underscores):

console.log(demo_plugin.ajax_url);
console.log(demo_plugin.nonce);

The method automatically:

  • Loads the .asset.php file generated by wp-scripts
  • Resolves dependencies (e.g., wp-element, wp-i18n)
  • Uses proper versioning for cache busting
  • Enqueues both JS and CSS if they exist
  • Applies wp_localize_script() when data is provided

jQuery is configured with global aliases ($ and jQuery) via webpack.ProvidePlugin in the extendConfig function in webpack.config.js:

webpack.config.js
plugins: [
...config.plugins,
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery',
}),
],

This allows you to use jQuery in your JavaScript without importing it:

app.js
// Both work out of the box
jQuery(document).ready(function() {
console.log('jQuery ready');
});
// Or using the $ alias
$(document).ready(function() {
console.log('$ ready');
});

jQuery is externalized by wp-scripts and loaded from WordPress core. The .asset.php file automatically includes jquery as a dependency when detected in your code.

Block assets are handled automatically. See Create Blocks for details on block development and asset management.

  • PHP67%
  • JavaScript33%