wp-build-tools — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited wp-build-tools (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Model note: Config setup and.asset.phpenqueue patterns are mechanical —haikucovers most cases. Debugging webpack entry-point conflicts or reusing a dependency plugin's bundled library may needsonnet.
Configure and operate the JS/CSS build pipeline for WordPress plugins: @wordpress/scripts (webpack-based), Vite alternative, asset manifest handling, and correct enqueuing with the generated .asset.php dependency file.
@wordpress/scripts", "configure webpack for my plugin".Not for: Block registration, block.json structure, or Gutenberg API — use the official wp-block-development skill. PHP-side REST API — use wp-rest-api.
npm install --save-dev @wordpress/scripts`package.json`:
{
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"lint:js": "wp-scripts lint-js",
"lint:css": "wp-scripts lint-style"
}
}Default entry point: src/index.js → build/index.js + build/index.asset.php.
Create webpack.config.js at plugin root to override the default entry:
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
module.exports = {
...defaultConfig,
entry: {
'admin': './src/admin/index.js',
'frontend': './src/frontend/index.js',
'block-editor': './src/blocks/index.js',
'style-admin': './src/admin/admin.scss',
},
};Outputs:
build/
├── admin.js + admin.asset.php
├── frontend.js + frontend.asset.php
├── block-editor.js + block-editor.asset.php
└── style-admin.css (no .asset.php for pure CSS entry)The .asset.php file contains the dependency array and a content hash — always use it.
function my_plugin_enqueue_admin_assets() {
$asset_file = plugin_dir_path( __FILE__ ) . 'build/admin.asset.php';
if ( ! file_exists( $asset_file ) ) return;
$asset = include $asset_file;
wp_enqueue_script(
'my-plugin-admin',
plugin_dir_url( __FILE__ ) . 'build/admin.js',
$asset['dependencies'], // auto-includes wp-element, wp-i18n, etc.
$asset['version'], // content hash — cache busted on change
true // in footer
);
wp_enqueue_style(
'my-plugin-admin-style',
plugin_dir_url( __FILE__ ) . 'build/style-admin.css',
[],
$asset['version']
);
// Pass PHP data to JS
wp_localize_script( 'my-plugin-admin', 'myPluginData', [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'my_plugin_action' ),
'apiUrl' => rest_url( 'my-plugin/v1/' ),
] );
}
add_action( 'admin_enqueue_scripts', 'my_plugin_enqueue_admin_assets' );For block assets registered via block.json — do NOT manually enqueue; WP handles it:
register_block_type( __DIR__ . '/build/my-block' ); // reads block.json automatically@wordpress/scripts supports Sass out of the box (via webpack sass-loader). No extra config needed for .scss files imported in JS:
// src/admin/index.js
import './admin.scss';For standalone .scss entry (CSS-only build):
// webpack.config.js entry
entry: {
'admin-styles': './src/admin/admin.scss',
}Output: build/admin-styles.css (no .asset.php generated for pure CSS entries — hardcode version or use filemtime()).
PostCSS config (postcss.config.js) is picked up automatically if present:
module.exports = {
plugins: {
autoprefixer: {},
'postcss-custom-properties': {},
},
};For non-block plugins where @wordpress/scripts dependency auto-detection isn't needed:
npm install --save-dev vite @vitejs/plugin-legacy`vite.config.js`:
import { defineConfig } from 'vite';
import legacy from '@vitejs/plugin-legacy';
export default defineConfig( {
plugins: [ legacy( { targets: [ 'defaults', 'ie >= 11' ] } ) ],
build: {
outDir: 'build',
rollupOptions: {
input: {
admin: 'src/admin/index.js',
frontend: 'src/frontend/index.js',
},
output: {
entryFileNames: '[name].js',
chunkFileNames: '[name]-[hash].js',
assetFileNames: '[name].[ext]',
},
},
},
} );Caveat: Vite does not generate .asset.php. Manage WP script dependencies manually, or use wp-scripts for anything that imports @wordpress/* packages (they must be externals).
@wordpress/scripts automatically externalises all @wordpress/* imports (they're on the global wp object). If you use a custom webpack config, preserve this:
const defaultConfig = require( '@wordpress/scripts/config/webpack.config' );
// defaultConfig already has the correct externals — spread it, don't replace it
module.exports = { ...defaultConfig, entry: { ... } };Never import { useState } from 'react' in WP code — import from @wordpress/element:
import { useState, useEffect } from '@wordpress/element';.gitignore and production buildsnode_modules/
build/Include build/ in the SVN/release zip but NOT in git. In the release workflow (wp-plugin-release + wp-org-submission), run npm run build before zipping.
CI build step for GitHub Actions:
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run buildWhen a plugin you already hard-depend on (e.g. EDD, WooCommerce) ships a front-end library you need — Tom Select, Select2, Choices, flatpickr — enqueue its copy rather than vendoring a second one. Saves bundle size and a maintenance surface, at the cost of coupling to the host's file paths.
function my_plugin_enqueue_tom_select(): bool {
if ( ! defined( 'EDD_PLUGIN_URL' ) ) {
return false; // dependency not active — caller falls back to native <select>
}
$url = EDD_PLUGIN_URL;
$dir = defined( 'EDD_PLUGIN_DIR' ) ? EDD_PLUGIN_DIR : '';
$js = 'assets/vendor/js/tom-select.complete.min.js';
$css = 'assets/build/css/admin/chosen.min.css'; // host's TS skin lives here
// Guard the paths so a host restructure degrades gracefully, never fatals.
if ( $dir && ( ! file_exists( $dir . $js ) || ! file_exists( $dir . $css ) ) ) {
return false;
}
$ver = defined( 'EDD_VERSION' ) ? EDD_VERSION : MY_PLUGIN_VERSION;
wp_enqueue_script( 'my-plugin-tom-select', $url . $js, [], $ver, true );
wp_enqueue_style( 'my-plugin-tom-select', $url . $css, [], $ver );
return true;
}
// Make your own script depend on it only when present:
$dep = my_plugin_enqueue_tom_select() ? [ 'my-plugin-tom-select' ] : [];
wp_enqueue_script( 'my-plugin-admin', $assets . 'js/admin.js', $dep, MY_PLUGIN_VERSION, true );Rules that make this hold up:
wp_enqueue_script('edd-tom-select')) when the host registers it on all admin pages; if registration is page-scoped or order-dependent, register your own handle pointing at the bundled file (as above) for deterministic loading.if (typeof TomSelect !== 'undefined'); leave the markup a real <select> so it works with the library absent.load callback hitting your wp_ajax_* endpoint and sync any hidden companion field (e.g. a stored label) on change..ts-control, .ts-dropdown, etc.) to your design system. WordPress admin skins carry version-gated, high-specificity selectors — EDD's body[class*="branch-7"] rules (WP 6.7+) out-specify a plain .my-wrap scope — so targeted !important is often required to win, and load your stylesheet after the host's.vendor/When the bug lives in a Composer dependency under vendor/, check two things before editing the vendor file:
git check-ignore vendor/<pkg>/file.php — if it prints the path, git won't track your edit (so it can't reach a PR).grep -rn "composer install" .github/workflows — the WP.org deploy action and most CI regenerate vendor/ from composer.lock, overwriting any hand-edit.If both are true, a vendor edit is futile — it never reaches the shipped zip. Never rely on it. Fix it in tracked consumer code instead (config you pass into the library, a hook/filter, an unhook), or patch the dependency upstream and run composer update so the new version is locked.
Real case: a bundled marketing library phoned home via wp_remote_post(), guarded by '' !== $hash. The hash was supplied from the plugin's own tracked bootstrap, so emptying it there tripped the library's guard and killed the call — surviving the deploy-time composer install that a vendor-file edit would not.
npm ci (not npm install) in CI — respects package-lock.json exactly.@wordpress/scripts pins its webpack/babel versions; don't add conflicting webpack or babel-loader to devDependencies.@wordpress/scripts supports .ts/.tsx out of the box — just rename files and add tsconfig.json.@wordpress/scripts v27+: Node 20.wp-scripts lint-js and wp-scripts lint-style in CI alongside PHPCS (wp-coding-standards) for full code quality coverage.references/block-json-patterns.md — block.json full schema reference: attributes, supports, style variations, view scripts, and interactivity API flagsreferences/enqueue-patterns.md — WordPress asset enqueue patterns with .asset.php dependency management, conditional loading, deferred scripts, and inline datareferences/webpack-config.md — @wordpress/scripts webpack configuration: extending defaults, custom entry points, aliasing, and build output patterns~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.