Asset Management

If you've been following the guides from the start, you will now have a small project that shows "Hello webpack". Now let's try to incorporate some other assets, like images, to see how they can be handled.

Prior to webpack, front-end developers would use tools like grunt and gulp to process these assets and move them from their /src folder into their /dist or /build directory. The same idea was used for JavaScript modules, but tools like webpack will dynamically bundle all dependencies (creating what's known as a dependency graph). This is great because every module now explicitly states its dependencies and we'll avoid bundling modules that aren't in use.

One of the coolest webpack features is that you can also include any other type of file, besides JavaScript, for which there is a loader. This means that the same benefits listed above for JavaScript (e.g. explicit dependencies) can be applied to everything used in building a website or web app. Let's start with CSS, as you may already be familiar with that setup.

Setup

Let's make a minor change to our project before we get started:

dist/index.html

  <!doctype html>
  <html>
    <head>
-    <title>Getting Started</title>
+    <title>Asset Management</title>
    </head>
    <body>
-     <script src="main.js"></script>
+     <script src="bundle.js"></script>
    </body>
  </html>

webpack.config.js

  const path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
-     filename: 'main.js',
+     filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
  };

Loading CSS

In order to import a CSS file from within a JavaScript module, you need to install and add the style-loader and css-loader to your module configuration:

npm install --save-dev style-loader css-loader

webpack.config.js

  const path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
+   module: {
+     rules: [
+       {
+         test: /\.css$/,
+         use: ['style-loader', 'css-loader'],
+       },
+     ],
+   },
  };

Module loaders can be chained. Each loader in the chain applies transformations to the processed resource. A chain is executed in reverse order. The first loader passes its result (resource with applied transformations) to the next one, and so forth. Finally, webpack expects JavaScript to be returned by the last loader in the chain.

The above order of loaders should be maintained: 'style-loader' comes first and followed by 'css-loader'. If this convention is not followed, webpack is likely to throw errors.

webpack uses a regular expression to determine which files it should look for and serve to a specific loader. In this case, any file that ends with .css will be served to the style-loader and the css-loader.

This enables you to import './style.css' into the file that depends on that styling. Now, when that module is run, a <style> tag with the stringified css will be inserted into the <head> of your html file.

Let's try it out by adding a new style.css file to our project and import it in our index.js:

project

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
+   |- style.css
    |- index.js
  |- /node_modules

src/style.css

.hello {
  color: red;
}

src/index.js

  import _ from 'lodash';
+ import './style.css';

  function component() {
    const element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
+   element.classList.add('hello');

    return element;
  }

  document.body.appendChild(component());

Now run your build command:

$ npm run build

...
[webpack-cli] Compilation finished
asset bundle.js 72.6 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1000 bytes 5 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 539 KiB
  modules by path ./node_modules/ 538 KiB
    ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
    ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
    ./node_modules/css-loader/dist/runtime/api.js 1.57 KiB [built] [code generated]
  modules by path ./src/ 963 bytes
    ./src/index.js + 1 modules 639 bytes [built] [code generated]
    ./node_modules/css-loader/dist/cjs.js!./src/style.css 324 bytes [built] [code generated]
webpack 5.2.0 compiled successfully in 3365 ms

Open up dist/index.html in your browser again and you should see that Hello webpack is now styled in red. To see what webpack did, inspect the page (don't view the page source, as it won't show you the result, because the <style> tag is dynamically created by JavaScript) and look at the page's head tags. It should contain the style block that we imported in index.js.

Note that you can, and in most cases should, minimize css for better load times in production. On top of that, loaders exist for pretty much any flavor of CSS you can think of -- postcss, sass, and less to name a few.

Loading Images

So now we're pulling in our CSS, but what about our images like backgrounds and icons? As of webpack 5, using the Asset Modules we can easily incorporate those in our system as well:

webpack.config.js

  const path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: ['style-loader', 'css-loader'],
        },
+       {
+         test: /\.(png|svg|jpg|jpeg|gif)$/i,
+         type: 'asset/resource',
+       },
      ],
    },
  };

Now, when you import MyImage from './my-image.png', that image will be processed and added to your output directory and the MyImage variable will contain the final url of that image after processing. When using the css-loader, as shown above, a similar process will occur for url('./my-image.png') within your CSS. The loader will recognize this is a local file, and replace the './my-image.png' path with the final path to the image in your output directory. The html-loader handles <img src="./my-image.png" /> in the same manner.

Let's add an image to our project and see how this works, you can use any image you like:

