DEV Community

Cover image for โœ Handle CSS in webpack | Inline CSS
Francesco Di Donato
Francesco Di Donato

Posted on

โœ Handle CSS in webpack | Inline CSS

These post is the first of a trilogy. This is where it all began... ๐Ÿง™

๐Ÿ”Ž Focus on ๐Ÿ–Œ CSS Handling Parts
(Index) #๏ธโƒฃ
development only inline CSS ๐Ÿ“Œ
both dev & prod mini-css-extract-plugin 2๏ธโƒฃ
production only CSS modules 3๏ธโƒฃ

Example Code ๐Ÿ“œ

webpack-inline-css


Final Product ๐Ÿค–

At the end of the first part you will have built a flexible scaffolding for your webpack configuration. Hard work will be rewarded in later parts.
The first-stage final product will run only in development and will be able to inject CSS directly in the bundle.


Flow of Thought ๐Ÿฎ

  1. Install packages
  2. Add start script
  3. Add JavaScript and CSS file
  4. Instruct webpack
    1. Read env
    2. Custom loader
    3. Inject rules + interface

Implementation ๐Ÿค“

- 1 - Install packages

Donwload the webpack triad, the two loaders and the only needed plugin. In the terminal invoke npm i -D webpack webpack-cli webpack-dev-server css-loader style-loader html-webpack-plugin.

- 2 - Add start script

If not present, quickly create a package.json by calling npm init -y. Add a script to activate the server in development mode:

package.json
{
 ...
 "scripts": {
    "start": "webpack-dev-server --env development"
  },
 ...
}
Enter fullscreen mode Exit fullscreen mode

- 3 - Add JavaScript and CSS file

Create a src folder. In there add a style.css and give it some instruction:

style.css
body {
 background-color: indigo;
 color: white;
 padding: 30px;
}
Enter fullscreen mode Exit fullscreen mode

In the src folder place a index.js. By default, webpack is educated to look for an entry at this path (/src/index.js).
For the purpose of this demonstration it is sufficient to add simply the import of the stylesheet:

index.js
import './style.css';
Enter fullscreen mode Exit fullscreen mode

Of course, in a real project it is advisable to use a nested architecture.

- 4 - Instruct webpack

If you try to give the command npm start you will see that the server actually starts, finds index.js and reads it. but when itโ€™s time to read the css it starts to complain - webpack still does not know the css language.
We can fix that creating in the root a configuration file named webpack.config.js.
The hardcoded solution is pretty linear:

webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
devServer: {
        open: true,
        stats: 'errors-only',
    },
    module: {
        rules: [
            {
                test: /\.css$/i,
                use: ['style-loader', 'css-loader'],
            },
        ],
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Webpack Inline CSS',
        }),
    ],
};
Enter fullscreen mode Exit fullscreen mode

In the terminal call npm start - the result is there, your favorite color is smeared on the newly opened tab.

However, this type of approach is not flexible and after a few additions we will start to suffer confusion.

I want to build my personal webpack-๐Ÿค– with these features:

  • able to integrate various parts
    • each with a clear interface
  • double behavior based on environment variable
  • (perhaps also polite and clean)

- 4|a - Instruct webpack | Read env

First export a function that returns a configuration object:

webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const useRules = require('./config/webpack.useRules');

module.exports = (env) => ({
    devServer: {
        open: true,
        stats: 'errors-only',
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Webpack Inline CSS',
        }),
    ],
    module: useRules(env),
});
Enter fullscreen mode Exit fullscreen mode

useRules it does not exist yet. However we can note that it requires a single argument, env. We will instruct useRules to return different rules (then different webpack behaviors) for different environments.

- 4|b - Instruct webpack | Custom loader

In the root create a config folder. In it place loaders.js. Here we can store all loaders that could be needed in the future. The following piece of code exposes the function loadCSS that may or may not get a config argument.

loaders.js
function addConfigs(r, c) {
        Object.entries(c).forEach(([key, value]) => {
            if (!value) return;
            r[key] = value;
        });
        return r
    }

exports.loadCSS = (config = {}) => {
    // basic rule
    const rule = {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader'],
    };

    return addConfigs(rule, config);
};
Enter fullscreen mode Exit fullscreen mode

The reference code below rappresent the output. Not passing any configuration object, it would lack of the exclude property (that is indeed the default output).

loadCss({ exclude: ['node_modules'] })
{
   test: /\.css$/i,
   use: ['style-loader', 'css-loader'],
   exclude: ['node_modules']
};
Enter fullscreen mode Exit fullscreen mode

Thanks to style-loader, the previously CSS parsed by css-loader is injected directly in the bundle. That's okay when developing but we'll need another solution for production. This file will contain the production-funtion (addressed in the second chapter).

- 4|c - Instruct webpack | Inject rules + interface

Lastly we need an adapter between the newly created function and the core webpack configuration file. Why not take advantage of it and build one that in addition to returning an informative message, allow in the future a simpler implementation of new loaders?

In the config folder create useRules.js. At first, the following code could seem overwhelming, but we are going through it togheter.

useRules.js
const { loadCSS } = require('./loaders'); //[1]

module.exports = (env) => { //[2]
    const loaders = { //[3]
        css: (i) => {
            switch (i) {
                case 'inline':
                    return loadCSS()
                default:
                    throw new Error('production is not implemented yet');
            }
        },
    };

    // developer interface [4]
    const instructions = { 
        css: {
            development: 'inline',
            production: 'extract',
        },
    };

    // business logic [5]
    let message = '[useRules] ';
    const rules = Object.entries(instructions).map(([key, value]) => {
        const i = instructions[key][env];
        message += key + '|' + i;
        return loaders[key](i);
    });

    console.info(message);
    return { rules };
};
Enter fullscreen mode Exit fullscreen mode
  1. Import the formerly made loader.
  2. Export a function that gets env
  3. Create a container of functions. Add one inside that will manage the CSS. It gets an instruction (i) as 'inline', 'extract' or 'modules' and activates the appropriate loader. At this point we only have one loader - it will be used when i is equal to 'inline'.
  4. Since we don't want to get hands dirty every time a loader need to be added, we are building a developer interface - it's just a set of informations (once again, only for CSS right now) that the developer can easily change and otherwise quickly get a different behavior.
  5. The last piece is iterate for each instruction (only one right now) and extract the env-associated command.

Checking the Outcome ๐Ÿ˜Ž

When using npm start, env equals development. So i is 'inline'. This causes the activation of the loader that, not by chance, injects the CSS in the bundle.

Let's suppose we already had built another loader and we had associated it with its case in the switch - simply changing the command in the developer interface [4] we could observe the different behavior.

Peek in the console - you should see [useRules] css|inline.


Upgrade the second-stage ๐Ÿ”

Let's go ๐Ÿ‰

Latest comments (0)