1

I would like to migrate an XSLT templating system to React and I've choose Express as the server of the React templates.

The server (Express) will serve to the client a template or another depending on the request hostname, I have JSONs with config of each hostname registered and it tells which template has to return.

The structure is the following:

  • A folder with a list of react projects:
templates/
    template_1/ (React Project)
        dist/server/template.js (Built)
        package.json
    template_2/ (React Project)
        dist/server/template.js (Built)
        package.json
  • An express folder for de server:
server/
    server.js (Express App)
        -> endpoint where depending on the hostname requestor it returns a template or another
        -> loads the dist/server/template.js file from the template folder
    renderer.jsx (React folder that renders the template for SSR)
builder/
    esbuild.server.js
    esbuild.template.js

Here I'll share the code of the relevant files:

// esbuild.server.js
require('esbuild')
    .build({
        entryPoints: ['server.js'],
        bundle: true,
        format: 'iife',
        external: ['express', 'react', 'react-dom', 'react-router-dom'],
        // minify: true,
        platform: 'node',
        outfile: 'dist/server.js',
        loader: {'.js': 'jsx'},
        logLevel: 'info',
        define: {'process.env.NODE_ENV': '"production"'},
    })
    .catch(() => process.exit(1));
// esbuild.template.js
const {existsSync, readFileSync, mkdirSync, writeFileSync, rmSync} = require('fs');
const {resolve} = require('path');

const {config} = require('dotenv');
config();

const template = process.argv.slice(2)[0] || process.env.npm_config_name;
const CONFIG_FOLDER = process.env.CONFIG_FOLDER || resolve('./config');

if (!template)
    throw new Error('No argument template specified, try: "--name=<template>"');

const TEMPLATES_FOLDER = './templates';
const TEMPLATE_PATH = `${TEMPLATES_FOLDER}/${template}`;

if (!existsSync(TEMPLATE_PATH))
    throw new Error(`The template ${TEMPLATE_PATH} doesn't exists`);

const getTemplateConfig = () => {
    try {
        return JSON.parse(readFileSync(`${TEMPLATE_PATH}/.template.json`));
    } catch (error) {
        return JSON.parse(readFileSync(`${CONFIG_FOLDER}/server/.template.json`));
    }
};

const templateConfig = getTemplateConfig();

if (!existsSync(`${TEMPLATE_PATH}/${templateConfig.app}`))
    throw new Error(
        `The template app ${TEMPLATE_PATH}/${templateConfig.app} doesn't exists`
    );

const renderer = readFileSync('./renderer.jsx', 'utf8').replace(
    /%TEMPLATE_PATH%/gim,
    resolve(`${TEMPLATE_PATH}/${templateConfig.app}`)
);
const rendererFile = `./tmp/renderer_${template}_${new Date().getTime()}.jsx`;

if (!existsSync('./tmp')) mkdirSync('./tmp');
writeFileSync(rendererFile, renderer);

require('esbuild')
    .build({
        entryPoints: [rendererFile],
        bundle: true,
        format: 'iife',
        minify: true,
        platform: 'node',
        outfile: `${TEMPLATE_PATH}/dist/server/template.js`,
        drop: ['debugger', 'console'],
        loader: {'.js': 'jsx'},
        logLevel: 'info',
        legalComments: 'none',
    })
    .then((response) => {
        if (response.errors.length > 0) console.error(response.errors);
        if (response.warnings.length > 0) console.warn(response.warnings);
    })
    .catch((error) => console.log(error))
    .finally(() => {
        rmSync(rendererFile);
    });

As you can see I use the renderer.jsx as a builder for the templates, I create a renderer_template_1_<date>.js file that contains the template with the renderToPipeableStream for SSR, if I do not have all that in the same compiled file I would have to React instances.

// renderer.jsx
import React from 'react';
import {renderToPipeableStream} from 'react-dom/server';
import {StaticRouter} from 'react-router-dom/server';
import Template from '%TEMPLATE_PATH%';

function App({url}) {
    return (
        <StaticRouter location={url}>
            <Template />
        </StaticRouter>
    );
}

function renderer({url, res, next, writable}) {
    const stream = renderToPipeableStream(<App url={url} />, {
        onShellReady() {
            res.setHeader('Content-type', 'text/html');
            stream.pipe(writable);
        },
        onShellError(error) {
            res.status(500);
            if (next) return next(error);
            console.error(error);
        },
        onError(error) {
            console.error(error);
        },
    });
}

export default renderer;

and finally the server endpoint:

const render = require(`${templateDirPath}/dist/server/template.js`).default;
console.log(render);

const writable = new HtmlWritable();
writable.on('finish', () => {
    const html = writable.getHtml();
    const response = indexHtml.replace(
        '<div id="root"></div>',
        `<div id="root">${html}</div>`
    );
    res.send(response);
});

render({req, res, next, writable});

I used the HtmlWritable from: React SSR with custom html

The Problem

The load of the renderer does not work in server.js endpoint, I've got a list of errors:

  • If I load into a vm.Script:
evalmachine.<anonymous>:1
[...]

Error: Dynamic require of "stream" is not supported
    at evalmachine.<anonymous>:1:431
    at evalmachine.<anonymous>:41:17631
    at evalmachine.<anonymous>:72:19751
    at evalmachine.<anonymous>:1:511
    at evalmachine.<anonymous>:103:18485
    at evalmachine.<anonymous>:1:511
    at evalmachine.<anonymous>:531:125320
    at evalmachine.<anonymous>:531:126039
    at Script.runInThisContext (node:vm:129:12)
    at /home/mjcabrer/Workspace/git/mbe-web2/dist/server.js:162:36
  • If I run it with mjs I get the same error of Error: Dynamic require of "stream" is not supported.
  • If I render for none node platform then the renderToPipeableStream is not in the built.

Questions

  • Is there a way better than that to serve react templates depending on the hostname?
  • Is there an standard for doing something like that?

Thanks a lot for your read and interest!

Wyck
  • 10,311
  • 6
  • 39
  • 60

0 Answers0