@nasriya/hypercloud
Version:
Nasriya HyperCloud is a lightweight Node.js HTTP2 framework.
572 lines (571 loc) • 25.8 kB
JavaScript
import helpers from '../../utils/helpers.js';
import pagesManager from './managers/pagesManager.js';
import compsManager from './managers/componentsManager.js';
import GlobalAssets from './managers/globalAssetsManager.js';
/**
* This class is used inside a {@link HyperCloudServer} as
* `{@link HyperCloudServer["rendering"]}`
*/
class RenderingManager {
#_cache = Object.seal({
pages: { extensions: { css: false, js: false, json: false } },
components: { extensions: { css: false, js: false, json: false } },
globalAssets: { extensions: { css: false, js: false, json: false } }
});
#_server;
#_siteName = { default: null };
#_globalAssets;
#_assetsBaseUrl = '/_assets/renderer';
#_helpers = {
sendStylesheet: (data, res) => {
const { stylesheet, isCached, type } = data;
if (stylesheet) {
if (isCached) {
res.setHeader('etag', stylesheet.eTag);
return res.status(200).send(stylesheet.content, 'text/css');
}
else {
return res.status(200).sendFile(stylesheet.filePath, { eTag: stylesheet.eTag });
}
}
else {
return res.status(500).json({ message: `${type} asset was not found. Report this issue to the framework owners` });
}
},
sendScript: (data, res) => {
const { script, isCached, type } = data;
if (script) {
if (isCached) {
res.setHeader('etag', script.eTag);
return res.status(200).send(script.content, 'text/javascript');
}
else {
return res.status(200).sendFile(script.filePath, { eTag: script.eTag });
}
}
else {
return res.status(500).json({ message: `${type} asset was not found. Report this issue to the framework owners` });
}
},
sendFile: (data, res) => {
const { record, isCached, type, fileType } = data;
if (fileType === 'css') {
return this.#_helpers.sendStylesheet({ stylesheet: record, isCached, type }, res);
}
else {
return this.#_helpers.sendScript({ script: record, isCached, type }, res);
}
}
};
constructor(server) {
this.#_server = server;
this.#_globalAssets = new GlobalAssets(server);
const router = server.Router();
router.get(`${this.#_assetsBaseUrl}/global/<:fileType>/<:file>`, (req, res, next) => {
const fileType = req.params.fileType.toLowerCase();
if (!(fileType === 'css' || fileType === 'js')) {
return next();
}
const fileName = req.params.file;
const scriptFile = this.#_globalAssets.scripts.get().find(i => i.scope === 'Internal' && i.fileName === fileName);
const stylesheetFile = this.#_globalAssets.stylesheets.get().find(i => i.scope === 'Internal' && i.fileName === fileName);
const file = fileType === 'js' ? scriptFile : fileType === 'css' ? stylesheetFile : undefined;
if (!file) {
return next();
}
res.setHeader('Referrer-Policy', 'strict-origin');
return this.#_helpers.sendFile({ record: file, isCached: this.#_globalAssets.cache.status()[fileType], type: 'Global Asset', fileType }, res);
});
router.get(`${this.#_assetsBaseUrl}/components/<:compName>/<:file>`, (req, res, next) => {
const component = this.components.storage[req.params.compName];
if (!component) {
return next();
}
const fileName = req.params.file;
const fileType = fileName.toLowerCase().endsWith('.js') ? 'js' : fileName.toLowerCase().endsWith('.css') ? 'css' : null;
if (!fileType) {
return next();
}
const scriptFile = component.script.get();
const stylesheetFile = component.stylesheet.get();
const file = fileType === 'js' ? scriptFile : fileType === 'css' ? stylesheetFile : undefined;
if (!file) {
return next();
}
res.setHeader('Referrer-Policy', 'strict-origin');
return this.#_helpers.sendFile({ record: file, isCached: component.cache.status()[fileType], type: 'Component', fileType }, res);
});
router.get(`${this.#_assetsBaseUrl}/pages/<:pageId>/<:file>`, (req, res, next) => {
const page = this.pages.all.find(i => i._id === req.params.pageId);
if (!page) {
return next();
}
const fileName = req.params.file;
const fileType = fileName.toLowerCase().endsWith('.js') ? 'js' : fileName.toLowerCase().endsWith('.css') ? 'css' : null;
if (!fileType) {
return next();
}
const scriptFile = page.scripts.get().find(i => i.scope === 'Internal' && i.fileName === fileName);
const stylesheetFile = page.stylesheets.get().find(i => i.scope === 'Internal' && i.fileName === fileName);
const file = fileType === 'js' ? scriptFile : fileType === 'css' ? stylesheetFile : undefined;
if (!file) {
return next();
}
res.setHeader('Referrer-Policy', 'strict-origin');
return this.#_helpers.sendFile({ record: file, isCached: page.cache.status()[fileType], type: 'Page', fileType }, res);
});
}
/**
* Set or get your site/brand name. This will be used in page rendering.
* @example
* // Setup a default name
* server.rendering.siteName.set('Nasriya Software');
* // or
* server.rendering.siteName.set('Nasriya Software', 'default');
* // or
* server.rendering.siteName.multilingual.set({
* default: 'Nasriya Software'
* });
* @example
* // Setup multilingual names
* server.rendering.siteName.multilingual.set({
* default: 'Nasriya Software',
* ar: "ناصرية سوفتوير"
* });
*/
siteName = {
/**
* Set the name of your site or brand
* @param name The site/brand name
* @param lang The language you want to bind this name to (optional)
*/
set: (name, lang = 'default') => {
if (!helpers.is.validString(name)) {
throw new TypeError(`The site name can only a valid string, instead got ${typeof name}`);
}
if (lang !== 'default') {
if (this.#_server.languages.supported.includes(lang)) {
throw new SyntaxError(`The site name language ${lang} is not supported by your server. Make sure to set it up first`);
}
}
this.#_siteName[lang] = name;
},
/**Set the name of your site in different languages */
multilingual: {
/**
* Set your site or brand name in multiple languages
* @param names An object containing a `lang: name` pairs
* @example
* server.rendering.siteName.multilingual.set({
* default: 'Nasriya Software',
* ar: "ناصرية سوفتوير"
* });
*/
set: (names) => {
if (helpers.isNot.realObject(names)) {
throw new TypeError(`The siteName multilingual value is expected to be a real object. Instead got ${typeof names}`);
}
for (const lang in names) {
if (!helpers.is.validString(names[lang])) {
throw new TypeError(`The siteName multilingual object has one or more non-string values`);
}
}
if ('default' in names) {
for (const lang in names) {
this.#_siteName[lang] = names[lang];
}
}
else {
throw new SyntaxError(`The siteName multilingual object does not include a "default" name`);
}
}
},
/**
* Get the site/brand name
* @param lang The language of the site/brand name
*/
get: (lang = 'default') => {
return this.#_siteName[lang] || this.#_siteName.default;
}
};
components = compsManager;
pages = pagesManager;
/**Set global assets for your server */
get assets() { return this.#_globalAssets; }
/**The base URL of the assets used in the renderer */
get assetsBaseUrl() { return this.#_assetsBaseUrl; }
/**
* Increase your server's performance by enabling caching.
* Caching stores the files in memory (RAM) which is way faster than
* any other type of storage.
*
* **NOTE:**
*
* You can enable/disable caching of certain files
*/
cache = {
/**Enable caching for certain assets */
enableFor: {
/**
* Enable caching for certain files extensions for pages
* @example
* // Enable caching for all supported files
* server.rendering.cache.enableFor.pages();
* @example
* // Enable caching for JSON files
* server.rendering.cache.enableFor.pages(['json']);
* @example
* // Enable caching for JavaScript and CSS files
* server.rendering.cache.enableFor.pages(['js', 'css']);
* @param extensions
*/
pages: (extensions) => {
if (extensions === undefined) {
this.#_cache.pages.extensions.css = this.#_cache.pages.extensions.js = this.#_cache.pages.extensions.json = true;
}
else {
if (typeof extensions === 'string') {
extensions = [extensions];
}
for (const ext of extensions) {
if (Object.keys(this.#_cache).includes(ext)) {
this.#_cache.pages.extensions[ext] = true;
}
else {
throw new Error(`The extension ${ext} is not a supported extension by the rendering cache`);
}
}
}
},
/**
* Enable caching for certain files extensions for components
* @example
* // Enable caching for all supported files
* server.rendering.cache.enableFor.components();
* @example
* // Enable caching for JSON files
* server.rendering.cache.enableFor.components(['json']);
* @example
* // Enable caching for JavaScript and CSS files
* server.rendering.cache.enableFor.components(['js', 'css']);
* @param extensions
*/
components: (extensions) => {
if (extensions === undefined) {
this.#_cache.components.extensions.css = this.#_cache.components.extensions.js = this.#_cache.components.extensions.json = true;
}
else {
if (typeof extensions === 'string') {
extensions = [extensions];
}
for (const ext of extensions) {
if (Object.keys(this.#_cache).includes(ext)) {
this.#_cache.components.extensions[ext] = true;
}
else {
throw new Error(`The extension ${ext} is not a supported extension by the rendering cache`);
}
}
}
},
/**
* Enable caching for certain files extensions for global assets
* @example
* // Enable caching for all supported files
* server.rendering.cache.enableFor.globalAssets();
* @example
* // Enable caching for JSON files
* server.rendering.cache.enableFor.globalAssets(['json']);
* @example
* // Enable caching for JavaScript and CSS files
* server.rendering.cache.enableFor.globalAssets(['js', 'css']);
* @param extensions
*/
globalAssets: (extensions) => {
if (extensions === undefined) {
this.#_cache.globalAssets.extensions.css = this.#_cache.globalAssets.extensions.js = this.#_cache.globalAssets.extensions.json = true;
}
else {
if (typeof extensions === 'string') {
extensions = [extensions];
}
for (const ext of extensions) {
if (Object.keys(this.#_cache).includes(ext)) {
this.#_cache.globalAssets.extensions[ext] = true;
}
else {
throw new Error(`The extension ${ext} is not a supported extension by the rendering cache`);
}
}
}
},
/**
* Enable caching for certain files extensions for pages and components
* @example
* // Enable caching for all supported files
* server.rendering.cache.enableFor.everything();
* @example
* // Enable caching for JSON files
* server.rendering.cache.enableFor.everything(['json']);
* @example
* // Enable caching for JavaScript and CSS files
* server.rendering.cache.enableFor.everything(['js', 'css']);
* @param extensions
*/
everything: (extensions) => {
this.cache.enableFor.pages(extensions);
this.cache.enableFor.components(extensions);
this.cache.enableFor.globalAssets(extensions);
}
},
/**Disable caching for certain assets */
disableFor: {
/**
* Disable caching for certain files extensions for pages
* @example
* // Disable caching for all supported files
* server.rendering.cache.disableFor.pages();
* @example
* // Disable caching for JSON files
* server.rendering.cache.disableFor.pages(['json']);
* @example
* // Disable caching for JavaScript and CSS files
* server.rendering.cache.disableFor.pages(['js', 'css']);
* @param extensions
*/
pages: (extensions) => {
if (extensions === undefined) {
this.#_cache.pages.extensions.css = this.#_cache.pages.extensions.js = this.#_cache.pages.extensions.json = false;
}
else {
if (typeof extensions === 'string') {
extensions = [extensions];
}
for (const ext of extensions) {
if (Object.keys(this.#_cache).includes(ext)) {
this.#_cache.pages.extensions[ext] = false;
}
else {
throw new Error(`The extension ${ext} is not a supported extension by the rendering cache`);
}
}
}
},
/**
* Disable caching for certain files extensions for components
* @example
* // Disable caching for all supported files
* server.rendering.cache.disableFor.components();
* @example
* // Disable caching for JSON files
* server.rendering.cache.disableFor.components(['json']);
* @example
* // Disable caching for JavaScript and CSS files
* server.rendering.cache.disableFor.components(['js', 'css']);
* @param extensions
*/
components: (extensions) => {
if (extensions === undefined) {
this.#_cache.components.extensions.css = this.#_cache.components.extensions.js = this.#_cache.components.extensions.json = false;
}
else {
if (typeof extensions === 'string') {
extensions = [extensions];
}
for (const ext of extensions) {
if (Object.keys(this.#_cache).includes(ext)) {
this.#_cache.components.extensions[ext] = false;
}
else {
throw new Error(`The extension ${ext} is not a supported extension by the rendering cache`);
}
}
}
},
/**
* Disable caching for certain files extensions for global assets
* @example
* // Disable caching for all supported files
* server.rendering.cache.disableFor.globalAssets();
* @example
* // Disable caching for JSON files
* server.rendering.cache.disableFor.globalAssets(['json']);
* @example
* // Disable caching for JavaScript and CSS files
* server.rendering.cache.disableFor.globalAssets(['js', 'css']);
* @param extensions
*/
globalAssets: (extensions) => {
if (extensions === undefined) {
this.#_cache.globalAssets.extensions.css = this.#_cache.globalAssets.extensions.js = this.#_cache.globalAssets.extensions.json = false;
}
else {
if (typeof extensions === 'string') {
extensions = [extensions];
}
for (const ext of extensions) {
if (Object.keys(this.#_cache).includes(ext)) {
this.#_cache.globalAssets.extensions[ext] = false;
}
else {
throw new Error(`The extension ${ext} is not a supported extension by the rendering cache`);
}
}
}
},
/**
* Disable caching for certain files extensions for pages and components
* @example
* // Disable caching for all supported files
* server.rendering.cache.disableFor.everything();
* @example
* // Disable caching for JSON files
* server.rendering.cache.disableFor.everything(['json']);
* @example
* // Disable caching for JavaScript and CSS files
* server.rendering.cache.disableFor.everything(['js', 'css']);
* @param extensions
*/
everything: (extensions) => {
this.cache.disableFor.pages(extensions);
this.cache.disableFor.components(extensions);
this.cache.disableFor.globalAssets(extensions);
}
},
/**Read the caching status of assets */
statusOf: {
/**Read the caching status of supported files extensions for pages */
pages: () => this.#_cache.pages.extensions,
/**Read the caching status of supported files extensions for components */
components: () => this.#_cache.components.extensions,
/**Read the caching status of supported files extensions for global assets */
globalAssets: () => this.#_cache.globalAssets.extensions,
/**Read the caching status of supported files extensions for everything */
everything: () => {
return {
pages: this.#_cache.pages.extensions,
components: this.#_cache.components.extensions,
globalAssets: this.#_cache.globalAssets.extensions,
};
}
},
/**A module to update caching assets */
update: {
/**Update the cache of all pages */
pages: async () => {
const promises = this.pages.all.map(async (page) => {
const pageStatus = page.cache.status();
const enable = [];
const disable = [];
if (pageStatus.css !== this.#_cache.pages.extensions.css) {
if (this.#_cache.pages.extensions.css) {
enable.push('css');
}
else {
disable.push('css');
}
}
if (pageStatus.js !== this.#_cache.pages.extensions.js) {
if (this.#_cache.pages.extensions.js) {
enable.push('js');
}
else {
disable.push('js');
}
}
if (enable.length > 0) {
page.cache.enable(enable);
}
if (disable.length > 0) {
page.cache.disable(disable);
}
await page.cache.update();
});
const updateRes = await Promise.allSettled(promises);
const fullfilled = updateRes.filter(i => i.status === 'fulfilled');
const rejected = updateRes.filter(i => i.status === 'rejected');
return {
updateType: 'pages',
total: updateRes.length,
updated: fullfilled.length,
failed: { total: rejected.length, errors: rejected.map(i => i.reason) }
};
},
/**Update the cache of all the components */
components: async () => {
const promises = this.components.all.map(async (component) => {
const compStatus = component.cache.status();
const enable = [];
const disable = [];
if (compStatus.css !== this.#_cache.components.extensions.css) {
if (this.#_cache.components.extensions.css) {
enable.push('css');
}
else {
disable.push('css');
}
}
if (compStatus.js !== this.#_cache.components.extensions.js) {
if (this.#_cache.components.extensions.js) {
enable.push('js');
}
else {
disable.push('js');
}
}
if (enable.length > 0) {
component.cache.enable(enable);
}
if (disable.length > 0) {
component.cache.disable(disable);
}
await component.cache.update();
});
const updateRes = await Promise.allSettled(promises);
const fulfilled = updateRes.filter(i => i.status === 'fulfilled');
const rejected = updateRes.filter(i => i.status === 'rejected');
return {
updateType: 'components',
total: updateRes.length,
updated: fulfilled.length,
failed: { total: rejected.length, errors: rejected.map(i => i.reason) }
};
},
/**Update the cache of global assets */
globalAssets: async () => {
const assetsStatus = this.assets.cache.status();
const enable = [];
const disable = [];
if (assetsStatus.css !== this.#_cache.components.extensions.css) {
if (this.#_cache.globalAssets.extensions.css) {
enable.push('css');
}
else {
disable.push('css');
}
}
if (assetsStatus.js !== this.#_cache.components.extensions.js) {
if (this.#_cache.globalAssets.extensions.js) {
enable.push('js');
}
else {
disable.push('js');
}
}
if (enable.length > 0) {
this.assets.cache.enable(enable);
}
if (disable.length > 0) {
this.assets.cache.disable(disable);
}
await this.assets.cache.update();
},
/**Update the cache of everything */
everything: async () => {
const promises = [this.cache.update.pages(), this.cache.update.components(), this.cache.update.globalAssets()];
return Promise.all(promises);
}
}
};
}
export default RenderingManager;