Handling Images with the CopyPlugin

Bonus! A really cool side-effect of using Webpack is that none of these files in the assets/ directory need to be public anymore! I mean they live in the public directory currently... but the user never needs to access them directly: Webpack processes and moves them into build/.

To celebrate, let's move assets/ out of the public/ directory and into the root of our project. We don't need to do this... but if something doesn't need to be publicly accessible, why make it public?

This change breaks almost nothing. The only things we need to update are the paths in webpack.config.js:

... lines 1 - 2
Encore
... lines 4 - 9
.addEntry('rep_log', './assets/js/rep_log.js')
.addEntry('login', './assets/js/login.js')
.addEntry('layout', './assets/js/layout.js')
... lines 13 - 16
;
... lines 18 - 21

After making that change, restart Encore!

yarn run encore dev --watch

And... refresh! Woohoo! Wait... there's a missing image! Bah! I was lying! There is one file that still needs to be publicly accessible!

Open index.html.twig... ah! We have a good, old-fashioned img tag that references one of the images in the assets/ directory:

... lines 1 - 2
{% block body %}
<div class="row">
... lines 5 - 34
<div class="col-md-5">
<div class="leaderboard">
<h2 class="text-center">
<img class="dumbbell" src="{{ asset('assets/images/dumbbell.png') }}" />
... line 39
</h2>
... lines 41 - 42
</div>
</div>
</div>
{% endblock %}
... lines 47 - 59

And... whoops! It's not public anymore. My bad!

This is one of the few cases - maybe the only case - where we need to reference public images from outside a file that Webpack processes. The simple problem is that Webpack doesn't know that it needs to move this file!

Of course, there's an easy fix: we could just move this one file back into the public/ directory. But... that sucks: I'd rather keep all of my assets in one place.

Installing copy-webpack-plugin

To do this, we can take advantage of a Webpack plugin that can copy the file for us. Google for copy-webpack-plugin to find its GitHub page. Encore gives you a lot of features... but it doesn't give you everything. But... no worries! We're using Webpack under-the-hood. So if you find a Webpack plugin you want, you can totally use it!

Side note, Encore will have a copy() method soon. Then you'll be able to do this without a plugin. Yay! But, this is still a great example of how to extend Webpack beyond Encore.

Anyways, install the plugin first. Notice that they use npm. I'm going to use yarn. So copy the name of that plugin, find your terminal, and run:

yarn add copy-webpack-plugin --dev

Adding Custom Webpack Config

To use the plugin, we need to require it at the top of the Webpack config file. No problem:

var Encore = require('@symfony/webpack-encore');
const CopyWebpackPlugin = require('copy-webpack-plugin');
... lines 3 - 27

And then below, um.... config =... and plugins:... what the heck does this mean?

Well... earlier, I told you that webpack.config.js normally returns a big configuration object. And Encore is just a tool to help generate that config. In fact, at the bottom, we can see what that config looks like if we want! Just console.log(module.exports).

Then, restart Encore:

yarn run encore dev --watch

Woh! There's our config! Actually, it's not so scary: there are keys for entry, output, module, plugins and a few other things.

For example, see the plugins key? Back on their docs, that is what they're referring to: they want you to add their plugin to that config key.

Ok, so how can we do that? Well, you could always just add it manually: module.exports.plugins.push() and then the plugin. Yep: you could literally add something to the plugins array!

But, fortunately, Encore gives you an easier way to modify the most common things. In this case, use addPlugin() and then new CopyWebpackPlugin(). Pass this an array - this will soon be the paths it should copy:

... line 1
const CopyWebpackPlugin = require('copy-webpack-plugin');
Encore
... lines 5 - 18
.addPlugin(new CopyWebpackPlugin([
... lines 20 - 21
]))
;
... lines 24 - 27

Copying Images into build/

But, before we fill that in... let's think about this. I don't need to copy all of my images to the build/ directory... just one of them right now. So let's create a new directory called static/ and move any files that need to be copied into that directory, like dumbell.png.

In the CopyWebpackPlugin config, set from to ./assets/static and to to just static:

... line 1
const CopyWebpackPlugin = require('copy-webpack-plugin');
Encore
... lines 5 - 18
.addPlugin(new CopyWebpackPlugin([
// copies to {output}/static
{ from: './assets/static', to: 'static' }
]))
;
... lines 24 - 27

This will copy to the output directory /static.

Ok, go restart Encore!

yarn run encore dev --watch

Once the build finishes... inside public/build... yes! We have a new static directory. It's nothing fancy, but this is a nice way to move files so that we can reference them publicly in a template:

... lines 1 - 2
{% block body %}
<div class="row">
... lines 5 - 34
<div class="col-md-5">
<div class="leaderboard">
<h2 class="text-center">
<img class="dumbbell" src="{{ asset('build/static/dumbbell.png') }}" />
... line 39
</h2>
... lines 41 - 42
</div>
</div>
</div>
{% endblock %}
... lines 47 - 59

There's one more reference in the login template: search for "bell" and... update this one too:

