@nasriya/hypercloud
Version:
Nasriya HyperCloud is a lightweight Node.js HTTP2 framework.
897 lines (896 loc) • 47.3 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HyperCloudServer = void 0;
const http_1 = __importDefault(require("http"));
const http2_1 = __importDefault(require("http2"));
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const helpers_1 = __importDefault(require("./utils/helpers"));
const manager_1 = __importDefault(require("./services/ssl/manager"));
const initializer_1 = __importDefault(require("./services/handler/initializer"));
const response_1 = __importDefault(require("./services/handler/assets/response"));
const manager_2 = __importDefault(require("./services/renderer/manager"));
const manager_3 = __importDefault(require("./services/routes/manager"));
const routesInitiator_1 = __importDefault(require("./services/routes/assets/routesInitiator"));
const router_1 = __importDefault(require("./services/routes/assets/router"));
const manager_4 = __importDefault(require("./services/helmet/manager"));
const rateLimiter_1 = __importDefault(require("./services/rateLimiter/rateLimiter"));
const manager_5 = __importDefault(require("./services/languages/manager"));
const uploads_1 = __importDefault(require("./services/uploads/uploads"));
const _dirname = __dirname;
/**HyperCloud HTTP2 server */
class HyperCloudServer {
#_receivedReqNum = 0;
#_system = {
httpServer: undefined,
httpsServer: undefined
};
#_helmet;
#_config = {
secure: false,
ssl: {
type: 'selfSigned',
storePath: undefined,
letsEncrypt: {
email: undefined,
domains: [],
certName: undefined,
staging: false,
challengePort: 80
},
credentials: {
cert: undefined,
key: undefined,
}
},
trusted_proxies: [],
locals: {},
cronJobs: {},
handlers: {},
siteName: {},
};
#_utils = Object.freeze({
config: {
/**
* @param {string} filePath The path of the configurations file. Or pass ```default``` to read from the default config. file.
* @returns {HyperCloudInitOptions} Initialization options
*/
read: (filePath) => {
if (filePath === 'default') {
filePath = path_1.default.resolve('./config.json');
}
try {
return helpers_1.default.loadJSON(filePath);
}
catch (error) {
if (error instanceof Error) {
error.message = `Unable to read server's config file: ${error.message}`;
}
throw error;
}
},
/**
* @param {string} filePath
* @param {HyperCloudInitOptions} options
* @returns {void}
*/
save: (filePath, options) => {
if (filePath === 'default') {
filePath = path_1.default.resolve('./');
}
else {
if (!fs_1.default.existsSync(filePath)) {
fs_1.default.mkdirSync(filePath);
}
}
fs_1.default.writeFileSync(path_1.default.resolve(`${filePath}/config.json`), JSON.stringify(options, null, 4), { encoding: 'utf-8' });
}
},
beforeListen: {
/**
* @description Initialize the server. If the server is supposed to be secure, generate an SSL certificate first and then create the server.
* @returns {Promise<http2.Http2SecureServer | http.Server>} The initialized server
*/
init: async () => {
if (this.#_config.secure) {
const { cert, key } = await (async () => {
if (this.#_config.ssl.type === 'credentials') {
return { cert: this.#_config.ssl.credentials.cert, key: this.#_config.ssl.credentials.key };
}
else {
return new manager_1.default().generate({
type: this.#_config.ssl.type,
storePath: this.#_config.ssl.storePath,
letsEncrypt: this.#_config.ssl.letsEncrypt
});
}
})();
this.#_system.httpsServer = http2_1.default.createSecureServer({ cert, key, allowHTTP1: true });
}
else {
this.#_system.httpServer = http_1.default.createServer();
}
return this.#_server;
},
/**
* Gets the listen configurations for the server.
* If the server is configured to use HTTPS, it will listen on port 443, otherwise it will listen on port 80.
* If options are provided, it will override the default configurations.
* @param {ServerListeningConfigs} [options] The listen configurations.
* @returns {ServerListeningConfigs} The listen configurations.
*/
getConfigs: (options) => {
const config = {
host: '0.0.0.0',
port: this.#_config.secure ? 443 : 80,
};
if (options !== undefined) {
if (helpers_1.default.isNot.realObject(options)) {
throw new TypeError(`The options object should be a real object, instead got ${typeof options}`);
}
if (helpers_1.default.hasOwnProperty(options, 'ipv6Only')) {
const ipv6Only = options.ipv6Only;
if (typeof ipv6Only !== 'boolean') {
throw new TypeError(`The options.ipv6Only should be a boolean, instead got ${typeof ipv6Only}`);
}
config.host = '::';
}
else {
if (helpers_1.default.hasOwnProperty(options, 'host')) {
const host = options.host;
if (typeof host !== 'string') {
throw new TypeError(`The options.host should be a string, instead got ${typeof host}`);
}
const validHosts = ['::', '::1', 'localhost'];
if (!validHosts.includes(host) && !helpers_1.default.validate.ipAddress(host)) {
throw new TypeError(`The options.host should be a valid IP address, instead got ${host}`);
}
config.host = host;
}
}
if (helpers_1.default.hasOwnProperty(options, 'port')) {
const port = options.port;
if (typeof port !== 'number') {
throw new TypeError(`The options.port should be a number, instead got ${typeof port}`);
}
if (port <= 0) {
throw new RangeError(`The options.port has been assigned an invalid value (${port}). Ports are numbers greater than zero`);
}
config.port = port;
}
if (helpers_1.default.hasOwnProperty(options, 'onListen')) {
const onListen = options.onListen;
if (typeof onListen !== 'function') {
throw new TypeError(`The options.onListen should be a function, instead got ${typeof onListen}`);
}
config.onListen = onListen;
}
if (helpers_1.default.hasOwnProperty(options, 'backlog')) {
const backlog = options.backlog;
if (typeof backlog !== 'number') {
throw new TypeError(`The options.backlog should be a number, instead got ${typeof backlog}`);
}
if (backlog <= 0) {
throw new RangeError(`The options.backlog has been assigned an invalid value (${backlog}). Backlog is a number greater than zero`);
}
config.backlog = backlog;
}
if (helpers_1.default.hasOwnProperty(options, 'exclusive')) {
const exclusive = options.exclusive;
if (typeof exclusive !== 'boolean') {
throw new TypeError(`The options.exclusive should be a boolean, instead got ${typeof exclusive}`);
}
config.exclusive = exclusive;
}
}
return config;
},
/**
* Scans for pages and components, and updates the cache storage.
*
* @returns {Promise<void>}
*/
scanSSRAssets: async () => {
helpers_1.default.printConsole('#'.repeat(50));
helpers_1.default.printConsole('Scanning for pages and components');
const scanResult = await Promise.allSettled([this.rendering.pages.scan(), this.rendering.components.scan()]);
const rejected = scanResult.filter(i => i.status === 'rejected');
if (rejected.length > 0) {
const error = new Error(`Unable to scan for pages and components`);
// @ts-ignore
error.details = `Reasons:\n${rejected.map(i => `- ${i.reason}`).join('\n')}`;
throw error;
}
helpers_1.default.printConsole('#'.repeat(50));
helpers_1.default.printConsole('#'.repeat(50));
helpers_1.default.printConsole('Checking/Updating cache storage');
await this.rendering.cache.update.everything();
helpers_1.default.printConsole('#'.repeat(50));
}
},
});
#_serverEvents = {
/**
* Handles incoming HTTP requests and sends appropriate responses.
*
* This asynchronous function processes each request by incrementing the request count,
* generating a unique request ID, and creating `HyperCloudRequest` and `HyperCloudResponse`
* objects. It sets specific server headers and attempts to match the request with available
* routes. If a match is found, it initializes route management with `RequestRoutesManager`.
* Otherwise, it sends a 404 Not Found response.
*
* In case of errors during request processing, it attempts to send a 500 Server Error response.
*
* @param {any} req - The incoming HTTP request object.
* @param {any} res - The HTTP response object to be sent back to the client.
*/
request: async (req, res) => {
/**A copy of the response to throw an error */
let resTemp = {};
try {
res.on('close', () => {
if (resTemp) {
resTemp._closed = true;
}
});
this.#_receivedReqNum++;
const request_id = `ns${btoa(`request-num:${this.#_receivedReqNum};date:${new Date().toISOString()}`)}`;
// @ts-ignore
req.id = request_id;
const request = await initializer_1.default.createRequest(this, req, { trusted_proxies: this.#_config.trusted_proxies });
const response = initializer_1.default.createResponse(this, request, res);
resTemp = response;
// Set the custom server headers
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-Server', 'Nasriya HyperCloud');
res.setHeader('X-Request-ID', request_id);
const matchedRoutes = this._routesManager.match(request);
if (matchedRoutes.length > 0) {
new routesInitiator_1.default(matchedRoutes, request, response);
}
else {
response.status(404).pages.notFound();
}
}
catch (error) {
if (!helpers_1.default.is.undefined(resTemp) && resTemp instanceof response_1.default) {
resTemp.pages.serverError({ error: error });
}
else {
console.error(error);
res.statusCode = 500;
res.end();
}
}
},
/**
* Returns an error handler function for a specific port.
*
* If the provided error indicates that the port is already in use (`EADDRINUSE`),
* it closes the server and throws a descriptive error.
*
* **Note:** When this handler is used, it must be called with a port number as an argument.
* It cannot be referenced directly as a callback.
*
* @param {number} port - The port number to associate with the error handler.
* @throws {Error} If the error code is `EADDRINUSE`, indicating the port is already in use.
*/
port: (port) => {
return (err) => {
if (err.code === 'EADDRINUSE') {
this.#_server.close();
throw new Error(`Port #${port} is already in use, please choose another port`);
}
};
}
};
constructor(userOptions, addOpt) {
this.languages = new manager_5.default();
this._routesManager = new manager_3.default();
this.#_helmet = new manager_4.default(this);
this.uploads = new uploads_1.default;
this.rateLimiter = new rateLimiter_1.default(this);
this.rendering = new manager_2.default(this);
this.rendering.pages.register(path_1.default.resolve(path_1.default.join(_dirname, './services/pages')));
try {
if (helpers_1.default.is.undefined(userOptions)) {
return;
}
if (helpers_1.default.isNot.realObject(userOptions)) {
throw `The server configuration is expecting an object, but instead got ${typeof userOptions}`;
}
let isFromFile = false;
if ('path' in userOptions) {
// Initialize the server from the config file.
if (typeof userOptions.path !== 'string') {
throw `The server configuration path must be a string, instead got ${typeof userOptions.path}`;
}
const savedConfig = this.#_utils.config.read(userOptions.path);
userOptions = savedConfig;
isFromFile = true;
}
const options = userOptions;
const config_src = isFromFile ? 'file' : 'options';
if ('secure' in options) {
if (typeof options.secure !== 'boolean') {
throw `The secure option in the configuration ${config_src} is expecting a boolean value, instead got ${typeof options.secure}`;
}
if (options.secure === true) {
this.#_config.secure = true;
if ('ssl' in options) {
if (helpers_1.default.is.undefined(options.ssl) || helpers_1.default.isNot.realObject(options.ssl)) {
throw `The SSL options used in server configurations ${config_src} is expectd to be an object, instead got ${options.ssl}`;
}
switch (options.ssl.type) {
case 'credentials':
{
if ('credentials' in options) {
if (helpers_1.default.is.undefined(options.ssl.credentials) || helpers_1.default.isNot.realObject(options.ssl.credentials)) {
throw `The SSL "credentials" option is expecting an object, but instead got ${typeof options.ssl.credentials}`;
}
if ('cert' in options.ssl.credentials && 'key' in options.ssl.credentials) {
if ('cert' in options.ssl.credentials) {
if (typeof options.ssl.credentials.cert !== 'string') {
throw `The "cert" option in server's configuration ${config_src} is expecting a string value, instead got ${typeof options.ssl.credentials.cert}`;
}
if (!(options.ssl.credentials.cert.startsWith('---- BEGIN CERTIFICATE----') && options.ssl.credentials.cert.startsWith('----END CERTIFICATE----'))) {
throw `The provided certificate (cert) in the configuration ${config_src} is not a valid certificate`;
}
this.#_config.ssl.credentials.cert = options.ssl.credentials.cert;
}
if ('key' in options.ssl.credentials) {
if (typeof options.ssl.credentials.key !== 'string') {
throw `The "key" option in server's configuration ${config_src} is expecting a string value, instead got ${typeof options.ssl.credentials.key}`;
}
if (!(options.ssl.credentials.key.startsWith('-----BEGIN') && options.ssl.credentials.key.startsWith('PRIVATE KEY-----'))) {
throw `The provided private key (key) in the configuration ${config_src} is not a valid key`;
}
this.#_config.ssl.credentials.key = options.ssl.credentials.key;
}
}
else {
throw `The SSL credentials object has been passed but is missing the "cert" and/or "key" values.`;
}
}
else {
throw `The SSL type was set to "credentials" without specifying a credentials object`;
}
}
break;
case 'letsEncrypt': {
if (helpers_1.default.is.undefined(options.ssl.letsEncrypt) || helpers_1.default.isNot.realObject(options.ssl.letsEncrypt)) {
throw `The SSL "letsEncrypt" option is expecting an object, but instead got ${typeof options.ssl.letsEncrypt}`;
}
if ('email' in options.ssl.letsEncrypt && !helpers_1.default.is.undefined(options.ssl.letsEncrypt)) {
if (!helpers_1.default.is.validString(options.ssl.letsEncrypt.email)) {
throw `The options.ssl.letsEncrypt.email option in the configuration ${config_src} is expecting a string value, instead got ${typeof options.ssl.letsEncrypt.email}`;
}
if (!helpers_1.default.validate.email(options.ssl.letsEncrypt.email)) {
throw `The provided options.ssl.letsEncrypt.email (${options.ssl.letsEncrypt.email}) is not a valid email address`;
}
this.#_config.ssl.letsEncrypt.email = options.ssl.letsEncrypt.email;
}
else {
throw `The "email" SSL option in the configuration ${config_src} is missing.`;
}
if ('domains' in options.ssl.letsEncrypt && !helpers_1.default.is.undefined(options.ssl.letsEncrypt.domains)) {
if (Array.isArray(options.ssl.letsEncrypt.domains)) {
if (!helpers_1.default.validate.domains(options.ssl.letsEncrypt.domains)) {
throw `The provided domains array (${options.ssl.letsEncrypt.domains.join(', ')}) is not a valid domains array`;
}
this.#_config.ssl.letsEncrypt.domains = options.ssl.letsEncrypt.domains;
}
else {
throw `The options.ssl.letsEncrypt.domains property expected an array as a value but instead got ${typeof options.ssl.letsEncrypt.domains}`;
}
}
else {
throw `The options.ssl.letsEncrypt.domains option in the configuration ${config_src} is missing.`;
}
if ('certName' in options.ssl.letsEncrypt && !helpers_1.default.is.undefined(options.ssl.letsEncrypt.certName)) {
if (!helpers_1.default.is.validString(options.ssl.letsEncrypt.certName)) {
throw `The options.ssl.letsEncrypt.certName option in the configuration ${config_src} is expecting a string value, instead got ${typeof options.ssl.letsEncrypt.certName}`;
}
this.#_config.ssl.letsEncrypt.certName = options.ssl.letsEncrypt.certName;
}
else {
this.#_config.ssl.letsEncrypt.certName = helpers_1.default.getProjectName().replace(/-/g, '');
}
if ('staging' in options.ssl.letsEncrypt && !helpers_1.default.is.undefined(options.ssl.letsEncrypt.staging)) {
if (typeof options.ssl.letsEncrypt.staging !== 'boolean') {
throw `The typeof options.ssl.letsEncrypt.staging option was used with an invalid value. Expected a boolean value but got ${typeof options.ssl.letsEncrypt.staging}`;
}
this.#_config.ssl.letsEncrypt.staging = options.ssl.letsEncrypt.staging;
}
if ('challengePort' in options.ssl.letsEncrypt && !helpers_1.default.is.undefined(options.ssl.letsEncrypt.challengePort)) {
if (typeof options.ssl.letsEncrypt.challengePort !== 'number') {
throw `The options.ssl.letsEncrypt.challengePort is expecting a number value, but instead got ${typeof options.ssl.letsEncrypt.challengePort}`;
}
if (options.ssl.letsEncrypt.challengePort <= 0) {
throw `The options.ssl.letsEncrypt.challengePort is expecting a port number greater than zero. You choosed: ${options.ssl.letsEncrypt.challengePort}`;
}
this.#_config.ssl.letsEncrypt.challengePort = options.ssl.letsEncrypt.challengePort;
}
}
}
if ('storePath' in options.ssl && !helpers_1.default.is.undefined(options.ssl.storePath)) {
const validity = helpers_1.default.checkPathAccessibility(options.ssl.storePath);
if (validity.valid === true) {
this.#_config.ssl.storePath = options.ssl.storePath;
}
else {
if (validity.errors.notString) {
throw new TypeError(`Invalid "storePath" was provided. Expected a string but instead got ${typeof options.ssl.storePath}`);
}
if (validity.errors.doesntExist) {
throw new Error(`The "storePath" that you've provided (${options.ssl.storePath}) doesn't exist`);
}
if (validity.errors.notAccessible) {
throw Error(`You don't have enough read permissions to access ${options.ssl.storePath}`);
}
}
}
else {
this.#_config.ssl.storePath = path_1.default.join(process.cwd(), 'SSL');
}
}
else {
this.#_config.ssl.type = 'selfSigned';
}
}
}
if ('proxy' in options && !helpers_1.default.is.undefined(options.proxy)) {
if (helpers_1.default.isNot.realObject(options.proxy)) {
throw `The options.proxy expected a real object but instead got ${typeof options.proxy}`;
}
const validProxies = [];
if ('isLocal' in options.proxy && options.proxy.isLocal === true) {
validProxies.push('127.0.0.1');
}
if ('isDockerContainer' in options.proxy && options.proxy.isDockerContainer === true) {
validProxies.push('172.17.0.1');
}
if ('trusted_proxies' in options.proxy) {
const invalidProxies = [];
if (!Array.isArray(options.proxy?.trusted_proxies)) {
throw `The server expected an array of trusted proxies in the options.proxy.trusted_proxies property but instead got ${typeof options.proxy.trusted_proxies}`;
}
for (let proxy of options.proxy.trusted_proxies) {
if (proxy === 'localhost') {
proxy = '127.0.0.1';
}
if (proxy === 'docker') {
proxy = '172.17.0.1';
}
if (helpers_1.default.validate.ipAddress(proxy)) {
if (!validProxies.includes(proxy)) {
validProxies.push(proxy);
}
}
else {
if (!invalidProxies.includes(proxy)) {
invalidProxies.push(proxy);
}
}
}
if (invalidProxies.length > 0) {
helpers_1.default.printConsole(invalidProxies);
throw `The server expected an array of trusted proxies, but some of them were invalid: ${invalidProxies.join(', ')}`;
}
}
if (validProxies.length === 0) {
throw `The 'proxy' option in the HyperCloud server was used without valid proxy IP addresses.`;
}
this.#_config.trusted_proxies = validProxies;
}
if ('languages' in options) {
if (helpers_1.default.is.undefined(options.languages) || helpers_1.default.isNot.realObject(options.languages)) {
throw `The options.languages option has been used with an invalid value. Expected an object but instead got ${typeof options.languages}`;
}
if ('supported' in options.languages && !helpers_1.default.is.undefined(options.languages.supported)) {
this.languages.supported = options.languages.supported;
}
if ('default' in options.languages && !helpers_1.default.is.undefined(options.languages.default)) {
this.languages.default = options.languages.default;
}
}
if ('locals' in options && !helpers_1.default.is.undefined(options.locals)) {
this.rendering.assets.locals.set(options.locals);
}
if ('handlers' in options && !helpers_1.default.is.undefined(options.handlers)) {
if (helpers_1.default.isNot.realObject(options.handlers)) {
throw `The options.handler was used with an invalid value. Expected an object but instead got ${typeof options.handlers}`;
}
for (const name in options.handlers) {
const handlerName = name;
this.handlers[handlerName](options.handlers[handlerName]);
}
}
if (!isFromFile) {
if (!helpers_1.default.is.undefined(addOpt) && helpers_1.default.is.realObject(addOpt)) {
if ('saveConfig' in addOpt) {
if (typeof addOpt.saveConfig !== 'boolean') {
throw `The saveConfig option in the server's management options expects a boolean value, but instead got ${addOpt.saveConfig}`;
}
if (addOpt.saveConfig === true) {
const savePath = (() => {
if ('configPath' in addOpt) {
if (helpers_1.default.is.undefined(addOpt.configPath) || !helpers_1.default.is.validString(addOpt.configPath)) {
throw `The "configPath" option in the server's management options expects a string value, but instead got ${addOpt.configPath}`;
}
return addOpt.configPath;
}
else {
return 'default';
}
})();
const toSave = {
secure: this.#_config.secure,
ssl: this.#_config.ssl,
proxy: { trusted_proxies: this.#_config.trusted_proxies },
locals: this.#_config.locals,
languages: {
default: this.languages.default,
supported: this.languages.supported
},
};
this.#_utils.config.save(savePath, toSave);
}
}
}
}
}
catch (error) {
if (typeof error === 'string') {
error = `Cannot initialize the server: ${error}`;
}
helpers_1.default.printConsole(error);
throw new Error('Cannot initialize the server');
}
}
/**
* Configuration object for handling file uploads.
*
* This object provides settings to manage and enforce upload limits for different types of files. It includes general limits for images and videos, as well as specific limits based on MIME types. The MIME type-specific limits take precedence over the general image and video limits. The configuration allows for the following:
*
* - Setting and retrieving maximum file size limits for images and videos.
* - Setting and retrieving file size limits for specific MIME types.
* - Removing limits by setting them to `0`.
*
* The configuration is intended to help manage resource usage and ensure that uploads adhere to defined size constraints.
*
* Example usage:
*
* ```ts
* // Set a maximum file size of 10 MB for images
* server.uploads.limits.images.set(10 * 1024 * 1024);
*
* // Set a maximum file size of 50 MB for videos
* server.uploads.limits.videos.set(50 * 1024 * 1024);
*
* // Set a maximum file size of 5 MB for application/pdf MIME type
* server.uploads.limits.mime.set('application/pdf', 5 * 1024 * 1024);
*
* // Get the maximum file size limit for images
* const imageLimit = server.uploads.limits.images.get();
*
* // Get the maximum file size limit for a specific MIME type
* const pdfLimit = server.uploads.limits.mime.get('application/pdf');
* ```
*/
uploads;
/**
* Set or get your site/brand name. This name is used
* for rendering pages and in other places
*/
siteName = {
/**
* Set your site's name
* @example
* server.siteName.set('Nasriya Software'); // Setting a name for the default language
* server.siteName.set('ناصرية سوفتوير', 'ar'); // Setting a name for the "ar" language
* @param name The name of your site or brand
* @param lang The language you want your site name to be associated with
*/
set: (name, lang) => {
if (!helpers_1.default.is.validString(name)) {
throw new Error(`The site name must be a string, but instead got ${typeof name}`);
}
if (lang === undefined) {
lang = this.languages.default;
}
else {
if (this.languages.supported.includes(lang)) {
this.#_config.siteName[lang] = name;
}
else {
throw new Error(`The language you choose (${lang}) for your (${name}) site name is not supported. Make sure to first add "${lang}" to the supported languages`);
}
}
},
/**
* Get the name of your site/brand based on the language
* @example
* // Getting the name of the default language
* server.siteName.get(); // returns: "Nasriya Software"
* server.siteName.get('ar'); // returns: "ناصرية سوفتوير"
* @param lang The language your site name is associated with
*/
get: (lang) => {
if (lang === undefined) {
lang = this.languages.default;
}
if (!this.languages.supported.includes(lang)) {
throw new Error(`Unable to get the site name for the "${lang}" language because it's not a supported language`);
}
return this.#_config.siteName[lang];
},
/**
* Set multiple site names for different languages.
* @example
* server.siteName.multilingual({
* default: 'Nasriya Software',
* ar: 'ناصرية سوفتوير'
* });
* @param record An object where the keys are language codes and the values are the site names.
*/
multilingual: (record) => {
if (helpers_1.default.isNot.realObject(record)) {
throw new TypeError(`The server's multilingual site names' can only be an object, instead got ${typeof record}`);
}
if ('default' in record) {
record[this.languages.default] = record.default;
delete record.default;
}
else {
throw new Error(`The server's multilingual site names' object is missing the "default" language`);
}
for (const lang in record) {
if (helpers_1.default.isNot.validString(record[lang])) {
throw new TypeError(`One the site names' multilingual object is expected to be a key:value pairs of strings, instead, one of the values ${record[lang]} was ${typeof record[lang]}`);
}
this.#_config.siteName[lang] = record[lang];
}
}
};
languages;
rateLimiter;
rendering;
/**@private */
_routesManager;
/**@private */
get _handlers() { return this.#_config.handlers; }
handlers = Object.freeze({
notFound: (handler) => {
const handlerName = 'notFound';
if (typeof handler !== 'function') {
throw new TypeError(`The provided handler isn't a function but a type of ${typeof handler}`);
}
const reqParams = 3;
const handlerParams = handler.length;
if (handlerParams !== reqParams) {
throw new RangeError(`The provided handler has ${handlerParams} parameters. The expected number of parameters is ${reqParams}`);
}
this.#_config.handlers[handlerName] = handler;
},
serverError: (handler) => {
const handlerName = 'serverError';
if (typeof handler !== 'function') {
throw new TypeError(`The provided handler isn't a function but a type of ${typeof handler}`);
}
const reqParams = 3;
const handlerParams = handler.length;
if (handlerParams !== reqParams) {
throw new RangeError(`The provided handler has ${handlerParams} parameters. The expected number of parameters is ${reqParams}`);
}
this.#_config.handlers[handlerName] = handler;
}, unauthorized: (handler) => {
const handlerName = 'unauthorized';
if (typeof handler !== 'function') {
throw new TypeError(`The provided handler isn't a function but a type of ${typeof handler}`);
}
const reqParams = 3;
const handlerParams = handler.length;
if (handlerParams !== reqParams) {
throw new RangeError(`The provided handler has ${handlerParams} parameters. The expected number of parameters is ${reqParams}`);
}
this.#_config.handlers[handlerName] = handler;
}, forbidden: (handler) => {
const handlerName = 'forbidden';
if (typeof handler !== 'function') {
throw new TypeError(`The provided handler isn't a function but a type of ${typeof handler}`);
}
const reqParams = 3;
const handlerParams = handler.length;
if (handlerParams !== reqParams) {
throw new RangeError(`The provided handler has ${handlerParams} parameters. The expected number of parameters is ${reqParams}`);
}
this.#_config.handlers[handlerName] = handler;
}, userSessions: (handler) => {
const handlerName = 'userSessions';
if (typeof handler !== 'function') {
throw new TypeError(`The provided handler isn't a function but a type of ${typeof handler}`);
}
const reqParams = 3;
const handlerParams = handler.length;
if (handlerParams !== reqParams) {
throw new RangeError(`The provided handler has ${handlerParams} parameters. The expected number of parameters is ${reqParams}`);
}
this.#_config.handlers[handlerName] = handler;
}, logger: (handler) => {
const handlerName = 'logger';
if (typeof handler !== 'function') {
throw new TypeError(`The provided handler isn't a function but a type of ${typeof handler}`);
}
const reqParams = 3;
const handlerParams = handler.length;
if (handlerParams !== reqParams) {
throw new RangeError(`The provided handler has ${handlerParams} parameters. The expected number of parameters is ${reqParams}`);
}
this.#_config.handlers[handlerName] = handler;
}, onHTTPError: (handler) => {
const handlerName = 'onHTTPError';
if (typeof handler !== 'function') {
throw new TypeError(`The provided handler isn't a function but a type of ${typeof handler}`);
}
const reqParams = 4;
const handlerParams = handler.length;
if (handlerParams !== reqParams) {
throw new RangeError(`The provided handler has ${handlerParams} parameters. The expected number of parameters is ${reqParams}`);
}
this.#_config.handlers[handlerName] = handler;
}
});
/**
* A protection "helmet" module that serves as a middleware or multiple middlewares
* that you can use on your routes.
*
* You can customize the behavior with options
*/
helmet(options) { this.#_helmet.config(options); }
/**
* Increase productivity by spreading routes into multiple files. All
* you need to do is to `export` the created server into the file that
* you want to create routes on, then mount the routes on the `Router`.
*
* **Example**:
* ```ts
* // Main file: main.js
* import hypercloud from '@nasriya/hypercloud';
* const server = hypercloud.Server();
*
* const router = server.Router();
*
* // Create routes on the main file
* router.get('/', (request, response) => {
* response.status(200).end(<h1>HyperCloud</h1>);
* })
*
* // Export the router
* module.exports = server;
* ```
* Now import the server on the API file:
* ```ts
* import server from './main.js';
*
* // Define a router for the APIs. All routes defined on this
* // router will be under the `api` sub-domain, unless
* // explicitly specified.
* const router = server.Router({subDomain: 'api'});
*
* router.get('v1/users', (request, response) => {
* response.status(200).json([{id: 'ahmad_id', name: 'Ahmad', role: 'Admin'}])
* })
*
* router.post('v1/users', (request, response) => {
* response.status(201).json(request.body)
* })
* ```
* Each created `Router` has a reference to the `HyperCloudServer`
* that created it. So routes are automatically mounted on the server.
* @param {{ caseSensitive?: boolean, subDomain?: string}} [options]
* @returns {Router}
*/
Router(options) {
return new router_1.default(this, options || {});
}
/**
* Extend the functionality of the server
* @param value
* @example
* import { Router } from '@nasriya/hypercloud';
*
* const router = new Router();
*
* router.get('/', (req, res, next) => {
* console.log(req.__toJSON());
* next();
* })
*
* server.extend(router);
*/
extend(value) {
if (value instanceof router_1.default) {
if (!value._data) {
return;
}
const routes = [...value._data.routes.dynamic, ...value._data.routes.static];
for (const route of routes) {
this._routesManager.add(route);
}
return;
}
}
/**
* Starts the server and makes it listen on a specified port or the default port.
* @param options - The options object that can be used to configure the server.
* @returns A promise that resolves when the server is listening.
* @example
* server.listen({
* host: 'localhost',
* port: 8080,
* onListen: (host, port) => console.log(`Server is listening on ${host}:${port}`),
* backlog: 100,
* exclusive: false
* });
*/
async listen(options) {
try {
const server = await this.#_utils.beforeListen.init();
const config = this.#_utils.beforeListen.getConfigs(options);
await this.#_utils.beforeListen.scanSSRAssets();
server.on('request', this.#_serverEvents.request);
server.on('error', this.#_serverEvents.port(config.port));
return new Promise((resolve, reject) => {
try {
server.listen(config, () => {
console.info(`HyperCloud Server is listening ${this.#_config.secure ? 'securely ' : ''}on port #${config.port}`);
config.onListen?.(config.host, config.port);
resolve();
});
}
catch (error) {
reject(error);
}
});
}
catch (error) {
if (typeof error === 'string') {
error = `Unable to start listening: ${error}`;
}
throw error;
}
}
/**
* Stops the server from accepting new connections and keeps existing connections.
* This method is asynchronous, the server is finally closed when all connections
* are ended and the server emits a `close` event. The optional callback will be
* called once the `close` event occurs. Unlike that event, it will be called with
* an Error as its only argument if the server was not open when it was closed.
* @param callback Called when the server is closed.
*/
close(callback) {
const runningServer = this.#_system.httpServer ? 'http' : 'https';
const finalCallback = typeof callback === 'function' ? callback : (err) => {
console.error(err?.message || err);
console.info(`HyperCloud HTTP${runningServer === 'https' ? 's' : ''} Server is now closed.`);
};
if (this.#_system.httpServer) {
this.#_system.httpServer.close(finalCallback);
}
if (this.#_system.httpsServer) {
this.#_system.httpsServer.close(finalCallback);
}
return this;
}
/**
* Returns the server instance during the listening state
* @returns {http2.Http2SecureServer | http.Server}
*/
get #_server() { return (this.#_config.secure ? this.#_system.httpsServer : this.#_system.httpServer); }
}
exports.HyperCloudServer = HyperCloudServer;
exports.default = HyperCloudServer;
;