UNPKG

@mojojs/core

Version:

Real-time web framework

302 lines 11.5 kB
import { inspect } from 'node:util'; import { Logger } from '../logger.js'; import { exceptionContext, SafeString } from '../util.js'; import DOM from '@mojojs/dom'; /** * Default helper plugin. */ export default function defaultHelpersPlugin(app) { // The types for all helpers are in "types.ts" app.addHelper('exception', exception); app.addHelper('htmlException', htmlException); app.addHelper('htmlNotFound', htmlNotFound); app.addHelper('httpException', httpException); app.addHelper('jsonException', jsonException); app.addHelper('jsonNotFound', jsonNotFound); app.addHelper('txtException', txtException); app.addHelper('txtNotFound', txtNotFound); app.addHelper('notFound', notFound); app.addHelper('websocketException', websocketException); app.addHelper('proxyGet', proxyGet); app.addHelper('proxyPost', proxyPost); app.addHelper('proxyRequest', proxyRequest); app.addHelper('currentRoute', currentRoute); app.addHelper('include', include); app.addHelper('tags.asset', assetTag); const fields = [ 'color', 'date', 'datetime', 'email', 'month', 'number', 'range', 'search', 'tel', 'time', 'text', 'url', 'week' ]; for (const type of fields) { app.addHelper(`tags.${type}Field`, async (ctx, name, attrs = {}) => { return await inputTag(ctx, name, { ...attrs, type }); }); } app.addHelper('tags.input', inputTag); app.addHelper('tags.checkBox', checkBoxTag); app.addHelper('tags.radioButton', radioButtonTag); app.addHelper('tags.textArea', textAreaTag); app.addHelper('tags.buttonTo', buttonTo); app.addHelper('tags.favicon', faviconTag); app.addHelper('tags.fileField', fileFieldTag); app.addHelper('tags.formFor', formFor); app.addHelper('tags.hiddenField', hiddenFieldTag); app.addHelper('tags.image', imageTag); app.addHelper('tags.labelFor', labelFor); app.addHelper('tags.linkTo', linkTo); app.addHelper('tags.passwordField', passwordFieldTag); app.addHelper('tags.script', scriptTag); app.addHelper('tags.style', styleTag); app.addHelper('tags.submitButton', submitButtonTag); // Temporary workaround for backwards compatibility, to be removed in 2.0 app.addHelper('assetTag', assetTag); for (const type of fields) { app.addHelper(`${type}FieldTag`, async (ctx, name, attrs = {}) => { return await inputTag(ctx, name, { ...attrs, type }); }); } app.addHelper('inputTag', inputTag); app.addHelper('checkBoxTag', checkBoxTag); app.addHelper('radioButtonTag', radioButtonTag); app.addHelper('textAreaTag', textAreaTag); app.addHelper('buttonTo', buttonTo); app.addHelper('faviconTag', faviconTag); app.addHelper('fileFieldTag', fileFieldTag); app.addHelper('formFor', formFor); app.addHelper('hiddenFieldTag', hiddenFieldTag); app.addHelper('imageTag', imageTag); app.addHelper('labelFor', labelFor); app.addHelper('linkTo', linkTo); app.addHelper('passwordFieldTag', passwordFieldTag); app.addHelper('scriptTag', scriptTag); app.addHelper('styleTag', styleTag); app.addHelper('submitButtonTag', submitButtonTag); app.addHelper('tag', tag); app.decorateContext('inspect', (object, options) => inspect(object, options)); } function assetTag(ctx, path, attrs) { const url = ctx.urlForAsset(path); if (/\.js$/.test(url)) return ctx.tag('script', { src: url, ...attrs }); if (/\.css$/.test(url)) return ctx.tag('link', { rel: 'stylesheet', href: url, ...attrs }); return ctx.tag('img', { src: url, ...attrs }); } function buttonTo(ctx, target, attrs, text) { return ctx.formFor(target, attrs, ctx.submitButtonTag(text)); } async function checkBoxTag(ctx, name, attrs = {}) { return await inputTag(ctx, name, { ...attrs, type: 'checkbox' }); } function currentRoute(ctx) { const { plan } = ctx; if (plan === null) return null; const { endpoint } = plan; if (endpoint === undefined) return null; return endpoint.customName ?? endpoint.defaultName ?? null; } async function exception(ctx, error) { if (ctx.isWebSocket) return ctx.websocketException(error); return ctx.httpException(error); } function faviconTag(ctx, file) { return ctx.tag('link', { rel: 'icon', href: ctx.urlForFile(file ?? '/mojo/favicon.ico') }); } async function fileFieldTag(ctx, name, attrs = {}) { return tag(ctx, 'input', { ...attrs, type: 'file', name }); } function formFor(ctx, target, attrs, content) { target = urlTarget(target); const route = ctx.app.router.lookup(target[0]); const method = route === null ? 'GET' : route.suggestedMethod(); const options = target[1]; if (method !== 'GET') { attrs.method = 'POST'; if (method !== 'POST') options.query = { _method: method }; } const url = ctx.urlFor(...target); if (url !== null) attrs.action = url; return ctx.tag('form', attrs, content); } async function hiddenFieldTag(ctx, name, value, attrs = {}) { return tag(ctx, 'input', { ...attrs, type: 'hidden', name, value }); } async function htmlException(ctx, error) { ctx.stash.exception = error; const mode = ctx.app.mode; if (await ctx.render({ view: `exception.${mode}`, maybe: true, status: 500 })) return true; if (await ctx.render({ view: 'exception', maybe: true, status: 500 })) return true; const view = mode === 'development' ? 'mojo/debug' : 'mojo/exception'; return await ctx.render({ view, status: 500, stringFormatter: Logger.stringFormatter, exceptionContext }); } async function htmlNotFound(ctx) { ctx.stash.exception = null; const { mode } = ctx.app; if (await ctx.render({ view: `not-found.${mode}`, maybe: true, status: 404 })) return true; if (await ctx.render({ view: 'not-found', maybe: true, status: 404 })) return true; const view = mode === 'development' ? 'mojo/debug' : 'mojo/not-found'; return await ctx.render({ view, status: 404, stringFormatter: Logger.stringFormatter }); } async function httpException(ctx, error) { error = ensureError(error); ctx.log.error(error.stack); const exceptionFormat = ctx.exceptionFormat; if (exceptionFormat === 'txt') return ctx.txtException(error); if (exceptionFormat === 'json') return ctx.jsonException(error); return ctx.htmlException(error); } function imageTag(ctx, target, attrs = {}) { return ctx.tag('img', { src: ctx.urlForFile(target), ...attrs }); } async function inputTag(ctx, name, attrs = {}) { attrs.name = name; const params = await ctx.params(); const values = params.getAll(name); if (values.length > 0) { // Checkbox and radio-button const type = attrs.type; if (type === 'checkbox' || type === 'radio') { const value = attrs.value ?? 'on'; delete attrs.checked; if (typeof value === 'string' && values.includes(value)) attrs.checked = true; } // Others else { attrs.value = values[values.length - 1]; } } return ctx.tag('input', attrs); } async function include(ctx, options, stash) { const str = await ctx.renderToString(options, stash); return str === null ? null : new SafeString(str); } async function jsonException(ctx, error) { if (ctx.app.mode === 'development') { return await ctx.render({ json: { error: { message: error.message, name: error.name, stack: error.stack ?? '' } }, pretty: true, status: 500 }); } return await ctx.render({ json: { error: { message: 'Internal Server Error' } }, pretty: true, status: 500 }); } async function jsonNotFound(ctx) { return await ctx.render({ json: { error: { message: 'Not Found' } }, pretty: true, status: 404 }); } async function labelFor(ctx, name, value, attrs = {}) { return tag(ctx, 'label', { ...attrs, type: 'hidden', for: name }, value); } function linkTo(ctx, target, attrs, content) { const href = ctx.urlFor(...urlTarget(target)) ?? ''; return ctx.tag('a', { href, ...attrs }, content); } async function notFound(ctx) { const { exceptionFormat } = ctx; if (exceptionFormat === 'txt') return ctx.txtNotFound(); if (exceptionFormat === 'json') return ctx.jsonNotFound(); return ctx.htmlNotFound(); } async function passwordFieldTag(ctx, name, attrs = {}) { return tag(ctx, 'input', { ...attrs, type: 'password', name }); } async function proxyGet(ctx, url, config) { await ctx.proxyRequest({ method: 'GET', url, ...config }); } async function proxyPost(ctx, url, config) { await ctx.proxyRequest({ method: 'POST', url, ...config }); } async function proxyRequest(ctx, config) { const proxyRes = await ctx.ua.request(config); proxyRes.autoDecompress = false; const { statusCode, headers } = proxyRes; const stream = proxyRes.createReadStream(); const { res } = ctx; res.statusCode = statusCode; res.headers = headers.clone().dehop(); process.nextTick(() => res.send(stream)); } async function radioButtonTag(ctx, name, attrs = {}) { return await inputTag(ctx, name, { ...attrs, type: 'radio' }); } function scriptTag(ctx, target, attrs = {}) { return ctx.tag('script', { src: ctx.urlForFile(target), ...attrs }); } function styleTag(ctx, target, attrs = {}) { return ctx.tag('link', { rel: 'stylesheet', href: ctx.urlForFile(target), ...attrs }); } function submitButtonTag(ctx, text = 'Ok', attrs = {}) { return ctx.tag('input', { value: text, ...attrs, type: 'submit' }); } function tag(ctx, name, attrs = {}, content = '') { if (content instanceof Function) content = content(); if (content instanceof Promise) return content.then(text => new SafeString(DOM.newTag(name, attrs, text).toString())); // Temporary workaround for backwards compatibility, to be removed in 2.0 return new SafeString(DOM.newTag(name, attrs, content).toString()); } async function textAreaTag(ctx, name, attrs = {}, content) { attrs.name = name; const params = await ctx.params(); const value = params.get(name); if (value !== null) content = value; return ctx.tag('textarea', { ...attrs, name }, content); } async function txtException(ctx, error) { if (ctx.app.mode === 'development') { return await ctx.render({ text: error.stack, status: 500 }); } return await ctx.render({ text: 'Internal Server Error', status: 500 }); } async function txtNotFound(ctx) { return await ctx.render({ text: 'Not Found', status: 404 }); } async function websocketException(ctx, error) { error = ensureError(error); ctx.log.error(error.stack); const ws = ctx.ws; if (ws !== null && ctx.isEstablished) ws.close(1011); return true; } function urlTarget(target) { return typeof target === 'string' ? [target, {}] : target; } // If you see this then your code has thrown something that was not an Error object function ensureError(error) { return error instanceof Error ? error : new Error(error); } //# sourceMappingURL=default-helpers.js.map