... lines 1 - 16
{% block fos_user_content %}
<div class="container">
<div class="wrapper">
<form action="{{ path("fos_user_security_check") }}" method="post" class="form-signin">
<h3><img class="dumbbell" src="{{ asset('build/static/dumbbell.png') }}">Login! Start Lifting!</h3>
... lines 22 - 67
</form>
</div>
</div>
{% endblock fos_user_content %}

Try it! Find your browser and refresh. There it is!

Next, let's make our CSS sassier... with... Sass of course!

Leave a comment!

  • 2018-08-18 Xavier de Moor

    I think adding your own copy with versioning is a very good idea. I will stay in touch. Thank you for your answer!

  • 2018-08-18 weaverryan

    Hey Xavier de Moor!

    Yea, this whole copying thing WITH versioning (copying works fine without versioning) has been a pain for a long time :/. It's something I hope to fix - we were waiting on a change in the copy plugin... then it still wasn't enough - we may just add our own copy functionality.

    But, I did recently learn about a trick - I haven't tried it yet, but check this out: https://twitter.com/SBISDan...

    This will copy all of the assets and put them into the manifest. It's an awesome little workaround - we'll document it soon.

    Cheers!

  • 2018-08-17 Xavier de Moor

    I'm a little bit disappointed. I read all this tutorial since this step for nothing :( I know it's not your fault.

  • 2018-08-17 Xavier de Moor

    So, copying images or files will never work for what I'm seeing? Almost a year this feature is awaiting in the copy plugin... I was thinking webpack encore (or Laravel mix) can replace Gulp, but I think I will need to use webpack AND Gulp or just continue to use Gulp just to have a correct manifest? :/

  • 2018-07-29 weaverryan

    Hey RoestVrijStaal!

    Oh man, I'm sorry that you wasted your time - that's definitely not our intention :/. This one is tough, we (from Webpack Encore) have been waiting for months for the upstream library to add some features we need - and we thought it would happen quickly. Now it appears that it won't, and we need to find a workaround.

    Why did we post this tutorial then? Because, people *do* need to be able to move their images into their build directory. And if you use the plugin like we do in this tutorial - *without* a [hash] in the filename - then it works perfectly fine :). You only have problems if you try to use [hash], because then your backend code doesn't know what dynamic filename to use. That's the problem we hope to solve some time.

    Anyways, I'm sorry you were frustrated - that is the opposite of our intention! I hope you'll enjoy other parts of Webpack Encore!

    Cheers!

  • 2018-07-26 RoestVrijStaal

    Thanks for wasting ~2 hours of my life for wasted efforts to get the stuff working for my project's images.

    Could you please put a big banner at the top of this tuto with the text "NOT FOR IMAGES YET" for the time
    being (read: forever, since the pull request is still open for months without any change) so others will look for a different solution than copy-webpack-plugin?

    Why are you publishing this tutorial anyway, when copy-webpack-plugin doesn't fullfill its intended goal?

  • 2018-03-21 Quentin

    Hey weaverryan, thank you for you response. I subscribed to this issue and I switched to a previous version of my code waiting webpack-manifest-plugin 2.0.

    Regards

  • 2018-03-21 weaverryan

    Hey Quentin!

    Yes, unfortunately, you're 100% correct. In fact, this is the reason that we haven't added the copy() method to Encore yet. We needed to wait for the webpack-manifest-plugin to tag their version 2.0, which contains a "hook" for allowing custom data to be added. We're *still* waiting for that release :/. Here is a related issue: https://github.com/webpack-...

    So for now, it's either not possible, or you will need to follow issues like the one linked to get it working. For this reason, I do *not* use a `hash` when I use the copy plugin. Yes, this means that any copied assets aren't versioned - that's the big bummer currently.

    Hopefully the webpack-manifest-plugin will get its 2.0 tag soon, and we can try to fit all the pieces together!

    Cheers!

  • 2018-03-20 Quentin

    Hey, thank you for this cool tutorial! We implemented it but we have one problem, in production our manifest.json is not taking into account images copied with CopyPlugin, do you know a way to fix this or to work around of this problem?

  • 2018-03-19 Cesar

    Thanks for your answer Ryan.

  • 2018-03-19 weaverryan

    Hey Cesar!

    Glad you're liking the tutorial :). The answer your question is no... at least currently: there's no feature in Encore today to optimize images. However, you can add any Webpack plugins or loaders to Encore that you want. For example, check out https://github.com/tcoopman... - I've never used it before, just googled for it quickly. You could add this to Encore with those code:


    Encore
    // ...
    .addLoader({
    test: /\.(gif|png|jpe?g|svg)$/i,
    use: [
    'file-loader',
    {
    loader: 'image-webpack-loader',
    options: {
    bypassOnDebug: true,
    },
    }
    ]
    }

    So, if that's something you're interested in, try it out!

    Cheers!

  • 2018-03-15 Cesar

    Thanks for this free tutorial guys. One little question, can we use webpack encore to optimise images? I was wondering this because with Assetic it's possible right?