Skip to content

Instantly share code, notes, and snippets.

@wpscholar
Last active March 14, 2022 10:21
Show Gist options
  • Star 25 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save wpscholar/261141cf7b2bf4efd45cb86ad0a43ff2 to your computer and use it in GitHub Desktop.
Save wpscholar/261141cf7b2bf4efd45cb86ad0a43ff2 to your computer and use it in GitHub Desktop.
Webpack 4 Config for WordPress plugin, theme, and block development
**/*.min.js
**/*.build.js
**/node_modules/**
**/vendor/**
build
coverage
cypress
node_modules
vendor
{
"root": true,
"parser": "babel-eslint",
"extends": [
"wordpress",
"plugin:react/recommended",
"plugin:jsx-a11y/recommended",
"plugin:jest/recommended"
],
"env": {
"browser": false,
"es6": true,
"node": true,
"mocha": true,
"jest/globals": true
},
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"globals": {
"wp": true,
"wpApiSettings": true,
"window": true,
"document": true
},
"plugins": [
"react",
"jsx-a11y",
"jest"
],
"settings": {
"react": {
"pragma": "wp"
}
},
"rules": {
"array-bracket-spacing": [
"error",
"always"
],
"brace-style": [
"error",
"1tbs"
],
"camelcase": [
"error",
{
"properties": "never"
}
],
"comma-dangle": [
"error",
"always-multiline"
],
"comma-spacing": "error",
"comma-style": "error",
"computed-property-spacing": [
"error",
"always"
],
"constructor-super": "error",
"dot-notation": "error",
"eol-last": "error",
"eqeqeq": "error",
"func-call-spacing": "error",
"indent": [
"error",
"tab",
{
"SwitchCase": 1
}
],
"jsx-a11y/label-has-for": [
"error",
{
"required": "id"
}
],
"jsx-a11y/media-has-caption": "off",
"jsx-a11y/no-noninteractive-tabindex": "off",
"jsx-a11y/role-has-required-aria-props": "off",
"jsx-quotes": "error",
"key-spacing": "error",
"keyword-spacing": "error",
"lines-around-comment": "off",
"no-alert": "error",
"no-bitwise": "error",
"no-caller": "error",
"no-console": "error",
"no-const-assign": "error",
"no-debugger": "error",
"no-dupe-args": "error",
"no-dupe-class-members": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-duplicate-imports": "error",
"no-else-return": "error",
"no-eval": "error",
"no-extra-semi": "error",
"no-fallthrough": "error",
"no-lonely-if": "error",
"no-mixed-operators": "error",
"no-mixed-spaces-and-tabs": "error",
"no-multiple-empty-lines": [
"error",
{
"max": 1
}
],
"no-multi-spaces": "error",
"no-multi-str": "off",
"no-negated-in-lhs": "error",
"no-nested-ternary": "error",
"no-redeclare": "error",
"no-restricted-syntax": [
"error",
{
"selector": "ImportDeclaration[source.value=/^@wordpress\\u002F.+\\u002F/]",
"message": "Path access on WordPress dependencies is not allowed."
},
{
"selector": "ImportDeclaration[source.value=/^blocks$/]",
"message": "Use @wordpress/blocks as import path instead."
},
{
"selector": "ImportDeclaration[source.value=/^components$/]",
"message": "Use @wordpress/components as import path instead."
},
{
"selector": "ImportDeclaration[source.value=/^date$/]",
"message": "Use @wordpress/date as import path instead."
},
{
"selector": "ImportDeclaration[source.value=/^editor$/]",
"message": "Use @wordpress/editor as import path instead."
},
{
"selector": "ImportDeclaration[source.value=/^element$/]",
"message": "Use @wordpress/element as import path instead."
},
{
"selector": "ImportDeclaration[source.value=/^i18n$/]",
"message": "Use @wordpress/i18n as import path instead."
},
{
"selector": "ImportDeclaration[source.value=/^data$/]",
"message": "Use @wordpress/data as import path instead."
},
{
"selector": "ImportDeclaration[source.value=/^utils$/]",
"message": "Use @wordpress/utils as import path instead."
},
{
"selector": "CallExpression[callee.name=/^__|_n|_x$/]:not([arguments.0.type=/^Literal|BinaryExpression$/])",
"message": "Translate function arguments must be string literals."
},
{
"selector": "CallExpression[callee.name=/^_n|_x$/]:not([arguments.1.type=/^Literal|BinaryExpression$/])",
"message": "Translate function arguments must be string literals."
},
{
"selector": "CallExpression[callee.name=_nx]:not([arguments.2.type=/^Literal|BinaryExpression$/])",
"message": "Translate function arguments must be string literals."
}
],
"no-shadow": "error",
"no-undef": "error",
"no-undef-init": "error",
"no-unreachable": "error",
"no-unsafe-negation": "error",
"no-unused-expressions": "error",
"no-unused-vars": "error",
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"no-useless-return": "error",
"no-var": "error",
"no-whitespace-before-property": "error",
"object-curly-spacing": [
"error",
"always"
],
"prefer-const": "error",
"quote-props": [
"error",
"as-needed"
],
"react/display-name": "off",
"react/jsx-curly-spacing": [
"error",
{
"when": "never",
"children": true
}
],
"react/jsx-equals-spacing": "error",
"react/jsx-indent": [
"error",
"tab"
],
"react/jsx-indent-props": [
"error",
"tab"
],
"react/jsx-key": "error",
"react/jsx-tag-spacing": "error",
"react/no-children-prop": "off",
"react/no-find-dom-node": "warn",
"react/prop-types": "off",
"semi": "error",
"semi-spacing": "error",
"space-before-blocks": [
"error",
"always"
],
"space-before-function-paren": [
"error",
"never"
],
"space-in-parens": [
"error",
"always"
],
"space-infix-ops": [
"error",
{
"int32Hint": false
}
],
"space-unary-ops": [
"error"
],
"template-curly-spacing": [
"error",
"always"
],
"valid-jsdoc": [
"error",
{
"requireReturn": false
}
],
"valid-typeof": "error",
"yoda": "off"
}
}
{
"name": "my-thing",
"description": "A description of my thing.",
"author": "Micah Wood <micah@wpscholar.com> (http://wpscholar.com)",
"license": "proprietary",
"private": true,
"scripts": {
"build": "cross-env NODE_ENV=production webpack",
"lint": "eslint build/js/**/*",
"start": "webpack --watch",
"test": "echo \"Error: no test specified\" && exit 1"
},
"dependencies": {
"classnames": "^2.2.5"
},
"devDependencies": {
"@babel/core": "^7.1.2",
"@wordpress/babel-plugin-makepot": "^2.1.1",
"@wordpress/babel-preset-default": "^3.0.0",
"@wordpress/browserslist-config": "^2.1.3",
"@wordpress/i18n": "^3.0.0",
"ajv": "^6.5.4",
"autoprefixer": "^9.1.5",
"babel-eslint": "^10.0.1",
"babel-loader": "^8.0.4",
"babel-plugin-transform-class-properties": "^6.24.1",
"cross-env": "^5.0.1",
"css-loader": "^1.0.0",
"eslint": "^5.6.1",
"eslint-config-wordpress": "^2.0.0",
"eslint-plugin-jest": "^21.6.1",
"eslint-plugin-jsx-a11y": "^6.0.3",
"eslint-plugin-react": "^7.5.1",
"eslint-plugin-wordpress": "^0.1.0",
"file-loader": "^2.0.0",
"import-glob": "^1.5.0",
"mini-css-extract-plugin": "^0.4.0",
"node-sass": "^4.9.0",
"node-sass-glob-importer": "^5.1.2",
"postcss-loader": "^3.0.0",
"raw-loader": "^0.5.1",
"sass-loader": "^7.0.1",
"style-loader": "^0.23.0",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.1"
}
}
'use strict';
const autoprefixer = require( 'autoprefixer' );
const fs = require( 'fs' );
const globImporter = require( 'node-sass-glob-importer' );
const browsers = require( '@wordpress/browserslist-config' );
const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' );
const path = require( 'path' );
const webpack = require( 'webpack' );
module.exports = function() {
const mode = process.env.NODE_ENV || 'development';
const extensionPrefix = mode === 'production' ? '.min' : '';
// This is the URL path relative to the root domain.
const publicPath = '/wp-content/mu-plugins/blocks/';
// These are the paths where different types of resources should end up.
const paths = {
css: 'assets/css/',
img: 'assets/img/',
font: 'assets/font/',
js: 'assets/js/',
lang: 'languages/',
};
// The property names will be the file names, the values are the files that should be included.
const entry = {
blocks: [
'./build/scss/blocks.scss',
],
editor: [
'./build/js/editor.js',
'./build/scss/editor.scss',
],
};
const loaders = {
css: {
loader: 'css-loader',
options: {
sourceMap: true,
},
},
postCss: {
loader: 'postcss-loader',
options: {
plugins: [
autoprefixer( {
browsers,
flexbox: 'no-2009',
} ),
],
sourceMap: true,
},
},
sass: {
loader: 'sass-loader',
options: {
importer: globImporter(),
sourceMap: true,
},
},
};
const config = {
mode,
entry,
output: {
path: path.join( __dirname, '/' ),
publicPath,
filename: `${ paths.js }[name]${ extensionPrefix }.js`,
},
externals: {
'@wordpress/a11y': 'wp.a11y',
'@wordpress/components': 'wp.components', // Not really a package.
'@wordpress/blocks': 'wp.blocks', // Not really a package.
'@wordpress/data': 'wp.data', // Not really a package.
'@wordpress/date': 'wp.date', // Not really a package.
'@wordpress/element': 'wp.element', // Not really a package.
'@wordpress/hooks': 'wp.hooks',
'@wordpress/i18n': 'wp.i18n',
'@wordpress/utils': 'wp.utils', // Not really a package
backbone: 'Backbone',
jquery: 'jQuery',
lodash: 'lodash',
moment: 'moment',
react: 'React',
'react-dom': 'ReactDOM',
tinymce: 'tinymce',
},
module: {
rules: [
{
enforce: 'pre',
test: /\.js|.jsx/,
loader: 'import-glob',
exclude: /(node_modules)/,
},
{
test: /\.js|.jsx/,
loader: 'babel-loader',
query: {
presets: [
'@wordpress/default',
],
plugins: [
[
'@wordpress/babel-plugin-makepot',
{
'output': `${ paths.lang }translation.pot`,
}
],
'transform-class-properties',
],
},
exclude: /(node_modules|bower_components)/,
},
{
test: /\.html$/,
loader: 'raw-loader',
exclude: /node_modules/,
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
loaders.css,
loaders.postCss,
],
exclude: /node_modules/,
},
{
test: /\.scss$/,
use: [
MiniCssExtractPlugin.loader,
loaders.css,
loaders.postCss,
loaders.sass,
],
exclude: /node_modules/,
},
{
test: /\.(ttf|eot|svg|woff2?)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: paths.font,
},
},
],
exclude: /(assets)/,
},
],
},
plugins: [
new MiniCssExtractPlugin( {
filename: `${ paths.css }[name]${ extensionPrefix }.css`,
} ),
new webpack.DefinePlugin( {
'process.env.NODE_ENV': JSON.stringify( mode ),
} ),
function() {
// Custom webpack plugin - remove generated JS files that aren't needed
this.hooks.done.tap( 'webpack', function( stats ) {
stats.compilation.chunks.forEach( chunk => {
if ( ! chunk.entryModule._identifier.includes( '.js' ) ) {
chunk.files.forEach( file => {
if ( file.includes( '.js' ) ) {
fs.unlinkSync( path.join( __dirname, `/${ file }` ) );
}
} );
}
} );
} );
},
],
};
if ( mode !== 'production' ) {
config.devtool = 'source-map';
}
return config;
};
@wpscholar
Copy link
Author

