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!