@ima/server
Version:
Default dev server for IMA.js applications.
206 lines (178 loc) • 6.3 kB
JavaScript
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { renderMeta, encodeHTMLEntities } = require('./utils/metaUtils');
const {
renderScript,
renderStyles,
prepareDefaultResources,
} = require('./utils/resourcesUtils');
module.exports = function responseUtilsFactory({ applicationFolder }) {
const contentInterpolationRe = /#{([\w\d\-._$]+)}/g;
const runnerPath = path.resolve(
applicationFolder,
'./build/server/runner.js'
);
const manifestPath = path.resolve(applicationFolder, './build/manifest.json');
const uuidPrefix = `${Date.now().toString(36)}-${(
Math.random() * 2057
).toString(36)}`;
/**
* Load manifest, runner resources and prepare sources object.
*/
function _loadTemplates() {
const manifest = fs.existsSync(manifestPath)
? JSON.parse(fs.readFileSync(manifestPath, 'utf8'))
: {};
return {
manifest,
runner: fs.existsSync(runnerPath)
? fs.readFileSync(runnerPath, 'utf8')
: '',
};
}
function _getRevivalSettings({ res, settings, response }) {
const requestID = `${uuidPrefix}-${crypto.randomUUID()}`;
res.locals.requestID = requestID;
return `(function (root) {
root.$Debug = ${settings.$Debug};
root.$IMA = root.$IMA || {};
$IMA.SPA = ${response?.SPA ?? false};
$IMA.$PublicPath = "${process.env.IMA_PUBLIC_PATH ?? ''}";
$IMA.$RequestID = "${requestID}";
$IMA.$Language = "${
settings.$Language && encodeHTMLEntities(settings.$Language)
}";
$IMA.$Env = "${settings.$Env}";
$IMA.$Debug = ${settings.$Debug};
$IMA.$Version = "${settings.$Version}";
$IMA.$App = ${JSON.stringify(settings.$App)};
$IMA.$Protocol = "${
settings.$Protocol && encodeHTMLEntities(settings.$Protocol)
}";
$IMA.$Host = "${settings.$Host && encodeHTMLEntities(settings.$Host)}";
$IMA.$Path = "${settings.$Path && encodeHTMLEntities(settings.$Path)}";
$IMA.$Root = "${settings.$Root && encodeHTMLEntities(settings.$Root)}";
$IMA.$LanguagePartPath = "${
settings.$LanguagePartPath &&
encodeHTMLEntities(settings.$LanguagePartPath)
}";
})(typeof window !== 'undefined' && window !== null ? window : global);
`;
}
function _getRevivalCache({ response }) {
return `(function (root) {
root.$IMA = root.$IMA || {};
$IMA.Cache = ${response?.page?.cache ?? JSON.stringify({})};
})(typeof window !== 'undefined' && window !== null ? window : global);
`;
}
function _setCookieHeaders({ res, context }) {
for (let [name, param] of context?.response?.page?.cookie ?? []) {
const options = _prepareCookieOptionsForExpress(param.options);
res.cookie(name, param.value, options);
}
}
function _prepareCookieOptionsForExpress(options) {
let expressOptions = Object.assign({}, options);
if (typeof expressOptions.maxAge === 'number') {
expressOptions.maxAge *= 1000;
} else {
delete expressOptions.maxAge;
}
return expressOptions;
}
function sendResponseHeaders({ context, res }) {
_setCookieHeaders({ res, context });
res.set(context?.response?.page?.headers ?? {});
}
// Preload resources
let templates = _loadTemplates();
/**
* Create base set of content variables, containing meta tags,
* runner, revival settings, revival cache and JS and CSS source
* tags generated from manifest.json in build folder.
*
* @param event IMA hooks server event.
* @returns object Object with default set of content variables.
*/
function createContentVariables({ res, context }) {
const { response, bootConfig, app } = context;
if (!bootConfig?.settings) {
return {};
}
const metaManager = app?.oc?.get('$MetaManager');
const { settings } = bootConfig;
// Always reload resources in dev mode to have fresh copy
if (process.env.IMA_CLI_WATCH) {
templates = _loadTemplates();
}
const { manifest, runner } = templates;
const defaultResources = prepareDefaultResources(
manifest,
settings.$Language
);
// Get current file sources to load
const processedResources =
settings?.$Resources?.(response, manifest, defaultResources) ??
defaultResources;
return {
_: {
manifest,
resources: processedResources,
},
// Add slashes to "" to fix terser minification on runner code.
scriptResources: JSON.stringify({
scripts: processedResources.scripts,
esScripts: processedResources.esScripts,
}).replace(/"/g, '\\"'),
revivalSettings: renderScript(
'revival-settings',
_getRevivalSettings({ response, settings, res })
),
revivalCache: renderScript(
'revival-cache',
_getRevivalCache({ response })
),
runner: renderScript('runner', runner),
styles: renderStyles(
processedResources.esStyles ?? processedResources.styles
),
meta: renderMeta(metaManager),
};
}
/**
* Processes resposne content containing content variables #{...} placeholders,
* which are replaced by the contents of the variables. The replacement
* is done recursively, until there are no placeholders to interpolate.
*
* Additionally to content variables, you can use any value from bootConfig
* which is included in the interpolation process.
*
* @param event IMA hooks server event.
* @returns string Processed response content.
*/
function processContent({ context }) {
const { response, bootConfig } = context;
if (!response?.content || !bootConfig) {
return response?.content;
}
const { settings } = bootConfig;
const extendedSettings = { ...settings, ...response.contentVariables };
const interpolate = (_, envKey) => extendedSettings[envKey] ?? '';
while (response.content.match(contentInterpolationRe)) {
response.content = response.content.replace(
contentInterpolationRe,
interpolate
);
}
return response.content;
}
return {
createContentVariables,
processContent,
encodeHTMLEntities,
sendResponseHeaders,
_prepareCookieOptionsForExpress,
};
};