'Generating Dynamic Virtual Modules with Webpack Virtual Modules Plugin' post illustration

Generating Dynamic Virtual Modules with Webpack Virtual Modules Plugin

avatar

In projects built with webpack, it can be convenient to dynamically create some files in memory. In other words, a file doesn't need to be written into the file system. Still, webpack must treat this virtual file as a real module and rebuild the project on the fly whenever we change it. This functionality is especially useful when you need to automatically generate documentation for a RESTful API.

To be able to generate and inject virtual modules into the builds, we created a dedicated plugin Webpack Virtual Modules.

Have a look at the code below. Notice how in the second line swagger.json is imported. The fact is, this file does not exist in the file system — it's virtual and is created by Webpack Virtual Module at compile time. You can look up the example in the plugin repository to see for yourself that there's no swagger.json.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Require a virtual module generated by Webpack Virtual Modules in memory
const swaggerJson = require('swagger.json');
const swaggerUi = require('swagger-ui');
require('swagger-ui/dist/swagger-ui.css');

/**
 * @swagger
 * /api/hello:
 *   get:
 *     description: Returns hello message
 *     parameters:
 *       - name: subject
 *         in: query
 *         schema:
 *           type: string
 *     responses:
 *       '200':
 *         content:
 *           application/json:
 *             schema:
 *               type: string
 */
function getHello(name) {
  // TODO: Replace the code with a REST API call when it's implemented on the backend
  return { message: 'Hello ' + name + '!' };
}

var helloDiv = document.getElementById('hello');
helloDiv.innerHTML = getHello('World').message;

swaggerUi({
  spec: swaggerJson, dom_id: '#apiDocs',
  presets: [
    swaggerUi.presets.apis,
    swaggerUi.SwaggerUIStandalonePreset
  ]
});

If you run the example and change the documentation (see the comment /**... @swagger...*/), you'll notice that webpack will recompile the project as if the swagger.json file really existed.

How does Webpack Virtual Modules actually work? Let's have a closer look at the example in the plugin repository.

Generating virtual files with Webpack Virtual Modules

We want to talk through a few important aspects how to use Webpack Virtual Modules.

To generate a virtual file with this plugin, you first need to create a path and a string with the contents for the file. The initial contents may be empty.

Second, you need to instantiate Webpack Virtual Modules. The VirtualModulesPlugin class optionally accepts an object of string key-value pairs where keys are paths to the virtual modules and values are the contents.

Next, you need to pass the created plugin instance to the plugins array in webpack configuration. Finally, you need to use webpack compiler hooks to track the plugin instance, for example, compiler.hooks.compilation.tap(ModuleName, (compilation) => {}. Inside the callback passed to the hook, you can write to the virtual module using the method VirtualModulesPlugin.writeModule(). And you can update the module in any project file using the same method.

writeModule(), as you might have guessed, accepts an object parameter: The key will again be the path to the virtual file, and a string with contents will be the value.

Below, you can see an example that follows the flow we described. This is a Swagger plugin we created to demo the Webpack Virtual Modules plugin (to recall how webpack plugins are created, check out the Writing a Plugin guide).

Here's the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
const VirtualModulesPlugin = require('../..');
const swaggerJsDoc = require('swagger-jsdoc');

function SwaggerPlugin() {}

SwaggerPlugin.prototype.apply = function(compiler) {
  // #1
  // Create a package.json module, path, and file
  const pkgJsonModule = './package.json';
  const pkgJsonPath = require.resolve(pkgJsonModule);
  const pkgJson = require(pkgJsonModule);
  
  // #2 
  // Sample data for the future virtual JSON file
  const info = {
    title: pkgJson.name,
    version: pkgJson.version,
    description: pkgJson.description
  };

  // #3
  // Using the 'package.json' path to create the path to 
  // the virtual module `swagger.json`.
  // Webpack will "see" the module `swagger.json`
  // by a path similar to this:
  // '/home/johndoe/webpack-virtual-modules/examples/node_modules/swagger.json'
  const swaggerJsonPath = path.join(
    path.dirname(pkgJsonPath), 
    'node_modules', 
    'swagger.json');
  
  // #4 
  // Create a new virtual module with the initial content
  const virtualModules = new VirtualModulesPlugin({
    [swaggerJsonPath]: JSON.stringify({
      openapi: '3.0.0',
      info: info
    })
  });
  
  // #5 
  // Set up webpack hooks to listen to `SwaggerPlugin` event
  virtualModules.apply(compiler);

  compiler.hooks.compilation.tap('SwaggerPlugin', function(compilation) {
    try {
      // Using swagger-jsdoc to generate a new virtual JSON file
      const swaggerJson = swaggerJsDoc({
        swaggerDefinition: {
          openapi: '3.0.0',
          info: info
        },
        apis: ['*.js', '!(node_modules)/**/*.js']
      });
      // Writing a new virtual module will happen each time the project is changed
      virtualModules.writeModule(swaggerJsonPath, JSON.stringify(swaggerJson));
    } catch (e) {
      compilation.errors.push(e);
    }
  });
}

Here's what happens in this file:

  1. The package.json module, path, and file are created
  2. We create sample data for the future virtual module swagger.json
  3. Using the path to package.json, we create a path to the virtual module
  4. We instantiate VirtualModulesPlugin() with an object for the new virtual module. The object must contain a path to the virtual file and stringified data to be written into the file
  5. virtualModules runs apply() to add webpack hooks to listen for the SwaggerPlugin event
  6. Inside the hook, the swagger.json file is generated

Our example is a bit more contrived than what we explained in the beginning of this section. For one, we created a webpack plugin to generate Swagger documentation. Notice how inside the plugin we invoke virtualModules.apply(compiler). This sets a few webpack hooks on the compiler to enable dynamic creation of webpack modules. Put simply, running virtualModules.apply(compiler) enables the compiler to start tracking the virtual module SwaggerPlugin.

Let's now get back to Swagger plugin. To make it work, we need to pass instantiated SwaggerPlugin into the plugins in webpack configurations:

1
2
3
4
5
6
7
// webpack configurations

module.exports = {
  entry: './index.js',
  plugins: [new SwaggerPlugin()], // Instantiate the plugin
  // ...
};

That's all. You can run the example, add your own comment with Swagger documentation or change the existing one, and view the updated documentation in your browser.


It's simple to use Webpack Virtual Modules plugin. Check out the plugin repository if you'd like to learn more about it.

If you're looking for a developer or considering starting a new project,
we are always ready to help!