Placed in the root of the project folder, this file is used to install the required packages and run the build process.
{
"name": "webpack-project",
"version": "1.0.0",
"description": "A project using Webpack to bundle JS and SCSS files in WordPress.",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"server": "npx serve ."
},
"keywords": [ "webpack", "wordpress", "js", "scss" ],
"author": "Author Name",
"license": "MIT",
}
Custom output folders:
"scripts": {
"build": "CSS_PATH=styles JS_PATH=scripts wp-scripts build",
"start": "CSS_PATH=styles JS_PATH=scripts wp-scripts start",
"server": "npx serve ."
}
All-in-one output folder:
"scripts": {
"build": "CSS_PATH=assets JS_PATH=assets wp-scripts build",
"start": "CSS_PATH=assets JS_PATH=assets wp-scripts start",
"server": "npx serve ."
}
Minimal generation, (but potentially huge files if including packages from WordPress):
"scripts": {
"build": "INC_RTL=false INC_MIN=false INC_ASSET_PHP=false wp-scripts build",
"start": "INC_RTL=false INC_MIN=false INC_ASSET_PHP=false wp-scripts start",
"server": "npx serve ."
}
Available environment variables:
Variable | Default value | Description |
---|---|---|
CSS_PATH |
css |
Output folder for CSS files. |
JS_PATH |
js |
Output folder for JS files. |
INC_RTL |
true |
Include RTL CSS files. |
INC_MIN |
true |
Include extra *.min.[js|css] minified CSS and JS files if false , generated *.css and *.js files will be minified. |
INC_ASSET_PHP |
true |
Include asset PHP files. |
Command to install the required modules:
npm install --save-dev \
@wordpress/scripts \
clean-webpack-plugin \
css-minimizer-webpack-plugin \
glob \
prettier \
rtlcss-webpack-plugin \
webpack-cli
Before running npm run [build|start]
:
my-webpack-project/
├── src/
│ ├── first/
│ │ ├── admin/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── index.js
│ │ └── styles.
│ ├── second/
│ │ ├── admin/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── index.js
│ │ └── styles.scss
│ └── third/
│ ├── index-with-only-styles-import.js
│ ├── styles.scss
│ └── extra-styles.scss
├── package.json
└── webpack.config.js
Assuming package.json
uses the default output folders js
and css
.
Assuming INC_RTL
is set to false
.
Expected result after running npm run build
:
my-webpack-project/
├── src/
│ ├── first/
│ │ ├── admin/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── index.js
│ │ └── styles.
│ ├── second/
│ │ ├── admin/
│ │ │ ├── index.js
│ │ │ └── styles.scss
│ │ ├── index.js
│ │ └── styles.scss
│ └── third/
│ ├── index-with-only-styles-import.js
│ ├── styles.scss
│ └── extra-styles.scss
├── js/
│ ├── admin/
│ │ ├── first.js
│ │ ├── first.min.js
│ │ ├── first.asset.php
│ │ ├── second.js
│ │ ├── second.min.js
│ │ └── second.asset.php
│ ├── first.js
│ ├── first.min.js
│ ├── first.asset.php
│ ├── second.js
│ ├── second.min.js
│ └── second.asset.php
├── css/
│ ├── admin/
│ │ ├── first.css
│ │ ├── first.min.css
│ │ ├── second.css
│ │ └── second.min.css
│ ├── first.css
│ ├── first.min.css
│ ├── second.css
│ ├── second.min.css
│ ├── third.css
│ └── third.min.css
├── package.json
└── webpack.config.js
/**
* WordPress Webpack Configuration
*
* Configuration file for Webpack (a tool that bundles code) that processes
* JavaScript and CSS files for a WordPress plugin or theme.
*
* Main features:
* - Automatically detects JavaScript files in the "src" folder
* - Processes them (converts modern JavaScript to browser-compatible code)
* - Creates both regular and minified versions (smaller files for production use)
* - Handles CSS processing, including RTL (right-to-left language) versions
* - Organizes output into a clean, customizable folder structure
*/
// ----- REQUIRED PACKAGES -----
// External tools and libraries needed for this configuration
// Core Node.js modules (built-in with Node.js)
const path = require('path'); // For working with file paths across different operating systems
const fs = require('fs'); // For reading and writing files on the system
const { execSync } = require('child_process'); // For running terminal commands from JavaScript
// External packages (must be installed via npm)
const defaultConfig = require('@wordpress/scripts/config/webpack.config'); // WordPress's default webpack settings
const { CleanWebpackPlugin } = require('clean-webpack-plugin'); // Deletes old files before building new ones
const RtlCssPlugin = require('rtlcss-webpack-plugin'); // Creates right-to-left versions of CSS files for languages like Arabic
const MiniCSSExtractPlugin = require('mini-css-extract-plugin'); // Extracts CSS into separate files
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');// Minifies CSS files by removing unnecessary characters
const TerserPlugin = require('terser-webpack-plugin'); // Minifies JavaScript files
const glob = require('glob'); // Finds files using pattern matching
/**
* Safety Check: Validate File Paths
*
* Ensures paths are safe and valid, preventing security issues
* like directory traversal attacks.
*
* @param {string} inputPath - The path to check for safety/validity
* @param {string} defaultPath - Fallback path to use if the input is invalid
* @return {string} - A cleaned-up, safe path without leading/trailing slashes
*/
const validatePath = (inputPath, defaultPath) => {
// Define a regex for removing leading/trailing slashes
const stripSlashesRegex = /^\/+|\/+$/g;
// Check for potentially unsafe paths:
// - Empty paths
// - Root paths (/)
// - Paths with parent directory references (..)
// - Paths starting with .
if (!inputPath ||
inputPath === '/' ||
inputPath.includes('..') ||
inputPath.startsWith('.')) {
console.warn(`Invalid path provided: "${inputPath}". Using default: "${defaultPath}"`);
return defaultPath.replace(stripSlashesRegex, '');
}
// Clean up the path by removing any leading/trailing slashes
return inputPath.replace(stripSlashesRegex, '');
};
// ----- CONFIGURATION OPTIONS -----
// Settings that control output locations and features
// Set paths for CSS and JavaScript output folders
// Can be overridden using environment variables (CSS_PATH, JS_PATH)
const rawCssPath = process.env.CSS_PATH || 'css';
const rawJsPath = process.env.JS_PATH || 'js';
// Make sure paths are safe and valid
const cssPath = validatePath(rawCssPath, 'css');
const jsPath = validatePath(rawJsPath, 'js');
// Feature flags - control what types of files to generate
// Set these to 'false' in environment variables to disable features
const includeRtl = process.env.INC_RTL !== 'false'; // Create right-to-left CSS versions?
const includeMin = process.env.INC_MIN !== 'false'; // Create minified (smaller) versions?
const includeAssetPhp = process.env.INC_ASSET_PHP !== 'false'; // Create PHP asset files for WordPress?
/**
* Find JavaScript Entry Points
*
* An entry point is where Webpack starts looking for files to bundle.
* Each entry point becomes a separate output file.
*
* This function scans for folders containing index.js files and adds them as entry points.
* For example:
* - src/index.js becomes "main.js"
* - src/admin/index.js becomes "admin.js"
* - src/admin/dashboard/index.js becomes "admin/dashboard.js"
*
* @param {string} dir - The directory to search in
* @param {string} baseDir - Used for tracking nested directories
* @param {Object} entries - Accumulator that collects entry points
* @return {Object} - Object mapping entry names to file paths
*/
const findEntryPoints = (dir, baseDir = '', entries = {}) => {
// Get a list of all files and folders in this directory
const items = fs.readdirSync(dir);
// If an index.js file exists, add it as an entry point
if (items.includes('index.js')) {
const relativePath = baseDir ? baseDir : path.basename(dir);
entries[relativePath] = path.join(dir, 'index.js');
}
// Recursively look inside any subfolders for more entry points
items.forEach(item => {
const itemPath = path.join(dir, item);
if (fs.statSync(itemPath).isDirectory()) {
const nestedBaseDir = baseDir ? path.join(baseDir, item) : item;
findEntryPoints(itemPath, nestedBaseDir, entries);
}
});
return entries;
};
// Set the source directory where JavaScript files live
const srcFolder = path.resolve(__dirname, "src");
// Scan the source folder to find all JavaScript entry points
const entryPoints = findEntryPoints(srcFolder);
// ----- PREPARE ENTRY POINTS -----
// Create separate configurations for regular and minified versions
const normalEntries = {}; // For regular versions (e.g., main.js)
const minifiedEntries = {}; // For minified versions (e.g., main.min.js)
// Process each entry point for webpack
Object.keys(entryPoints).forEach(key => {
// Add the regular version
normalEntries[key] = entryPoints[key];
// Add the minified version (if enabled)
if (includeMin) {
minifiedEntries[`${key}.min`] = entryPoints[key];
}
});
/**
* Helper function to handle nested paths in output filenames
*
* @param {string} baseName - The base name of the entry point
* @param {string} outputPath - The output path (js or css)
* @param {boolean} isMin - Whether this is a minified version
* @return {string} - The formatted output path
*/
const formatOutputPath = (baseName, outputPath, isMin) => {
// Handle nested paths (like "admin/dashboard")
if (baseName.includes('/')) {
// For paths like 'admin/dashboard', output to 'outputPath/dashboard/admin.ext'
const [parentFolder, subfolderName] = baseName.split('/');
return `${outputPath}/${subfolderName}/${parentFolder}${isMin ? '.min' : ''}`;
}
// For top-level entries (like "main"), output to 'outputPath/main.ext'
return `${outputPath}/${baseName}${isMin ? '.min' : ''}`;
};
/**
* Create JavaScript Output Filenames
*
* Determines where each JavaScript file should be saved and its name.
* Handles both regular and minified versions.
*
* For example:
* - An entry point named "main" becomes "js/main.js"
* - An entry point named "admin/dashboard" becomes "js/dashboard/admin.js"
* - Minified versions get ".min" added to their names
*
* @param {Object} pathData - Information about the file being processed
* @return {string} - The output filename with its path
*/
const getFilename = (pathData) => {
const entryName = pathData.chunk.name;
const isMin = entryName.endsWith('.min'); // Is this a minified version?
const baseName = isMin ? entryName.replace('.min', '') : entryName;
return `${formatOutputPath(baseName, jsPath, isMin)}.js`;
};
/**
* Create CSS Output Filenames
*
* Similar to getFilename, but for CSS files.
* Determines where CSS files should be saved.
*
* @param {Object} pathData - Information about the file being processed
* @param {boolean} isMin - Whether this should be a minified version
* @return {string} - The output CSS filename with its path
*/
const getCssFilename = (pathData, isMin) => {
const entryName = pathData.chunk.name;
// Remove .min from the entry name if it's already there
const baseName = entryName.endsWith('.min') ? entryName.replace('.min', '') : entryName;
return `${formatOutputPath(baseName, cssPath, isMin)}.css`;
};
/**
* CSS Beautifier Plugin
*
* This custom plugin makes regular CSS files look nice and readable.
* It runs after webpack finishes and formats CSS files using Prettier.
*/
class CssBeautifierPlugin {
/**
* Set up the plugin with options
*
* @param {Object} options - Configuration options
*/
constructor(options = {}) {
this.options = {
pattern: `${cssPath}/**/*.css`, // Which files to format (all CSS files)
exclude: /\.min\.css$/, // Don't format minified CSS files
...options
};
}
/**
* Apply the plugin to webpack
*
* @param {Object} compiler - The webpack compiler instance
*/
apply(compiler) {
// Register this plugin to run after webpack generates all the files
compiler.hooks.afterEmit.tapAsync('CssBeautifierPlugin', (compilation, callback) => {
try {
// Find all regular CSS files (excluding minified ones)
const cssFiles = glob.sync(this.options.pattern, { cwd: compiler.outputPath })
.filter(file => !this.options.exclude.test(file));
// If no files to beautify, exit early
if (cssFiles.length === 0) {
console.log('No CSS files to beautify');
callback();
return;
}
console.log(`Beautifying ${cssFiles.length} CSS files...`);
// Use the "prettier" tool to format all CSS files
const filePaths = cssFiles.map(file => path.join(compiler.outputPath, file)).join(' ');
execSync(`npx prettier --write ${filePaths}`, { stdio: 'inherit' });
console.log('CSS beautification completed');
callback();
} catch (error) {
console.error('CSS beautification error:', error);
callback();
}
});
}
}
/**
* Empty File Cleanup Plugin
*
* Sometimes webpack generates empty JavaScript files when there's no actual JavaScript code.
* This plugin removes those empty files and their associated files to keep things clean.
*/
class EmptyMinifiedFileCleanupPlugin {
/**
* Set up the plugin with options
*
* @param {Object} options - Configuration options
*/
constructor(options = {}) {
this.options = {
jsPattern: `${jsPath}/**/*.js`, // Look for all JavaScript files
...options
};
}
/**
* Apply the plugin to webpack
*
* @param {Object} compiler - The webpack compiler instance
*/
apply(compiler) {
// Run this plugin after webpack has generated all files
compiler.hooks.afterEmit.tapAsync('EmptyMinifiedFileCleanupPlugin', (compilation, callback) => {
try {
// Find all JavaScript files
const jsFiles = glob.sync(this.options.jsPattern, { cwd: compiler.outputPath });
let removedCount = 0;
// Check each file to see if it's empty (0 bytes)
jsFiles.forEach(filePath => {
const fullPath = path.join(compiler.outputPath, filePath);
const fileStats = fs.statSync(fullPath);
if (fileStats.size === 0) {
// Get the file's name without extensions (e.g., "admin" from "admin.min.js")
const fileName = path.basename(filePath.replace(/\.min$/, ''), '.js');
const fileDir = path.dirname(filePath);
console.log(`Found empty file: ${filePath}`);
// Find all associated files with the same base name
const jsPattern = path.join(fileDir, `${fileName}.*`);
const relatedFiles = glob.sync(jsPattern, { cwd: compiler.outputPath });
console.log(`Removing ${relatedFiles.length} associated files for ${fileName}`);
// Delete all the found files
relatedFiles.forEach(file => {
const fileToRemove = path.join(compiler.outputPath, file);
console.log(`Removing: ${file}`);
fs.unlinkSync(fileToRemove);
removedCount++;
});
}
});
if (removedCount > 0) {
console.log(`Removed ${removedCount} files associated with empty JS files.`);
} else {
console.log('No empty JS files found.');
}
callback();
} catch (error) {
console.error('Error cleaning empty files:', error);
callback();
}
});
}
}
/**
* PHP Asset File Cleaner Plugin
*
* WordPress uses .asset.php files to track dependencies, but duplicates
* for minified files aren't necessary. This plugin removes those unnecessary .min.asset.php files.
*/
class RemoveMinAssetPhpPlugin {
/**
* Set up the plugin with options
*
* @param {Object} options - Configuration options
*/
constructor(options = {}) {
this.options = {
pattern: '**/*.min.asset.php', // Target all .min.asset.php files
...options
};
}
/**
* Apply the plugin to webpack
*
* @param {Object} compiler - The webpack compiler instance
*/
apply(compiler) {
// Run this plugin after webpack has generated all files
compiler.hooks.afterEmit.tapAsync('RemoveMinAssetPhpPlugin', (compilation, callback) => {
try {
// Find all .min.asset.php files
const files = glob.sync(this.options.pattern, { cwd: compiler.outputPath });
let removedCount = 0;
// Delete each found file
files.forEach(file => {
const filePath = path.join(compiler.outputPath, file);
fs.unlinkSync(filePath);
console.log(`Removed unwanted file: ${file}`);
removedCount++;
});
if (removedCount > 0) {
console.log(`Removed ${removedCount} *.min.asset.php files.`);
}
callback();
} catch (error) {
console.error('Error removing *.min.asset.php files:', error);
callback();
}
});
}
}
// ----- MAIN WEBPACK CONFIGURATION -----
// The complete configuration that tells webpack what to do
module.exports = {
// Start with WordPress's default webpack configuration
...defaultConfig,
// Tell webpack which JavaScript files to process (both regular and minified versions)
entry: { ...normalEntries, ...minifiedEntries },
// Configure where and how to save the processed files
output: {
...defaultConfig.output,
path: __dirname, // Save files in the current directory
filename: getFilename, // Use custom function to determine filenames
},
// Define dependencies not included in the bundle
externals: {
// Define jQuery as an external dependency
jquery: 'jQuery',
},
// Configure optimization settings for minification
optimization: {
...defaultConfig.optimization,
minimize: true, // Enable code minification
minimizer: [
// Configure JavaScript minification
new TerserPlugin({
include: (includeMin ? /\.min\.js$/ : /\.js$/), // Which files to minify
terserOptions: {
output: {
comments: /translators:/i, // Keep WordPress translation comments
},
compress: {
passes: 2, // Run compression twice for better results
},
mangle: {
reserved: ['__', '_n', '_nx', '_x'], // Don't change WordPress translation function names
},
},
extractComments: false, // Don't create separate comment files
}),
],
},
// Add plugins that enhance the build process
plugins: [
// Clean up old files before building new ones
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [
`${jsPath}/**/*`, // Delete all JavaScript files in the output folder
`${cssPath}/**/*` // Delete all CSS files in the output folder
],
cleanStaleWebpackAssets: false, // Don't remove files that weren't directly generated
verbose: true // Show what's being deleted
}),
// Include WordPress's default plugins, except the ones being replaced
...defaultConfig.plugins.filter(plugin => (
plugin.constructor.name !== 'CleanWebpackPlugin' &&
plugin.constructor.name !== 'MiniCssExtractPlugin' &&
plugin.constructor.name !== 'RtlCssPlugin' &&
// Remove asset PHP file generation plugin if disabled
(includeAssetPhp || plugin.constructor.name !== 'DependencyExtractionWebpackPlugin')
)),
// Extract CSS into separate files (regular version)
new MiniCSSExtractPlugin({
filename: (pathData) => getCssFilename(pathData, false),
}),
// Add RTL CSS support if enabled
...(includeRtl ? [
// Generate RTL versions of regular CSS
new RtlCssPlugin({
filename: (pathData) => getCssFilename(pathData, false).replace(/\.css$/, '-rtl.css')
}),
// Generate RTL versions of minified CSS (if minification is enabled)
...(includeMin ? [
new RtlCssPlugin({
filename: (pathData) => getCssFilename(pathData, true).replace(/\.min\.css$/, '-rtl.min.css')
})
] : []),
] : []),
// Add minification support if enabled
...(includeMin ? [
// Extract CSS into separate minified files
new MiniCSSExtractPlugin({
filename: (pathData) => getCssFilename(pathData, true),
}),
// Minify CSS files
new CssMinimizerPlugin({
include: /\.min\.css$/, // Only minify files with .min.css extension
minimizerOptions: {
preset: [
'default',
{ discardComments: { removeAll: true } }, // Remove all CSS comments
],
},
}),
// Make regular CSS files pretty and readable
new CssBeautifierPlugin(),
] : []),
// Clean up unwanted PHP asset files if PHP asset generation is enabled
...(includeAssetPhp ? [new RemoveMinAssetPhpPlugin()] : []),
// Clean up empty JavaScript files and their associated files
new EmptyMinifiedFileCleanupPlugin(),
],
};