In this blog post I will show how to write a Webpack loader. We will load Markdown files, will find a JavaScript code block inside and will return it to be processed by the rest of the pipeline.
Imagine we have a Markdown text file, and it has JavaScript code blocks like this:
1 2 3 4 5
# example This file has a JS code block const add = (a, b) => a + b console.log('2 + 3 =', add(2, 3)) End of the file
Can we extract the JavaScript code block from the Markdown file and bundle it into an executable JavaScript resource to run on a web page? Let's try writing a Webpack loader to do so.
[webpack-cli] Compilation finished assets by status 297 bytes [cached] 1 asset ./example.md 132 bytes [built] [code generated] [1 error]
ERROR in ./example.md 1:0 Module parse failed: Unexpected character '#' (1:0) You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders > # example | > This file has a JS code block |
webpack 5.3.0 compiled with 1 error in 163 ms
Webpack does not "know" how to load .md files. Let's tell Webpack to load them "as is".
The loader does try to export an ES6 module by default, and since the markdown has no exports, there is nothing generated. We can output at least the wrapper code if we use CommonJS modules.
$ cat dist/bundle.js (()=>{var n={221:n=>{n.exports="# example\n> This file has a JS code block\n\n```js\nconst add = (a, b) => a + b\nconsole.log('2 + 3 =', add(2, 3))\n```\n\nEnd of the file\n"}},e={}; !function o(r){if(e[r])return e[r].exports;var s=e[r]={exports:{}}; return n[r](s,s.exports,o),s.exports}(221)})();
Notice that the source code has the Markdown file as a plain string - that's how raw-loader works.
Markdown to HTML
Let's convert Markdown to HTML using Webpack and markdown-loader. We need to chain markdown-loader with html-loader.
Webpack loaders are applied last to first. Thus our Markdown file first will be processed by the markdown-loader, then the output will be passed through the html-loader. Let's run the Webpack using NPM script
$ cat dist/bundle.js (()=>{var e={978:e=>{e.exports='<h1 id="example">example</h1> <blockquote> <p> This file has a JS code block</p> </blockquote> <pre><code class="language-js"> const add = (a, b) => a + b\nconsole.log('2 + 3 =', add(2, 3))</code></pre> <p>End of the file</p> '}},o={};!function a(p){if(o[p])return o[p].exports; var r=o[p]={exports:{}};return e[p](r,r.exports,a),r.exports}(978)})();
Note the produced bundle has the Markdown tags converted into HTML tags, and then into a JavaScript code string.
Import statements
So far we were transforming the Markdown file directly as the entry point. But it is more likely that we will have HTML or JavaScript as the entry point. That file will import the Markdown file (and we want to extract the JavaScript blocks from the Markdown file to export in the future).
We can bundle the file and see that Webpack was smart enough to optimize the import and directly substitute the Markdown text into the JavaScript code.
$ cat dist/bundle.js (()=>{"use strict";console.log("imported example.md"), console.log("# example\n> This file has a JS code block\n\n```js\nconst add = (a, b) => a + b\nconsole.log('2 + 3 =', add(2, 3))\n```\n\nEnd of the file\n")})();
We can even run the bundle directly from Node
1 2 3 4 5 6 7 8 9 10
$ node ./dist/bundle.js imported example.md # example > This file has a JS code block
const add = (a, b) => a + b console.log('2 + 3 =', add(2, 3))
End of the file
Nice!
Per-import loader
If we generally do not bundle Markdown files, we might avoid writing the special loader rules for .md files in the Webpack config file. Instead we can specify the loaders to use directly in the import statement. This is called inline configuration.
The Webpack config in this case is very bare
webpack.plain.js
1 2 3 4 5 6 7 8 9 10 11
const path = require('path')
// this Webpack config file does not specify // how to load Markdown files module.exports = { entry: './plain.js', output: { path: path.resolve(__dirname, 'dist'), filename: 'bundle.js', }, }
The plain.js that wants to import example.md specifies the loader name
$ node ./dist/bundle.js imported example.md # example > This file has a JS code block
const add = (a, b) => a + b console.log('2 + 3 =', add(2, 3))
End of the file
Our Markdown loader
Let's write our own loader that would find the JavaScript blocks in the Markdown text and returns them. For now let's say we return just the JavaScript code block as a string. We can start the loader using the loader example from the Webpack documentation.
// load the JavaScript block as text from the specified file // using a custom local loader from a local file import jsBlockText from'./block-loader!./example.md' console.log('imported js block text') console.log(jsBlockText)
$ node ./dist/bundle.js imported js block text # example > This file has a JS code block
const add = (a, b) => a + b console.log('2 + 3 =', add(2, 3))
End of the file
But it does not transform the source code yet. Let's extract the code block and return it. Let's install a utility module to parse Markdown source into an abstract syntax tree (AST).
$ node ./dist/bundle.js imported js block text constadd = (a, b) => a + b console.log('2 + 3 =', add(2, 3))
Bundling extracted JavaScript code
We have extracted the JavaScript code block from the Markdown example.md file. But what if we want to bundle this JavaScript? What if we want the Markdown files to be pretty much the same as JavaScript source files?
Let me change the example.md file to have the following JS code block
1 2 3
constadd = (a, b) => a + b console.log('computing 2 + 3') exportdefaultadd(2, 3)
Right now the bundled code has the code block as a string. But we want to bundle the extracted code block, just like regular source code. First, let's update our custom loader to return the raw code block text.
And now let's chain our loaders - first our custom loader should extract the JavaScript code block from the Markdown file, then the JavaScript code should be processed. We can chain loaders inline using ! syntax.