@trifrost/core
Version:
Blazingly fast, runtime-agnostic server framework for modern edge and node environments
244 lines (243 loc) • 7.79 kB
JavaScript
"use strict";
/* eslint-disable no-use-before-define */
Object.defineProperty(exports, "__esModule", { value: true });
exports.escape = escape;
exports.fromLruCookie = fromLruCookie;
exports.toLruCookie = toLruCookie;
exports.render = render;
exports.rootRender = rootRender;
const runtime_1 = require("./runtime");
const Engine_1 = require("./style/Engine");
const use_1 = require("./style/use");
const util_1 = require("./style/util");
const Engine_2 = require("./script/Engine");
const Script_1 = require("./script/Script");
const use_2 = require("./script/use");
const use_3 = require("./ctx/use");
const SCRIPT_LRU = 'tfscriptlru';
const MODULES_LRU = 'tfmoduleslru';
const VOID_TAGS = {
/* HTML */
area: true,
base: true,
br: true,
col: true,
embed: true,
hr: true,
img: true,
input: true,
link: true,
meta: true,
source: true,
track: true,
wbr: true,
/* SVG */
path: true,
circle: true,
ellipse: true,
line: true,
polygon: true,
polyline: true,
rect: true,
stop: true,
use: true,
};
const BOOL_PROPS = {
allowfullscreen: true,
async: true,
autofocus: true,
autoplay: true,
checked: true,
controls: true,
default: true,
defer: true,
disabled: true,
formnovalidate: true,
hidden: true,
ismap: true,
loop: true,
multiple: true,
muted: true,
nomodule: true,
novalidate: true,
open: true,
readonly: true,
required: true,
reversed: true,
selected: true,
};
const ESCAPE_LOOKUP = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
};
const RGX_ESCAPE = /(?:&(?![a-z#0-9]+;))|[<>"']/gi;
/**
* Escape HTML entities in strings to prevent XSS attacks.
*
* @param {string} str - Input string to escape.
*/
function escape(str) {
return str.replace(RGX_ESCAPE, (ch) => ESCAPE_LOOKUP[ch]);
}
/**
* Takes an lru cookie and converts it to a set
* @param {string|null} val - Value to convert
*/
function fromLruCookie(val) {
if (!val)
return new Set();
const acc = new Set();
const parts = val.split('|');
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part)
acc.add(part);
}
return acc;
}
/**
* Convert a set back to an LRU cookie
* @param {Set<string>} val - Set to convert
*/
function toLruCookie(val) {
if (!val.size)
return null;
/* We cap at 64 latest (lru) entries */
return [...val].slice(-64).join('|');
}
/**
* Renders properties such as style/attributes
*
* @param {Record<string, unknown>} props - Props to render
*/
function renderProps(props) {
let acc = '';
for (const key in props) {
const val = props[key];
switch (key) {
case 'style':
if (Object.prototype.toString.call(val) === '[object Object]') {
const style = (0, util_1.styleToString)(val);
if (style)
acc += ' style="' + escape(style) + '"';
}
break;
case 'children':
case 'dangerouslySetInnerHTML':
break;
case 'className': {
if (val !== undefined && val !== null)
acc += ' class="' + escape(val + '') + '"';
break;
}
default: {
if (val !== undefined && val !== null) {
acc += ' ' + key + (val !== true || !BOOL_PROPS[key] ? '="' + escape(val + '') + '"' : '');
}
break;
}
}
}
return acc;
}
/**
* Renders child elements
*/
function renderChildren(children, parentProps) {
if (!Array.isArray(children))
return children ? render(children, parentProps) : '';
let output = '';
for (let i = 0; i < children.length; i++)
output += render(children[i], parentProps);
return output;
}
/**
* Renders a JSXElement or primitive to a string.
* @param node - JSX tree or primitive.
*/
function render(node, parentProps = {}) {
switch (typeof node) {
case 'string':
return node ? escape(node) : '';
case 'number':
return node + '';
case 'boolean':
return '';
default: {
switch (typeof node?.type) {
case 'string': {
const tag = node.type;
if (tag === Script_1.SCRIPT_MARKER) {
if (node.props.fn_id) {
parentProps['data-tfhf'] = node.props.fn_id;
if (node.props.data_id)
parentProps['data-tfhd'] = node.props.data_id;
}
return ''; /* Dont render the marker */
}
/* Render children */
const innerHTML = typeof node.props.dangerouslySetInnerHTML?.__html === 'string'
? node.props.dangerouslySetInnerHTML.__html
: renderChildren(node.props.children, node.props);
return VOID_TAGS[tag]
? '<' + tag + renderProps(node.props) + ' />'
: '<' + tag + renderProps(node.props) + '>' + innerHTML + '</' + tag + '>';
}
case 'function':
return node.type === runtime_1.Fragment
? renderChildren(node.props.children, parentProps)
: render(node.type(node.props), parentProps);
default: {
if (!node) {
return '';
}
else if (Array.isArray(node)) {
let output = '';
for (let i = 0; i < node.length; i++)
output += render(node[i], parentProps);
return output;
}
else {
return '';
}
}
}
}
}
}
/**
* Starts the render process for a JSX element
*/
function rootRender(ctx, tree, options = {}) {
/* Instantiate globals */
const style_engine = (0, use_1.getActiveStyleEngine)() || (0, use_1.setActiveStyleEngine)(new Engine_1.StyleEngine());
const script_engine = (0, use_2.getActiveScriptEngine)() || (0, use_2.setActiveScriptEngine)(new Engine_2.ScriptEngine());
(0, use_3.setActiveCtx)(ctx);
/* Known scripts on frontend */
const script_lru_cookie = ctx.cookies.get(SCRIPT_LRU);
const script_lru = fromLruCookie(script_lru_cookie);
/* Known modules on frontend */
const module_lru_cookie = ctx.cookies.get(MODULES_LRU);
const module_lru = fromLruCookie(module_lru_cookie);
/* Auto-call root() if script or css provided in ctx config */
options?.script?.root?.();
options?.css?.root?.();
/* Render jsx to html */
const html = script_engine.inject(style_engine.inject(render(tree)), { scripts: script_lru, modules: module_lru });
/* Set script cookie */
const script_cookie = toLruCookie(script_lru);
if (script_cookie && script_cookie !== script_lru_cookie)
ctx.cookies.set(SCRIPT_LRU, script_cookie, { httponly: true, secure: true });
/* Set modules cookie */
const module_cookie = toLruCookie(module_lru);
if (module_cookie && module_cookie !== module_lru_cookie)
ctx.cookies.set(MODULES_LRU, module_cookie, { httponly: true, secure: true });
/* Cleanup globals */
(0, use_3.setActiveCtx)(null);
(0, use_1.setActiveStyleEngine)(null);
(0, use_2.setActiveScriptEngine)(null);
return html;
}