
05 Nov 2018 Using Webpack to Unleash Your Drupal 8 Project with Modern JavaScript
Our use case
It has been close to 3 years since Drupal 8 was released. With lots of amazing new features released in every 6 months we are getting more and more excited to see how Drupal has evolved and improved. With the release of Drupal 8.4, it is starting to use ES6 in core. However, there is not yet a standard practice to write ES6 in custom modules and themes.
The ideal solution is for compiled JS to still stay in the original module or theme and make our output JS still be compatible with Drupal 8. Drupal is heavily modular, so Javascript files written for a module should ideally be stored with it. This atomic design pattern allows a module to easily be enabled or uninstalled, and makes our code clean. JS files could be also added in a theme level if they are more related to that theme.
In this blog we will share our story on how to configure the project with webpack and other tools to facilitate ES6 in the whole Drupal 8 project including both custom modules and at the theme level.
Our solution
We are assuming you have sound knowledge of Drupal, Linux, Bash and PHP tools like Composer. In this article we will setup a Drupal example website in our local machine with Drupal 8.6.0, Drush 9.4.0.
Drupal site installed on your local machine
Assuming your local site is up and running. You could also check the tip “Install on your machine” in the end of the post.
Setup frontend project
Create .nvmrc file
echo 8.10.0 .nvmrc
Create package.json
npm init
Here is the generated package.json file:
{ "name": "drupal-project", "version": "1.0.0", "description": "[](https://travis-ci.org/drupal-composer/drupal-project)", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" }
Install the dev dependency
npm i -D babel-core babel-eslint babel-loader babel-polyfill @babel/preset-env eslint eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react glob stylelint stylelint-checkstyle-formatter stylelint-config-standard stylelint-no-browser-hacks webpack webpack-cli
Half of the dev dependencies are copied and used in the Drupal core. With the similar settings we could leverage the eslint config files shipped in the Drupal core.
Webpack Settings
Webpack is static bundler tool for modern JavaScript application.
Create the webpack.config.js file
const path = require('path'); const glob = require('glob'); const rootDir = path.resolve(__dirname); // ES6 JS in the custom Drupal modules. const modulePath = path.resolve(__dirname, 'web/modules/custom'); const matchesInModules = glob.sync(`${modulePath}/*/js/*.es6.js`); // ES6 JS in the custom theme. const themePath = path.resolve(__dirname, 'web/themes/custom/mytheme'); const themeSrcPath = path.resolve(__dirname, `${themePath}/src/js`); const themeDistPath = path.resolve(__dirname, `${themePath}/assets/js`); const matchesInTheme = glob.sync(`${themeSrcPath}/*.js`); const entry = {}; const allMatches = matchesInModules.concat(matchesInTheme); allMatches.forEach((match) => { entry[match] = match; }); /** * Gets the file path of the compiled JS in the Drupal module. * * The compiled module JS files stay in the same directory of source js files. * The new path will be generated based on /path/to/module/js/*.es6.js, and will * be /path/to/module/js/*.js. * * @param filename * @returns {string} */ const moduleJsOutputPath = (filePath) => { if (filePath.slice(-7) === '.es6.js') { const fileName = path.basename(filePath); const fileDir = path.dirname(filePath); const relativeDir = path.relative(rootDir, fileDir); // Remove .es6.js file extension. const newFileName = fileName.slice(0, -7); const newFilePath = `${relativeDir}/${newFileName}.js`; return newFilePath; } }; /** * Get the file path of the compiled JS in the Drupal theme. * * The compiled theme JS files stay in the assets/js directory. * * @param filePath * @returns {string} */ const themeJsOutputPath = (filePath) => { const fileName = path.basename(filePath); const relativeDir = path.relative(rootDir, themeDistPath); const newFilePath = `${relativeDir}/${fileName}`; return newFilePath; }; module.exports = { entry: entry, output: { path: rootDir, filename(object) { const filePath = object.chunk.name; let newFilePath; if (filePath.indexOf(themePath) !== -1) { newFilePath = themeJsOutputPath(filePath); } else { newFilePath = moduleJsOutputPath(filePath); } return newFilePath; }, }, devtool: 'hidden-source-map', externals: { jquery: 'jQuery', Drupal: 'Drupal', drupalSettings: 'drupalSettings', }, module: { rules: [ { test: /\.es6\.js/, include: [new RegExp(modulePath)], loader: 'babel-loader', }, { test: /\.js/, include: [new RegExp(themeSrcPath)], loader: 'babel-loader', }, ], }, resolve: { alias: { _shared: path.resolve(themeSrcPath, 'shared') }, } };
Once we configure the webpack, we could add it to the npm task in package.json as below
"scripts": { "build:js": "webpack --config webpack.config.js -p", "build:js-dev": "webpack --config webpack.config.js --mode development --devtool source-map", }
Here is the complete version of the package.json file
{ "name": "drupal-project", "version": "1.0.0", "description": "[](https://travis-ci.org/drupal-composer/drupal-project)", "main": "index.js", "scripts": { "build:js": "webpack --config webpack.config.js -p", "build:js-dev": "webpack --config webpack.config.js --mode development --devtool source-map", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "devDependencies": { "@babel/preset-env": "^7.1.0", "babel-core": "^6.26.3", "babel-eslint": "^9.0.0", "babel-loader": "^8.0.2", "babel-polyfill": "^6.26.0", "eslint": "^5.5.0", "eslint-config-airbnb": "^17.1.0", "eslint-plugin-import": "^2.14.0", "eslint-plugin-jsx-a11y": "^6.1.1", "eslint-plugin-react": "^7.11.1", "glob": "^7.1.3", "stylelint": "^9.5.0", "stylelint-checkstyle-formatter": "^0.1.2", "stylelint-config-standard": "^18.2.0", "stylelint-no-browser-hacks": "^1.2.1", "webpack": "^4.18.0", "webpack-cli": "^3.1.0" } }
Babel
Babel is a JavaScript compiler. It compiles ES6 JavaScript to the old version that is compatible with old browsers. Create a Babel preset file and put which versions of browser your website will support.
Create .babelrc in the project root directory
{ "presets": [ [ "@babel/preset-env", { "targets": "> 0.25%, not dead", "debug": true } ] ] }
ESLint
ESLint is JavaScript linting utility. It is used to check our custom JavaScript files in the custom modules and themes compared to the JavaScript coding standards. Here we just leverage the default eslint settings shipped in Drupal core.
Create .eslintrc.json in the project root directory
{ "extends": "./docroot/core/.eslintrc.json", }
Beyond, you can configure the settings to fit your project unique requirements like including the more global libraries. For example we could include a global library called CookieMonster.
{ "extends": "./docroot/core/.eslintrc.json", "globals": { "CookieMonster": true } }
Once this is set we could use CookieMonster directly without using ‘import CookieMonster from CookieMonster“ in our ES6 scripts.
Create .eslintignore file
echo "webpack.config.js" > .eslintignore
Ignoring the webpack.config.js file and other JavaScript files which you don’t want ESLint to them, as no-ES6 code is used in webpack.config.js and ESLint will make complains about it.
Write ES6 in Drupal
Now we are free to use ES6 in both Drupal custom modules and themes by following a couple of conventions. In the webpack.config.js, we have setup the source and output directory of our ES6 scripts. However they are slightly different in the modules and themes.
The rule of thumb is we want to retain a Drupal native way to interact with JavaScript by using libraries. This means the compiled JS will be output into the original directory next to the source JS. To differentiate the source and output JS file, we will use extension ‘.es6.js’ to the ES6 JS and ‘.js’ to the output one. This idea is borrowed from Drupal core. For example when we create a new JS in the module, the file could be called mymodule/js/mymodule.es6.js and the compiled file will be mymodule/js/mymodule.js
The theme JS convention is slightly different from that JS in the modules because we get more flexibility to write JS in the theme. Instead of using the .es6.js extension, we could use different directories for source and output JS files. In this example, the source directory is configured as mytheme/src/js and the output directory is mytheme/assets/js. This means we could use same file name for both source and output versions. The rationale behind this is that it is more likely to write more complex JS in the theme and we could spit code to separate JS files for better architecture. In the example below we will create a JS in mytheme/src/js/mytheme.js and compiled file will be found in the mytheme/assets/js/mytheme.js.
Alright, it’s demo time. In the following section I will create a custom module and a custom theme in Drupal to demonstrate how to use ES6 script in Drupal. When we enable this module and set the example theme as default, we could see both JS in the module and theme output a hello message in the browser console.
Create a custom theme
In the repo let’s create an example module called “mytheme” in the directory web/themes/custom
mytheme/mytheme.info.yml
name: My example theme type: theme description: This is a sub theme of Bartik core: 8.x base theme: bartik libraries: - mytheme/global-styling
mytheme/mytheme.libraries.yml
global-styling: js: assets/js/mytheme.js: {} dependencies: - core/jquery - core/drupal
mytheme/src/js/mytheme.js
import HelloComponent from '_shared/HelloComponent'; (function ($, Drupal) { Drupal.behaviors.mytheme = { attach(context) { if (context === document) { const component = new HelloComponent('mytheme'); component.sayHello(); } } } }(jQuery, Drupal));
In this example we create a path alias in webpack called ‘_shared’, this makes it easy to import JS in both theme and module scripts.
mytheme/src/js/shared/hello.component.js
export default class HelloComponent { constructor(name) { this.name = name } sayHello() { console.log(`Hello from ${this.name}.`); } }
We could take advantage of import feature in ES6 to build small blocks.
Create a custom module
In the repo I will create a example module called “mymodule” in the directory web/modules/custom
mymodule/mymodule.info.yml
name: mymodule type: module description: My example module core: 8.x
Define the info of this example module
mymodule/mymodule.libraries.yml
example: js: js/mymodule.js: {} dependencies: - core/jquery - core/drupal
Define an example library which will load a compiled mymodule.js file.
mymodule/js/mymodule.es6.js
import HelloComponent from '_shared/HelloComponent'; (function ($, Drupal) { Drupal.behaviors.mymodule = { attach(context) { if (context === document) { const component = new HelloComponent('mymodule'); component.sayHello(); } } } }(jQuery, Drupal));
In this JS file, we import a custom component by using path alias defined in the Webpack and log hello message in the browser.
mymodule/mymodule.module
<?php /** * @file * Load example js in every page. */ /** * Implements hook_page_attachments(). */ function mymodule_page_attachments(array &$attachments) { $attachments['#attached']['library'][] = 'mymodule/example'; }
We load module JS on every page.
Test
Build JS
# Switch to the right node version nvm use # Build JS for the development mode npm run build:js-dev # Build JS for the production mode npm run build:js
After the build task is completed, you should see the compiled JS file in green colour in the terminal.
Enable the custom module
drush pm:enable mymodule –yes
Enable the custom theme and set it as the default theme
drush config:set system.theme default mytheme –yes
Disable JS aggregation for testing
drush config:set system.performance js.preprocess 0 –yes
Open the Drupal website in Chrome with developer console opened, refresh the page Voila, now you see those files are loaded in the browser and working now.
What’s next?
We use this approach in our project and we see benefits when the JS code is growing bigger and bigger. By configuring the Webpack, we could include more ES6 JS in other locations, for example writing CKEditor plugin with ES6 style. Of course, we could also bundle SCSS files by using Webpack. There are more things we could do to modernize the JavaScript framework in Drupal. One thing could be using TypeScript to enforce strong typed programming. Another thing would be exploring how to write JS in a component way with or without using those modern JS frameworks like Angular, React or VueJS. We will try those things and share our experiences in the future.
Summary
In this article, we have setup ESLint, Webpack, babel to support and compile ES6 scripts in both custom module and theme, so ES6 JS source can be compiled to old browser compatible version. Later we create a custom module and theme and show you the examples of how to add ES6 scripts to those places. Once both module and theme have been enabled and used as default, we should see the hello message in the Chrome browser console.
Tips
Install Drupal on your machine
Preparation
Before starting, get the following tools ready on your local machine:
- Composer, install from https://getcomposer.org/download/
- Drush 9 or Drush-launcher: install from http://docs.drush.org/en/master/install/
- MAMP or similar tool (this is used to set up the Drupal local dev website) install from https://www.mamp.info/en/downloads/
Assuming:
- you have created a virtual host in your MAMP
- your MySQL root user is root and password is root
- you have create a database in MySQL database called drupal-project
Use a composer template to create a new Drupal project
composer create-project drupal-composer/drupal-project:8.x-dev . --no-interaction
Once this is completed, we need to sort out the MySQL DB connection. Create a settings.php page by
cd web/sites/default cp default.settings.php settings.php
Open the settings.php file and config the database settings as below
$databases['default']['default'] = array ( 'database' => 'drupal-project', 'username' => 'root', 'password' => 'root', 'prefix' => '', 'host' => '127.0.0.1', 'port' => '3306', 'namespace' => 'Drupal\\Core\\Database\\Driver\\mysql', 'driver' => 'mysql', );
Install the Drupal site
drush site:install
Git ignore
If you also use git in the project, you need add some git ignore rules. As the compiled JS could be built in the CI, we could git ignore them as well as node_modules.
Add the following lines into .gitignore
/node_modules/ /web/modules/custom/*/js/*.js !/web/modules/custom/*/js/*.es6.js /web/theme/custom/mytheme/assets/js
Thank you to all contributors and reviewers to encourage and help me write this blog:D
HJ
Posted at 19:40h, 15 FebruaryNice, interesting post! I am looking for something like this. Great idea to make the custom modules part of the build! Is there a github repo where I can check the full code?
Eric Chen
Posted at 21:02h, 23 FebruaryHi HJ, I have pushed the example code to the github repo here:
https://github.com/cityreader/drupal-webpack-example
Eric Chen
Posted at 21:01h, 23 FebruaryHi HJ, I have pushed the example code to the github repo here:
https://github.com/cityreader/drupal-webpack-example
HJ
Posted at 19:18h, 25 FebruaryThnx! 🙂
Blazej
Posted at 03:48h, 09 AprilNice article, thanks for sharing. You might want to also check out the webpack module (https://www.drupal.org/project/webpack). It’s a standardized solution that can be used to do what’s described here in a generic manner, e.g. multiple contrib modules can share dependencies, etc.
Eric Chen
Posted at 12:00h, 20 MayHi Blazej, thanks. I have checked this webpack module and it looks awesome. I will do some experiments to see how it could fit to our project.
Anders Söderström
Posted at 00:21h, 03 MayHi Eric!
Very nice post and a great guide :). I’m trying to build upon this and add scss compiling. Coming from Gulp I’m quite new to Webpack and can’t get it to work. What I’m after is to compile scss to css using post-css loader and simply putting the compiled css in the same folder as the scss. Do you have any pointers on how I might achieve that?
Thanks again for a great post!
Eric Chen
Posted at 15:11h, 20 MayHI Anders,
Sorry for the late response. If you want to compile SCSS in the Webpack for Drupal project, it could be a little bit tricky. As a general rule to compile SCSS in Webpack, you need import SCSS file in your JavaScript. For example
“`
import ‘./styles/styles.scss’;
“`
So Webpack will resolve the SCSS file with scss-loader and compile scss to css.
Maggie
Posted at 00:28h, 27 JuneHi Eric, many thanks for the great post.
Where do you run npm init, in the \web folder of the Drupal installation or withing the custom module that will contain the es6.js files?
Many thanks,
Maggie
Eric Chen
Posted at 11:27h, 27 JuneI will run npm init in the root of the project repository as I only want to have a node project in this repo and all node packages could be shared across both modules and themes.
Hannes
Posted at 21:28h, 27 JanuaryI think
echo 8.10.0 .nvmrc
should be
echo 8.10.0 > .nvmrc
hkirsman
Posted at 21:28h, 27 JanuaryI think
echo 8.10.0 .nvmrc
should be
echo 8.10.0 > .nvmrc