expresswebpackexpress-handlebarswebpack-loader

webpack with express-handlebars


I'm trying to migrate an old express site to webpack, but don't wanna rework all the layout tempaltes which use express-handlebars. Is it possible to use express-handlebars with webpack?

handlebars-loader seems not to support the layout concept of express-handlebars, so is no help here.


Solution

  • A custom loader seems to do the trick:

    express-handlebars-loader.js:

    const loaderUtils = require('loader-utils');
    const validateOptions = require('schema-utils');
    const path = require('path');
    const express = require('express');
    const exphbs = require('express-handlebars');
    
    module.exports = function (content) {
        const options = loaderUtils.getOptions(this);
        const app = options.app;
        const contextCallback = options.contextCallback;
        const view = path.relative(options.basePath, this.resourcePath);
        const context = contextCallback(this.resourcePath, view);
        
        var loaderAsyncCallback = this.async();
        app.render(view, context, function (err, html) {
            if (err) {
                return loaderAsyncCallback(err);
            }
            
            const slug = 
                '// Module\n'
                + 'var code = ' + JSON.stringify(html) + ';\n'
                + '// Exports\n'
                + 'module.exports = code;'
            
            loaderAsyncCallback(null, slug);
        });
      
    };
    

    webpack.config.js:

    const CopyPlugin = require("copy-webpack-plugin");
    const HtmlWebpackPlugin = require('html-webpack-plugin');
    const path = require('path');
    const fs = require('fs');
    const url = require('url');
    const express = require('express');
    const exphbs = require('express-handlebars');
    
    const app = express();
    
    // Handlebars Setup
    
    /**
     * Instantiate a Handlebars instance with our config (default layout, helpers, etc.)
     */
    const handlebasInstance = exphbs.create({
      defaultLayout: 'mainLayout',
      // Specify helpers which are only registered on this instance.
      helpers
    });
    
    app.engine('handlebars', handlebasInstance.engine);
    app.set('view engine', 'handlebars');
    app.use('/assets', express.static('assets'));
    
    const basePath = path.resolve(__dirname, './views');
    
    function generateHtmlPlugins(templateDir) {
      const itemList = fs.readdirSync(templateDir);
      return itemList.flatMap(item => {
        const [ name, extension ] = item.split('.');
        if (extension == 'handlebars') {
            const templatePath = path.resolve(templateDir, item);
            const outputPath = path.resolve(templateDir, name + '.html');
            const outputName = path.relative(basePath, outputPath);
            return new HtmlWebpackPlugin({
              filename: outputName,
              inject: false,
              template: templatePath
            })
        } else {
            return [];
        }
      })
    }
    
    const siteHtmlPlugins = generateHtmlPlugins(basePath);
    
    function contextCallback(resourcePath, view) {
        var context = {};
        if (view.includes('documentation/')) {
            context.layout = 'documentationLayout';
        }
        return context;
    }
    
    
    module.exports = {
      mode: 'development',
      resolveLoader: {
        modules: [ 'node_modules', path.resolve(__dirname, 'loaders') ]
      },
      entry: './src/entry-workaround.js',
      output: {
        filename: 'entry-workaround.js',
        path: path.resolve(__dirname, 'public'),
      },
      module: {
        rules: [{
            test: /\.handlebars$/,
            loader: "express-handlebars-loader",
            options: {
                app: app,
                basePath: basePath,
                contextCallback: contextCallback,
            }
        }]
      },
      plugins: []
    };