UNPKG

oneside

Version:

OneSide is a HTTP Node Server for websites.

723 lines (720 loc) 29 kB
'use strict'; var __createBinding = (this && this.__createBinding) || (Object.create ? function (o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function () { return m[k]; }, }); } : function (o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; }); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? function (o, v) { Object.defineProperty(o, 'default', { enumerable: true, value: v }); } : function (o, v) { o['default'] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== 'default' && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator['throw'](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __asyncValues = (this && this.__asyncValues) || function (o) { if (!Symbol.asyncIterator) throw new TypeError('Symbol.asyncIterator is not defined.'); var m = o[Symbol.asyncIterator], i; return m ? m.call(o) : ((o = typeof __values === 'function' ? __values(o) : o[Symbol.iterator]()), (i = {}), verb('next'), verb('throw'), verb('return'), (i[Symbol.asyncIterator] = function () { return this; }), i); function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { (v = o[n](v)), settle(resolve, reject, v.done, v.value); }); }; } function settle(resolve, reject, d, v) { Promise.resolve(v).then(function (v) { resolve({ value: v, done: d }); }, reject); } }; var __importDefault = (this && this.__importDefault) || function (mod) { return mod && mod.__esModule ? mod : { default: mod }; }; Object.defineProperty(exports, '__esModule', { value: true }); exports.Application = void 0; const http_1 = require('http'); const deepmerge_1 = __importDefault(require('deepmerge')); const fs_1 = require('fs'); const path_1 = __importStar(require('path')); const cli_progress_1 = __importDefault(require('cli-progress')); const fs_extra_1 = require('fs-extra'); const ansi_colors_1 = __importDefault(require('ansi-colors')); const tiny_glob_1 = __importDefault(require('tiny-glob')); const cheerio_1 = __importDefault(require('cheerio')); const line_reader_1 = __importDefault(require('line-reader')); const html_minifier_1 = require('html-minifier'); const socket_io_1 = __importDefault(require('socket.io')); const portfinder_1 = __importDefault(require('portfinder')); const dns_1 = __importDefault(require('dns')); const open_1 = __importDefault(require('open')); const chokidar_1 = __importDefault(require('chokidar')); const os_1 = require('os'); const mime_types_1 = __importDefault(require('mime-types')); const url_1 = __importDefault(require('url')); const router_1 = require('./router'); const utils_1 = require('./utils'); const response_1 = require('./response'); const next_1 = require('./next'); class Application extends router_1.Router { constructor(settings) { super(); this.middlewares = []; this.settings = { port: 5000, address: 'localhost', openAutomatically: true, showCompiling: true, global: {}, baseFile: 'index.ejs', useLocalIp: false, favicon: '', ignores: [], publicPaths: [], reloadPaths: [], printPublicUrl: true, parseCookies: true, parserBody: true, paths: { views: './views', public: './public', components: './components', routes: './routes', }, }; this.dev = process.argv.length > 2 && process.argv[2].toLowerCase() === 'dev'; this.settings = (0, deepmerge_1.default)(this.settings, settings); if (this.settings.baseFile === '' || !(0, fs_1.existsSync)((0, path_1.join)(process.cwd(), this.settings.baseFile))) (0, utils_1.print)('error', `Base file ${this.settings.baseFile} not found !`, true); if (this.settings.favicon !== '') this.public((0, path_1.resolve)(this.settings.favicon)); if (this.settings.paths.public !== '') this.public(this.settings.paths.public); this.settings.publicPaths.forEach((pblPath) => { this.public(pblPath); }); this.server = (0, http_1.createServer)((req, res) => { const method = req.method !== undefined ? req.method.toLocaleLowerCase() : null; const reqEndpoint = req.url !== undefined ? req.url.split('?')[0] : null; if (method && reqEndpoint) { if (this.endpoints[method.toLocaleLowerCase()]) { let found = false; for (const endpoint of this.endpoints[method.toLocaleLowerCase()]) { const params = urlMatch(reqEndpoint, endpoint.path); if (params !== null) { found = true; if (endpoint.static) { res.writeHead(200, { 'Content-Type': mime_types_1.default.contentType((0, path_1.basename)(endpoint.path)).toString(), }); const buffer = (0, fs_1.createReadStream)((0, path_1.join)(process.cwd(), endpoint.path)); buffer.on('open', () => buffer.pipe(res)); } else { const data = []; let body; req .on('data', (chunk) => { data.push(chunk); }) .on('end', () => { body = Buffer.concat(data).toString(); const request = { params, ip: req.socket.remoteAddress || '', url: req.url || '', endpoint: reqEndpoint, hostname: req.headers.host || '', method, queries: url_1.default.parse(req.url || '', true).query, body: this.settings.parserBody ? parseBody(body) : '', cookies: this.settings.parseCookies ? parseCookies(req.headers.cookie || '') : {}, }; const response = new response_1.Response(res, { global: this.settings.global, ejs: {}, file: '', useCache: false, dev: this.dev, }); if (this.middlewares.length === 0) { if (endpoint.middlewares.length === 1) { endpoint.middlewares[0](request, response); } else { const next = new next_1.Next(request, response, endpoint.middlewares, this.notFoundEndpoint); endpoint.middlewares[0](request, response, next); } } else { const middlewares = this.middlewares.concat(endpoint.middlewares); const next = new next_1.Next(request, response, middlewares, this.notFoundEndpoint); middlewares[0](request, response, next); } }); } break; } } if (!found && !res.writableEnded) { if (this.notFoundEndpoint) { const request = { params: {}, ip: req.socket.remoteAddress || '', url: req.url || '', endpoint: '', hostname: req.headers.host || '', method, queries: url_1.default.parse(req.url || '', true).query, body: '', cookies: {}, }; const response = new response_1.Response(res, { global: this.settings.global, ejs: {}, file: '', useCache: false, dev: this.dev, }); this.notFoundEndpoint(request, response, undefined); } else { res.writeHead(404, { 'Content-Type': 'text/html', }); res.end(`<p>Endpoint ${method.toUpperCase()} ${reqEndpoint} not found</p>`); } } } else { if (this.notFoundEndpoint) { const request = { params: {}, ip: req.socket.remoteAddress || '', url: req.url || '', endpoint: '', hostname: req.headers.host || '', method, queries: url_1.default.parse(req.url || '', true).query, body: '', cookies: {}, }; const response = new response_1.Response(res, { global: this.settings.global, ejs: {}, file: '', useCache: false, dev: this.dev, }); this.notFoundEndpoint(request, response, undefined); } else { res.writeHead(404, { 'Content-Type': 'text/html', }); res.end(`<p>Endpoint ${method.toUpperCase()} ${reqEndpoint} not found</p>`); } } } else { if (this.notFoundEndpoint) { const request = { params: {}, ip: req.socket.remoteAddress || '', url: req.url || '', endpoint: '', hostname: req.headers.host || '', method: method || '', queries: url_1.default.parse(req.url || '', true).query, body: '', cookies: {}, }; const response = new response_1.Response(res, { global: this.settings.global, ejs: {}, file: '', useCache: false, dev: this.dev, }); this.notFoundEndpoint(request, response, undefined); } else { res.writeHead(404, { 'Content-Type': 'text/html', }); res.end(`<p>Endpoint ${method} ${reqEndpoint} not found</p>`); } } }); if (this.dev) { this.io = new socket_io_1.default.Server(this.server); (0, utils_1.print)('info', 'Dev mode activated'); } } notFound(callback) { this.notFoundEndpoint = callback; } use(...args) { if (typeof args[0] === 'string') { if (args[1] !== undefined) { if (args[1] instanceof router_1.Router) { for (const [key, value] of Object.entries(args[1].endpoints)) { if (!this.endpoints[key]) this.endpoints[key] = []; value.forEach((val) => { if (typeof args[0] === 'string') { val.path = args[0] + '/' + val.path; val.path = val.path .split('/') .filter((el) => el !== '') .join('/'); if (val.path === '') val.path = '/'; if (!val.path.startsWith('/')) val.path = '/' + val.path; } this.endpoints[key].push(val); }); } } else this.middlewares.push(args[1]); } else { (0, utils_1.print)('error', 'Middleware or Router undefined !', true); } } else { if (args[0] instanceof router_1.Router) { for (const [key, value] of Object.entries(args[0].endpoints)) { if (!this.endpoints[key]) this.endpoints[key] = []; this.endpoints[key].push(...value); } } else this.middlewares.push(args[0]); } } listen(callback) { const nmlz = path_1.default.normalize(this.settings.paths.routes.replace(/ /g, '-').toLocaleLowerCase()); (0, tiny_glob_1.default)(`${this.settings.paths.routes}/**/*.js`) .then((files) => { files.forEach((file) => __awaiter(this, void 0, void 0, function* () { const fl = file.replace(/ /g, '-').toLocaleLowerCase().replace(nmlz, '').replace(/\\/g, '/').split('/'); if (fl[fl.length - 1] === 'index.js') fl.pop(); let endpoint = ''; if (fl.length === 0) endpoint = '/'; else endpoint = fl.join('/').replace('.js', '') + '/'; Promise.resolve() .then(() => __importStar( require((0, path_1.resolve)( file .replace(path_1.default.normalize(this.settings.paths.routes), this.settings.paths.routes) .replace(/\\/g, '/'), )), ), ) .then((route) => { this.use(endpoint, route.default); }); }), ); }) .catch((err) => { (0, utils_1.print)('error', err, true); }) .finally(() => { compile( this.settings.paths.views, getBaseHtml(this.settings.baseFile), this.settings.showCompiling, this.dev, () => { portfinder_1.default.getPort( { port: this.settings.port, }, (err, port) => { if (err) (0, utils_1.print)('error', 'All ports seem to be busy !', true); if (port !== this.settings.port) (0, utils_1.print)('info', `Port ${this.settings.port} is already in use`); dns_1.default.lookup((0, os_1.hostname)(), (dnsErr, add) => { if (dnsErr && this.settings.useLocalIp) (0, utils_1.print)('error', `Unable to retrieve local IP address !\n${dnsErr}`, true); if (this.settings.useLocalIp) this.settings.address = add; if (this.dev) { if (this.io) { this.io.on('connection', (socket) => { socket.emit('connected_live'); }); this.settings.ignores.push('./node_modules'); this.settings.ignores.push('./compiled'); this.settings.ignores.forEach((pth, i) => { if (typeof pth === 'string') this.settings.ignores[i] = (0, path_1.resolve)(pth); }); this.settings.ignores.push(/(^|[\\])\../); this.settings.reloadPaths.push(this.settings.paths.views); chokidar_1.default .watch(process.cwd(), { ignoreInitial: true, ignored: this.settings.ignores, persistent: true, }) .on('all', (event, path) => { var _a; if ( path.includes((0, path_1.resolve)(this.settings.paths.public)) || path.includes((0, path_1.resolve)(this.settings.paths.components)) ) { (_a = this.io) === null || _a === void 0 ? void 0 : _a.sockets.emit('reload_live'); } else if ( path === (0, path_1.join)(process.cwd(), this.settings.baseFile) || this.settings.reloadPaths.some((el) => path.includes((0, path_1.resolve)(el))) ) { compile( this.settings.paths.views, getBaseHtml(this.settings.baseFile), this.settings.showCompiling, true, () => { var _a; (_a = this.io) === null || _a === void 0 ? void 0 : _a.sockets.emit('reload_live'); }, ); } else { if (process.send) process.send('restart'); } }); } this.server.listen(port, this.settings.address, () => { var _a; (0, utils_1.print)('log', `Server OneSide started on http://${this.settings.address}:${port} !`); if (this.settings.printPublicUrl) (0, utils_1.print)( 'gray', `Public folder Url: http://${this.settings.address}:${port}${(0, utils_1.normalize)( this.settings.paths.public, )}`, ); if (process.argv[3] === 'first') { if (this.settings.openAutomatically) (0, open_1.default)(`http://${this.settings.address}:${port}`); } else { (_a = this.io) === null || _a === void 0 ? void 0 : _a.sockets.emit('reload_live'); } if (callback) callback(); }); } else { this.server.listen(port, this.settings.address, () => { (0, utils_1.print)('log', `Server OneSide started on http://${this.settings.address}:${port} !`); if (this.settings.printPublicUrl) (0, utils_1.print)( 'gray', `Public folder Url: http://${this.settings.address}:${port}${(0, utils_1.normalize)( this.settings.paths.public, )}`, ); if (callback) callback(); }); } }); }, ); }, ); }); } } exports.Application = Application; function parseCookies(cookieHeader) { const list = {}; if (!cookieHeader) return list; cookieHeader.split(`;`).forEach((cookie) => { const [name, ...rest] = cookie.split(`=`); const nm = name === null || name === void 0 ? void 0 : name.trim(); if (!nm) return; const value = rest.join(`=`).trim(); if (!value) return; list[nm] = decodeURIComponent(value); }); return list; } function parseBody(body) { try { if (body === '') return body; return JSON.parse(body); } catch (e) { return body; } } function getBaseHtml(baseFile) { const baseHtml = (0, fs_1.readFileSync)((0, path_1.join)(process.cwd(), baseFile), { encoding: 'utf-8' }); if (baseHtml === '') (0, utils_1.print)('error', 'Base file is empty !', true); const miss = []; if (!baseHtml.includes('<body')) miss.push('body tag'); if (!baseHtml.includes('<head')) miss.push('head tag'); if (miss.length > 0) (0, utils_1.print)('error', `Missing ${miss.join(' and ')} in base file !`, true); return baseHtml; } function urlMatch(reqUrl, routeUrl) { if (reqUrl == routeUrl) return {}; const reqSplt = reqUrl.replace(/\/$/, '').split('/'); const routeSplt = routeUrl.replace(/\/$/, '').split('/'); if (reqSplt.length != routeSplt.length) return null; const params = {}; for (let i = 0; i < routeSplt.length; i++) { if (routeSplt[i].startsWith(':')) params[routeSplt[i].replace(':', '')] = reqSplt[i]; else if (routeSplt[i] != reqSplt[i]) return null; } return params; } function compile(pages, baseHtml, showCompiling, dev, callback) { const path = (0, path_1.resolve)('./compiled'); const bar = showCompiling ? new cli_progress_1.default.SingleBar({ format: ansi_colors_1.default.blue('[INFO] Compiling {value}/{total} pages in {duration}s'), hideCursor: true, }) : null; if (!(0, fs_1.existsSync)(path)) (0, fs_1.mkdirSync)(path); else (0, fs_extra_1.emptyDirSync)(path); (0, tiny_glob_1.default)(`${pages}/**/*.ejs`) .then((views) => { var views_1, views_1_1; return __awaiter(this, void 0, void 0, function* () { var e_1, _a; if (bar) bar.start(views.length, 0); let pageId = 0; try { for (views_1 = __asyncValues(views); (views_1_1 = yield views_1.next()), !views_1_1.done; ) { const el = views_1_1.value; pageId++; if (bar) bar.update(pageId); const splt = el.replace(/\\/g, '/').split('/'); splt.shift(); const pth = './compiled/' + splt.join('/'); yield (0, fs_1.mkdir)((0, path_1.dirname)(pth), { recursive: true }, (err) => __awaiter(this, void 0, void 0, function* () { if (err) (0, utils_1.print)('error', 'Failed to compile !', true); let $ = cheerio_1.default.load(baseHtml); const splt2 = el.replace(/\\/g, '/').split('/'); splt2.shift(); const pth2 = (0, path_1.resolve)((0, path_1.join)(pages, splt2.join('/'))); const lines = []; const fileTags = []; const tags = ['title', 'description', 'keywords', 'author', 'viewport']; yield line_reader_1.default.eachLine( pth2, (line) => { const lnTag = tags.find((tag) => line.trim().startsWith(`:${tag}:`)); if (lnTag) fileTags.push({ name: lnTag, value: line.replace(`:${lnTag}:`, '').trim(), }); else if (!isEmptyOrSpaces(line)) lines.push(line); }, () => __awaiter(this, void 0, void 0, function* () { var e_2, _b, e_3, _c; let pageHtml = lines.join('\n'); try { for ( var fileTags_1 = __asyncValues(fileTags), fileTags_1_1; (fileTags_1_1 = yield fileTags_1.next()), !fileTags_1_1.done; ) { const tag = fileTags_1_1.value; switch (tag.name) { case 'title': if ($('title').length > 0) $('title').first().text(tag.value); else $('head').append(`<title>${tag.value}</title>`); break; default: if ($(`meta[name="${tag.name}"]`).length > 0) $(`meta[name="${tag.name}"]`).attr('content', tag.value); else $('head').append(`<meta name="${tag.name}" content="${tag.value}">`); break; } } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (fileTags_1_1 && !fileTags_1_1.done && (_b = fileTags_1.return)) yield _b.call(fileTags_1); } finally { if (e_2) throw e_2.error; } } if (pageHtml !== '') { if (pageHtml.includes('<body') || pageHtml.includes('<head')) (0, utils_1.print)('error', `Failed to compile ! Page ${el} contain body or head tag.`, true); const $2 = cheerio_1.default.load(pageHtml, null, false); try { for ( var _d = __asyncValues(['title', 'meta', 'link', 'style']), _e; (_e = yield _d.next()), !_e.done; ) { const pageTag = _e.value; $2(pageTag).each((i, item) => { const styleHtml = $2.html(item); $2(item).remove(); $('head').append(styleHtml); }); } } catch (e_3_1) { e_3 = { error: e_3_1 }; } finally { try { if (_e && !_e.done && (_c = _d.return)) yield _c.call(_d); } finally { if (e_3) throw e_3.error; } } pageHtml = $2.html(); const htmlSplt = splitOnce(pageHtml, '<script'); const body = $('body').children(); if (baseHtml.includes('$page$')) { $ = cheerio_1.default.load($.html().replace('$page$', htmlSplt[0])); } else { if (body.length > 0) { let found = false; for (let i = body.length - 1; i >= 0; i--) { if (!found && ($(body[i])[0].name !== 'script' || i === 0)) { found = true; $(body[i]).after(htmlSplt[0] + '$GLOBAL$'); } } } else { $('body').prepend(htmlSplt[0] + '$GLOBAL$'); } } if (htmlSplt.length > 1) $('body').append(htmlSplt[1]); } if (dev) { if (!$.html().includes('socket.io/socket.io.js')) { $('body').append(`<script src="/socket.io/socket.io.js"></script> <script> const socket = io(); let live_s_connected = false; socket.on('connected_live', () => { if(live_s_connected) location.reload() live_s_connected = true; console.log("Connected to OneSide Live Server !") }) socket.on('reload_live', () => { location.reload() }) </script>`); } else { $('body').append(`<script> let live_s_connected = false; socket.on('connected_live', () => { if(live_s_connected) location.reload() live_s_connected = true; console.log("Connected to OneSide Live Server !") }) socket.on('reload_live', () => { location.reload() }) </script>`); } } yield (0, fs_1.writeFileSync)( (0, path_1.resolve)(pth), unescapeHTML( (0, html_minifier_1.minify)($.html(), { removeComments: true, collapseWhitespace: true, }), ), ); }), ); }), ); } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (views_1_1 && !views_1_1.done && (_a = views_1.return)) yield _a.call(views_1); } finally { if (e_1) throw e_1.error; } } }); }) .catch((err) => { (0, utils_1.print)('failed', `Failed to compile !\n${err}`, true); }) .finally(() => { if (bar) bar.stop(); if (callback) callback(); }); } function isEmptyOrSpaces(str) { return str === null || str.match(/^ *$/) !== null; } function splitOnce(s, on) { const el = s.split(on); if (el.length > 1) return [el.shift() || '', on + el.join(on)]; else return [el[0]]; } function unescapeHTML(escapedHTML) { return escapedHTML.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&'); }