project

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
+   |- icon.png
    |- style.css
    |- index.js
  |- /node_modules

src/index.js

  import _ from 'lodash';
  import './style.css';
+ import Icon from './icon.png';

  function component() {
    const element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    element.classList.add('hello');

+   // Add the image to our existing div.
+   const myIcon = new Image();
+   myIcon.src = Icon;
+
+   element.appendChild(myIcon);

    return element;
  }

  document.body.appendChild(component());

src/style.css

  .hello {
    color: red;
+   background: url('./icon.png');
  }

Let's create a new build and open up the index.html file again:

$ npm run build

...
[webpack-cli] Compilation finished
asset bundle.js 73.4 KiB [emitted] [compared for emit] [minimized] (name: main) 1 related asset
asset bc67ebaf980e8f20b8c3.png 8.29 KiB [emitted] [immutable] [from: src/icon.png] (auxiliary name: main)
runtime modules 1.82 KiB 6 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 540 KiB (javascript) 8.29 KiB (asset)
  modules by path ./node_modules/ 539 KiB
    modules by path ./node_modules/css-loader/dist/runtime/*.js 2.38 KiB
      ./node_modules/css-loader/dist/runtime/api.js 1.57 KiB [built] [code generated]
      ./node_modules/css-loader/dist/runtime/getUrl.js 830 bytes [built] [code generated]
    ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
    ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
  modules by path ./src/ 1.45 KiB (javascript) 8.29 KiB (asset)
    ./src/index.js + 1 modules 794 bytes [built] [code generated]
    ./src/icon.png 42 bytes (javascript) 8.29 KiB (asset) [built] [code generated]
    ./node_modules/css-loader/dist/cjs.js!./src/style.css 646 bytes [built] [code generated]
webpack 5.2.0 compiled successfully in 2671 ms

If all went well, you should now see your icon as a repeating background, as well as an img element beside our Hello webpack text. If you inspect this element, you'll see that the actual filename has changed to something like bc67ebaf980e8f20b8c3.png. This means webpack found our file in the src folder and processed it!

Loading Fonts

So what about other assets like fonts? The Asset Modules will take any file you load through them and output it to your build directory. This means we can use them for any kind of file, including fonts. Let's update our webpack.config.js to handle font files:

webpack.config.js

  const path = require('path');

  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: ['style-loader', 'css-loader'],
        },
        {
          test: /\.(png|svg|jpg|jpeg|gif)$/i,
          type: 'asset/resource',
        },
+       {
+         test: /\.(woff|woff2|eot|ttf|otf)$/,
+         type: 'asset/resource'
+       },
      ],
    },
  };

Add some font files to your project:

project

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
+   |- google-fonts.woff2
    |- icon.png
    |- style.css
    |- index.js
  |- /node_modules

With the loader configured and fonts in place, you can incorporate them via an @font-face declaration. The local url(...) directive will be picked up by webpack just as it was with the image:

src/style.css

+ @font-face {
+   font-family: 'GoogleFont';
+   src:  url('./google-font.woff2') format('woff2');
+   font-weight: 600;
+   font-style: normal;
+ }

  .hello {
    color: red;
+   font-family: 'GoogleFont';
    background: url('./icon.png');
  }

Now run a new build and let's see if webpack handled our fonts:

$ npm run build

...
[webpack-cli] Compilation finished
assets by status 21.3 KiB [cached] 2 assets
asset bundle.js 73.6 KiB [emitted] [minimized] (name: main) 1 related asset
runtime modules 1.82 KiB 6 modules
orphan modules 326 bytes [orphan] 1 module
cacheable modules 541 KiB (javascript) 21.3 KiB (asset)
  javascript modules 541 KiB
    modules by path ./node_modules/ 539 KiB
      modules by path ./node_modules/css-loader/dist/runtime/*.js 2.38 KiB 2 modules
      ./node_modules/lodash/lodash.js 530 KiB [built] [code generated]
      ./node_modules/style-loader/dist/runtime/injectStylesIntoStyleTag.js 6.67 KiB [built] [code generated]
    modules by path ./src/ 1.76 KiB
      ./src/index.js + 1 modules 794 bytes [built] [code generated]
      ./node_modules/css-loader/dist/cjs.js!./src/style.css 1010 bytes [built] [code generated]
  asset modules 84 bytes (javascript) 21.3 KiB (asset)
    ./src/icon.png 42 bytes (javascript) 8.29 KiB (asset) [built] [code generated]
    ./src/google-font.woff2 42 bytes (javascript) 13 KiB (asset) [built] [code generated]
webpack 5.2.0 compiled successfully in 2622 ms

Open up index.html again and see if our Hello webpack text has changed to the new font. If all is well, you should see the changes.

Loading Data

Another useful asset that can be loaded is data, like JSON files, CSVs, TSVs, and XML. Support for JSON is actually built-in, similar to NodeJS, meaning import Data from './data.json' will work by default. To import CSVs, TSVs, and XML you could use the csv-loader and xml-loader. Let's handle loading all three:

npm install --save-dev csv-loader xml-loader

webpack.config.js

const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: 'asset/resource',
      },
      {
        test: /\.(woff|woff2|eot|ttf|otf)$/i,
        type: 'asset/resource'
      },
+     {
+       test: /\.(csv|tsv)$/,
+       use: ['csv-loader'],
+     },
+     {
+       test: /\.xml$/,
+       use: ['xml-loader'],
+     },
    ],
  },
};

Add some data files to your project:

project

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
+   |- data.xml
+   |- data.csv
    |- my-font.woff
    |- my-font.woff2
    |- icon.png
    |- style.css
    |- index.js
  |- /node_modules

src/data.xml

<?xml version="1.0" encoding="UTF-8"?>
<note>
  <to>Mary</to>
  <from>John</from>
  <heading>Reminder</heading>
  <body>Call Cindy on Tuesday</body>
</note>

src/data.csv

to,from,heading,body
Mary,John,Reminder,Call Cindy on Tuesday
Zoe,Bill,Reminder,Buy orange juice
Autumn,Lindsey,Letter,I miss you

Now you can import any one of those four types of data (JSON, CSV, TSV, XML) and the Data variable you import, will contain parsed JSON for easy consumption:

src/index.js

  import _ from 'lodash';
  import './style.css';
  import Icon from './icon.png';
+ import Data from './data.xml';
+ import Notes from './data.csv';

  function component() {
    const element = document.createElement('div');

    // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
    element.classList.add('hello');

    // Add the image to our existing div.
    const myIcon = new Image();
    myIcon.src = Icon;

    element.appendChild(myIcon);

+   console.log(Data);
+   console.log(Notes);

    return element;
  }

  document.body.appendChild(component());

Re-run the npm run build command and open dist/index.html. If you look at the console in your developer tools, you should be able to see your imported data being logged to the console!

This can be especially helpful when implementing some sort of data visualization using a tool like d3. Instead of making an ajax request and parsing the data at runtime you can load it into your module during the build process so that the parsed data is ready to go as soon as the module hits the browser.

Only the default export of JSON modules can be used without warning.

// No warning
import data from './data.json';

// Warning shown, this is not allowed by the spec.
import { foo } from './data.json';

Customize parser of JSON modules

It's possible to import any toml, yaml or json5 files as a JSON module by using a custom parser instead of a specific webpack loader.

Let's say you have a data.toml, a data.yaml and a data.json5 files under src folder:

src/data.toml

title = "TOML Example"

[owner]
name = "Tom Preston-Werner"
organization = "GitHub"
bio = "GitHub Cofounder & CEO\nLikes tater tots and beer."
dob = 1979-05-27T07:32:00Z

src/data.yaml

title: YAML Example
owner:
  name: Tom Preston-Werner
  organization: GitHub
  bio: |-
    GitHub Cofounder & CEO
    Likes tater tots and beer.
  dob: 1979-05-27T07:32:00.000Z

src/data.json5

{
  // comment
  title: "JSON5 Example",
  owner: {
    name: "Tom Preston-Werner",
    organization: "GitHub",
    bio: "GitHub Cofounder & CEO\n\
Likes tater tots and beer.",
    dob: "1979-05-27T07:32:00.000Z"
  }
}

Install toml, yamljs and json5 packages first:

npm install toml yamljs json5 --save-dev

And configure them in your webpack configuration:

webpack.config.js

+ const toml = require('toml'); 
+ const yaml = require('yamljs');
+ const json5 = require('json5');
  module.exports = {
    // ...
    module: {
      rules: [
        // ...
+       {
+         test: /\.toml$/,
+         type: 'json',
+         parser: {
+           parse: toml.parse,
+         },
+       },
+       {
+         test: /\.yaml$/,
+         type: 'json',
+         parser: {
+           parse: yaml.parse,
+         },
+       },
+       {
+         test: /\.json5$/,
+         type: 'json',
+         parser: {
+           parse: json5.parse,
+         },
+       },
      ]
    }
  };

src/index.js

  // ...
  import Notes from './data.csv';
+ import toml from './data.toml';
+ import yaml from './data.yaml';
+ import json from './data.json5';
+ 
+ console.log(toml.title); // output `TOML Example`
+ console.log(toml.owner.name); // output `Tom Preston-Werner`
+ 
+ console.log(yaml.title); // output `YAML Example`
+ console.log(yaml.owner.name); // output `Tom Preston-Werner`
+ 
+ console.log(json.title); // output `JSON5 Example`
+ console.log(json.owner.name); // output `Tom Preston-Werner`
  function component() {
    const element = document.createElement('div');
    // ...
  }
  // ...

Re-run the npm run build command and open dist/index.html. You should be able to see your imported data being logged to the console!

Global Assets

The coolest part of everything mentioned above, is that loading assets this way allows you to group modules and assets in a more intuitive way. Instead of relying on a global /assets directory that contains everything, you can group assets with the code that uses them. For example, a structure like this can be useful:

- |- /assets
+ |– /components
+ |  |– /my-component
+ |  |  |– index.jsx
+ |  |  |– index.css
+ |  |  |– icon.svg
+ |  |  |– img.png

This setup makes your code a lot more portable as everything that is closely coupled now lives together. Let's say you want to use /my-component in another project, simply copy or move it into the /components directory over there. As long as you've installed any external dependencies and your configuration has the same loaders defined, you should be good to go.

However, let's say you're locked into your old ways or you have some assets that are shared between multiple components (views, templates, modules, etc.). It's still possible to store these assets in a base directory and even use aliasing to make them easier to import.

Wrapping up

For the next guides we won't be using all the different assets we've used in this guide, so let's do some cleanup so we're prepared for the next piece of the guides Output Management:

project

  webpack-demo
  |- package.json
  |- webpack.config.js
  |- /dist
    |- bundle.js
    |- index.html
  |- /src
-   |- data.xml
-   |- data.csv
-   |- data.toml
-   |- data.yaml
-   |- data.json5
-   |- google-font.woff2
-   |- icon.png
-   |- style.css
    |- index.js
  |- /node_modules

webpack.config.js

  const path = require('path');
- const toml = require('toml'); 
- const yaml = require('yamljs');
- const json5 = require('json5');
  module.exports = {
    entry: './src/index.js',
    output: {
      filename: 'bundle.js',
      path: path.resolve(__dirname, 'dist'),
    },
-   module: {
-     rules: [
-       {
-         test: /\.css$/,
-         use: ['style-loader', 'css-loader'],
-       },
-       {
-         test: /\.(png|svg|jpg|jpeg|gif)$/i,
-         type: 'asset/resource',
-       },
-       {
-         test: /\.(woff|woff2|eot|ttf|otf)$/i,
-         type: 'asset/resource',
-       },
-       {
-         test: /\.(csv|tsv)$/,
-         use: ['csv-loader'],
-       },
-       {
-         test: /\.xml$/,
-         use: ['xml-loader'],
-       },
-       {
-         test: /\.toml$/,
-         type: 'json',
-         parser: {
-           parse: toml.parse,
-         },
-       },
-       {
-         test: /\.yaml$/,
-         type: 'json',
-         parser: {
-           parse: yaml.parse,
-         },
-       },
-       {
-         test: /\.json5$/,
-         type: 'json',
-         parser: {
-           parse: json5.parse,
-         },
-       },
-     ],
-   },
  }

src/index.js

  import _ from 'lodash';
- import './style.css';
- import Icon from './icon.png';
- import Data from './data.xml';
- import Notes from './data.csv';
- import toml from './data.toml';
- import yaml from './data.yaml';
- import json from './data.json5';
- 
- console.log(toml.title); // output `TOML Example`
- console.log(toml.owner.name); // output `Tom Preston-Werner`
- 
- console.log(yaml.title); // output `YAML Example`
- console.log(yaml.owner.name); // output `Tom Preston-Werner`
- 
- console.log(json.title); // output `JSON5 Example`
- console.log(json.owner.name); // output `Tom Preston-Werner`
-
  function component() {
    const element = document.createElement('div');
-
-   // Lodash, now imported by this script
    element.innerHTML = _.join(['Hello', 'webpack'], ' ');
-   element.classList.add('hello');
-
-   // Add the image to our existing div.
-   const myIcon = new Image();
-   myIcon.src = Icon;
-
-   element.appendChild(myIcon);
-
-   console.log(Data);
-   console.log(Notes);

    return element;
  }

  document.body.appendChild(component());

Next guide

Let's move on to Output Management

Further Reading