Compile your assets with Symfony Encore

symfony encore webpack manage assets

These days I’m playing with Symfony Encore, the new tool for managing assets in Symfony 3.3. It is a tool inspired from Laravel Mix which is the asset manager from Laravel Framework, build by Jeffrey Way the owner of Laracasts. At the time Jeff created Mix, I wanted to use it on Symfony projects, so I decided to wrap laravel-mix package into a symfony bundle. So elixir-mix-bundle was created. If you used it already, Symfony Encore will look a lot like it.

A few days ago Ryan Weaver the creator of SymfonyCasts and a Symfony evangelist, announced Symfony Encore a simple API around Webpack for compiling assets and the Assetic replacement. The official documentation could be found on Symfony website which in fact is already using Encore.

How to install Symfony Encore

First you have to install Node.js and also Yarn or NPM. When you’re ready you can install encore by running on your symfony root project:

$ yarn add @symfony/webpack-encore --dev

or using npm

$ npm init
$ npm install @symfony/webpack-encore --save-dev

You will notice that a package.json file was generated and all the dependencies were downloaded into a new directory node_modules. You should version the json file but ignore the node_modules.

How to use Encore

First thing to do is to create a webpack.config.js file at the root of your project. I personally keep all my assets into the app/Resources/public folder so let’s say we have a sass and a js file:

The webpack.config.js file will then look like this:

// webpack.config.js
var Encore = require('@symfony/webpack-encore');

Encore
    // directory where all compiled assets will be stored
    .setOutputPath('web/build/')

    // what's the public path to this directory (relative to your project's document root dir)
    .setPublicPath('/build')

    // empty the outputPath dir before each build
    .cleanupOutputBeforeBuild()

    // will output as web/build/app.js
    .addEntry('app', './app/Resources/public/js/app.js')

    // will output as web/build/global.css
    .addStyleEntry('main', './app/Resources/public/sass/main.scss')

    // allow sass/scss files to be processed
    .enableSassLoader()

    // allow legacy applications to use $/jQuery as a global variable
    .autoProvidejQuery()

    .enableSourceMaps(!Encore.isProduction())

    // create hashed filenames (e.g. app.abc123.css)
    //.enableVersioning()
;

// export the final configuration
module.exports = Encore.getWebpackConfig();

This configuration of Encore is already complex. In order to compile everything up you should execute encore:

# compile assets
$ ./node_modules/.bin/encore dev

# watch when assets change and recompile them automatically
$ ./node_modules/.bin/encore dev --watch

# compile, minify and optimize assets for production use
$ ./node_modules/.bin/encore production

Because sometimes I’m lazy I like to set those commands in the package.json:

{
  "devDependencies": {},
  "scripts": {
    "dev-server": "./node_modules/.bin/encore dev-server",
    "dev": "./node_modules/.bin/encore dev",
    "webpack": "./node_modules/.bin/encore dev --watch",
    "prod": "./node_modules/.bin/encore production"
  },
}

Then you can execute encore like $ npm run dev for development or $ npm run prod for production.

At first, because you are trying to compile sass file it will fail and ask you to install sass-loader and node-sass.

After Encore will compile your files you can find in the web/build/ directory all the generated files. You will have an app.js file, next there will be a global.css file and also you will see the manifest.json which contains the real name for each asset. This is very useful when you’ll use versioning with Encore, will explain more later.

You can now include your new compiled files into the html.

