mirror of
https://github.com/noodlapp/noodl.git
synced 2026-01-12 07:12:52 +01:00
Initial commit
Co-Authored-By: Eric Tuvesson <eric.tuvesson@gmail.com> Co-Authored-By: mikaeltellhed <2311083+mikaeltellhed@users.noreply.github.com> Co-Authored-By: kotte <14197736+mrtamagotchi@users.noreply.github.com> Co-Authored-By: Anders Larsson <64838990+anders-topp@users.noreply.github.com> Co-Authored-By: Johan <4934465+joolsus@users.noreply.github.com> Co-Authored-By: Tore Knudsen <18231882+torekndsn@users.noreply.github.com> Co-Authored-By: victoratndl <99176179+victoratndl@users.noreply.github.com>
This commit is contained in:
194
packages/noodl-viewer-react/static/ssr/index.js
Normal file
194
packages/noodl-viewer-react/static/ssr/index.js
Normal file
@@ -0,0 +1,194 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import express from 'express';
|
||||
import fetch from 'node-fetch';
|
||||
import React from 'react';
|
||||
import ReactDOMServer from 'react-dom/server';
|
||||
import NodeCache from "node-cache";
|
||||
import { XMLHttpRequest } from 'xmlhttprequest';
|
||||
|
||||
const myCache = new NodeCache();
|
||||
async function cacheFetch(args, callback) {
|
||||
const cacheKey = typeof args === 'string' ? args : args.key;
|
||||
const cached = myCache.get(cacheKey);
|
||||
if (cached) return Promise.resolve(cached);
|
||||
|
||||
const result = await callback();
|
||||
myCache.set(cacheKey, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// In the DOM, these are global.
|
||||
globalThis.React = React;
|
||||
globalThis.ReactDOM = ReactDOMServer;
|
||||
globalThis.XMLHttpRequest = XMLHttpRequest;
|
||||
globalThis.File = class File {};
|
||||
|
||||
globalThis.__noodl_modules = [];
|
||||
globalThis.Noodl = {
|
||||
defineModule: function (m) {
|
||||
globalThis.__noodl_modules.push(m);
|
||||
},
|
||||
deployed: true
|
||||
};
|
||||
|
||||
globalThis.projectData = {{#export#}};
|
||||
|
||||
// Add some ugly polyfill
|
||||
globalThis.requestAnimationFrame = (callback) => setImmediate(callback);
|
||||
globalThis.fetch = async (args) => {
|
||||
if (typeof args === 'string') {
|
||||
const relativePath = '.' + args;
|
||||
if (args.startsWith('/noodl_bundles') && fs.existsSync(relativePath)) {
|
||||
const fileContent = await fs.promises.readFile(relativePath, 'utf-8');
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
json() {
|
||||
return Promise.resolve(JSON.parse(fileContent));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return await fetch(args);
|
||||
};
|
||||
|
||||
class LocalStorageMock {
|
||||
constructor() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
getItem(key) {
|
||||
return this.store[key] || null;
|
||||
}
|
||||
|
||||
setItem(key, value) {
|
||||
this.store[key] = value.toString();
|
||||
}
|
||||
|
||||
removeItem(key) {
|
||||
delete this.store[key];
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.store = {};
|
||||
}
|
||||
|
||||
get(key) {
|
||||
return this.store[key] || null;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this.store[key] = value.toString();
|
||||
}
|
||||
|
||||
delete(key) {
|
||||
delete this.store[key];
|
||||
}
|
||||
|
||||
// Allow direct access like localStorageMock['key']
|
||||
get store() {
|
||||
return this._store;
|
||||
}
|
||||
|
||||
set store(data) {
|
||||
this._store = data;
|
||||
}
|
||||
}
|
||||
|
||||
globalThis.localStorage = new LocalStorageMock();
|
||||
|
||||
// Import the Noodl runtime
|
||||
require('./noodl.deploy');
|
||||
|
||||
// From that file we get some runtime stuff defined on "NoodlSSR"
|
||||
const { createElement, ssrSetupRuntime } = globalThis.NoodlSSR;
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const app = express();
|
||||
|
||||
app.use(express.static('public', { index: false }));
|
||||
|
||||
function log(...args) {
|
||||
// Uncomment to see full request log
|
||||
// console.log(...args);
|
||||
}
|
||||
|
||||
let htmlData = '';
|
||||
|
||||
async function setup() {
|
||||
htmlData = await fs.promises.readFile(path.resolve('./public/index.html'), 'utf8');
|
||||
}
|
||||
|
||||
async function buildPage(path) {
|
||||
return new Promise((resolve) => {
|
||||
const noodlModules = globalThis.__noodl_modules;
|
||||
const projectData = globalThis.projectData;
|
||||
|
||||
// TODO: Maybe fix page router
|
||||
globalThis.location = {
|
||||
pathname: path,
|
||||
search: ""
|
||||
}
|
||||
|
||||
log('Create Component...');
|
||||
const ViewerComponent = createElement(noodlModules, projectData);
|
||||
log('created.');
|
||||
|
||||
const noodlRuntime = ViewerComponent.props.noodlRuntime;
|
||||
|
||||
noodlRuntime.eventEmitter.on('SSR_PageLoading', (id) => {
|
||||
console.log('SSR_PageLoading', id);
|
||||
});
|
||||
|
||||
noodlRuntime.eventEmitter.on('SSR_PageReady', (id) => {
|
||||
console.log('SSR_PageReady', id);
|
||||
});
|
||||
|
||||
noodlRuntime.eventEmitter.on('rootComponentUpdated', async () => {
|
||||
log('Spin up...');
|
||||
noodlRuntime.rootComponent.triggerDidMount();
|
||||
for (let index = 0; index < 1000; index++) {
|
||||
await new Promise((resolve) => setImmediate(() => resolve(), 0));
|
||||
noodlRuntime.rootComponent.triggerDidMount();
|
||||
noodlRuntime._doUpdate();
|
||||
}
|
||||
log('done.');
|
||||
|
||||
log('Rendering...');
|
||||
const output1 = ReactDOMServer.renderToString(ViewerComponent);
|
||||
log('result:', output1);
|
||||
|
||||
const result = htmlData.replace('<div id="root"></div>', `<div id="root">${output1}</div>`);
|
||||
|
||||
// TODO: Inject Noodl.SEO.meta
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
|
||||
log('Setup Runtime...');
|
||||
ssrSetupRuntime(noodlRuntime, noodlModules, projectData);
|
||||
log('done.');
|
||||
});
|
||||
}
|
||||
|
||||
app.get('*', async (req, res) => {
|
||||
const path = req.path;
|
||||
|
||||
try {
|
||||
const cacheKey = `cache__${path}`
|
||||
const cached = await cacheFetch(cacheKey, () => buildPage(req.path));
|
||||
res.send(cached);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
// We failed to render SSR, lets just respond with the index.html file,
|
||||
// and then the user should be able to render the page client side.
|
||||
res.send(htmlData);
|
||||
}
|
||||
});
|
||||
|
||||
setup().then(() => {
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server is listening on port ${PORT}`);
|
||||
});
|
||||
});
|
||||
5
packages/noodl-viewer-react/static/ssr/index.json
Normal file
5
packages/noodl-viewer-react/static/ssr/index.json
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{"url":"index.js", "injectExport": true},
|
||||
{"url":"noodl.deploy.js"},
|
||||
{"url":"package.json"}
|
||||
]
|
||||
25
packages/noodl-viewer-react/static/ssr/package.json
Normal file
25
packages/noodl-viewer-react/static/ssr/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "noodl-ssr",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "esbuild index.js --bundle --outfile=server.js --platform=node",
|
||||
"start": "node ./server.js",
|
||||
"dev": "node --inspect-brk ./server.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.18.10",
|
||||
"express": "^4.18.2",
|
||||
"node-fetch": "^3.3.1",
|
||||
"node-cache": "^5.1.2",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"xmlhttprequest": "^1.8.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.22"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user