Webpacking our First Assets

So, Webpack only needs to know three things. The first - setOutputPath() - tells it where to put the final, built files and the second - setPublicPath() - tells it the public path to this directory:

... lines 1 - 2
Encore
// directory where compiled assets will be stored
.setOutputPath('public/build/')
// public path used by the web server to access the output path
.setPublicPath('/build')
... lines 8 - 65
;
... lines 67 - 68

The Entry File

The third important piece, and where everything truly starts, is addEntry():

... lines 1 - 2
Encore
... lines 4 - 10
/*
* ENTRY CONFIG
*
* Add 1 entry for each "page" of your app
* (including one that's included on every page - e.g. "app")
*
* Each entry will result in one JavaScript file (e.g. app.js)
* and one CSS file (e.g. app.css) if you JavaScript imports CSS.
*/
.addEntry('app', './assets/js/app.js')
//.addEntry('page1', './assets/js/page1.js')
//.addEntry('page2', './assets/js/page2.js')
... lines 23 - 65
;
... lines 67 - 68

Here's the idea: we point Webpack at just one JavaScript file - assets/js/app.js. Then, it parses through all the import statements it finds, puts all the code together, and outputs one file in public/build called app.js. The first argument - app - is the entry's name, which can be anything, but it controls the final filename: app becomes public/build/app.js.

And the recipe gave us a few files to start. Open up assets/js/app.js:

15 lines assets/js/app.js
/*
* Welcome to your app's main JavaScript file!
*
* We recommend including the built version of this JavaScript file
* (and its CSS file) in your base layout (base.html.twig).
*/
// any CSS you require will output into a single css file (app.css in this case)
require('../css/app.css');
// Need jQuery? Install it with "yarn add jquery", then uncomment to require it.
// const $ = require('jquery');
console.log('Hello Webpack Encore! Edit me in assets/js/app.js');

This is the file that Webpack will start reading. There's not much here yet - a console.log() and... woh! There is one cool thing: a require() call to a CSS file! We'll talk more about this later, but in the same way that you can import other JavaScript files, you can import CSS too! And, by the way, this require() function and the import statement we saw earlier on Webpack's docs, do basically the same thing. More on that soon.

To make the CSS a bit more obvious, open app.css and change the background to lightblue and add an !important so it will override my normal background:

body {
background-color: lightblue !important;
}

disableSingleRuntimeChunk()

Before we execute Encore, back in webpack.config.js, we need to make one other small tweak. Find the enableSingleRuntimeChunk() line, comment it out, and put disableSingleRuntimeChunk() instead:

... lines 1 - 2
Encore
... lines 4 - 26
// will require an extra script tag for runtime.js
// but, you probably want this, unless you're building a single-page app
//.enableSingleRuntimeChunk()
.disableSingleRuntimeChunk()
... lines 31 - 66
;
... lines 68 - 69

Don't worry about this yet - we'll see exactly what it does later.

Running Encore

Ok! We've told Webpack where to put the built files and which one file to start parsing. Let's do this! Find your terminal and run the Encore executable with:

./node_modules/.bin/encore dev

Tip

For Windows, your command may need to be node_modules\bin\encore.cmd dev

Because we want a development build. And... hey! A nice little notification that it worked!

And... interesting - it built two files: app.js and app.css. You can see them inside the public/build directory. The app.js file... well... basically just contains the code from the assets/js/app.js file because... that file didn't import any other JavaScript files. We'll change that soon. But our app entry file did require a CSS file:

15 lines assets/js/app.js
... lines 1 - 7
// any CSS you require will output into a single css file (app.css in this case)
require('../css/app.css');
... lines 10 - 15

And yea, Webpack understands this!

Here's the full flow. First, Webpack looks at assets/js/app.js. It then looks for all the import and require() statements. Each time we import a JavaScript file, it puts those contents into the final, built app.js file. And each time we import a CSS file, it puts those contents into the final, built app.css file.

Oh, and the final filename - app.css? It's app.css because our entry is called app. If we changed this to appfoo.css, renamed the file, then ran Encore again, it would still build app.js and app.css files thanks to the first argument to addEntry():

