diff --git a/config/default.json5 b/config/default.json5 index 52dbd6f..b853789 100644 --- a/config/default.json5 +++ b/config/default.json5 @@ -4,8 +4,8 @@ app: { listen_addr: '127.0.0.1', port: 4899, - public_url: "http://localhost:4899", - public_websocket_url: "ws://localhost:4899", + public_url: "http://127.0.0.1:4899", + public_websocket_url: "ws://127.0.0.1:4899", name: 'Example App', contact_email: 'contact@example.net', display_email_warning: true, @@ -36,7 +36,7 @@ }, mysql: { connectionLimit: 10, - host: "localhost", + host: "127.0.0.1", user: "root", password: "", database: "swaf", diff --git a/config/test.json5 b/config/test.json5 index 53997df..351913e 100644 --- a/config/test.json5 +++ b/config/test.json5 @@ -3,7 +3,7 @@ approval_mode: true, }, mysql: { - host: "localhost", + host: "127.0.0.1", user: "root", password: "", database: "swaf_test", diff --git a/jest.config.js b/jest.config.js index 3502ddc..4ec21d1 100644 --- a/jest.config.js +++ b/jest.config.js @@ -16,6 +16,5 @@ module.exports = { '**/test/**/*.test.ts' ], testEnvironment: 'node', - testTimeout: 60000, resolver: "jest-ts-webcompat-resolver", }; diff --git a/package.json b/package.json index 9761224..bc9d9c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "swaf", - "version": "0.24.8", + "version": "0.24.9", "description": "Structure Web Application Framework.", "repository": "https://eternae.ink/ashpie/swaf", "author": "Alice Gaudon ", @@ -19,7 +19,7 @@ "compile": "yarn clean && yarn prepare-sources && tsc --build", "build": "yarn compile && node . pre-compile-views && node scripts/dist.js", "build-production": "NODE_ENV=production yarn build", - "dev": "yarn compile && concurrently -k -n \"Maildev,Typescript,ViewPreCompile,Node\" -p \"[{name}]\" -c \"yellow,blue,red,green\" \"maildev\" \"tsc --build --watch --preserveWatchOutput\" \"nodemon -i public -i intermediates -- pre-compile-views --watch\" \"nodemon -i public -i intermediates\"", + "dev": "yarn compile && concurrently -k -n \"Maildev,Typescript,ViewPreCompile,Node\" -p \"[{name}]\" -c \"yellow,blue,red,green\" \"maildev --ip 127.0.0.1\" \"tsc --build --watch --preserveWatchOutput\" \"nodemon -i public -i intermediates -- pre-compile-views --watch\" \"nodemon -i public -i intermediates\"", "lint": "eslint .", "release": "yarn build && yarn lint && yarn test && cd dist && yarn publish" }, @@ -38,6 +38,7 @@ "@types/jest": "^27.0.2", "@types/mjml": "^4.0.4", "@types/mysql": "^2.15.10", + "@types/node": "^16.11.10", "@types/nodemailer": "^6.4.0", "@types/nunjucks": "^3.1.3", "@types/on-finished": "^2.3.1", diff --git a/src/Application.ts b/src/Application.ts index e6f4ae9..c5c53ec 100644 --- a/src/Application.ts +++ b/src/Application.ts @@ -21,6 +21,8 @@ import SecurityError from "./SecurityError.js"; import {doesFileExist, Type} from "./Utils.js"; import WebSocketListener from "./WebSocketListener.js"; import TemplateError = nunjucks.lib.TemplateError; +import AppLocalsCoreComponents from "./components/core/AppLocalsCoreComponents.js"; +import LazyLocalsCoreComponent from "./components/core/LazyLocalsCoreComponent.js"; export default abstract class Application implements Extendable> { private readonly version: string; @@ -98,6 +100,8 @@ export default abstract class Application implements Extendable { + res.formatViewData('errors/' + httpError.errorCode, locals, (err: Error | undefined, html) => { if (err) { - res.render('errors/Error', locals); + res.formatViewData('templates/ErrorTemplate', locals); } else { res.send(html); } @@ -247,6 +251,7 @@ export default abstract class Application implements Extendable { const args = process.argv; + // Flags const flags = { verbose: false, @@ -272,9 +277,13 @@ export default abstract class Application implements Extendable { - const form = this.makeForm(); + const form = formidable(this.getFormidableOptions()); try { await new Promise((resolve, reject) => { form.parse(req, (err, fields, files) => { diff --git a/src/TestApp.ts b/src/TestApp.ts index 5f5df23..f8e33fc 100644 --- a/src/TestApp.ts +++ b/src/TestApp.ts @@ -148,10 +148,10 @@ export default class TestApp extends Application { this.use(new class extends Controller { public routes(): void { this.get('/', (req, res) => { - res.render('home'); + res.formatViewData('home'); }, 'home'); this.get('/tests', (req, res) => { - res.render('tests'); + res.formatViewData('tests'); }, 'tests'); this.get('/design', (req, res) => { req.flash('success', 'Success.'); @@ -159,7 +159,7 @@ export default class TestApp extends Application { req.flash('warning', 'Warning.'); req.flash('error', 'Error.'); req.flash('error-alert', 'Error alert.'); - res.render('design'); + res.formatViewData('design'); }, 'design'); } }()); diff --git a/src/assets/ts/datetime-local.ts b/src/assets/ts/datetime-local.ts new file mode 100644 index 0000000..9bd934f --- /dev/null +++ b/src/assets/ts/datetime-local.ts @@ -0,0 +1,25 @@ +export function dateToDatetimeLocal(date: Date): string { + function ten(i: number) { + return (i < 10 ? '0' : '') + i; + } + const YYYY = date.getFullYear(); + const MM = ten(date.getMonth() + 1); + const DD = ten(date.getDate()); + const HH = ten(date.getHours()); + const II = ten(date.getMinutes()); + const SS = ten(date.getSeconds()); + return YYYY + '-' + MM + '-' + DD + 'T' + + HH + ':' + II + ':' + SS; +} + +export const dateToIsoString = (function (BST) { + // BST should not be present as UTC time + if (new Date(BST).toISOString().slice(0, 16) === BST) { + return (date: Date): string => { + return new Date(date.getTime() + date.getTimezoneOffset() * 60000) + .toISOString(); + }; + } else { + return (date: Date) => date.toISOString(); + } +}('2006-06-06T06:06')); diff --git a/src/assets/views/auth/auth.svelte b/src/assets/views/auth/auth.svelte index c34d380..331acc4 100644 --- a/src/assets/views/auth/auth.svelte +++ b/src/assets/views/auth/auth.svelte @@ -6,11 +6,11 @@ import Icon from "../utils/Icon.svelte"; import {hasRoute, route} from "../../../common/Routing"; - let registerUsingMagicLink = $locals.previousFormData()?.['auth_method'] !== 'password'; + let registerUsingMagicLink = $locals.previousFormData?.['auth_method'] !== 'password'; let loginUsingMagicLink = true; let queryStr = ''; - let previousUrl = $locals.getPreviousUrl(); + let previousUrl: string = $locals.previousUrl as string | undefined; if ($locals.query?.redirect_uri) { queryStr = '?' + new URLSearchParams({redirect_uri: $locals.query?.redirect_uri}).toString(); } else if (previousUrl) { diff --git a/src/assets/views/components/FlashMessages.svelte b/src/assets/views/components/FlashMessages.svelte index 43a6a88..c49d8e4 100644 --- a/src/assets/views/components/FlashMessages.svelte +++ b/src/assets/views/components/FlashMessages.svelte @@ -2,7 +2,7 @@ import {locals} from "../../ts/stores.js"; import Message from "./Message.svelte"; - export let flashed = $locals.flash(); + export let flashed = $locals.flash; + +{#if show} +
+
+
+
+
+
+{/if} \ No newline at end of file diff --git a/src/assets/views/components/NavMenuItem.svelte b/src/assets/views/components/NavMenuItem.svelte index b01d4c8..6e40479 100644 --- a/src/assets/views/components/NavMenuItem.svelte +++ b/src/assets/views/components/NavMenuItem.svelte @@ -19,11 +19,15 @@ line-height: 1; @include medium-le { - margin-top: 8px; + &:not(:first-child) { + margin-top: 8px; + } } @include large-ge { - margin-left: 8px; + &:not(:first-child) { + margin-left: 8px; + } } a { diff --git a/src/assets/views/magic_link_lobby.svelte b/src/assets/views/magic_link_lobby.svelte index 21b8eb9..2999794 100644 --- a/src/assets/views/magic_link_lobby.svelte +++ b/src/assets/views/magic_link_lobby.svelte @@ -14,7 +14,7 @@ let countdown; let validUntilDate = new Date(validUntil); - $: countdown = $locals.isPreRender ? '...' : Time.humanizeTimeTo(validUntilDate); + $: countdown = $locals.isSsr ? '...' : Time.humanizeTimeTo(validUntilDate); onMount(() => { const interval = setInterval(() => { diff --git a/src/assets/views/templates/BaseTemplate.svelte b/src/assets/views/templates/BaseTemplate.svelte index 755d987..9b27141 100644 --- a/src/assets/views/templates/BaseTemplate.svelte +++ b/src/assets/views/templates/BaseTemplate.svelte @@ -8,6 +8,12 @@ export let h1: string = title; export let description: string; export let refresh_after: number | undefined = undefined; + export let noHeader: boolean = false; + export let noH1: boolean = false; + export let noFooter: boolean = false; + + export let noLogoLabel = false; + export let noLoginLink = false; @@ -46,14 +52,16 @@ - +{#if !noHeader} + +{/if}
- {#if h1} + {#if h1 && !noH1}

{h1}

{/if} {#if $$slots.subtitle} @@ -64,4 +72,6 @@
- +{#if !noFooter} + +{/if} diff --git a/src/assets/views/templates/ErrorTemplate.svelte b/src/assets/views/templates/ErrorTemplate.svelte index 5c7c05b..9c4939f 100644 --- a/src/assets/views/templates/ErrorTemplate.svelte +++ b/src/assets/views/templates/ErrorTemplate.svelte @@ -4,7 +4,7 @@ import Icon from "../utils/Icon.svelte"; import CommonScripts from "./CommonScripts.svelte"; - const previousUrl = $locals.getPreviousUrl(); + const previousUrl = $locals.previousUrl; export let code; code = $locals.error_code || code; diff --git a/src/assets/views/templates/base/BaseFooter.svelte b/src/assets/views/templates/base/BaseFooter.svelte index 9bc4122..b58e166 100644 --- a/src/assets/views/templates/base/BaseFooter.svelte +++ b/src/assets/views/templates/base/BaseFooter.svelte @@ -9,4 +9,4 @@ } -
{$locals.app.name} v{$locals.app_version} - all rights reserved.
+
{$locals.app.name} v{$locals.app.version} - all rights reserved.
diff --git a/src/assets/views/templates/base/BaseHeader.svelte b/src/assets/views/templates/base/BaseHeader.svelte index 32e6c31..3df399b 100644 --- a/src/assets/views/templates/base/BaseHeader.svelte +++ b/src/assets/views/templates/base/BaseHeader.svelte @@ -3,6 +3,9 @@ import NavMenu from "../../components/NavMenu.svelte"; import BaseNavMenuLinks from "./BaseNavMenuLinks.svelte"; import BaseNavMenuAuth from "./BaseNavMenuAuth.svelte"; + + export let noLoginLink = false; + export let noLogoLabel = false;
- + - +
diff --git a/src/assets/views/templates/base/BaseHeaderLogo.svelte b/src/assets/views/templates/base/BaseHeaderLogo.svelte index b964873..4efd373 100644 --- a/src/assets/views/templates/base/BaseHeaderLogo.svelte +++ b/src/assets/views/templates/base/BaseHeaderLogo.svelte @@ -1,5 +1,7 @@ - + diff --git a/src/assets/views/templates/base/BaseNavMenuAuth.svelte b/src/assets/views/templates/base/BaseNavMenuAuth.svelte index 052ea70..03b4544 100644 --- a/src/assets/views/templates/base/BaseNavMenuAuth.svelte +++ b/src/assets/views/templates/base/BaseNavMenuAuth.svelte @@ -2,6 +2,8 @@ import {locals} from "../../../ts/stores.js"; import NavMenuItem from "../../components/NavMenuItem.svelte"; import {hasRoute, route} from "../../../../common/Routing"; + + export let noLoginLink = false; {#if hasRoute('auth')} @@ -12,7 +14,7 @@ - {:else} + {:else if !noLoginLink} {/if} {/if} diff --git a/src/assets/views/templates/svelte_template.html b/src/assets/views/templates/svelte_template.html index 9ded83c..1def48e 100644 --- a/src/assets/views/templates/svelte_template.html +++ b/src/assets/views/templates/svelte_template.html @@ -12,13 +12,7 @@ import * as stores from '/js/stores.js'; const localStore = stores.l; - const localMap = %locals%; - localStore.set((key, args) => { - return localMap[args ? - `'${key}', \`${args}\`` - : `'${key}'`]; - }); - + localStore.set(%locals%); setRoutes(%routes%); setPublicUrl(`%publicUrl%`); diff --git a/src/assets/views/utils/CsrfTokenField.svelte b/src/assets/views/utils/CsrfTokenField.svelte index 85f946f..d749c40 100644 --- a/src/assets/views/utils/CsrfTokenField.svelte +++ b/src/assets/views/utils/CsrfTokenField.svelte @@ -3,4 +3,4 @@ import Field from "./Field.svelte"; - + diff --git a/src/assets/views/utils/Field.svelte b/src/assets/views/utils/Field.svelte index c76df9a..18292e7 100644 --- a/src/assets/views/utils/Field.svelte +++ b/src/assets/views/utils/Field.svelte @@ -4,23 +4,29 @@ import Message from "../components/Message.svelte"; import Icon from "./Icon.svelte"; import {getContext} from "svelte"; + import {dateToDatetimeLocal, dateToIsoString} from "../../ts/datetime-local.js"; export let type: string; export let name: string; type FieldValue = string | number | Record; export let value: FieldValue | undefined = undefined; + export let initialValue: FieldValue | undefined = undefined; export let placeholder: string | undefined = undefined; export let hint: string | undefined = undefined; export let extraData: string[] | undefined = undefined; export let icon: string | undefined = undefined; - export let validation: { message: string, value?: string } | undefined = $locals.validation()?.[name]; + export let validation = $locals.validation?.[name] as { message: string, value?: string } | undefined; const formId = getContext('formId'); const fieldId = `${formId}-${name}-field`; - const previousFormData = $locals.previousFormData() || []; + const previousFormData = $locals.previousFormData as FieldValue | undefined || []; + let previousFieldData = previousFormData[name]; + if (typeof value === 'number' && previousFieldData) previousFieldData = Number(previousFieldData); - value = type !== 'hidden' && previousFormData[name] || value || validation?.value || ''; + value = type !== 'hidden' && previousFieldData || value || initialValue || validation?.value || ''; + + $: initialDatetimeLocalValue = type === 'datetime-local' && typeof value === 'string' ? dateToDatetimeLocal(new Date(value)) : undefined; function durationValue(f: string): number { if (previousFormData[name]) { @@ -61,6 +67,10 @@ if (this.type === 'file') { handleFileInput(); } + + if (this.type === 'datetime-local') { + value = dateToIsoString(new Date(this.value)); + } } let input: HTMLInputElement; @@ -398,10 +408,10 @@ {/each} {:else if type === 'select'} - {#each extraData as option} + >{option.display || option} {/each} @@ -414,6 +424,9 @@ {:else if type === 'checkbox'} + {:else if type === 'datetime-local'} + + {:else} diff --git a/src/assets/views/utils/Form.svelte b/src/assets/views/utils/Form.svelte index ff1e714..3ea80a3 100644 --- a/src/assets/views/utils/Form.svelte +++ b/src/assets/views/utils/Form.svelte @@ -14,6 +14,7 @@ export let isBoldSubmit: boolean = true; export let resetButton: boolean = false; export let confirm: string = undefined; + export let withFiles: boolean = false; const formId = nextAvailableFormId++; setContext('formId', formId); @@ -37,7 +38,7 @@ } -
+
diff --git a/src/auth/AccountController.ts b/src/auth/AccountController.ts index e99d70a..769eee2 100644 --- a/src/auth/AccountController.ts +++ b/src/auth/AccountController.ts @@ -57,7 +57,7 @@ export default class AccountController extends Controller { const nameChangedAt = nameComponent?.getNameChangedAt()?.getTime() || Date.now(); const nameChangeRemainingTime = new Date(nameChangedAt + nameChangeWaitPeriod); - res.render('auth/account/account', { + res.formatViewData('auth/account/account', { user_personal_info_fields: user.getPersonalInfoFields(), main_email: await user.mainEmail.get(), emails: await user.emails.get(), diff --git a/src/auth/AuthController.ts b/src/auth/AuthController.ts index ef77732..262e5ac 100644 --- a/src/auth/AuthController.ts +++ b/src/auth/AuthController.ts @@ -35,7 +35,7 @@ export default class AuthController extends Controller { const userModelFactory = ModelFactory.get(User); const hasUsername = userModelFactory.hasComponent(UserNameComponent); - res.render('auth/auth', { + res.formatViewData('auth/auth', { auth_methods: authGuard.getAuthMethodNames(), has_username: hasUsername, register_with_password: hasUsername && userModelFactory.hasComponent(UserPasswordComponent), diff --git a/src/auth/magic_link/MagicLinkController.ts b/src/auth/magic_link/MagicLinkController.ts index ec006aa..8639e05 100644 --- a/src/auth/magic_link/MagicLinkController.ts +++ b/src/auth/magic_link/MagicLinkController.ts @@ -147,7 +147,7 @@ export default class MagicLinkController extends Controll return; } - res.render('magic_link_lobby', { + res.formatViewData('magic_link_lobby', { email: validLink.getOrFail('email'), type: validLink.getOrFail('action_type'), validUntil: validLink.getExpirationDate().getTime(), @@ -179,7 +179,7 @@ export default class MagicLinkController extends Controll } } - res.render('magic_link', { + res.formatViewData('magic_link', { magicLink: magicLink, err: err, success: success && err === null, diff --git a/src/common/Routing.ts b/src/common/Routing.ts index d246adc..cbe6e07 100644 --- a/src/common/Routing.ts +++ b/src/common/Routing.ts @@ -1,4 +1,4 @@ -export type RouteParams = { [p: string]: string | number } | string[] | string | number; +export type RouteParams = { [p: string]: string | number | undefined } | string[] | string | number; export type QueryParamsRecord = Record; export type QueryParams = string[][] | QueryParamsRecord | string | URLSearchParams; @@ -43,7 +43,10 @@ export function route( path = path.replace(/\/+/g, '/'); } else { for (const key of Object.keys(params)) { - path = path.replace(getRouteParamRegExp(key), params[key].toString()); + const paramValue = params[key]; + if (paramValue) { + path = path.replace(getRouteParamRegExp(key), paramValue.toString()); + } } } diff --git a/src/components/CsrfProtectionComponent.ts b/src/components/CsrfProtectionComponent.ts index 43926ed..ba65127 100644 --- a/src/components/CsrfProtectionComponent.ts +++ b/src/components/CsrfProtectionComponent.ts @@ -5,26 +5,10 @@ import {Session, SessionData} from "express-session"; import ApplicationComponent from "../ApplicationComponent.js"; import {AuthMiddleware} from "../auth/AuthComponent.js"; import {BadRequestError} from "../HttpError.js"; -import FrontendToolsComponent from "./FrontendToolsComponent.js"; export default class CsrfProtectionComponent extends ApplicationComponent { private static readonly excluders: ((req: Request) => boolean)[] = []; - public static getCsrfToken(session: Session & Partial): string { - if (typeof session.csrf !== 'string') { - session.csrf = crypto.randomBytes(64).toString('base64'); - } - return session.csrf; - } - - - public async init(): Promise { - const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals(); - if (globals) { - globals.set('getCsrfToken', () => null); - } - } - public static addExcluder(excluder: (req: Request) => boolean): void { this.excluders.push(excluder); } @@ -36,9 +20,7 @@ export default class CsrfProtectionComponent extends ApplicationComponent { } const session = req.getSession(); - res.locals.getCsrfToken = () => { - return CsrfProtectionComponent.getCsrfToken(session); - }; + res.setLazyLocal('csrfToken', () => this.getSessionCsrfToken(session)); if (!['GET', 'HEAD', 'OPTIONS'].find(s => s === req.method)) { try { @@ -58,6 +40,13 @@ export default class CsrfProtectionComponent extends ApplicationComponent { next(); }); } + + public getSessionCsrfToken(session: Session & Partial): string { + if (typeof session.csrf !== 'string') { + session.csrf = crypto.randomBytes(64).toString('base64'); + } + return session.csrf; + } } class InvalidCsrfTokenError extends BadRequestError { diff --git a/src/components/ExpressAppComponent.ts b/src/components/ExpressAppComponent.ts index 6977837..caeb4f4 100644 --- a/src/components/ExpressAppComponent.ts +++ b/src/components/ExpressAppComponent.ts @@ -21,7 +21,7 @@ export default class ExpressAppComponent extends ApplicationComponent { public async start(app: Express): Promise { this.server = app.listen(this.port, this.addr, () => { - logger.info(`Web server running on http://${this.addr}:${this.port}.`); + logger.info(`Web server running on http://${this.addr}:${this.port}`); }); // Proxy @@ -48,6 +48,26 @@ export default class ExpressAppComponent extends ApplicationComponent { if (!middleware) throw new Error('Middleware ' + type.name + ' not present in this request.'); return middleware as M; }; + res.formatViewData = function ( + viewName: string, + data?: Record, + callback?: (err: Error, html: string) => void, + ) { + this.format({ + html: () => { + this.render(viewName, data, callback); + }, + json: () => { + if (typeof data === 'undefined') data = {}; + const serialized = JSON.stringify({...data, viewName}, (key, value) => { + if (key.startsWith('_') || typeof value === 'function') return undefined; + else return value; + }); + this.contentType('application/json'); + this.send(serialized); + }, + }); + }; next(); }); } diff --git a/src/components/FormHelperComponent.ts b/src/components/FormHelperComponent.ts index 2f3f6c7..d7fd071 100644 --- a/src/components/FormHelperComponent.ts +++ b/src/components/FormHelperComponent.ts @@ -1,39 +1,20 @@ import {Router} from "express"; import ApplicationComponent from "../ApplicationComponent.js"; -import FrontendToolsComponent from "./FrontendToolsComponent.js"; export default class FormHelperComponent extends ApplicationComponent { - public async init(): Promise { - const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals(); - if (globals) { - globals.set('validation', () => ({})); - globals.set('previousFormData', () => ({})); - } - } - public async initRoutes(router: Router): Promise { router.use((req, res, next) => { - let _validation: unknown | null; - res.locals.validation = () => { - if (!_validation) { - const v = req.flash('validation'); - _validation = v.length > 0 ? v[0] : null; - } + res.setLazyLocal('validation', () => { + const validation = req.flash('validation'); + return validation.length > 0 ? validation[0] : null; + }); - return _validation; - }; - - let _previousFormData: unknown | null = null; - res.locals.previousFormData = () => { - if (!_previousFormData) { - const v = req.flash('previousFormData'); - _previousFormData = v.length > 0 ? v[0] : null; - } - - return _previousFormData; - }; + res.setLazyLocal('previousFormData', () => { + const previousFormData = req.flash('previousFormData'); + return previousFormData.length > 0 ? previousFormData[0] : null; + }); next(); }); diff --git a/src/components/FrontendToolsComponent.ts b/src/components/FrontendToolsComponent.ts index 81aa0f8..66ea0b3 100644 --- a/src/components/FrontendToolsComponent.ts +++ b/src/components/FrontendToolsComponent.ts @@ -1,23 +1,14 @@ -import config from "config"; import {Express, Router} from "express"; -import path from "path"; -import util from "util"; import ApplicationComponent from "../ApplicationComponent.js"; -import {QueryParams, route, RouteParams} from "../common/Routing.js"; import AssetCompiler from "../frontend/AssetCompiler.js"; import AssetPreCompiler from "../frontend/AssetPreCompiler.js"; -import Globals from "../frontend/Globals.js"; import ViewEngine from "../frontend/ViewEngine.js"; -import {logger} from "../Logger.js"; -import {listFilesRecursively} from "../Utils.js"; -import FileCache from "../utils/FileCache.js"; +import LazyLocalsCoreComponent from "./core/LazyLocalsCoreComponent.js"; export default class FrontendToolsComponent extends ApplicationComponent { private readonly publicDir: string; - private readonly publicAssetsCache: FileCache = new FileCache(); private readonly assetPreCompilers: AssetPreCompiler[]; - private readonly globals: Globals = new Globals(); public constructor( private readonly assetCompiler: AssetCompiler, @@ -31,48 +22,17 @@ export default class FrontendToolsComponent extends ApplicationComponent { if (assetPreCompiler.isPublic()) { this.assetCompiler.addExtension(assetPreCompiler.getExtension()); } - - assetPreCompiler.setGlobals(this.globals); } } - - public async init(): Promise { - this.globals.set('route', ( - routeName: string, - params: RouteParams = [], - query: QueryParams = '', - absolute: boolean = false, - ) => route(routeName, params, query, absolute)); - this.globals.set('app_version', this.getApp().getVersion()); - this.globals.set('core_version', this.getApp().getCoreVersion()); - this.globals.set('app', config.get('app')); - this.globals.set('dump', (val: unknown) => { - return util.inspect(val); - }); - this.globals.set('hex', (v: number) => { - return v.toString(16); - }); - } - public async start(app: Express): Promise { - // Cache public assets - if (config.get('asset_cache')) { - logger.info('Caching assets from', this.publicDir, '...'); - for (const file of await listFilesRecursively(this.publicDir)) { - await this.publicAssetsCache.load(file); - } - } else { - logger.info('Asset cache disabled.'); - } - this.hookPreCompilers(); // Setup express view engine let main = true; for (const assetPreCompiler of this.assetPreCompilers) { if (assetPreCompiler instanceof ViewEngine) { - assetPreCompiler.setup(app, main); + assetPreCompiler.setup(app, main, this.getApp().as(LazyLocalsCoreComponent)); main = false; } } @@ -84,17 +44,9 @@ export default class FrontendToolsComponent extends ApplicationComponent { } } - public async handleRoutes(router: Router): Promise { - router.use((req, res, next) => { - res.locals.inlineAsset = (urlPath: string) => { - return this.publicAssetsCache.getOrFail(path.join(this.publicDir, urlPath)); - }; - - next(); - }); - - // Add request context locals + public async initRoutes(router: Router): Promise { router.use((req, res, next) => { + // Request context locals res.locals.url = req.url; res.locals.params = req.params; res.locals.query = req.query; @@ -126,8 +78,4 @@ export default class FrontendToolsComponent extends ApplicationComponent { this.hookPreCompilers(); } } - - public getGlobals(): Globals { - return this.globals; - } } diff --git a/src/components/MailComponent.ts b/src/components/MailComponent.ts index 4109c06..2d12b55 100644 --- a/src/components/MailComponent.ts +++ b/src/components/MailComponent.ts @@ -10,10 +10,11 @@ import {logger} from "../Logger.js"; import Mail from "../mail/Mail.js"; import MailError from "../mail/MailError.js"; import SecurityError from "../SecurityError.js"; -import FrontendToolsComponent from "./FrontendToolsComponent.js"; +import LazyLocalsCoreComponent from "./core/LazyLocalsCoreComponent.js"; export default class MailComponent extends ApplicationComponent { private transporter?: Transporter; + private readonly additionalLocals: Record = {}; public constructor( private readonly viewEngine: MailViewEngine, @@ -59,7 +60,7 @@ export default class MailComponent extends ApplicationComponent { logger.info(`Mail ready to be distributed via ${config.get('mail.host')}:${config.get('mail.port')}`); }); - await this.viewEngine.setup(app, false); + await this.viewEngine.setup(app, false, this.getApp().as(LazyLocalsCoreComponent)); } public async stop(): Promise { @@ -77,7 +78,6 @@ export default class MailComponent extends ApplicationComponent { for (const destEmail of to) { const template = mail.getTemplate(); - const locals = mail.getData(); const options = mail.getOptions(); // Reset options @@ -91,17 +91,17 @@ export default class MailComponent extends ApplicationComponent { }; // Set locals - locals.mail_subject = options.subject; - locals.mail_to = options.to; - locals.mail_link = config.get('app.public_url') + - route('mail', [template.template], locals); - Object.assign(locals, this.getApp().as(FrontendToolsComponent).getGlobals().get()); + const urlLocals = mail.getData(); + urlLocals.mail_subject = options.subject; + urlLocals.mail_to = options.to; + urlLocals.mail_link = config.get('app.public_url') + + route('mail', [template.template], urlLocals); + const locals = {...this.additionalLocals, ...urlLocals}; // Log logger.debug(`Send mail from ${options.from.address} to ${options.to}`); // Render email - options.html = await this.viewEngine.render('mails/' + template.template + '.mnjk', locals, false); options.text = await this.viewEngine.render('mails/' + template.template + '.mnjk', locals, true); @@ -112,6 +112,10 @@ export default class MailComponent extends ApplicationComponent { return results; } + public setAdditionalLocal(key: string, value: unknown): void { + this.additionalLocals[key] = value; + } + private getTransporter(): Transporter { if (!this.transporter) throw new MailError('Mail system was not prepared.'); return this.transporter; diff --git a/src/components/PreviousUrlComponent.ts b/src/components/PreviousUrlComponent.ts index 79f29c0..9e87f40 100644 --- a/src/components/PreviousUrlComponent.ts +++ b/src/components/PreviousUrlComponent.ts @@ -3,17 +3,9 @@ import onFinished from "on-finished"; import ApplicationComponent from "../ApplicationComponent.js"; import {logger} from "../Logger.js"; -import FrontendToolsComponent from "./FrontendToolsComponent.js"; import SessionComponent from "./SessionComponent.js"; export default class PreviousUrlComponent extends ApplicationComponent { - public async init(): Promise { - const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals(); - if (globals) { - globals.set('getPreviousUrl', () => null); - } - } - public async handleRoutes(router: Router): Promise { router.use((req, res, next) => { req.getPreviousUrl = () => { @@ -31,7 +23,7 @@ export default class PreviousUrlComponent extends ApplicationComponent { return null; }; - res.locals.getPreviousUrl = req.getPreviousUrl; + res.setLazyLocal('previousUrl', () => req.getPreviousUrl()); req.getIntendedUrl = () => { return req.query.redirect_uri?.toString() || null; diff --git a/src/components/RedisComponent.ts b/src/components/RedisComponent.ts index 9ceb9e9..d31953f 100644 --- a/src/components/RedisComponent.ts +++ b/src/components/RedisComponent.ts @@ -8,6 +8,7 @@ import CacheProvider from "../CacheProvider.js"; import {logger} from "../Logger.js"; export default class RedisComponent extends ApplicationComponent implements CacheProvider { + private readonly prefix: string = config.get('redis.prefix'); private redisClient?: RedisClient; private store: Store = new RedisStore(this); @@ -42,7 +43,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach return; } - this.redisClient.get(key, (err, val) => { + this.redisClient.get(this.prefix + key, (err, val) => { if (err) { reject(err); return; @@ -63,7 +64,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach return; } - this.redisClient.del(key, (err) => { + this.redisClient.del(this.prefix + key, (err) => { if (err) { reject(err); return; @@ -81,7 +82,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach return; } - this.redisClient.psetex(key, ttl, value, (err) => { + this.redisClient.psetex(this.prefix + key, ttl, value, (err) => { if (err) return reject(err); resolve(); }); @@ -95,7 +96,7 @@ export default class RedisComponent extends ApplicationComponent implements Cach return; } - this.redisClient.pexpire(key, ttl, (err) => { + this.redisClient.pexpire(this.prefix + key, ttl, (err) => { if (err) return reject(err); resolve(); }); diff --git a/src/components/SessionComponent.ts b/src/components/SessionComponent.ts index 6b920c8..08774df 100644 --- a/src/components/SessionComponent.ts +++ b/src/components/SessionComponent.ts @@ -5,7 +5,6 @@ import session from "express-session"; import ApplicationComponent from "../ApplicationComponent.js"; import SecurityError from "../SecurityError.js"; -import FrontendToolsComponent from "./FrontendToolsComponent.js"; import RedisComponent from "./RedisComponent.js"; export default class SessionComponent extends ApplicationComponent { @@ -16,13 +15,6 @@ export default class SessionComponent extends ApplicationComponent { this.storeComponent = storeComponent; } - public async init(): Promise { - const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals(); - if (globals) { - globals.set('flash', () => ''); - } - } - public async checkSecuritySettings(): Promise { this.checkSecurityConfigField('session.secret'); if (!config.get('session.cookie.secure')) { @@ -68,40 +60,16 @@ export default class SessionComponent extends ApplicationComponent { res.locals.session = session; // Views flash function - const _flash: FlashStorage = {}; - res.locals.flash = (key?: string): FlashMessages | unknown[] => { - if (key !== undefined) { - if (_flash[key] === undefined) _flash[key] = req.flash(key); - return _flash[key] || []; - } - - if (_flash._messages === undefined) { - _flash._messages = { - info: req.flash('info'), - success: req.flash('success'), - warning: req.flash('warning'), - error: req.flash('error'), - 'error-alert': req.flash('error-alert'), - }; - } - return _flash._messages; - }; + res.setLazyLocal('flash', () => { + return { + info: req.flash('info'), + success: req.flash('success'), + warning: req.flash('warning'), + error: req.flash('error'), + 'error-alert': req.flash('error-alert'), + }; + }); next(); }); } } - -export type FlashMessages = { - [k: string]: unknown[] | undefined -}; - -export type DefaultFlashMessages = FlashMessages & { - info?: unknown[] | undefined; - success?: unknown[] | undefined; - warning?: unknown[] | undefined; - error?: unknown[] | undefined; -}; - -type FlashStorage = FlashMessages & { - _messages?: DefaultFlashMessages, -}; diff --git a/src/components/WebSocketServerComponent.ts b/src/components/WebSocketServerComponent.ts index 483421a..d4ec4d4 100644 --- a/src/components/WebSocketServerComponent.ts +++ b/src/components/WebSocketServerComponent.ts @@ -1,7 +1,7 @@ import config from "config"; import cookie from "cookie"; import cookieParser from "cookie-parser"; -import {Express, Request} from "express"; +import {Express, Request, Router} from "express"; import {Session} from "express-session"; import {WebSocketServer} from "ws"; @@ -10,7 +10,6 @@ import ApplicationComponent from "../ApplicationComponent.js"; import {logger} from "../Logger.js"; import WebSocketListener from "../WebSocketListener.js"; import ExpressAppComponent from "./ExpressAppComponent.js"; -import FrontendToolsComponent from "./FrontendToolsComponent.js"; import RedisComponent from "./RedisComponent.js"; export default class WebSocketServerComponent extends ApplicationComponent { @@ -21,11 +20,13 @@ export default class WebSocketServerComponent extends ApplicationComponent { app.require(ExpressAppComponent); app.require(RedisComponent); + } - const globals = app.asOptional(FrontendToolsComponent)?.getGlobals(); - if (globals) { - globals.set('websocketUrl', config.get('app.public_websocket_url')); - } + public async initRoutes(router: Router): Promise { + router.use((req, res, next) => { + res.locals.websocketUrl = config.get('app.public_websocket_url'); + next(); + }); } public async start(_app: Express): Promise { diff --git a/src/components/core/AppLocalsCoreComponents.ts b/src/components/core/AppLocalsCoreComponents.ts new file mode 100644 index 0000000..4fd10f4 --- /dev/null +++ b/src/components/core/AppLocalsCoreComponents.ts @@ -0,0 +1,26 @@ +import config from "config"; +import {Router} from "express"; + +import ApplicationComponent from "../../ApplicationComponent.js"; +import MailComponent from "../MailComponent.js"; + +export default class AppLocalsCoreComponents extends ApplicationComponent { + private appLocals: Record | undefined; + + + public async init(): Promise { + this.appLocals = { + ...config.get('app'), + version: this.getApp().getVersion(), + core_version: this.getApp().getCoreVersion(), + }; + this.getApp().as(MailComponent).setAdditionalLocal('app', this.appLocals); + } + + public async initRoutes(router: Router): Promise { + router.use((req, res, next) => { + res.locals.app = this.appLocals; + next(); + }); + } +} diff --git a/src/components/core/LazyLocalsCoreComponent.ts b/src/components/core/LazyLocalsCoreComponent.ts new file mode 100644 index 0000000..2363e5d --- /dev/null +++ b/src/components/core/LazyLocalsCoreComponent.ts @@ -0,0 +1,30 @@ +import {Router} from "express"; + +import ApplicationComponent from "../../ApplicationComponent.js"; + +export default class LazyLocalsCoreComponent extends ApplicationComponent { + public async initRoutes(router: Router): Promise { + router.use((req, res, next) => { + res.locals._lazyLocals = {}; + res.setLazyLocal = (key: string, valueProvider: () => unknown) => { + res.locals._lazyLocals[key] = valueProvider; + }; + next(); + }); + } + + public setupLazyLocals(localsObject: Record): void { + const lazyLocals = localsObject._lazyLocals as Record unknown> | undefined; + if (!lazyLocals) throw new Error('No _lazyLocals field found on referenced object.'); + + for (const lazyLocal of Object.keys(lazyLocals)) { + Object.defineProperty(localsObject, lazyLocal, { + get: function () { + delete this[lazyLocal]; + return this[lazyLocal] = lazyLocals[lazyLocal](); + }, + configurable: true, + }); + } + } +} diff --git a/src/db/MysqlConnectionManager.ts b/src/db/MysqlConnectionManager.ts index e5ee183..deaa9fc 100644 --- a/src/db/MysqlConnectionManager.ts +++ b/src/db/MysqlConnectionManager.ts @@ -242,6 +242,9 @@ export default class MysqlConnectionManager { if (args.length > i + 1) { migrationId = parseInt(args[i + 1]); } + + logger.info('Rolling back migration', migrationId); + await this.prepare(false); await this.rollbackMigration(migrationId); return; diff --git a/src/frontend/AssetPreCompiler.ts b/src/frontend/AssetPreCompiler.ts index c87d8b7..1d86d58 100644 --- a/src/frontend/AssetPreCompiler.ts +++ b/src/frontend/AssetPreCompiler.ts @@ -4,11 +4,9 @@ import path from "path"; import {logger} from "../Logger.js"; import {doesFileExist, listFilesRecursively} from "../Utils.js"; -import Globals from "./Globals.js"; export default abstract class AssetPreCompiler { protected readonly assetPaths: string[]; - private globals?: Globals; private watcher?: FSWatcher; private afterPreCompileHandlers: ((watch: boolean) => Promise)[] = []; private inputChangeHandler?: (restart: boolean) => Promise; @@ -57,15 +55,6 @@ export default abstract class AssetPreCompiler { return this.assetPaths; } - protected getGlobals(): Globals { - if (!this.globals) throw new Error('globals field not intialized.'); - return this.globals; - } - - public setGlobals(globals: Globals): void { - this.globals = globals; - } - public async stop(): Promise { if (this.watcher) { await this.watcher.close(); diff --git a/src/frontend/Globals.ts b/src/frontend/Globals.ts deleted file mode 100644 index ed237da..0000000 --- a/src/frontend/Globals.ts +++ /dev/null @@ -1,11 +0,0 @@ -export default class Globals { - private readonly globals: Record = {}; - - public get(): Record { - return {...this.globals}; - } - - public set(key: string, value: unknown): void { - this.globals[key] = value; - } -} diff --git a/src/frontend/SvelteViewEngine.ts b/src/frontend/SvelteViewEngine.ts index e267b90..62bc756 100644 --- a/src/frontend/SvelteViewEngine.ts +++ b/src/frontend/SvelteViewEngine.ts @@ -2,7 +2,6 @@ import "./register_svelte/register_svelte.js"; import clearModule from "clear-module"; import config from "config"; -import crypto from "crypto"; import {promises as fs} from 'fs'; import path from "path"; import requireFromString from "require-from-string"; @@ -14,23 +13,10 @@ import {logger} from "../Logger.js"; import FileCache from "../utils/FileCache.js"; import ViewEngine from "./ViewEngine.js"; -const BACKEND_CODE_PREFIX = '$locals.'; -const BACKEND_CODE_PREFIX_TEMPORARY_HOLDER = '$$locals$$'; -const COMPILED_SVELTE_EXTENSION = '.precompiled'; - export default class SvelteViewEngine extends ViewEngine { - public static getPreCompileSeparator(canonicalViewName: string): string { - return '\n---' + - crypto.createHash('sha1') - .update(path.basename(path.resolve(canonicalViewName))) - .digest('base64') + - '---\n'; - } - - private readonly fileCache: FileCache = new FileCache(); - private readonly dependencyCache: Record> = {}; - private readonly preprocessingCache: Record = {}; + private readonly reverseDependencyCache: Record> = {}; + private readonly preprocessingCache: Record = {}; private readonly cssCache: Record = {}; public constructor( @@ -48,8 +34,8 @@ export default class SvelteViewEngine extends ViewEngine { public async onFileRemove(file: string): Promise { const canonicalName = this.toCanonicalName(file); delete this.preprocessingCache[canonicalName]; - delete this.dependencyCache[canonicalName]; - Object.values(this.dependencyCache).forEach(set => set.delete(canonicalName)); + delete this.reverseDependencyCache[canonicalName]; + Object.values(this.reverseDependencyCache).forEach(set => set.delete(canonicalName)); await super.onFileRemove(file); } @@ -59,34 +45,26 @@ export default class SvelteViewEngine extends ViewEngine { ): Promise { const canonicalViewName = this.toCanonicalName(file); - // View - const actualFile = path.join(this.targetDir, canonicalViewName + COMPILED_SVELTE_EXTENSION); - const view = await this.fileCache.get(actualFile, !config.get('view.cache')); + const rootTemplateFile = await this.resolveFileFromCanonicalNameOrFail('templates/svelte_template.html'); + const rawOutput = await this.fileCache.get(rootTemplateFile, !config.get('view.cache')); - // Root template - const templateFile = await this.resolveFileFromCanonicalNameOrFail('templates/svelte_template.html'); - const rawOutput = await this.fileCache.get(templateFile, !config.get('view.cache')); - - // Pre-compiled parts - const [ - backendCalls, + locals.isSsr = true; + const { head, html, css, - ] = view.split(SvelteViewEngine.getPreCompileSeparator(canonicalViewName)); + } = await this.renderSvelteSsr(canonicalViewName, locals); - const localMap: Record = this.compileBackendCalls(backendCalls.split('\n'), locals, false); - const actualLocals = JSON.stringify(localMap, (key, value) => { - if (key.startsWith('_')) return undefined; - return typeof value === 'function' ? - value.toString() : - value; + locals.isSsr = false; + const serializedLocals = JSON.stringify(locals, (key, value) => { + if (key.startsWith('_') || typeof value === 'function') return undefined; + return value; }); // Replaces const replaceMap: Record = { canonicalViewName: canonicalViewName, - locals: actualLocals, + locals: serializedLocals, head: head, html: html, css: css, @@ -104,55 +82,17 @@ export default class SvelteViewEngine extends ViewEngine { const targetFile = path.join(this.targetDir, canonicalName); logger.info(canonicalName + ' > ', 'Pre-compiling', canonicalName, '->', targetFile); - const {backendCalls} = await this.preprocess(canonicalName); + await this.preprocessSvelte(canonicalName); - // Server Side Render (initial HTML and CSS, no-js) - const ssr = await this.compileSsr(canonicalName); - - const separator = SvelteViewEngine.getPreCompileSeparator(canonicalName); - const finalCode = [ - [...new Set(backendCalls).values()].join('\n'), - ssr.head, - ssr.html, - ssr.css, - ].join(separator); - - const swafViewFile = path.join(this.targetDir, canonicalName + COMPILED_SVELTE_EXTENSION); - await fs.mkdir(path.dirname(swafViewFile), {recursive: true}); - await fs.writeFile(swafViewFile, finalCode); - - if (alsoCompileDependents && Object.keys(this.dependencyCache).indexOf(canonicalName) >= 0) { - logger.info(canonicalName + ' > ', 'Compiling dependents...'); - for (const dependent of [...this.dependencyCache[canonicalName]]) { + if (alsoCompileDependents && Object.keys(this.reverseDependencyCache).indexOf(canonicalName) >= 0) { + logger.info(canonicalName + ' > ', 'Pre-compiling dependents...'); + for (const dependent of [...this.reverseDependencyCache[canonicalName]]) { await this.preCompile(dependent, true); } } } - private resolveDependencies(source: string, canonicalViewName: string): string[] { - const dependencies: string[] = []; - - for (const match of source.matchAll(/import .+ from ['"](.+?\.svelte)['"];/gm)) { - dependencies.push(path.join(path.dirname(canonicalViewName), match[1])); - } - - // Clear existing links from cache - for (const dependency of Object.keys(this.dependencyCache)) { - this.dependencyCache[dependency].delete(canonicalViewName); - } - - // Add new links to cache - for (const dependency of dependencies) { - if (Object.keys(this.dependencyCache).indexOf(dependency) < 0) { - this.dependencyCache[dependency] = new Set(); - } - this.dependencyCache[dependency].add(canonicalViewName); - } - - return dependencies; - } - - private async preprocess(canonicalName: string): Promise { + private async preprocessSvelte(canonicalName: string): Promise { // Cache if (Object.keys(this.preprocessingCache).indexOf(canonicalName) >= 0) { return this.preprocessingCache[canonicalName]; @@ -168,13 +108,10 @@ export default class SvelteViewEngine extends ViewEngine { // Read source file if code was not already provided const code = await this.fileCache.get(file, !config.get('view.cache')); - // Replace backend calls - const replacedBackendCalls = await this.replaceBackendCalls(canonicalName, code); - // Preprocess svelte logger.info(canonicalName + ' > ', 'Svelte preprocessing'); - const preprocessed = await preprocess( - replacedBackendCalls.code, + const processed = await preprocess( + code, sveltePreprocess({ typescript: { tsconfigFile: 'src/assets/views/tsconfig.json', @@ -186,95 +123,26 @@ export default class SvelteViewEngine extends ViewEngine { ); // Write to output file - await fs.writeFile(outputFile, preprocessed.code); + await fs.writeFile(outputFile, processed.code); - // Preprocess dependencies - const backendCalls: string[] = replacedBackendCalls.backendCalls; - for (const dependency of this.resolveDependencies(code, canonicalName)) { - backendCalls.push(...(await this.preprocess(dependency)).backendCalls); - } + this.resolveAndCacheDependencies(processed.code, canonicalName); - return this.preprocessingCache[canonicalName] = { - backendCalls: backendCalls, - code: preprocessed.code, - }; + return this.preprocessingCache[canonicalName] = processed.code; } - private async replaceBackendCalls(canonicalName: string, code: string): Promise { - logger.info(canonicalName + ' > ', 'Replacing backend calls'); - - // Skip replace if there is no swaf export - if (!code.match(/import[ \n]+{[ \n]*locals[ \n]*}[ \n]+from[ \n]+["'](\.\.\/)+ts\/stores(\.js)?["']/)) { - return { - backendCalls: [], - code: code, - }; - } - - - let output = code; - const backendCalls = new Set(); - - let index = 0; - while ((index = output.indexOf(BACKEND_CODE_PREFIX, index + 1)) >= 0) { - // Escaping - if (index > 0 && output[index - 1] === '\\') { - const isEscapingEscaped: boolean = index > 1 && output[index - 2] === '\\'; - output = output.substring(0, index - 1 - (isEscapingEscaped ? 1 : 0)) + - output.substring(index, output.length); - continue; - } - - const startIndex = index + BACKEND_CODE_PREFIX.length; - let endIndex = startIndex; - let struct = 0; - - while (endIndex < output.length) { - if (['(', '[', '{'].indexOf(output[endIndex]) >= 0) struct++; - if ([')', ']', '}'].indexOf(output[endIndex]) >= 0) { - struct--; - if (struct <= 0) { - if (struct === 0) endIndex++; - break; - } - } - if ([' ', '\n', '<', '.', '\'', '"', '?', ','].indexOf(output[endIndex]) >= 0 && struct === 0) break; - endIndex++; - } - - let backendCall = output.substring(startIndex, endIndex); - if (backendCall.match(/([^()]+)\((.*?)\)/)) { - backendCall = backendCall.replace(/([^()]+)\((.*?)\)/, "'$1', `[$2]`"); - } else { - backendCall = backendCall.replace(/([^()]+)(\(\))?/, "'$1'"); - } - backendCalls.add(backendCall); - - output = output.substring(0, index) + - '$locals(' + backendCall.split(BACKEND_CODE_PREFIX).join(BACKEND_CODE_PREFIX_TEMPORARY_HOLDER) + ')' + - output.substring(endIndex, output.length); - } - output = output.split(BACKEND_CODE_PREFIX_TEMPORARY_HOLDER).join(BACKEND_CODE_PREFIX); - - return { - backendCalls: [...backendCalls], - code: output, - }; - } - - private async compileSsr(canonicalName: string): Promise<{ + private async renderSvelteSsr(canonicalName: string, locals: {[key: string]: unknown}): Promise<{ head: string, css: string, html: string, }> { const targetFile = path.join(this.targetDir, canonicalName); - const {backendCalls, code} = await this.preprocess(canonicalName); + const code = await this.fileCache.get(targetFile, !config.get('view.cache')); // Get dependencies css const dependenciesCss: string[] = []; - for (const dependency of this.resolveDependencies(code, canonicalName)) { - if (this.cssCache[dependency] === undefined) { - await this.compileSsr(dependency); + for (const dependency of this.resolveAndCacheDependencies(code, canonicalName)) { + if (this.cssCache[dependency] === undefined || !config.get('view.cache')) { + await this.renderSvelteSsr(dependency, locals); } const css = this.cssCache[dependency]; if (css === undefined) { @@ -295,15 +163,8 @@ export default class SvelteViewEngine extends ViewEngine { }); // Load locals into locals store - const localsModulePath = path.resolve(this.targetDir, "../ts/stores.js"); - const localsModule = await import(localsModulePath); - const locals = this.getGlobals().get(); - const localMap = this.compileBackendCalls(backendCalls, locals, true); - localsModule.locals.set((key: string, args: string) => { - return localMap[args ? - `'${key}', \`${args}\`` - : `'${key}'`]; - }); + const storesModule = await import(path.resolve(this.targetDir, "../ts/stores.js")); + storesModule.locals.set(locals); // Load module and render const moduleId = path.resolve(targetFile); @@ -324,40 +185,26 @@ export default class SvelteViewEngine extends ViewEngine { }; } - private compileBackendCalls( - backendCalls: string[], - locals: Record, - isPreRender: boolean, - ): Record { - locals = {...locals, isPreRender}; + private resolveAndCacheDependencies(source: string, canonicalViewName: string): string[] { + const dependencies: string[] = []; - const localMap: Record = {}; - backendCalls.forEach(code => { - const key = code.substring(1, code.indexOf(',') >= 0 ? code.indexOf(',') - 1 : code.length - 1); - if (code.indexOf('`[') >= 0) { - const args = code.substring(code.indexOf('`[') + 2, code.length - 2) - .split(/, *?/) - .map(arg => { - if (arg.startsWith("'")) return '"' + arg.substring(1, arg.length - 1) + '"'; - return arg; - }) - .filter(arg => arg.length > 0) - .map(arg => { - return Function(`"use strict";const $locals = arguments[0];return (${arg});`)(locals); - }); // Uses named parameter locals + for (const match of source.matchAll(/import .+ from ['"](.+?\.svelte)['"];/gm)) { + dependencies.push(path.join(path.dirname(canonicalViewName), match[1])); + } - const f = locals[key]; - if (typeof f !== 'function') throw new Error(key + ' is not a function.'); - localMap[`'${key}', \`[${code.substring(code.indexOf('`[') + 2, code.length - 2)}]\``] = f.call(locals, ...args); - } else { - localMap[`'${key}'`] = locals[key]; + // Clear existing links from cache + for (const dependency of Object.keys(this.reverseDependencyCache)) { + this.reverseDependencyCache[dependency].delete(canonicalViewName); + } + + // Add new links to cache + for (const dependency of dependencies) { + if (Object.keys(this.reverseDependencyCache).indexOf(dependency) < 0) { + this.reverseDependencyCache[dependency] = new Set(); } - }); - return localMap; + this.reverseDependencyCache[dependency].add(canonicalViewName); + } + + return dependencies; } } - -type PreprocessingCacheEntry = { - backendCalls: string[], - code: string, -}; diff --git a/src/frontend/ViewEngine.ts b/src/frontend/ViewEngine.ts index b8992f3..9363271 100644 --- a/src/frontend/ViewEngine.ts +++ b/src/frontend/ViewEngine.ts @@ -1,5 +1,6 @@ import {Express} from "express"; +import LazyLocalsCoreComponent from "../components/core/LazyLocalsCoreComponent.js"; import AssetPreCompiler from "./AssetPreCompiler.js"; export default abstract class ViewEngine extends AssetPreCompiler { @@ -18,10 +19,11 @@ export default abstract class ViewEngine extends AssetPreCompiler { locals: Record, ): Promise; - public setup(app: Express, main: boolean): void { + public setup(app: Express, main: boolean, lazyLocalsComponent: LazyLocalsCoreComponent): void { app.engine(this.extension, (path, options, callback) => { - // Props (locals) - const locals = Object.assign(this.getGlobals().get(), options); + const locals = {...options}; + + lazyLocalsComponent.setupLazyLocals(locals); this.render(path, locals) .then(value => callback(null, value)) diff --git a/src/helpers/BackendController.ts b/src/helpers/BackendController.ts index 0e27ae8..e0f48ee 100644 --- a/src/helpers/BackendController.ts +++ b/src/helpers/BackendController.ts @@ -53,7 +53,7 @@ export default class BackendController extends Controller { } protected async getIndex(req: Request, res: Response): Promise { - res.render('backend/index', { + res.formatViewData('backend/index', { menu: await Promise.all(BackendController.menu.map(async m => ({ link: await m.getLink(), display_string: await m.getDisplayString(), @@ -66,7 +66,7 @@ export default class BackendController extends Controller { const accounts = await User.paginate(req, 20, User.select() .where('approved', 0) .with('mainEmail')); - res.render('backend/accounts_approval', { + res.formatViewData('backend/accounts_approval', { accounts: accounts.map(account => Object.assign({ mainEmailStr: account.mainEmail.getOrFail()?.email, created_at_iso: account.created_at?.toISOString(), diff --git a/src/mail/MailController.ts b/src/mail/MailController.ts index bb302db..1040429 100644 --- a/src/mail/MailController.ts +++ b/src/mail/MailController.ts @@ -7,8 +7,8 @@ export default class MailController extends Controller { this.get("/mail/:template", this.getMail, 'mail'); } - protected async getMail(request: Request, response: Response): Promise { + protected async getMail(request: Request, res: Response): Promise { const template = request.params['template']; - response.render(`mails/${template}.mnjk`, request.query); + res.formatViewData(`mails/${template}.mnjk`, request.query); } } diff --git a/src/types/Express.d.ts b/src/types/Express.d.ts index 3e1ae17..eef6759 100644 --- a/src/types/Express.d.ts +++ b/src/types/Express.d.ts @@ -33,6 +33,16 @@ declare global { getIntendedUrl(): string | null; } + + export interface Response { + setLazyLocal(key: string, valueProvider: () => unknown): void; + + formatViewData( + viewName: string, + data?: Record, + callback?: (err: Error, html: string) => void, + ): void; + } } } diff --git a/test/AuthenticationApprovalMode.test.ts b/test/AuthenticationApprovalMode.test.ts index 3ddd350..fba3cda 100644 --- a/test/AuthenticationApprovalMode.test.ts +++ b/test/AuthenticationApprovalMode.test.ts @@ -142,6 +142,7 @@ describe('Authenticate with email (magic_link)', () => { // Authenticate await agent.post('/auth/login?' + querystring.stringify({redirect_uri: '/redirect-uri'})) + .accept('json') .set('Cookie', cookies) .send({ csrf: csrf, diff --git a/test/CsrfProtectionComponent.test.ts b/test/CsrfProtectionComponent.test.ts index 302b780..b2c29c9 100644 --- a/test/CsrfProtectionComponent.test.ts +++ b/test/CsrfProtectionComponent.test.ts @@ -12,7 +12,7 @@ useApp(async (addr, port) => { this.use(new class extends Controller { public routes(): void { this.get('/', (req, res) => { - res.send(CsrfProtectionComponent.getCsrfToken(req.getSession())); + res.send(this.getApp().as(CsrfProtectionComponent).getSessionCsrfToken(req.getSession())); }, 'csrf_test'); this.post('/', (req, res) => { @@ -35,6 +35,7 @@ describe('Test CSRF protection', () => { test('no csrf token should be in session at first', (done) => { const agent = supertest(app.getExpressApp()); agent.post('/') + .accept('json') .expect(401) .then(res => { expect(res.text).toContain(`You weren't assigned any CSRF token.`); @@ -55,6 +56,7 @@ describe('Test CSRF protection', () => { const agent = supertest(app.getExpressApp()); agent.post('/') + .accept('json') .set('Cookie', cookies) .expect(401) .then((res) => { @@ -68,6 +70,7 @@ describe('Test CSRF protection', () => { const agent = supertest(app.getExpressApp()); agent.post('/') + .accept('json') .set('Cookie', cookies) .set('Content-Type', 'application/json') .send({csrf: 'not_a_valid_csrf'}) diff --git a/test/_authentication_common.ts b/test/_authentication_common.ts index b2df631..58d2e66 100644 --- a/test/_authentication_common.ts +++ b/test/_authentication_common.ts @@ -23,8 +23,10 @@ export async function followMagicLinkFromMail( expect(query).toBeDefined(); await agent.get('/magic/link?' + query) + .accept('json') .expect(200); await agent.get('/magic/lobby') + .accept('json') .set('Cookie', cookies) .expect(302) .expect('Location', expectedRedirectUrl); @@ -55,10 +57,10 @@ export function authAppProvider(withUsername: boolean = true, approvalMode: bool this.use(new class extends Controller { public routes(): void { this.get('/', (req, res) => { - res.render('home'); + res.formatViewData('home'); }, 'home'); this.get('/csrf', (req, res) => { - res.send(CsrfProtectionComponent.getCsrfToken(req.getSession())); + res.send(this.getApp().as(CsrfProtectionComponent).getSessionCsrfToken(req.getSession())); }, 'csrf'); this.get('/is-auth', async (req, res) => { const proofs = await this.getApp().as(AuthComponent).getAuthGuard().getProofs(req); diff --git a/test/_mail_server.ts b/test/_mail_server.ts index 5761401..92f282b 100644 --- a/test/_mail_server.ts +++ b/test/_mail_server.ts @@ -1,7 +1,7 @@ import MailDev, {Mail} from "maildev"; export const MAIL_SERVER = new MailDev({ - ip: 'localhost', + ip: '127.0.0.1', }); export async function setupMailServer(): Promise { diff --git a/test/assets/views/AllTests.svelte b/test/assets/views/AllTests.svelte index bbbf5a9..e837dd9 100644 --- a/test/assets/views/AllTests.svelte +++ b/test/assets/views/AllTests.svelte @@ -12,7 +12,7 @@ } -
{$locals.isPreRender ? 'SSR' : 'CSR'}
+
{$locals.isSsr ? 'SSR' : 'CSR'}
diff --git a/yarn.lock b/yarn.lock index cb58d26..7da86e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -535,7 +535,7 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" -"@mapbox/node-pre-gyp@^1.0.1": +"@mapbox/node-pre-gyp@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.7.tgz#a26919cac6595662703330d1820a0ca206f45521" integrity sha512-PplSvl4pJ5N3BkVjAdDzpPhVUPdC73JgttkR+LnBx2OORC1GCQsBjUeEuipf9uOaAM1SbxcdZFfR3KDTKm2S0A== @@ -905,10 +905,10 @@ dependencies: "@types/node" "*" -"@types/node@*": - version "16.11.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.9.tgz#879be3ad7af29f4c1a5c433421bf99fab7047185" - integrity sha512-MKmdASMf3LtPzwLyRrFjtFFZ48cMf8jmX5VRYrDQiJa8Ybu5VAmkqBWqKU8fdCwD8ysw4mQ9nrEHvzg6gunR7A== +"@types/node@*", "@types/node@^16.11.10": + version "16.11.10" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.10.tgz#2e3ad0a680d96367103d3e670d41c2fed3da61ae" + integrity sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA== "@types/nodemailer@^6.4.0": version "6.4.4" @@ -974,9 +974,9 @@ "@types/node" "*" "@types/sass@^1.16.0": - version "1.43.0" - resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.0.tgz#b4cebe057d887ed66dc6813fd6cbef22043057bb" - integrity sha512-DPSXNJ1rYLo88GyF9tuB4bsYGfpKI1a4+wOQmc+LI1SUoocm9QLRSpz0GxxuyjmJsYFIQo/dDlRSSpIXngff+w== + version "1.43.1" + resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.1.tgz#86bb0168e9e881d7dade6eba16c9ed6d25dc2f68" + integrity sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g== dependencies: "@types/node" "*" @@ -1277,13 +1277,13 @@ arg@^4.1.0: integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== argon2@^0.28.2: - version "0.28.2" - resolved "https://registry.yarnpkg.com/argon2/-/argon2-0.28.2.tgz#b583e4ef5b052a83bfe146752844b9fa526dba29" - integrity sha512-8oRk3kPlL0lLletENzhpbF9zoZJqvIHwTkjBseMrg1uD4gBMqhqnjJz1z3lEtwT0oqQAEkEwsEpsjaQBBRHcWw== + version "0.28.3" + resolved "https://registry.yarnpkg.com/argon2/-/argon2-0.28.3.tgz#e5234eccf20a643ffc3b1bbd1aa9e81092e0d8e9" + integrity sha512-NkEJOImg+T7nnkx6/Fy8EbjZsF20hbBBKdVP/YUxujuLTAjIODmrFeY4vVpekKwGAGDm6roXxluFQ+CIaoVrbg== dependencies: - "@mapbox/node-pre-gyp" "^1.0.1" + "@mapbox/node-pre-gyp" "^1.0.7" "@phc/format" "^1.0.0" - node-addon-api "^3.0.2" + node-addon-api "^4.2.0" opencollective-postinstall "^2.0.3" argparse@^1.0.7: @@ -1741,9 +1741,9 @@ camelcase@^6.2.0: integrity sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA== caniuse-lite@^1.0.30001280: - version "1.0.30001282" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001282.tgz#38c781ee0a90ccfe1fe7fefd00e43f5ffdcb96fd" - integrity sha512-YhF/hG6nqBEllymSIjLtR2iWDDnChvhnVJqp+vloyt2tEHFG1yBR+ac2B/rOw0qOK0m0lEXU2dv4E/sMk5P9Kg== + version "1.0.30001283" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001283.tgz#8573685bdae4d733ef18f78d44ba0ca5fe9e896b" + integrity sha512-9RoKo841j1GQFSJz/nCXOj0sD7tHBtlowjYlrqIUS812x9/emfBLBt6IyMz1zIaYc/eRL8Cs6HPUVi2Hzq4sIg== caw@^2.0.0, caw@^2.0.1: version "2.0.1" @@ -1838,9 +1838,9 @@ ci-info@^2.0.0: integrity sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ== ci-info@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.2.0.tgz#2876cb948a498797b5236f0095bc057d0dca38b6" - integrity sha512-dVqRX7fLUm8J6FgHJ418XuIgDLZDkYcDFTeL6TA2gt5WlIZUQrrH6EZrNClwT/H0FateUsZkGIOPRrLbP+PR9A== + version "3.3.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" + integrity sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw== cjs-module-lexer@^1.0.0: version "1.2.2" @@ -2273,9 +2273,9 @@ debug@2.6.9, debug@^2.6.9: ms "2.0.0" debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2: - version "4.3.2" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b" - integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw== + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== dependencies: ms "2.1.2" @@ -2624,9 +2624,9 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= electron-to-chromium@^1.3.896: - version "1.3.904" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.904.tgz#52a353994faeb0f2a9fab3606b4e0614d1af7b58" - integrity sha512-x5uZWXcVNYkTh4JubD7KSC1VMKz0vZwJUqVwY3ihsW0bst1BXDe494Uqbg3Y0fDGVjJqA8vEeGuvO5foyH2+qw== + version "1.4.4" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.4.tgz#57311918524c1a26878c330537f967804d43788a" + integrity sha512-teHtgwcmVcL46jlFvAaqjyiTLWuMrUQO1JqV303JKB4ysXG6m8fXSFhbjal9st0r9mNskI22AraJZorb1VcLVg== emittery@^0.8.1: version "0.8.1" @@ -4260,9 +4260,9 @@ is-string@^1.0.5, is-string@^1.0.7: has-tostringtag "^1.0.0" is-svg@^4.2.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.1.tgz#8c63ec8c67c8c7f0a8de0a71c8c7d58eccf4406b" - integrity sha512-h2CGs+yPUyvkgTJQS9cJzo9lYK06WgRiXUqBBHtglSzVKAuH4/oWsqk7LGfbSa1hGk9QcZ0SyQtVggvBA8LZXA== + version "4.3.2" + resolved "https://registry.yarnpkg.com/is-svg/-/is-svg-4.3.2.tgz#a119e9932e1af53f6be1969d1790d6cc5fd947d3" + integrity sha512-mM90duy00JGMyjqIVHu9gNTjywdZV+8qNasX8cm/EEYZ53PHDgajvbBwNVvty5dwSAxLUD3p3bdo+7sR/UMrpw== dependencies: fast-xml-parser "^3.19.0" @@ -5092,9 +5092,9 @@ lru-cache@^6.0.0: yallist "^4.0.0" lucide@^0.16.17: - version "0.16.17" - resolved "https://registry.yarnpkg.com/lucide/-/lucide-0.16.17.tgz#52466954e97ea3efac0191b2452f42932d41fdf7" - integrity sha512-zAB+aRo6/naheCMQ8AYNAXCnsxCZQb7pI52taqjcfBDPkH3jJnx+b5UnvR4kncVPw4t7/U4GARH/23PleFgnHQ== + version "0.16.19" + resolved "https://registry.yarnpkg.com/lucide/-/lucide-0.16.19.tgz#ac79f36dc544f34fc3930a3bac82226c2f7aff18" + integrity sha512-OoA7LlDTo4Q65cwEgDZBlsvd3veRz5dnAeJZQnvDSjqldUCocBZOxupbRFSkGjKncII+jColKBQZBTvvdtTnhg== magic-string@^0.25.7: version "0.25.7" @@ -5289,46 +5289,46 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" -mjml-accordion@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-accordion/-/mjml-accordion-4.10.4.tgz#f28933f737a54fd3a995e0024f8c85b6b95f6fc4" - integrity sha512-C9yyHcQt1yh9c0nnq/VaUiMCScJVY0HGvWgsDhwG6VGoUttPDpGZLHf9aWc4ypkLTrA7rk7PSn1XDmfYCbnFbg== +mjml-accordion@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-accordion/-/mjml-accordion-4.11.0.tgz#91893ed5dcb5f81f8876cc0117d16c9655adf75b" + integrity sha512-u6cPMl4z8JeRIq0sGHWfzwE5SqwBhSPHJ8wElhfU7CK6gAoUTz4BIL/03da2whXk0S34n5CSU453JpuWV+3bQQ== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-body@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-body/-/mjml-body-4.10.4.tgz#a0e41960ab0734de3a351041588e38d53ac21fbd" - integrity sha512-MN4t9ZR09LXVRmHI/FrSm04gTvfEqasZ+1GAbbmb7G0X/NyDXuh2DZTSlFS9gxmYwDYRQeZbOixv1T4MxpbT2Q== +mjml-body@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-body/-/mjml-body-4.11.0.tgz#486b0e4a96b720d47ad1bac36c70d41915882482" + integrity sha512-+SQXOjKOr9IMblmslrUbv+ahF/SkqT7mAuix1P/F9ev3aS+WN7Gy/lyJLaq1zhSQCmzrBDovHez3bJ7NGToU0g== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-button@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-button/-/mjml-button-4.10.4.tgz#df6d6aa46c196703c4cafcb5b3e0460baa93f11b" - integrity sha512-gGV2X/vrjhfHgQ+e3OH7WcyKnsYZs4PuI7+jl7ajO66veyRoZZteyeAtrEu+NHub0iBbQ/ft12B7yuCf5vgBhw== +mjml-button@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-button/-/mjml-button-4.11.0.tgz#f91791cfedc3ec32a8d64c1cdab2d71be376300b" + integrity sha512-v/MwA2Kq2MlHJwrajdAqUihjAQivD8FpkpOAcqdgqI1ffaEDzd6FGFt5qOtVD9BncChQ4a51haOnPd+kbBLlCA== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-carousel@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-carousel/-/mjml-carousel-4.10.4.tgz#ad964c9c691b6db105b7fd09be916f31f61c08f8" - integrity sha512-jwt63Gz27iwnTSHWY3W3jiBlCvKar/6SU/fezLZPdwcEl1dF+kBN1zQYwqO/uXBcxLnar0cg1p0jKU5SfL3XpQ== +mjml-carousel@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-carousel/-/mjml-carousel-4.11.0.tgz#c848c50437be95d9956b22d8610b259e68be9a5a" + integrity sha512-/owKKSg3DNes1rauPPhlGCFdZ4zzoxdztPLGOm9TSjkbL5q2cN9NMfzNdTYLnnVG5G5XWX0THjDeeq+bGnHw8w== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-cli@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-cli/-/mjml-cli-4.10.4.tgz#aa71e57d4a23a0df8f6b887700c2c31a10d4330a" - integrity sha512-FzNuKJ3iqYslejRGJa88IIHtUrUNtox4+bCaroVubvxaLkosi0phyg1g5ndQgAFfX+TKmMslgXnFUsbOxvOeOQ== +mjml-cli@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-cli/-/mjml-cli-4.11.0.tgz#192713b3a97594a9366b8f97c8d8b9572eba147f" + integrity sha512-jgkxNY+sY+CwiUlO6VX8cwVjlLBXIzE7X4fXkQ2RjXpbAvsN8uDafST8oL8PDlxNIjOwK6YJySzZSLXSKTF5Zg== dependencies: "@babel/runtime" "^7.14.6" chokidar "^3.0.0" @@ -5336,25 +5336,25 @@ mjml-cli@4.10.4: html-minifier "^4.0.0" js-beautify "^1.6.14" lodash "^4.17.21" - mjml-core "4.10.4" - mjml-migrate "4.10.4" - mjml-parser-xml "4.10.4" - mjml-validator "4.10.4" + mjml-core "4.11.0" + mjml-migrate "4.11.0" + mjml-parser-xml "4.11.0" + mjml-validator "4.11.0" yargs "^16.1.0" -mjml-column@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-column/-/mjml-column-4.10.4.tgz#53a1b5a34a04f3760129e7ac215e153647116eb2" - integrity sha512-XUgrGRsxIWR0UBXI2jBRrGqg/RZSeiFBQSB6+oQ7Jz7VaWHD+zBShJF3FkeKw9oTA7Deq1D9CY8Hd4BJN+7tEQ== +mjml-column@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-column/-/mjml-column-4.11.0.tgz#6fafea6e09e6463a7656cdfe5d3c61bc08dd18fe" + integrity sha512-yuupexywYXuXTvxxGLPZw6S/D6fz4b41ZarOkEPW8Pbj7FkfnBTO3QokAS5KapJ9x6sIAVs+cCgUul87dwOBzg== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-core@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-core/-/mjml-core-4.10.4.tgz#ed165b692498d7d729105fb3ec2cd10aad374f89" - integrity sha512-y8qtLr+jzoRxNbiQ2/yLfOMtjsCEKhj7t8KOZmUH4wvP/V8VtzR3+5uPyIhquKEr0FKS3gxPYlErW/9/K8K8MA== +mjml-core@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-core/-/mjml-core-4.11.0.tgz#e673bc70250861e2c687cb8834e9af31b690eea6" + integrity sha512-UTI1exu9/0lOF40aacAzC9A7RrJPcay7WYurYsb+X2LbDyfBlSx5ZCtuUTHwLfIz3PL6rSg8yrZ3KtlmhQJ/kQ== dependencies: "@babel/runtime" "^7.14.6" cheerio "1.0.0-rc.10" @@ -5363,263 +5363,263 @@ mjml-core@4.10.4: js-beautify "^1.6.14" juice "^7.0.0" lodash "^4.17.21" - mjml-migrate "4.10.4" - mjml-parser-xml "4.10.4" - mjml-validator "4.10.4" + mjml-migrate "4.11.0" + mjml-parser-xml "4.11.0" + mjml-validator "4.11.0" -mjml-divider@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-divider/-/mjml-divider-4.10.4.tgz#7c0ced8709be4b8760d1562939ca4ec734cf6e86" - integrity sha512-HMAwrSSsnHICr4gosyfdNKqzr6optlOTwT37Rpl15QDBrmJa3ph9cuIH7GhlhIlJB4AIw/dVqbNojvI9tPqphA== +mjml-divider@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-divider/-/mjml-divider-4.11.0.tgz#4eb737f0d369d6736c022cd8bc76e7a27c8dd28f" + integrity sha512-jS8Sa7uuFTmThtrcV3FICtbqmA0yJplldaNJwW5upiq7MEXuSwLNzjNeodaTpsVWevf/stE3C4lBA3+V4ascMg== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-group@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-group/-/mjml-group-4.10.4.tgz#4515f876c919e276c4188deb74f4a231a6e21edc" - integrity sha512-BmrByiEFvJG97gph9ItYgtGWhDGg5kE0Zg5oda3+m4OfXeyEb6NzdqlgMM9VgRiyqnFA3P0UnwMqRpidOMNohg== +mjml-group@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-group/-/mjml-group-4.11.0.tgz#fe57cd63f1ac12187124122268820ace785f8b7f" + integrity sha512-Yv1ZcPNk4bxOK/eol6bjJvRJBaOCzVY88QO8IfCQifAZuiXhatxwsuwaRKNcTi4cB+IHwl4JguZHgsFRi/Gf/g== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-head-attributes@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-head-attributes/-/mjml-head-attributes-4.10.4.tgz#919cc2b33239ca6dad61fa71997a5a6980a533de" - integrity sha512-JnL/glaPmtLVEdFashvrXi7qcFpodpt830J+Camabc9LQNGYS/7d4PqhMNAmVI7BAaxBgmhjHGvCeUDkwg8jHg== +mjml-head-attributes@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-head-attributes/-/mjml-head-attributes-4.11.0.tgz#9f493cb3a78deed5d7ac25d7098440ed274be274" + integrity sha512-azeDRLGH7cU6PoXLd08E/H3UDHFsblBTcme++OWrj5B21vaFguOBySO2eWFTyWLthy+xst8FftWV46f8BWvciw== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-head-breakpoint@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-head-breakpoint/-/mjml-head-breakpoint-4.10.4.tgz#43e6d388c7d9fb195ef74cf40e285c4cc25dfb50" - integrity sha512-XbHo/1RkGDs5Nh64c1AOfXFIkJpCLuCVQwwPpM0FwKJnUh7jPTykJKHj5FqvQLHNzsNCgpvs4fKchDte77AGsQ== +mjml-head-breakpoint@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-head-breakpoint/-/mjml-head-breakpoint-4.11.0.tgz#fb39f8919dc64085e307b0db67d00a2d3c7512f6" + integrity sha512-OQ/WpXHagGoiUmBjoMHuxdigyLQrHNL6+Op0LHO87vN1GH2ap4YLcSSegIsThQLi/I7V4/JFZbzS5jV4xd4SMg== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-head-font@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-head-font/-/mjml-head-font-4.10.4.tgz#02fadf80df5e8c3a23fc432f7288f786f825f4c1" - integrity sha512-P1pXsZtntkpdWcpVe1cyWE9BzJ17tfq4WxtaFtRpnz9L/w3SvTm/OdyJT+GI3LS5D9HoR0dkk6kC9HvPs5aDvw== +mjml-head-font@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-head-font/-/mjml-head-font-4.11.0.tgz#25379dd23b597522dc6a944af21c0cf725a73ee5" + integrity sha512-AJQ9lhNzNr0hwPjUID76LNna3U8ycmFH1lyipZOWHTivOVGkSYs8PaboxORfU/QAEDKo/p8LafBAoDPEdSigxA== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-head-html-attributes@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-head-html-attributes/-/mjml-head-html-attributes-4.10.4.tgz#612c0ea67cd118998da1005eb89f96ef0ba19007" - integrity sha512-abF4nNJeXEpx6zE2TwmDTbuDHwe9787ecPQ/5uGK37WbNCbmg2D99XtkdK+96VpdL+1enT5q0ztasdg9ixOBZg== +mjml-head-html-attributes@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-head-html-attributes/-/mjml-head-html-attributes-4.11.0.tgz#ad0b1e3a0496a8e07103d803ac09aeffc0e2aa65" + integrity sha512-Gew9EAuqFu9QR3g+8FyE9WHNMszx1ejzo8dRDIuTaBCDEpaIaPVO7G5rGhT7ADqZufeYtRhBXfXjcShPD/a30g== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-head-preview@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-head-preview/-/mjml-head-preview-4.10.4.tgz#ba2d8938d88d05c117a8a8b42adf59e85c6d3d4c" - integrity sha512-Oe5Ecn+9AnEgo/RWxTEci0OK/7+ft/dUoma0k/H0Fv1CCKcyrMCFPr97A5hEeooGilxU+Jz0/gbt9PXQKpHKLA== +mjml-head-preview@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-head-preview/-/mjml-head-preview-4.11.0.tgz#2dccc5dfae58a9ef3d67c4229909313c77eb9219" + integrity sha512-f79+OtwC8CYcQpglGNfm3s74qVecdwLlDm9DD/LJvZwIdaALeIfZF36nz6+rtCLMhND+wxgqxqkmbbuS6AbO+A== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-head-style@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-head-style/-/mjml-head-style-4.10.4.tgz#3ae12c2ea254cec51595d05acd5b264f6badd61f" - integrity sha512-V3Tg1VKVqMGNLqOGXy7vQk/Rp52iidR56Vtkp5O0hITd072URyOLw1+qL4rPDqaYrbN8keK7p4T8nFL1QySA6w== +mjml-head-style@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-head-style/-/mjml-head-style-4.11.0.tgz#b02f342ecfa946626c9b50e6b49cc8a094815dba" + integrity sha512-jw0r9Ld56SkJmDUBx+27xOH+oY2O06FBcKSJIkZLJ/sefbjgJa2/ti2eso/LeGgrmTw8zge7g1pp0Fl+kPjsGA== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-head-title@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-head-title/-/mjml-head-title-4.10.4.tgz#4dfbdfc3d2f4a415543ab55dd1a9c782239fa71f" - integrity sha512-ixVNZdisq/UNUiwiCrzXkJGoIS4GfRoL/rdZLaxfJvccdJxT9LzcNvm/KC0NsWC4kjAvipo1Wo54kmaLwXy/+Q== +mjml-head-title@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-head-title/-/mjml-head-title-4.11.0.tgz#8d322e576675fa28e4425789182982e078c91895" + integrity sha512-mjYsKoE9+5LI8V0mK9SAKVKspF19TVius2q5I1FXLRdBmt4Ei87CdrSui+iixGrug0Lwt4P2W3+mK8kVGNnJAQ== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-head@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-head/-/mjml-head-4.10.4.tgz#21f5d67915bb594976af891d6358240170de2169" - integrity sha512-hADFAWgptBHnESft9gPt5JuOmlDu+N+F8gNVNjjISQt/k4ZQkyambAAuJvds/Ahc14LOgag6CWLzPQ4B0HPXPA== +mjml-head@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-head/-/mjml-head-4.11.0.tgz#3d3bbbd05beb4153936f7544ea511ef6a8a27428" + integrity sha512-KNaSsOlf5FNwHyZQD6YNZN2Eo/o7n+mZISFLyp9MvNFaT3NKIRJDaInD1WjN+w2aHdXAw4sDV3+/9/EET2bh1A== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-hero@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-hero/-/mjml-hero-4.10.4.tgz#a0bf6df250e8d28c59849141b98873542b9451f0" - integrity sha512-rk10wj7bCVkgN+Bd670BGwE8iO9u4XDHPhyx8h8eZp/PunIST1CGPYZUQ5PVzkPhnJlhI3MOtUYmpd7U1cMRIA== +mjml-hero@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-hero/-/mjml-hero-4.11.0.tgz#feddefdfc8a3a531a742ccbba5abaf571a51d9c3" + integrity sha512-tZDCGjQrDICwbsDnLvfVEsQ5+GdrIozB3oO3NxC6m2Eq04VKNBkIqq+QvJyouOxzJ3CZgO2B4rylzxc6YXuHsw== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-image@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-image/-/mjml-image-4.10.4.tgz#194b2800d1c5570a7d47ab0bad1188e7f54f7ead" - integrity sha512-xW2SdVAmTA0ZuzWfCKDYJOMiFfdhiEJDQW1rz1L3NRqI/KemSOthDKlGaXzH/0DobKcYf2N22RSAOSYULDtmmw== +mjml-image@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-image/-/mjml-image-4.11.0.tgz#4a890a5f9747eb9115fc792ca5712367421af756" + integrity sha512-qjvKS/x2arDNgKppPecmg69VyXdQbb1CNVqPwowJLyfjioTw4hxQ93suWoBKJJojRjkwauj9IRMC8TR9ffn0HQ== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-migrate@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-migrate/-/mjml-migrate-4.10.4.tgz#d56e5688fbbf216ed5c7fb7132078027a1807d9c" - integrity sha512-zBAwwhDRNA67S+U2Zb6zvQMZUW9wKGDdU7fi8tEwSH3YKNXTiXuEFFv4NuvkwPJuwmDSRbi7M2Oc50AkotJw+w== +mjml-migrate@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-migrate/-/mjml-migrate-4.11.0.tgz#266aa92369beb43c51d2e9fd606a06f1d52ebf70" + integrity sha512-Y+9U4w9LwlTkfTkHX9GdalIQDO4JQxboG4PM8g7vRKNhKSZDZH8QSr0SNhX0+fMQmjb9b0nztWbMVVvNZfMoPQ== dependencies: "@babel/runtime" "^7.14.6" js-beautify "^1.6.14" lodash "^4.17.21" - mjml-core "4.10.4" - mjml-parser-xml "4.10.4" + mjml-core "4.11.0" + mjml-parser-xml "4.11.0" yargs "^16.1.0" -mjml-navbar@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-navbar/-/mjml-navbar-4.10.4.tgz#4028c6bc40268ecd28afec131275775a774119c5" - integrity sha512-IZP3o/V4nAG6k2OzUQ7hkWOiiBqVTGXkzLeUFNb5VAkAk67us4gjJu/w/SkRmZii4y8Mhbv99HZuB5G99LxLsA== +mjml-navbar@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-navbar/-/mjml-navbar-4.11.0.tgz#8459ecac109debf4ee744bb7020d886ad0c24bb4" + integrity sha512-FEd+8RD6ra8652jXdMbhfhzT1YY8TA30c34qjJ+ULlFgvLH4GNnYRIOHp/Mgd/L38Qf3bhK/rK9ELBidJ36dLQ== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-parser-xml@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-parser-xml/-/mjml-parser-xml-4.10.4.tgz#faf885a58cbba8a74a23e3d12209c4bcf3b952d4" - integrity sha512-yN5RtUnRiZuTzbXD+r6e1IfNNyggOQcTIVadCANQjy2JLzn4FjtbXzk64cRLOPaSCKm2+eUqCE5fmOL+DMLCPg== +mjml-parser-xml@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-parser-xml/-/mjml-parser-xml-4.11.0.tgz#384e50834ba50d3060b87e54a094be2c7ed5bc81" + integrity sha512-3toQ9UKyfzXWXJ7PlTExBjzGXAzJNORv39sorPv5sG2KJsvPC6NE+e+/1GyqYFhk4JffHoDfd4mQ6w050qYATQ== dependencies: "@babel/runtime" "^7.14.6" detect-node "2.0.4" htmlparser2 "^4.1.0" lodash "^4.17.15" -mjml-preset-core@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-preset-core/-/mjml-preset-core-4.10.4.tgz#8273e603f95951e9d1cd035a63f0692a7e21db0d" - integrity sha512-FdX63CJbRpg72uEoJVOBTC/i8jd1g31akAheFTtb5/zew3sBIdPvkQGMkxY8Hn1eEZyduJiTc2SCmHiigrqD9g== +mjml-preset-core@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-preset-core/-/mjml-preset-core-4.11.0.tgz#edf93e678d3c955d01400df2fd3e36819bc25428" + integrity sha512-HU9ZFzS+A7Dx0B/EbnqYjdcIQQuxpOVRfFReQBlllZYwjWaRUjDIeIjF+cdnCb9QveuSNo4msIG00h/MKPYiGQ== dependencies: "@babel/runtime" "^7.14.6" - mjml-accordion "4.10.4" - mjml-body "4.10.4" - mjml-button "4.10.4" - mjml-carousel "4.10.4" - mjml-column "4.10.4" - mjml-divider "4.10.4" - mjml-group "4.10.4" - mjml-head "4.10.4" - mjml-head-attributes "4.10.4" - mjml-head-breakpoint "4.10.4" - mjml-head-font "4.10.4" - mjml-head-html-attributes "4.10.4" - mjml-head-preview "4.10.4" - mjml-head-style "4.10.4" - mjml-head-title "4.10.4" - mjml-hero "4.10.4" - mjml-image "4.10.4" - mjml-navbar "4.10.4" - mjml-raw "4.10.4" - mjml-section "4.10.4" - mjml-social "4.10.4" - mjml-spacer "4.10.4" - mjml-table "4.10.4" - mjml-text "4.10.4" - mjml-wrapper "4.10.4" + mjml-accordion "4.11.0" + mjml-body "4.11.0" + mjml-button "4.11.0" + mjml-carousel "4.11.0" + mjml-column "4.11.0" + mjml-divider "4.11.0" + mjml-group "4.11.0" + mjml-head "4.11.0" + mjml-head-attributes "4.11.0" + mjml-head-breakpoint "4.11.0" + mjml-head-font "4.11.0" + mjml-head-html-attributes "4.11.0" + mjml-head-preview "4.11.0" + mjml-head-style "4.11.0" + mjml-head-title "4.11.0" + mjml-hero "4.11.0" + mjml-image "4.11.0" + mjml-navbar "4.11.0" + mjml-raw "4.11.0" + mjml-section "4.11.0" + mjml-social "4.11.0" + mjml-spacer "4.11.0" + mjml-table "4.11.0" + mjml-text "4.11.0" + mjml-wrapper "4.11.0" -mjml-raw@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-raw/-/mjml-raw-4.10.4.tgz#0584d55035c4012aabd8590c8c36ab8dfc6bc6b0" - integrity sha512-O5HvmpLpLD9YESb3xXp1vMHObpUl7wF2AFcEp2MZBRJyyBQdUPLZsCGld5hQ462NWh/2WZjN/bWcUqgVqpnwyg== +mjml-raw@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-raw/-/mjml-raw-4.11.0.tgz#ccc09dcd0d65731957ab8013cc1f113017360673" + integrity sha512-DyUwC/JrE8tF7v9XaKQpQ/yAg5tT2uAWSDI6J5x3t3TNFJmRPd3iorSu8v6t1s3OiekuxfagsDDPLFuzpGwZSw== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-section@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-section/-/mjml-section-4.10.4.tgz#81d5a527d0cc52201d51609e647697366d59edd9" - integrity sha512-8J0gSi+B2Z5ag4ECUbZ1I2/1WE452pdfkwO2wXdD/gGrqkIyWC9WQKGlbT7BHEmDOKalElBTV3YwwFFhbqYWwg== +mjml-section@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-section/-/mjml-section-4.11.0.tgz#2728bfb5de97ec6ea898528e3021016b9c9c2ac6" + integrity sha512-OpinxE019Z1symrEFku3UddsvSql2aolcAiOThFuAAXz3+tUuooofGMy/XyyMOuOnktOx/5PMcVFsV84/Jye/g== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-social@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-social/-/mjml-social-4.10.4.tgz#8e859f79317184827cd2cecdbc2e2624dae45997" - integrity sha512-Tpc0CH8VBa6X66gY81BKZfzF2E6Mj6lE/R1V9h2CHG5cXJSm1TSd27KkiurNMyuEwRLQ51r5X1AlyL4LTrqt2w== +mjml-social@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-social/-/mjml-social-4.11.0.tgz#62bf699eb0f86a3d803f95c4ccbd623b32135a8e" + integrity sha512-tM5njGtang0VRWt+XCuMTnZP2IJehUd+kdsC7CnMKQhYI2X4vzEaOBLgXRWvhcT4pSBzEJkXB9sQ+y7JmLO10g== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-spacer@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-spacer/-/mjml-spacer-4.10.4.tgz#5ab85acc0900e5bc6fb2309b2904f07421adb301" - integrity sha512-NvhOZpPHy34RhruUNZYPJuX6TtKjLYfayuZNZiRaeWnmoqWOtTqWnTqx+85bM2yTuqtAmLVv8lWdJlaxkFnVxQ== +mjml-spacer@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-spacer/-/mjml-spacer-4.11.0.tgz#a7f1f95811a026b10388b66cc5a987a54ca3a361" + integrity sha512-9s6PjFgznKEFgFZCSa4vTYVDQ6kRH5ucMHGraoHS4VMZLA8QIaVakHd4Sl0n+SGWCqair4xZ3vYYMhOBKIXmMw== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-table@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-table/-/mjml-table-4.10.4.tgz#6d472bf0bcdace76f86aeefc5c94f95387691f77" - integrity sha512-3MNgG0+W4B5lmnXUEA+XRMjIxpkRDPaCsQQs906CTSXKEN2rTKuiIRtL6ij7kjIWKPFRxpq1sh8+6ueQd5GvRw== +mjml-table@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-table/-/mjml-table-4.11.0.tgz#0675ccbeba5fcf181bfb62fc775eb77e0cd480fb" + integrity sha512-ccniRz1MoDqKS6zoNM59xi81M7zy/tkvd/6weyH9XAZiU7ATgOxoBuIMy+d0uSTlKPWNwoi2FqR5YSedxT2YPw== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-text@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-text/-/mjml-text-4.10.4.tgz#4041dcba8d8e2dad2847c8d62a335595da81c00e" - integrity sha512-r53M6bN6tphToWk+3v+yD7mP/5MhCU+OnkKyVASnOSkjB/EeRB8XmPloALJu8FB9sV2vTYEkCHWyVYj3G2Y0DA== +mjml-text@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-text/-/mjml-text-4.11.0.tgz#a61da306b31eb9b20633a2ec7427c7b98f94c090" + integrity sha512-xrY5+uepxUg+q5KKuKX2s9KQnnlsMN5aoCt0JSbjt4H9ZwBpqXPD4Z2uJ8eZS33FvnXzUYPNVdKralvXnrsXKA== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" + mjml-core "4.11.0" -mjml-validator@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-validator/-/mjml-validator-4.10.4.tgz#ab5e3fe0fb148fedbe4fdce8ff7fa6c1224ebd50" - integrity sha512-IZbnsA7BSmfUyl7NBLalGSuK0+LazbfSguJimA6/5NyUOkNkUk6dqq6P0ZWslvRbg1aXsrLxgM9s3YJQgf3dvg== +mjml-validator@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-validator/-/mjml-validator-4.11.0.tgz#840b8c54d04f3755057230feefcd8762dcb125d9" + integrity sha512-oDL9tHcL4PaCZMwH6T/lLpS7LV9cm9lIwnzJy+y5/S81MGMV6kPr6xXHgS0A01G7sseg6+rHgqBnUgzUob4Ilg== dependencies: "@babel/runtime" "^7.14.6" -mjml-wrapper@4.10.4: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml-wrapper/-/mjml-wrapper-4.10.4.tgz#7d4399782df5928e42fc3d33bc957de469142802" - integrity sha512-mdjDizjoKQGnoSzuaDDGvjqeRcwZj4ftkqYRTZn/mR9OI8zWSY3CGqi9Q0PO2ZzgmugvzP3siuL+Te6sChWtRQ== +mjml-wrapper@4.11.0: + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml-wrapper/-/mjml-wrapper-4.11.0.tgz#379dacf96c3c8a56d9d139a3e514664f8a7322aa" + integrity sha512-WXGKxS+DlYalx5ofwq3bttj26BLEcaueeQ+0BcffXIELojUzd0+xTyX09v0QemUCBmKpHB2QaKMGhzYuSkupyg== dependencies: "@babel/runtime" "^7.14.6" lodash "^4.17.21" - mjml-core "4.10.4" - mjml-section "4.10.4" + mjml-core "4.11.0" + mjml-section "4.11.0" mjml@^4.6.2: - version "4.10.4" - resolved "https://registry.yarnpkg.com/mjml/-/mjml-4.10.4.tgz#9ec0ce624c91306e66f61549b453843770254d8f" - integrity sha512-8oXXKlXcVi+QDfwz5GIi9r8mT61zWBePp5g6R89fBcQK25xL7pqAvgKR1j1FWpR+JnTkpToK2DU8QH6M/LVVFQ== + version "4.11.0" + resolved "https://registry.yarnpkg.com/mjml/-/mjml-4.11.0.tgz#8aa411ccf61e283949fa8ab7acb62a2ed9d74477" + integrity sha512-kYuCAds/8F7m7xNEs3TFkrc0jxnNYIkexQIUNByPLQJFoFRltpgXLSdoyreGAniQtDGITPE+p8FIocLUBKsOHg== dependencies: "@babel/runtime" "^7.14.6" - mjml-cli "4.10.4" - mjml-core "4.10.4" - mjml-migrate "4.10.4" - mjml-preset-core "4.10.4" - mjml-validator "4.10.4" + mjml-cli "4.11.0" + mjml-core "4.11.0" + mjml-migrate "4.11.0" + mjml-preset-core "4.11.0" + mjml-validator "4.11.0" mkdirp@^0.5.1, mkdirp@~0.5.1: version "0.5.5" @@ -5709,10 +5709,10 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-addon-api@^3.0.2: - version "3.2.1" - resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-3.2.1.tgz#81325e0a2117789c0128dab65e7e38f07ceba161" - integrity sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A== +node-addon-api@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-4.2.0.tgz#117cbb5a959dff0992e1c586ae0393573e4d2a87" + integrity sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q== node-fetch@^2.6.0, node-fetch@^2.6.5: version "2.6.6" @@ -5756,9 +5756,9 @@ nodemailer@^3.1.1: integrity sha1-/r+sy0vSc2eEc6MJxstLSi88SOM= nodemailer@^6.4.6: - version "6.7.1" - resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.1.tgz#09f72f8b375f7b259291757007bcd902c0174c6e" - integrity sha512-E1C8G3rnXrGjznwGP1k+OrW5k4rl0XtqTEB19f7vtJAMYwfxZVSsAu2iY5xJkrZsbVYr6PwwAwRmFlakPoFC0A== + version "6.7.2" + resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.7.2.tgz#44b2ad5f7ed71b7067f7a21c4fedabaec62b85e0" + integrity sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q== nodemon@^2.0.6: version "2.0.15" @@ -6732,9 +6732,9 @@ rollup-pluginutils@^2.8.1, rollup-pluginutils@^2.8.2: estree-walker "^0.6.1" rollup@^2.42.3: - version "2.60.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.60.0.tgz#4ee60ab7bdd0356763f87d7099f413e5460fc193" - integrity sha512-cHdv9GWd58v58rdseC8e8XIaPUo8a9cgZpnCMMDGZFDZKEODOiPPEQFXLriWr/TjXzhPPmG5bkAztPsOARIcGQ== + version "2.60.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.60.1.tgz#4b34cd247f09b421f10a3c9286eda2ecf9972079" + integrity sha512-akwfnpjY0rXEDSn1UTVfKXJhPsEBu+imi1gqBA1ZkHGydUnkV/fWCC90P7rDaLEW8KTwBcS1G3N4893Ndz+jwg== optionalDependencies: fsevents "~2.3.2" @@ -6785,9 +6785,9 @@ sander@^0.5.0: rimraf "^2.5.2" sass@^1.32.12: - version "1.43.4" - resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.4.tgz#68c7d6a1b004bef49af0d9caf750e9b252105d1f" - integrity sha512-/ptG7KE9lxpGSYiXn7Ar+lKOv37xfWsZRtFYal2QHNigyVQDx685VFT/h7ejVr+R8w7H4tmUgtulsKl5YpveOg== + version "1.43.5" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.43.5.tgz#25a9d91dd098793ef7229d7b04dd3daae2fc4a65" + integrity sha512-WuNm+eAryMgQluL7Mbq9M4EruyGGMyal7Lu58FfnRMVWxgUzIvI7aSn60iNt3kn5yZBMR7G84fAGDcwqOF5JOg== dependencies: chokidar ">=3.0.0 <4.0.0" @@ -8016,14 +8016,14 @@ write-file-atomic@^3.0.0: typedarray-to-buffer "^3.1.5" ws@^7.4.3, ws@^7.4.6: - version "7.5.5" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.5.tgz#8b4bc4af518cfabd0473ae4f99144287b33eb881" - integrity sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w== + version "7.5.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.5.6.tgz#e59fc509fb15ddfb65487ee9765c5a51dec5fe7b" + integrity sha512-6GLgCqo2cy2A2rjCNFlxQS6ZljG/coZfZXclldI8FB/1G3CCI36Zd8xy2HrFVACi8tfk5XrgLQEk+P0Tnz9UcA== ws@^8.2.3: - version "8.2.3" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba" - integrity sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA== + version "8.3.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.3.0.tgz#7185e252c8973a60d57170175ff55fdbd116070d" + integrity sha512-Gs5EZtpqZzLvmIM59w4igITU57lrtYVFneaa434VROv4thzJyV6UjIL3D42lslWlI+D4KzLYnxSwtfuiO79sNw== ws@~6.1.0: version "6.1.4"