Webpack integration into Nette projects

about a month ago by Srigi     edit

In the age of single-page-application, developers build webapps with help of frontend frameworks instead of classic templating systems. One of the tools that enable us to build such applications is Webpack. It allows structuring frontend apps in the nice, maintainable way using import/export keywords. Resulting bundles needs to be served using standard HTML document. And Nette is a great framework to create such a document for you.

In this tutorial, we will teach Nette to serve Webpack bundles. We start with a small sandbox project that contains AJAX form for sending letters. But you can of-course extend your existing project too. Our webapp looks like this:

We use Naja library for enabling AJAX in this app. We will not go through the code of the form. We will focus only on the frontend aspect of the app. If you're interested in how letter form is created and “AJAXified”, we have Github repository ready for you.

Naja is loaded in main @layout.latte template:

	{block scripts}
	<script src="{$basePath}/js/lib/Naja.js"></script>
	<script src="{$basePath}/js/app.js"></script>
	{/block}

We can see we are including two javascript files – the library itself and code to initialize it.

We want to also include the famous netteForms.js file to validate our letter form before submitting it. Usually this file is loaded from public server like this – <script src="https://nette.github.io/resources/js/3/netteForms.min.js"></script>.

But let's say we want to serve it together with files already hosted by our webapp:

	{block scripts}
	<script src="{$basePath}/js/lib/Naja.js"></script>
+	<script src="{$basePath}/js/lib/netteForms.js"></script>
	<script src="{$basePath}/js/app.js"></script>
	{/block}

This is only a very small webapp, but you can see we're getting into a point where managing scripts by hand can become uncomfortable. Bigger apps are composed of tens or even hundreds of javascript files. Managing them by hand will quickly become impossible. Let's call Webpack for help…

Building the Webpack bundle

We use Webpack to combine all three files into a single bundle file. Since Webpack is a tool written for Node.js, we must extend our project of its runtime and configuration. The most straightforward way is just install needed Node.js packages in the root of the project:

yarn add --dev cross-env webpack webpack-cli

This command will install Node.js packages with all dependencies into node_modules folder. It will also create package.json and yarn.lock files in the root of the project. We need to tweak package.json – add proper meta data, but mainly scripts section, which enables run Webpack from console:

{
+  "name": "nette-webpack-example",
+  "license": "MIT",
+  "private": true,
+  "scripts": {
+    "build": "cross-env NODE_ENV=production webpack"
+  },
  "devDependencies": {
    "cross-env": "^6.0.3",
    "webpack": "^4.41.5",
    "webpack-cli": "^3.3.10"
  }
}

With Webpack we also need to install our libraries as Node.js packages too:

yarn add naja nette-forms

Now you can/should delete www/js/libs folder completely. Before running Webpack we need to add cofiguration file:

webpack.config.js

const path = require('path');

module.exports = {
  entry: './www/js/app.js',
  mode: (process.env.NODE_ENV === 'production') ? 'production' : 'development',
  resolve: {
    extensions: ['*', '.js', '.jsx']
  },
  output: {
    filename: 'bundle.js',
    path: path.join(__dirname, 'www', 'assets'),
  },
};

We also need to tweak our www/js/app.js file so it properly initializes both libraries:

import naja from 'naja';
import netteForms from 'nette-forms';

window.Nette = netteForms;

document.addEventListener('DOMContentLoaded', naja.initialize.bind(naja));
netteForms.initOnLoad();

You can see how we are importing both libraries via import keyword. This is enabled because app.js will be processed by Webpack. It understands the import keyword and will inline all needed files in the correct order into the final bundle.

Now build bundle by calling Webpack from a console in the root of the project:

yarn build

As we see from the configuration resulting bundle will be placed into www/assets folder. We need to fix the link in @layout.latte:

	{block scripts}
-	<script src="{$basePath}/js/lib/Naja.js"></script>
-	<script src="{$basePath}/js/lib/netteForms.js"></script>
-	<script src="{$basePath}/js/app.js"></script>
+	<script src="{$basePath}/assets/bundle.js"></script>
	{/block}

Application is fully functional, and our javascript sources are managed by Webpack:

Smart development with Oops/WebpackNetteAdapter

As your application will grow you will want Webpack to rebuild your bundle on demand during development. For that purpose, there is a Node.js package called Webpack DevServer It will watch your sources and rebuilds the bundle file for you automatically. Install the Webpack DevServer package:

yarn add --dev webpack-dev-server

Now add a new script entry to the package.json:

  "private": true,
  "scripts": {
+   "start": "webpack-dev-server --config ./webpack.config.js --mode development",
    "build": "cross-env NODE_ENV=production webpack"
  },
  "dependencies": {

and tweak to Webpack's configuration:

devServer: {
  publicPath: '/assets/',
},

You can now start dev-server in console, it will start watching your frontend sources and rebuilds bundle accordingly…

yarn start

Dev-server is not emitting bundle to the filesystem, but instead, it serves it as any other web server (as a javascript file under configured host:port/path).

But remember, we linked our bundle statically in @layout.latte. How does Nette know which bundle to use? The one we build with yarn build which is sitting in www/assets folder on the filesystem or the one provided by dev-server.

To solve such a dilemma a Nette extension was created by Jiří Pudil which will do smart switching between two for you. If dev-server is available, it will instruct Nette to serve bundle provided by Webpack's dev-server. If the dev-server is not reachable, it will failsafe to bundle sitting on the filesystem.

The magic ingredient is the new latte macro that Oops/WebpackNetteAdapter provides for you. Change link to bundle in @layout.latte as shown below:

	{block scripts}
-	<script src="{$basePath}/assets/bundle.js"></script>
+	<script src="{webpack bundle.js}"></script>
	{/block}

Now install the Nette extension:

composer req oops/webpack-nette-adapter

The last thing to do is to add this configuration to your config.neon:

extensions:
    webpack: Oops\WebpackNetteAdapter\DI\WebpackExtension(%debugMode%, %consoleMode%)

webpack:
    build:
        directory: %wwwDir%/assets
        publicPath: assets/
    devServer:
        enabled: %debugMode%
        url: http://localhost:8080/assets

You can observe how the extension is selecting the correct way of linking bundle for example in Chrome dev tools. Also notice that extension adds nice panel into Tracy debug bar:


Webpack is a powerful tool to bundle frontend scripts. With the help of the community, we can unlock the full potential of Webpack and its dev-server in Nette projects.

Leave a comment if you have any questions.