... lines 1 - 2
Encore
... lines 4 - 19
.addEntry('app', './assets/js/app.js')
... lines 21 - 66
;
... lines 68 - 69

What this means is... we know have one JavaScript file that contains all the code we need and one CSS file that contains all the CSS! All we need to do is add them to our page!

Open up templates/base.html.twig. Let's keep the existing stylesheets for now and add a new one: <link rel="stylesheet" href=""> the asset() function and the public path: build/app.css:

<!doctype html>
<html lang="en">
<head>
... lines 5 - 8
{% block stylesheets %}
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
... lines 11 - 14
{% endblock %}
</head>
... lines 17 - 107
</html>

At the bottom, add the script tag with src="{{ asset('build/app.js') }}". Oh, make that app.js:

<!doctype html>
<html lang="en">
... lines 3 - 17
<body>
... lines 19 - 90
{% block javascripts %}
<script src="{{ asset('build/app.js') }}"></script>
... lines 93 - 105
{% endblock %}
</body>
</html>

If you're not familiar with the asset() function, it's not doing anything important for us. Because the build/ directory is our document root, we're literally pointing to the public path.

Let's try it! Move over, refresh and... hello, weird blue background. And in the console... yes! There's the log!

We've only started to scratch the surface of the possibilities of Webpack. So if you're still wondering: "why is going through this build process so useful?". Stay tuned. Because next, we're going to talk about the require() and import statements and start organizing our code.