wpscholar commented Apr 22, 2018

Had to add this to my SCSS config to get FontAwesome fonts to load correctly: $fa-font-path: "~font-awesome/fonts";

@khoipro
Copy link

khoipro commented Feb 7, 2019

@wpscholar do you have any idea about define wp_enqueue_scripts() between development and production mode?

@wpscholar
Copy link
Author

Sorry, @khoipro, I'm not sure why I wasn't notified of your comment. Thankfully I noticed it. ;)

Pippin does a great job of explaining this in his post here: https://pippinsplugins.com/use-script_debug-enable-non-minified-asset-files/

I've been using this approach for a long time now and it works perfectly. You would just use SCRIPT_DEBUG on your local and not on production. However, you will want to make sure and test with that constant set to false before you deploy just to make sure your production JS is working. If you ever forget to run a prod build, this quick check will save you a lot of frustration.

@jjwilliams42
Copy link

I'm new to WordPress block development (but familiar with React / Webpack), but one thing I don't understand: how do you separate editor.css from style.css loading? Webpack puts them all together in one file. So I can't bundle an editor.css and styles.css separately.

@wpscholar
Copy link
Author

@jackjwilliams A new file will be created for each entry point. You can have as many entry points as you want. See https://gist.github.com/wpscholar/261141cf7b2bf4efd45cb86ad0a43ff2#file-webpack-config-js-L29

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment