/home/jtanguy/blog

Writing a webpack loader

Julien Tanguy, . Tagged

This article is the first in a series of articles about webpack. It will cover non-trivial configuration and extensions via custom loaders and plugins.

What are webpack loaders?

Webpack loaders are simple transformers that converts files, or node modules into other node modules. You can think of it like commandline tools that you chain in a shell pipeline, like cat file | grep "pattern" | wc -l Yes I know, you don’t need to cat into grep.

For instance, let’s see how loaders are used for the following react component:

import React from 'react';

import logo from './logo.png';

const Logo = ({size}) => (
  <a href="https://www.example.org">
    <img src={logo} width={size} height={size}/>
  </a>
);
Logo.defaultProps = {
  size: "100",
};
export default Logo;

The line import logo from './logo.png' has one main role for webpack: to indicate that our component’s file depends on the png file. What webpack does of this dependency depends on its configuration.

If Webpack does not know about the file, you may have an error message like this one:

ERROR in ./src/components/logo/logo.png 1:0
Module parse failed: Unexpected character '' (1:0)
You may need an appropriate loader to handle this file type.
(Source code omitted for this binary file)

Otherwise, you may see logo.png in your output folder, depending on the loader used.

A simple loader for Play i18n messages

In my work at Valwin we work on a stack based on playframework, with React for the frontend. The frontend is packaged with webpack.

Play has it’s own i18n module and translations that are used throughout the application, in the API and the emails sent by the app. The translations are stored in a messages file, using the java MessageFormat format.

event.name=Event
event.name.tip=Enter the event's description.

event.type=Type
event.type.tip=Select the event type.

event.place=Place
event.place.tip=Enter the event's place.

event.date=Date & time
event.date.tip=Enter event's date and time. Format: aaaa-mm-dd hh:mm.

event.noEvents=No events in sight...

event.button.newEvent=New event

event.form.updateEvent=Update event
event.form.createEvent=Create a new event

event.button.saveEvent=Save changes
event.button.cancel=Cancel

In order to reuse these translations in the frontend, we have two options:

  1. Duplicate the translations in a json-y format, or
  2. Reuse the play translations directly.

The first option can be dangerous at times, because it introduces another source of truth (at least for the frontend). We can (and should) automate the synchronization between these two files, but we chose the second way, using a custom webpack loader to use the play translation directly.

A loader is a function that transforms the contents of a file, like in a bash pipeline.

For the first version of this loader, we do the following transformations:

  1. Split the input files by lines, and remove empty lines
  2. Extract the key=value for each line.
  3. Output a CommonJS module that exports these key=value as a big object.

/** @format */
import _ from 'lodash/fp';

export default function(source) {
  const transformed = _.flow([
    _.split('\n'),
    _.map(_.trim),
    _.filter(_.negate(_.isEmpty)),
    _.map(_.split('=')),
    _.map(_.take(2)),
    _.fromPairs,
  ])(source);
  return `module.exports = ${JSON.stringify(transformed)}`;
}

This is a really simple loader. For more complicated messages, we could for instance use intl-messageformat for parsing messages with variables, and use another loader to interpolate the messages later in the chain.

For more examples, check out the [writing] guide.