{# layout.html.twig #}
<!DOCTYPE html>
<html>
    <head>
        <!-- ... -->
        <link rel="stylesheet" href="{{ asset('build/global.css') }}">
    </head>
    <body>
        <!-- ... -->
        <script src="{{ asset('build/app.js') }}"></script>
    </body>
</html>

The main Encore methods

Compile Js & Sass

As you’ve already seen, Encore can compile Js and Sass files through .addEntry() and .addStyleEntry() methods. Sass files are compiled and output as CSS in build/css/global.css, because of .enableSassLoader() method which you should have in your webpack.config.js.

You can also compile both js and sass files at the same time with .addEntry() method.

.addEntry('app', [
    './app/Resources/public/js/app.js',
    './app/Resources/public/sass/main.scss'
])

The output will be an build/app.js and an build/app.css file.

Compile Less

You could also compile less files if you’d like. All you have to do is install less-loader and less packages, then enable them in webpack.config.js like so Encore.enableLessLoader().

How to use Source Maps

Source Maps helps you to easily debug your sass, less, typescript files allowing browsers to point to the original file related to some asset. Let’s say you have a main.scss and pages.scss files that you compile into main.css stylesheet. When browser will interpret your main.css file if you’re using Source Maps, it will point to the pages.scss file if you are debugging something from that file not at the generated main.css file.

Encore includes by default source maps only in development mode .enableSourceMaps(!Encore.isProduction()) but you could change this anytime.

Assets Versioning

If you would like to version your assets, encore gives you the best way to do so. Just by calling .enableVersioning() method into your webpack.config.js file, each asset name you are compiling with encore will contain a hash app.abc432.js instead of app.js. Whenever your app.js content will change, encore will generate a new hash on your filename and your browser will need to load your new changes.

But wait, how could you know which hash encore used for your new generated file. Should you change the asset name every time you compile your files? The answer is “No way!”. If you remember a few moments before I said something about a manifest.json file that is generated together with your compiled assets. This file is the magic behind finding your asset real name.

{
  "build/app.css": "/build/app.bf4fd3bb74643601faf6b4f985f9a7d7.js",
  "build/app.js": "/build/global.9bb94772fa673c01ebf7.css"
}

In Symfony this file is used by the json_manifest_file versioning strategy from config.yml.

# app/config/config.yml
framework:
    # ...
    assets:
        # feature is supported in Symfony 3.3 and higher
        json_manifest_path: '%kernel.project_dir%/web/build/manifest.json'

The Twig asset() function

That’s how it works, just be sure to use Twig {{ asset() }} function when you want to load js or css files.

<link href="{{ asset('build/main.css') }}" rel="stylesheet" />

<script src="{{ asset('build/app.js') }}"></script>

Remember that if you’re not using cache busting you don’t have to use asset() function, but as a best practice you always should use it because it will make it a lot easier when you decide to add asset versioning.

Using bootstrap Js & Css

I know many projects are using bootstrap so here’s how it works with Encore. First install bootstrap-sass:

$ npm install bootstrap-sass --dev

Import Bootstrap from sass

After that bootstrap is downloaded into your node_modules directory and you can import it in any of your sass or javascript files.

// app/Resources/public/sass/main.scss
@import '~bootstrap-sass/assets/stylesheets/bootstrap';

After you include bootstrap-sass the Webpack builds might become slower but you can fix that with resolve_url_loader option:

Encore
    .enableSassLoader({
        resolve_url_loader: false
    })

// For Encore v0.15.0 you have to pass as a first parameter a closure that receives an options parameter and 
// as a second parameter the object with `resolveUrlLoader` option:

Encore
    .enableSassLoader(function(options) {
        // options.includePaths = [...]
    }, {
        resolveUrlLoader: false
    })

If you want to learn more about what this is about you can check problems with url()

Import Bootstrap from Js

As you already know bootstrap.js requires jquery so you have to install it also.

$ npm install jquery --dev

You should also include in webpack.config.js the .autoProvidejQuery() method because Bootstrap needs jQuery as a global variable. All this does is expose the $ and jQuery global variables so that Bootstrap could find them.

WebpackConfig.autoProvideVariables({
   $: 'jquery',
   jQuery: 'jquery'
});

Finally you can require bootstrap in your custom js file:

var $ = require('jquery');
require('bootstrap-sass');

(function() {
    $('body').html('Using jQuery');
})();

Extracting your vendors

Sometimes you end up with the scenario in which you want your vendors code saved in a different file then your custom code. For example it’s possible that you’d want that jquery and bootstrap js files saved in a vendors.js because that code is on all your pages and will not change often, but your custom js code saved in the app.js file. For that encore comes with .createSharedEntry() function which does exactly that.

Encore
    // ...
    .addEntry('app', [
        'app/Resources/public/js/custom.js',
        'app/Resources/public/js/anotherCustom.js'
    ])
    .createSharedEntry('vendor', ['jquery', 'bootstrap'])

This configuration will compile and extract in a vendor.js the jquery and bootstrap libraries. This file will need to be included into your html, but beside this another manifest.js file is created and need to be also loaded.

<!-- included in your layout.html.twig -->
<script src="{{ asset('build/manifest.js') }}"></script>
<script src="{{ asset('build/vendor.js') }}"></script>
<script src="{{ asset('build/app.js') }}"></script>

The manifest.js file is used by Webpack in order to know how to load the shared modules.

As I already said, the benefit of going with this strategy is long-term caching, meaning that the browsers will cache your vendors.js because it’s code will not change often, but also have your custom code busting the browser cache when it changes.

Also this approach can be useful when you want to create page-specific css/js.

Encore
    // ...
    .addEntry('app', [
        'app/Resources/public/js/custom-global.js',
        'app/Resources/public/sass/main.scss'
    ])

    .createSharedEntry('vendor', ['jquery', 'bootstrap'])

    .addEntry('checkout', [
        'app/Resources/public/js/checkout-page.js',
        'app/Resources/public/css/checkout-page.scss'
    ])
<!-- included in your layout.html.twig -->
<script src="{{ asset('build/manifest.js') }}"></script>
<script src="{{ asset('build/vendor.js') }}"></script>
<script src="{{ asset('build/app.js') }}"></script>

<!-- include in your checkout file -->
<script src="{{ asset('build/checkout.css') }}"></script>
<script src="{{ asset('build/checkout.js') }}"></script>

In conclusion

Symfony Encore is the new tool that replaces Assetic in Symfony. Of course if you prefer Laravel Mix you could use elixir-mix-bundle, but now that Encore is out there you can make your own mind and use whatever feels great to you. Go build great things.


If you liked this post, you can share it on Twitter. Also you can follow me on Github or endorse me on LinkedIn.