Leave a comment!

  • 2019-05-16 Diego Aguiar

    Hmm, that's weird, I don't get it why you are getting problems with Browserslist if you didn't configure it. What's your code in `app.js`?

  • 2019-05-16 Xav

    Of course, thank you Diego.
    webpack.config.js is still the default one (I haven't touched it yet)


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


    Encore
    // directory where compiled assets will be stored
    .setOutputPath('public/build/')
    // public path used by the web server to access the output path
    .setPublicPath('/build')
    // only needed for CDN's or sub-directory deploy
    //.setManifestKeyPrefix('build/')
    /*
    * ENTRY CONFIG
    *
    * Add 1 entry for each "page" of your app
    * (including one that's included on every page - e.g. "app")
    *
    * Each entry will result in one JavaScript file (e.g. app.js)
    * and one CSS file (e.g. app.css) if you JavaScript imports CSS.
    */
    .addEntry('app', './assets/js/app.js')
    //.addEntry('page1', './assets/js/page1.js')
    //.addEntry('page2', './assets/js/page2.js')


    // When enabled, Webpack "splits" your files into smaller pieces for greater optimization.
    .splitEntryChunks()


    // will require an extra script tag for runtime.js
    // but, you probably want this, unless you're building a single-page app
    .enableSingleRuntimeChunk()


    /*
    * FEATURE CONFIG
    *
    * Enable & configure other features below. For a full
    * list of features, see:
    * https://symfony.com/doc/current/frontend.html#adding-more-features
    */
    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    // enables hashed filenames (e.g. app.abc123.css)
    .enableVersioning(Encore.isProduction())


    // enables @babel/preset-env polyfills
    .configureBabel(() => {}, {
    useBuiltIns: 'usage',
    corejs: 3
    })


    // enables Sass/SCSS support
    //.enableSassLoader()


    // uncomment if you use TypeScript
    //.enableTypeScriptLoader()


    // uncomment to get integrity="..." attributes on your script & link tags
    // requires WebpackEncoreBundle 1.4 or higher
    //.enableIntegrityHashes()


    // uncomment if you're having problems with a jQuery plugin
    //.autoProvidejQuery()


    // uncomment if you use API Platform Admin (composer req api-admin)
    //.enableReactPreset()
    //.addEntry('admin', './assets/js/admin.js')
    ;


    module.exports = Encore.getWebpackConfig();

    And my package.json :

    {
    "devDependencies": {
    "@symfony/webpack-encore": "^0.27.0",
    "core-js": "^3.0.0",
    "webpack-notifier": "^1.6.0"
    },
    "license": "UNLICENSED",
    "private": true,
    "scripts": {
    "dev-server": "encore dev-server",
    "dev": "encore dev",
    "watch": "encore dev --watch",
    "build": "encore production --progress"
    }
    }

    Basically, it's still the default configuration. I just started a new S4 project to test webpack !

  • 2019-05-15 Diego Aguiar

    Hey Xav looks like your babel config is wrong. Can you show me your webpack.config.js & package.json file? Thanks!

  • 2019-05-15 Xav

    Hi !

    I'm very sorry, but I wanted to try that webpack encore, so i created a brand new S4 project on local. I just installed what I really needed (annotations, twig, toolbar and debug) then I installed Encore. Nothing else I swear ! So I watched first the entire course, then I did exactly what was written in the first chapter, typed "node_modules\.bin\encore.cmd dev" (by the way it misses the dot for the windows line) - I was pretty excited at that time - aaaaaand...Error : Module Build Failed (from ;/node_modules/babel-loader/lib/index.js): BrowserslistError: [BABEL] C:\wamp64\www\la_marine\assets\js\app.js: Unknown browser query `basedir=$(dirname "$(echo "$0" | sed -e 's`. Maybe you are using old Browserslist or made typo in query...
    I have no idea what I did wrong. What did I miss ?
    Thank you ! (as usual, awesome course)

  • 2019-05-15 Victor Bocharsky

    Ah, ok, that's great! Glad you fixed it :)

    Cheers!

  • 2019-05-15 Victor Bocharsky

    Hey Jennifer,

    OK, what version of Symfony you're on? And what exactly version of WebpackEncoreBundle is installed? You can check it with Composer:
    $ composer info symfony/webpack-encore-bundle

    Actually, "Symfony\WebpackEncoreBundle\WebpackEncoreBundle" is the correct namespace. Did you install symfony/webpack-encore-bundle globally, i.e. is it in "require" or in "require-dev" section of your composer.json? Hm, let's try to remove and install it again:
    $ composer remove symfony/webpack-encore-bundle
    $ composer require symfony/webpack-encore-bundle

    Do you still see the same error?

    Cheers!

  • 2019-05-14 Jennifer

    I fixed it. I'm running my application in Docker and I'm new to Docker so I didn't realize I needed to run a build and then restart. I really appreciate your help with this issue.

  • 2019-05-14 Jennifer

    Hi Victor,

    I'm trying to add it to an existing project at work. I'm getting that error on any template I try to load. Here's the full message I'm seeing:


    ClassNotFoundException
    Attempted to load class "WebpackEncoreBundle" from namespace "Symfony\WebpackEncoreBundle".
    Did you forget a "use" statement for another namespace?

    in Kernel.php line 33
    at Kernel->registerBundles()
    in Kernel.php line 424
    at Kernel->initializeBundles()
    in Kernel.php line 130
    at Kernel->boot()
    in Kernel.php line 193
    at Kernel->handle(object(Request))
    in index.php line 37

  • 2019-05-14 Victor Bocharsky

    Hey Jennifer,

    Hm, looks good! Then, what page are you trying to access? Did you download course code or did you create an empty Symfony 4.2 project from scratch?

    Cheers!

  • 2019-05-13 Jennifer

    Hi Victor Bocharsky ,

    Thanks for your help with this! I checked and that line exists in config/bundles.php. I was able to clear the cache successfully but I'm still seeing the same error message. I'm using version 4.2 of Symfony. Here's what I see when I run "composer require encore":

    Using version ^1.5 for symfony/webpack-encore-bundle
    ./composer.json has been updated
    Loading composer repositories with package information
    Updating dependencies (including require-dev)
    Restricting packages listed in "symfony/symfony" to "4.2.*"
    Nothing to install or update
    Generating autoload files
    ocramius/package-versions: Generating version class...
    ocramius/package-versions: ...done generating version class
    Executing script cache:clear [OK]
    Executing script assets:install public [OK]
    Executing script security-checker security:check [OK]

  • 2019-05-13 Victor Bocharsky

    Hey Jennifer,

    Please, make sure you have the exact line in your config/bundles.php file:

    return [
    // ...
    Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
    ];

    Also, make sure your cache is cleared by running in the terminal:


    $ bin/console cache:clear

    Btw, what Symfony version do you use? Is "composer require encore" command executed without any errors for you?

    Cheers!

  • 2019-05-12 Jennifer

    Hi weaverryan,

    I'm able to follow along and install everything up to this point but when I load my app I get the following Symfony error:

    "ClassNotFoundException
    Attempted to load class "WebpackEncoreBundle" from namespace "Symfony\WebpackEncoreBundle".
    Did you forget a "use" statement for another namespace?"

    I'm new to Symfony, and after searching multiple times for a solution to this error I can't seem to find one. Do you have any idea what could be causing this?

    Thanks!

  • 2019-05-07 weaverryan

    Hey Marcel dos Santos!

    Hmm, nice questions! Let's look at each of them:

    > Why you decided to choose `build` directory as the output for the built files? It seems a bit weird and not common as `public/css` and `public/js` or something like that.

    The main reason is that Webpack likes to have a single "build" directory that *everything* goes inside of. In order to have directories like public/css and public/js, we would have needed to set public as the build directory. That's not a great choice because you want to have your build directory a bit isolated, for a few reasons. First, it's nice to be able to just "clean out" the entire build directory (without accidentally deleting other files) - there's an option in Encore to do that. Second, for deploying, you only need to upload the "built" files from Encore. So having them in one, isolated directory (with no other files), makes that easier. So, it's mostly because of Webpack's requirement that there be one build directory... and we want it to only contain Webpack stuff.

    > Other thing that seems odd to me is require CSS dependencies inside my JavaScript code.

    Yea, this one *can* feel weird. The important thing to realize is that Webpack was created by people who primarily build single-page applications. And if your JavaScript is what is building your HTML... then it really makes *perfect* sense for each JavaScript file to require the CSS that it needs to style itself. If you're not building an SPA, it can sometimes feel weird.

    > 'm used to have two different entries: one for my CSS file (compiled from a Sass source code) and another for JS file (compiled from my JavaScript source code). In fact, I'm used to split my JavaScript output in two different files: one for vendor (with jQuery, Parsley and other libraries) that almost don't change and can be cached for a long time and another for app (with my own code) that changes all the time.

    Ok, so let's talk about this... because it sounds like you're already doing what Webpack wants you to do ;). Creating 1 CSS entry and 1 JS entry is really identical to creating 1 entry and then requiring your main SASS file from the top of that 1 .js file. The result in both cases is exactly the same: 1 final built CSS file and 1 final JS file. So, that's just my way of saying that if you do it your way... you're really already doing it the Webpack way.

    Second, you're correct to split your vendor libraries from your app libraries. However... with the new splitEntryChunks() feature, Webpack does that automatically. Check out that chapter if you haven't already - it's craziness.

    Let me know if you still have some doubts :).

    Cheers!

  • 2019-05-06 weaverryan

    Nice find Bertin van den Ham! A very annoying issue :)

  • 2019-05-05 Bertin van den Ham

    Hi @weaverryan,

    In your video i see autocompletion on file webpack.config.js. I'm using Phpstorm as well, but there is no autocompletion, does i needs to configure this?

    In PHPstorm i see the following message:
    Can't analyse webpack.config.js: coding assistance will ignore module resolution rules in this file.
    Possible reasons: this file is not a valid webpack configuration file or its format is not currently supported by the IDE. Error details: Cannot find module '@symfony/webpack-encore'

    #UPDATE
    Fixed it, i was trying to run node and yarn through vagrant but when i run it localy all files are generated correctly.

  • 2019-05-03 Marcel dos Santos

    Hi @weaverryan!

    Why you decided to choose `build` directory as the output for the built files? It seems a bit weird and not common as `public/css` and `public/js` or something like that.

    Other thing that seems odd to me is require CSS dependencies inside my JavaScript code. I'm used to have two different entries: one for my CSS file (compiled from a Sass source code) and another for JS file (compiled from my JavaScript source code). In fact, I'm used to split my JavaScript output in two different files: one for vendor (with jQuery, Parsley and other libraries) that almost don't change and can be cached for a long time and another for app (with my own code) that changes all the time.

    []s