front/SvelteViewEngine: pre-compile ssr on demand, refactor globals into proper locals and lazy locals
This commit is contained in:
parent
2b85bea9dd
commit
9980c54fcf
@ -21,6 +21,8 @@ import SecurityError from "./SecurityError.js";
|
|||||||
import {doesFileExist, Type} from "./Utils.js";
|
import {doesFileExist, Type} from "./Utils.js";
|
||||||
import WebSocketListener from "./WebSocketListener.js";
|
import WebSocketListener from "./WebSocketListener.js";
|
||||||
import TemplateError = nunjucks.lib.TemplateError;
|
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<ApplicationComponent | WebSocketListener<Application>> {
|
export default abstract class Application implements Extendable<ApplicationComponent | WebSocketListener<Application>> {
|
||||||
private readonly version: string;
|
private readonly version: string;
|
||||||
@ -98,6 +100,8 @@ export default abstract class Application implements Extendable<ApplicationCompo
|
|||||||
MysqlConnectionManager.registerMigrations(this.getMigrations());
|
MysqlConnectionManager.registerMigrations(this.getMigrations());
|
||||||
|
|
||||||
// Register and initialize all components and alike
|
// Register and initialize all components and alike
|
||||||
|
this.use(new AppLocalsCoreComponents());
|
||||||
|
this.use(new LazyLocalsCoreComponent());
|
||||||
await this.init();
|
await this.init();
|
||||||
for (const component of this.components) {
|
for (const component of this.components) {
|
||||||
await component.init?.();
|
await component.init?.();
|
||||||
|
@ -6,11 +6,11 @@
|
|||||||
import Icon from "../utils/Icon.svelte";
|
import Icon from "../utils/Icon.svelte";
|
||||||
import {hasRoute, route} from "../../../common/Routing";
|
import {hasRoute, route} from "../../../common/Routing";
|
||||||
|
|
||||||
let registerUsingMagicLink = $locals.previousFormData()?.['auth_method'] !== 'password';
|
let registerUsingMagicLink = $locals.previousFormData?.['auth_method'] !== 'password';
|
||||||
let loginUsingMagicLink = true;
|
let loginUsingMagicLink = true;
|
||||||
|
|
||||||
let queryStr = '';
|
let queryStr = '';
|
||||||
let previousUrl = $locals.getPreviousUrl();
|
let previousUrl: string = $locals.previousUrl as string | undefined;
|
||||||
if ($locals.query?.redirect_uri) {
|
if ($locals.query?.redirect_uri) {
|
||||||
queryStr = '?' + new URLSearchParams({redirect_uri: $locals.query?.redirect_uri}).toString();
|
queryStr = '?' + new URLSearchParams({redirect_uri: $locals.query?.redirect_uri}).toString();
|
||||||
} else if (previousUrl) {
|
} else if (previousUrl) {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import {locals} from "../../ts/stores.js";
|
import {locals} from "../../ts/stores.js";
|
||||||
import Message from "./Message.svelte";
|
import Message from "./Message.svelte";
|
||||||
|
|
||||||
export let flashed = $locals.flash();
|
export let flashed = $locals.flash;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import Icon from "../utils/Icon.svelte";
|
import Icon from "../utils/Icon.svelte";
|
||||||
import CommonScripts from "./CommonScripts.svelte";
|
import CommonScripts from "./CommonScripts.svelte";
|
||||||
|
|
||||||
const previousUrl = $locals.getPreviousUrl();
|
const previousUrl = $locals.previousUrl;
|
||||||
|
|
||||||
export let code;
|
export let code;
|
||||||
code = $locals.error_code || code;
|
code = $locals.error_code || code;
|
||||||
|
@ -12,13 +12,7 @@
|
|||||||
import * as stores from '/js/stores.js';
|
import * as stores from '/js/stores.js';
|
||||||
const localStore = stores.l;
|
const localStore = stores.l;
|
||||||
|
|
||||||
const localMap = %locals%;
|
localStore.set(%locals%);
|
||||||
localStore.set((key, args) => {
|
|
||||||
return localMap[args ?
|
|
||||||
`'${key}', \`${args}\``
|
|
||||||
: `'${key}'`];
|
|
||||||
});
|
|
||||||
|
|
||||||
setRoutes(%routes%);
|
setRoutes(%routes%);
|
||||||
setPublicUrl(`%publicUrl%`);
|
setPublicUrl(`%publicUrl%`);
|
||||||
|
|
||||||
|
@ -3,4 +3,4 @@
|
|||||||
import Field from "./Field.svelte";
|
import Field from "./Field.svelte";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Field type="hidden" name="csrf" value={$locals.getCsrfToken()}/>
|
<Field type="hidden" name="csrf" value={$locals.csrfToken}/>
|
||||||
|
@ -15,12 +15,12 @@
|
|||||||
export let hint: string | undefined = undefined;
|
export let hint: string | undefined = undefined;
|
||||||
export let extraData: string[] | undefined = undefined;
|
export let extraData: string[] | undefined = undefined;
|
||||||
export let icon: 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 formId = getContext('formId');
|
||||||
const fieldId = `${formId}-${name}-field`;
|
const fieldId = `${formId}-${name}-field`;
|
||||||
|
|
||||||
const previousFormData = $locals.previousFormData() || [];
|
const previousFormData = $locals.previousFormData as FieldValue | undefined || [];
|
||||||
let previousFieldData = previousFormData[name];
|
let previousFieldData = previousFormData[name];
|
||||||
if (typeof value === 'number' && previousFieldData) previousFieldData = Number(previousFieldData);
|
if (typeof value === 'number' && previousFieldData) previousFieldData = Number(previousFieldData);
|
||||||
|
|
||||||
|
@ -5,26 +5,10 @@ import {Session, SessionData} from "express-session";
|
|||||||
import ApplicationComponent from "../ApplicationComponent.js";
|
import ApplicationComponent from "../ApplicationComponent.js";
|
||||||
import {AuthMiddleware} from "../auth/AuthComponent.js";
|
import {AuthMiddleware} from "../auth/AuthComponent.js";
|
||||||
import {BadRequestError} from "../HttpError.js";
|
import {BadRequestError} from "../HttpError.js";
|
||||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
|
||||||
|
|
||||||
export default class CsrfProtectionComponent extends ApplicationComponent {
|
export default class CsrfProtectionComponent extends ApplicationComponent {
|
||||||
private static readonly excluders: ((req: Request) => boolean)[] = [];
|
private static readonly excluders: ((req: Request) => boolean)[] = [];
|
||||||
|
|
||||||
public static getCsrfToken(session: Session & Partial<SessionData>): string {
|
|
||||||
if (typeof session.csrf !== 'string') {
|
|
||||||
session.csrf = crypto.randomBytes(64).toString('base64');
|
|
||||||
}
|
|
||||||
return session.csrf;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async init(): Promise<void> {
|
|
||||||
const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals();
|
|
||||||
if (globals) {
|
|
||||||
globals.set('getCsrfToken', () => null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static addExcluder(excluder: (req: Request) => boolean): void {
|
public static addExcluder(excluder: (req: Request) => boolean): void {
|
||||||
this.excluders.push(excluder);
|
this.excluders.push(excluder);
|
||||||
}
|
}
|
||||||
@ -36,9 +20,7 @@ export default class CsrfProtectionComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const session = req.getSession();
|
const session = req.getSession();
|
||||||
res.locals.getCsrfToken = () => {
|
res.setLazyLocal('csrfToken', () => this.getSessionCsrfToken(session));
|
||||||
return CsrfProtectionComponent.getCsrfToken(session);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!['GET', 'HEAD', 'OPTIONS'].find(s => s === req.method)) {
|
if (!['GET', 'HEAD', 'OPTIONS'].find(s => s === req.method)) {
|
||||||
try {
|
try {
|
||||||
@ -58,6 +40,13 @@ export default class CsrfProtectionComponent extends ApplicationComponent {
|
|||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getSessionCsrfToken(session: Session & Partial<SessionData>): string {
|
||||||
|
if (typeof session.csrf !== 'string') {
|
||||||
|
session.csrf = crypto.randomBytes(64).toString('base64');
|
||||||
|
}
|
||||||
|
return session.csrf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InvalidCsrfTokenError extends BadRequestError {
|
class InvalidCsrfTokenError extends BadRequestError {
|
||||||
|
@ -1,39 +1,20 @@
|
|||||||
import {Router} from "express";
|
import {Router} from "express";
|
||||||
|
|
||||||
import ApplicationComponent from "../ApplicationComponent.js";
|
import ApplicationComponent from "../ApplicationComponent.js";
|
||||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
|
||||||
|
|
||||||
export default class FormHelperComponent extends ApplicationComponent {
|
export default class FormHelperComponent extends ApplicationComponent {
|
||||||
|
|
||||||
public async init(): Promise<void> {
|
|
||||||
const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals();
|
|
||||||
if (globals) {
|
|
||||||
globals.set('validation', () => ({}));
|
|
||||||
globals.set('previousFormData', () => ({}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async initRoutes(router: Router): Promise<void> {
|
public async initRoutes(router: Router): Promise<void> {
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
let _validation: unknown | null;
|
res.setLazyLocal('validation', () => {
|
||||||
res.locals.validation = () => {
|
const validation = req.flash('validation');
|
||||||
if (!_validation) {
|
return validation.length > 0 ? validation[0] : null;
|
||||||
const v = req.flash('validation');
|
});
|
||||||
_validation = v.length > 0 ? v[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _validation;
|
res.setLazyLocal('previousFormData', () => {
|
||||||
};
|
const previousFormData = req.flash('previousFormData');
|
||||||
|
return previousFormData.length > 0 ? previousFormData[0] : null;
|
||||||
let _previousFormData: unknown | null = null;
|
});
|
||||||
res.locals.previousFormData = () => {
|
|
||||||
if (!_previousFormData) {
|
|
||||||
const v = req.flash('previousFormData');
|
|
||||||
_previousFormData = v.length > 0 ? v[0] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return _previousFormData;
|
|
||||||
};
|
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,23 +1,14 @@
|
|||||||
import config from "config";
|
|
||||||
import {Express, Router} from "express";
|
import {Express, Router} from "express";
|
||||||
import path from "path";
|
|
||||||
import util from "util";
|
|
||||||
|
|
||||||
import ApplicationComponent from "../ApplicationComponent.js";
|
import ApplicationComponent from "../ApplicationComponent.js";
|
||||||
import {QueryParams, route, RouteParams} from "../common/Routing.js";
|
|
||||||
import AssetCompiler from "../frontend/AssetCompiler.js";
|
import AssetCompiler from "../frontend/AssetCompiler.js";
|
||||||
import AssetPreCompiler from "../frontend/AssetPreCompiler.js";
|
import AssetPreCompiler from "../frontend/AssetPreCompiler.js";
|
||||||
import Globals from "../frontend/Globals.js";
|
|
||||||
import ViewEngine from "../frontend/ViewEngine.js";
|
import ViewEngine from "../frontend/ViewEngine.js";
|
||||||
import {logger} from "../Logger.js";
|
import LazyLocalsCoreComponent from "./core/LazyLocalsCoreComponent.js";
|
||||||
import {listFilesRecursively} from "../Utils.js";
|
|
||||||
import FileCache from "../utils/FileCache.js";
|
|
||||||
|
|
||||||
export default class FrontendToolsComponent extends ApplicationComponent {
|
export default class FrontendToolsComponent extends ApplicationComponent {
|
||||||
private readonly publicDir: string;
|
private readonly publicDir: string;
|
||||||
private readonly publicAssetsCache: FileCache = new FileCache();
|
|
||||||
private readonly assetPreCompilers: AssetPreCompiler[];
|
private readonly assetPreCompilers: AssetPreCompiler[];
|
||||||
private readonly globals: Globals = new Globals();
|
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly assetCompiler: AssetCompiler,
|
private readonly assetCompiler: AssetCompiler,
|
||||||
@ -31,48 +22,17 @@ export default class FrontendToolsComponent extends ApplicationComponent {
|
|||||||
if (assetPreCompiler.isPublic()) {
|
if (assetPreCompiler.isPublic()) {
|
||||||
this.assetCompiler.addExtension(assetPreCompiler.getExtension());
|
this.assetCompiler.addExtension(assetPreCompiler.getExtension());
|
||||||
}
|
}
|
||||||
|
|
||||||
assetPreCompiler.setGlobals(this.globals);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async init(): Promise<void> {
|
|
||||||
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<void> {
|
public async start(app: Express): Promise<void> {
|
||||||
// Cache public assets
|
|
||||||
if (config.get<boolean>('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();
|
this.hookPreCompilers();
|
||||||
|
|
||||||
// Setup express view engine
|
// Setup express view engine
|
||||||
let main = true;
|
let main = true;
|
||||||
for (const assetPreCompiler of this.assetPreCompilers) {
|
for (const assetPreCompiler of this.assetPreCompilers) {
|
||||||
if (assetPreCompiler instanceof ViewEngine) {
|
if (assetPreCompiler instanceof ViewEngine) {
|
||||||
assetPreCompiler.setup(app, main);
|
assetPreCompiler.setup(app, main, this.getApp().as(LazyLocalsCoreComponent));
|
||||||
main = false;
|
main = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,17 +44,9 @@ export default class FrontendToolsComponent extends ApplicationComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleRoutes(router: Router): Promise<void> {
|
public async initRoutes(router: Router): Promise<void> {
|
||||||
router.use((req, res, next) => {
|
|
||||||
res.locals.inlineAsset = (urlPath: string) => {
|
|
||||||
return this.publicAssetsCache.getOrFail(path.join(this.publicDir, urlPath));
|
|
||||||
};
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add request context locals
|
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
|
// Request context locals
|
||||||
res.locals.url = req.url;
|
res.locals.url = req.url;
|
||||||
res.locals.params = req.params;
|
res.locals.params = req.params;
|
||||||
res.locals.query = req.query;
|
res.locals.query = req.query;
|
||||||
@ -126,8 +78,4 @@ export default class FrontendToolsComponent extends ApplicationComponent {
|
|||||||
this.hookPreCompilers();
|
this.hookPreCompilers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getGlobals(): Globals {
|
|
||||||
return this.globals;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,11 @@ import {logger} from "../Logger.js";
|
|||||||
import Mail from "../mail/Mail.js";
|
import Mail from "../mail/Mail.js";
|
||||||
import MailError from "../mail/MailError.js";
|
import MailError from "../mail/MailError.js";
|
||||||
import SecurityError from "../SecurityError.js";
|
import SecurityError from "../SecurityError.js";
|
||||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
import LazyLocalsCoreComponent from "./core/LazyLocalsCoreComponent.js";
|
||||||
|
|
||||||
export default class MailComponent extends ApplicationComponent {
|
export default class MailComponent extends ApplicationComponent {
|
||||||
private transporter?: Transporter;
|
private transporter?: Transporter;
|
||||||
|
private readonly additionalLocals: Record<string, unknown> = {};
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly viewEngine: MailViewEngine,
|
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')}`);
|
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<void> {
|
public async stop(): Promise<void> {
|
||||||
@ -77,7 +78,6 @@ export default class MailComponent extends ApplicationComponent {
|
|||||||
|
|
||||||
for (const destEmail of to) {
|
for (const destEmail of to) {
|
||||||
const template = mail.getTemplate();
|
const template = mail.getTemplate();
|
||||||
const locals = mail.getData();
|
|
||||||
const options = mail.getOptions();
|
const options = mail.getOptions();
|
||||||
|
|
||||||
// Reset options
|
// Reset options
|
||||||
@ -91,17 +91,17 @@ export default class MailComponent extends ApplicationComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Set locals
|
// Set locals
|
||||||
locals.mail_subject = options.subject;
|
const urlLocals = mail.getData();
|
||||||
locals.mail_to = options.to;
|
urlLocals.mail_subject = options.subject;
|
||||||
locals.mail_link = config.get<string>('app.public_url') +
|
urlLocals.mail_to = options.to;
|
||||||
route('mail', [template.template], locals);
|
urlLocals.mail_link = config.get<string>('app.public_url') +
|
||||||
Object.assign(locals, this.getApp().as(FrontendToolsComponent).getGlobals().get());
|
route('mail', [template.template], urlLocals);
|
||||||
|
const locals = {...this.additionalLocals, ...urlLocals};
|
||||||
|
|
||||||
// Log
|
// Log
|
||||||
logger.debug(`Send mail from ${options.from.address} to ${options.to}`);
|
logger.debug(`Send mail from ${options.from.address} to ${options.to}`);
|
||||||
|
|
||||||
// Render email
|
// Render email
|
||||||
|
|
||||||
options.html = await this.viewEngine.render('mails/' + template.template + '.mnjk', locals, false);
|
options.html = await this.viewEngine.render('mails/' + template.template + '.mnjk', locals, false);
|
||||||
options.text = await this.viewEngine.render('mails/' + template.template + '.mnjk', locals, true);
|
options.text = await this.viewEngine.render('mails/' + template.template + '.mnjk', locals, true);
|
||||||
|
|
||||||
@ -112,6 +112,10 @@ export default class MailComponent extends ApplicationComponent {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setAdditionalLocal(key: string, value: unknown): void {
|
||||||
|
this.additionalLocals[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
private getTransporter(): Transporter {
|
private getTransporter(): Transporter {
|
||||||
if (!this.transporter) throw new MailError('Mail system was not prepared.');
|
if (!this.transporter) throw new MailError('Mail system was not prepared.');
|
||||||
return this.transporter;
|
return this.transporter;
|
||||||
|
@ -3,17 +3,9 @@ import onFinished from "on-finished";
|
|||||||
|
|
||||||
import ApplicationComponent from "../ApplicationComponent.js";
|
import ApplicationComponent from "../ApplicationComponent.js";
|
||||||
import {logger} from "../Logger.js";
|
import {logger} from "../Logger.js";
|
||||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
|
||||||
import SessionComponent from "./SessionComponent.js";
|
import SessionComponent from "./SessionComponent.js";
|
||||||
|
|
||||||
export default class PreviousUrlComponent extends ApplicationComponent {
|
export default class PreviousUrlComponent extends ApplicationComponent {
|
||||||
public async init(): Promise<void> {
|
|
||||||
const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals();
|
|
||||||
if (globals) {
|
|
||||||
globals.set('getPreviousUrl', () => null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async handleRoutes(router: Router): Promise<void> {
|
public async handleRoutes(router: Router): Promise<void> {
|
||||||
router.use((req, res, next) => {
|
router.use((req, res, next) => {
|
||||||
req.getPreviousUrl = () => {
|
req.getPreviousUrl = () => {
|
||||||
@ -31,7 +23,7 @@ export default class PreviousUrlComponent extends ApplicationComponent {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
res.locals.getPreviousUrl = req.getPreviousUrl;
|
res.setLazyLocal('previousUrl', () => req.getPreviousUrl());
|
||||||
|
|
||||||
req.getIntendedUrl = () => {
|
req.getIntendedUrl = () => {
|
||||||
return req.query.redirect_uri?.toString() || null;
|
return req.query.redirect_uri?.toString() || null;
|
||||||
|
@ -5,7 +5,6 @@ import session from "express-session";
|
|||||||
|
|
||||||
import ApplicationComponent from "../ApplicationComponent.js";
|
import ApplicationComponent from "../ApplicationComponent.js";
|
||||||
import SecurityError from "../SecurityError.js";
|
import SecurityError from "../SecurityError.js";
|
||||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
|
||||||
import RedisComponent from "./RedisComponent.js";
|
import RedisComponent from "./RedisComponent.js";
|
||||||
|
|
||||||
export default class SessionComponent extends ApplicationComponent {
|
export default class SessionComponent extends ApplicationComponent {
|
||||||
@ -16,13 +15,6 @@ export default class SessionComponent extends ApplicationComponent {
|
|||||||
this.storeComponent = storeComponent;
|
this.storeComponent = storeComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async init(): Promise<void> {
|
|
||||||
const globals = this.getApp().asOptional(FrontendToolsComponent)?.getGlobals();
|
|
||||||
if (globals) {
|
|
||||||
globals.set('flash', () => '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkSecuritySettings(): Promise<void> {
|
public async checkSecuritySettings(): Promise<void> {
|
||||||
this.checkSecurityConfigField('session.secret');
|
this.checkSecurityConfigField('session.secret');
|
||||||
if (!config.get<boolean>('session.cookie.secure')) {
|
if (!config.get<boolean>('session.cookie.secure')) {
|
||||||
@ -68,40 +60,16 @@ export default class SessionComponent extends ApplicationComponent {
|
|||||||
res.locals.session = session;
|
res.locals.session = session;
|
||||||
|
|
||||||
// Views flash function
|
// Views flash function
|
||||||
const _flash: FlashStorage = {};
|
res.setLazyLocal('flash', () => {
|
||||||
res.locals.flash = (key?: string): FlashMessages | unknown[] => {
|
return {
|
||||||
if (key !== undefined) {
|
info: req.flash('info'),
|
||||||
if (_flash[key] === undefined) _flash[key] = req.flash(key);
|
success: req.flash('success'),
|
||||||
return _flash[key] || [];
|
warning: req.flash('warning'),
|
||||||
}
|
error: req.flash('error'),
|
||||||
|
'error-alert': req.flash('error-alert'),
|
||||||
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;
|
|
||||||
};
|
|
||||||
next();
|
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,
|
|
||||||
};
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import config from "config";
|
import config from "config";
|
||||||
import cookie from "cookie";
|
import cookie from "cookie";
|
||||||
import cookieParser from "cookie-parser";
|
import cookieParser from "cookie-parser";
|
||||||
import {Express, Request} from "express";
|
import {Express, Request, Router} from "express";
|
||||||
import {Session} from "express-session";
|
import {Session} from "express-session";
|
||||||
import {WebSocketServer} from "ws";
|
import {WebSocketServer} from "ws";
|
||||||
|
|
||||||
@ -10,7 +10,6 @@ import ApplicationComponent from "../ApplicationComponent.js";
|
|||||||
import {logger} from "../Logger.js";
|
import {logger} from "../Logger.js";
|
||||||
import WebSocketListener from "../WebSocketListener.js";
|
import WebSocketListener from "../WebSocketListener.js";
|
||||||
import ExpressAppComponent from "./ExpressAppComponent.js";
|
import ExpressAppComponent from "./ExpressAppComponent.js";
|
||||||
import FrontendToolsComponent from "./FrontendToolsComponent.js";
|
|
||||||
import RedisComponent from "./RedisComponent.js";
|
import RedisComponent from "./RedisComponent.js";
|
||||||
|
|
||||||
export default class WebSocketServerComponent extends ApplicationComponent {
|
export default class WebSocketServerComponent extends ApplicationComponent {
|
||||||
@ -21,11 +20,13 @@ export default class WebSocketServerComponent extends ApplicationComponent {
|
|||||||
|
|
||||||
app.require(ExpressAppComponent);
|
app.require(ExpressAppComponent);
|
||||||
app.require(RedisComponent);
|
app.require(RedisComponent);
|
||||||
|
}
|
||||||
|
|
||||||
const globals = app.asOptional(FrontendToolsComponent)?.getGlobals();
|
public async initRoutes(router: Router): Promise<void> {
|
||||||
if (globals) {
|
router.use((req, res, next) => {
|
||||||
globals.set('websocketUrl', config.get('app.public_websocket_url'));
|
res.locals.websocketUrl = config.get('app.public_websocket_url');
|
||||||
}
|
next();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(_app: Express): Promise<void> {
|
public async start(_app: Express): Promise<void> {
|
||||||
|
26
src/components/core/AppLocalsCoreComponents.ts
Normal file
26
src/components/core/AppLocalsCoreComponents.ts
Normal file
@ -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<string, unknown> | undefined;
|
||||||
|
|
||||||
|
|
||||||
|
public async init(): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
res.locals.app = this.appLocals;
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
30
src/components/core/LazyLocalsCoreComponent.ts
Normal file
30
src/components/core/LazyLocalsCoreComponent.ts
Normal file
@ -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<void> {
|
||||||
|
router.use((req, res, next) => {
|
||||||
|
res.locals._lazyLocals = {};
|
||||||
|
res.setLazyLocal = (key: string, valueProvider: () => unknown) => {
|
||||||
|
res.locals._lazyLocals[key] = valueProvider;
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setupLazyLocals(localsObject: Record<string, unknown>): void {
|
||||||
|
const lazyLocals = localsObject._lazyLocals as Record<string, () => 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -4,11 +4,9 @@ import path from "path";
|
|||||||
|
|
||||||
import {logger} from "../Logger.js";
|
import {logger} from "../Logger.js";
|
||||||
import {doesFileExist, listFilesRecursively} from "../Utils.js";
|
import {doesFileExist, listFilesRecursively} from "../Utils.js";
|
||||||
import Globals from "./Globals.js";
|
|
||||||
|
|
||||||
export default abstract class AssetPreCompiler {
|
export default abstract class AssetPreCompiler {
|
||||||
protected readonly assetPaths: string[];
|
protected readonly assetPaths: string[];
|
||||||
private globals?: Globals;
|
|
||||||
private watcher?: FSWatcher;
|
private watcher?: FSWatcher;
|
||||||
private afterPreCompileHandlers: ((watch: boolean) => Promise<void>)[] = [];
|
private afterPreCompileHandlers: ((watch: boolean) => Promise<void>)[] = [];
|
||||||
private inputChangeHandler?: (restart: boolean) => Promise<void>;
|
private inputChangeHandler?: (restart: boolean) => Promise<void>;
|
||||||
@ -57,15 +55,6 @@ export default abstract class AssetPreCompiler {
|
|||||||
return this.assetPaths;
|
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<void> {
|
public async stop(): Promise<void> {
|
||||||
if (this.watcher) {
|
if (this.watcher) {
|
||||||
await this.watcher.close();
|
await this.watcher.close();
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
export default class Globals {
|
|
||||||
private readonly globals: Record<string, unknown> = {};
|
|
||||||
|
|
||||||
public get(): Record<string, unknown> {
|
|
||||||
return {...this.globals};
|
|
||||||
}
|
|
||||||
|
|
||||||
public set(key: string, value: unknown): void {
|
|
||||||
this.globals[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
@ -2,7 +2,6 @@ import "./register_svelte/register_svelte.js";
|
|||||||
|
|
||||||
import clearModule from "clear-module";
|
import clearModule from "clear-module";
|
||||||
import config from "config";
|
import config from "config";
|
||||||
import crypto from "crypto";
|
|
||||||
import {promises as fs} from 'fs';
|
import {promises as fs} from 'fs';
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import requireFromString from "require-from-string";
|
import requireFromString from "require-from-string";
|
||||||
@ -14,23 +13,10 @@ import {logger} from "../Logger.js";
|
|||||||
import FileCache from "../utils/FileCache.js";
|
import FileCache from "../utils/FileCache.js";
|
||||||
import ViewEngine from "./ViewEngine.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 {
|
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 fileCache: FileCache = new FileCache();
|
||||||
private readonly dependencyCache: Record<string, Set<string>> = {};
|
private readonly reverseDependencyCache: Record<string, Set<string>> = {};
|
||||||
private readonly preprocessingCache: Record<string, PreprocessingCacheEntry> = {};
|
private readonly preprocessingCache: Record<string, string> = {};
|
||||||
private readonly cssCache: Record<string, string[] | undefined> = {};
|
private readonly cssCache: Record<string, string[] | undefined> = {};
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
@ -48,8 +34,8 @@ export default class SvelteViewEngine extends ViewEngine {
|
|||||||
public async onFileRemove(file: string): Promise<void> {
|
public async onFileRemove(file: string): Promise<void> {
|
||||||
const canonicalName = this.toCanonicalName(file);
|
const canonicalName = this.toCanonicalName(file);
|
||||||
delete this.preprocessingCache[canonicalName];
|
delete this.preprocessingCache[canonicalName];
|
||||||
delete this.dependencyCache[canonicalName];
|
delete this.reverseDependencyCache[canonicalName];
|
||||||
Object.values(this.dependencyCache).forEach(set => set.delete(canonicalName));
|
Object.values(this.reverseDependencyCache).forEach(set => set.delete(canonicalName));
|
||||||
await super.onFileRemove(file);
|
await super.onFileRemove(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,34 +45,24 @@ export default class SvelteViewEngine extends ViewEngine {
|
|||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const canonicalViewName = this.toCanonicalName(file);
|
const canonicalViewName = this.toCanonicalName(file);
|
||||||
|
|
||||||
// View
|
const rootTemplateFile = await this.resolveFileFromCanonicalNameOrFail('templates/svelte_template.html');
|
||||||
const actualFile = path.join(this.targetDir, canonicalViewName + COMPILED_SVELTE_EXTENSION);
|
const rawOutput = await this.fileCache.get(rootTemplateFile, !config.get<boolean>('view.cache'));
|
||||||
const view = await this.fileCache.get(actualFile, !config.get<boolean>('view.cache'));
|
|
||||||
|
|
||||||
// Root template
|
const {
|
||||||
const templateFile = await this.resolveFileFromCanonicalNameOrFail('templates/svelte_template.html');
|
|
||||||
const rawOutput = await this.fileCache.get(templateFile, !config.get<boolean>('view.cache'));
|
|
||||||
|
|
||||||
// Pre-compiled parts
|
|
||||||
const [
|
|
||||||
backendCalls,
|
|
||||||
head,
|
head,
|
||||||
html,
|
html,
|
||||||
css,
|
css,
|
||||||
] = view.split(SvelteViewEngine.getPreCompileSeparator(canonicalViewName));
|
} = await this.renderSvelteSsr(canonicalViewName, locals);
|
||||||
|
|
||||||
const localMap: Record<string, unknown> = this.compileBackendCalls(backendCalls.split('\n'), locals, false);
|
const serializedLocals = JSON.stringify(locals, (key, value) => {
|
||||||
const actualLocals = JSON.stringify(localMap, (key, value) => {
|
if (key.startsWith('_') || typeof value === 'function') return undefined;
|
||||||
if (key.startsWith('_')) return undefined;
|
return value;
|
||||||
return typeof value === 'function' ?
|
|
||||||
value.toString() :
|
|
||||||
value;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replaces
|
// Replaces
|
||||||
const replaceMap: Record<string, string> = {
|
const replaceMap: Record<string, string> = {
|
||||||
canonicalViewName: canonicalViewName,
|
canonicalViewName: canonicalViewName,
|
||||||
locals: actualLocals,
|
locals: serializedLocals,
|
||||||
head: head,
|
head: head,
|
||||||
html: html,
|
html: html,
|
||||||
css: css,
|
css: css,
|
||||||
@ -104,55 +80,17 @@ export default class SvelteViewEngine extends ViewEngine {
|
|||||||
const targetFile = path.join(this.targetDir, canonicalName);
|
const targetFile = path.join(this.targetDir, canonicalName);
|
||||||
logger.info(canonicalName + ' > ', 'Pre-compiling', canonicalName, '->', targetFile);
|
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)
|
if (alsoCompileDependents && Object.keys(this.reverseDependencyCache).indexOf(canonicalName) >= 0) {
|
||||||
const ssr = await this.compileSsr(canonicalName);
|
logger.info(canonicalName + ' > ', 'Pre-compiling dependents...');
|
||||||
|
for (const dependent of [...this.reverseDependencyCache[canonicalName]]) {
|
||||||
const separator = SvelteViewEngine.getPreCompileSeparator(canonicalName);
|
|
||||||
const finalCode = [
|
|
||||||
[...new Set<string>(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]]) {
|
|
||||||
await this.preCompile(dependent, true);
|
await this.preCompile(dependent, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private resolveDependencies(source: string, canonicalViewName: string): string[] {
|
private async preprocessSvelte(canonicalName: string): Promise<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<string>();
|
|
||||||
}
|
|
||||||
this.dependencyCache[dependency].add(canonicalViewName);
|
|
||||||
}
|
|
||||||
|
|
||||||
return dependencies;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async preprocess(canonicalName: string): Promise<PreprocessingCacheEntry> {
|
|
||||||
// Cache
|
// Cache
|
||||||
if (Object.keys(this.preprocessingCache).indexOf(canonicalName) >= 0) {
|
if (Object.keys(this.preprocessingCache).indexOf(canonicalName) >= 0) {
|
||||||
return this.preprocessingCache[canonicalName];
|
return this.preprocessingCache[canonicalName];
|
||||||
@ -168,13 +106,10 @@ export default class SvelteViewEngine extends ViewEngine {
|
|||||||
// Read source file if code was not already provided
|
// Read source file if code was not already provided
|
||||||
const code = await this.fileCache.get(file, !config.get<boolean>('view.cache'));
|
const code = await this.fileCache.get(file, !config.get<boolean>('view.cache'));
|
||||||
|
|
||||||
// Replace backend calls
|
|
||||||
const replacedBackendCalls = await this.replaceBackendCalls(canonicalName, code);
|
|
||||||
|
|
||||||
// Preprocess svelte
|
// Preprocess svelte
|
||||||
logger.info(canonicalName + ' > ', 'Svelte preprocessing');
|
logger.info(canonicalName + ' > ', 'Svelte preprocessing');
|
||||||
const preprocessed = await preprocess(
|
const processed = await preprocess(
|
||||||
replacedBackendCalls.code,
|
code,
|
||||||
sveltePreprocess({
|
sveltePreprocess({
|
||||||
typescript: {
|
typescript: {
|
||||||
tsconfigFile: 'src/assets/views/tsconfig.json',
|
tsconfigFile: 'src/assets/views/tsconfig.json',
|
||||||
@ -186,95 +121,26 @@ export default class SvelteViewEngine extends ViewEngine {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Write to output file
|
// Write to output file
|
||||||
await fs.writeFile(outputFile, preprocessed.code);
|
await fs.writeFile(outputFile, processed.code);
|
||||||
|
|
||||||
// Preprocess dependencies
|
this.resolveAndCacheDependencies(processed.code, canonicalName);
|
||||||
const backendCalls: string[] = replacedBackendCalls.backendCalls;
|
|
||||||
for (const dependency of this.resolveDependencies(code, canonicalName)) {
|
|
||||||
backendCalls.push(...(await this.preprocess(dependency)).backendCalls);
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.preprocessingCache[canonicalName] = {
|
return this.preprocessingCache[canonicalName] = processed.code;
|
||||||
backendCalls: backendCalls,
|
|
||||||
code: preprocessed.code,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async replaceBackendCalls(canonicalName: string, code: string): Promise<PreprocessingCacheEntry> {
|
private async renderSvelteSsr(canonicalName: string, locals: {[key: string]: unknown}): 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<string>();
|
|
||||||
|
|
||||||
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<{
|
|
||||||
head: string,
|
head: string,
|
||||||
css: string,
|
css: string,
|
||||||
html: string,
|
html: string,
|
||||||
}> {
|
}> {
|
||||||
const targetFile = path.join(this.targetDir, canonicalName);
|
const targetFile = path.join(this.targetDir, canonicalName);
|
||||||
const {backendCalls, code} = await this.preprocess(canonicalName);
|
const code = await this.fileCache.get(targetFile, !config.get<boolean>('view.cache'));
|
||||||
|
|
||||||
// Get dependencies css
|
// Get dependencies css
|
||||||
const dependenciesCss: string[] = [];
|
const dependenciesCss: string[] = [];
|
||||||
for (const dependency of this.resolveDependencies(code, canonicalName)) {
|
for (const dependency of this.resolveAndCacheDependencies(code, canonicalName)) {
|
||||||
if (this.cssCache[dependency] === undefined) {
|
if (this.cssCache[dependency] === undefined) {
|
||||||
await this.compileSsr(dependency);
|
await this.renderSvelteSsr(dependency, locals);
|
||||||
}
|
}
|
||||||
const css = this.cssCache[dependency];
|
const css = this.cssCache[dependency];
|
||||||
if (css === undefined) {
|
if (css === undefined) {
|
||||||
@ -295,15 +161,8 @@ export default class SvelteViewEngine extends ViewEngine {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Load locals into locals store
|
// Load locals into locals store
|
||||||
const localsModulePath = path.resolve(this.targetDir, "../ts/stores.js");
|
const storesModule = await import(path.resolve(this.targetDir, "../ts/stores.js"));
|
||||||
const localsModule = await import(localsModulePath);
|
storesModule.locals.set(locals);
|
||||||
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}'`];
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load module and render
|
// Load module and render
|
||||||
const moduleId = path.resolve(targetFile);
|
const moduleId = path.resolve(targetFile);
|
||||||
@ -324,40 +183,26 @@ export default class SvelteViewEngine extends ViewEngine {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private compileBackendCalls(
|
private resolveAndCacheDependencies(source: string, canonicalViewName: string): string[] {
|
||||||
backendCalls: string[],
|
const dependencies: string[] = [];
|
||||||
locals: Record<string, unknown>,
|
|
||||||
isPreRender: boolean,
|
|
||||||
): Record<string, unknown> {
|
|
||||||
locals = {...locals, isPreRender};
|
|
||||||
|
|
||||||
const localMap: Record<string, unknown> = {};
|
for (const match of source.matchAll(/import .+ from ['"](.+?\.svelte)['"];/gm)) {
|
||||||
backendCalls.forEach(code => {
|
dependencies.push(path.join(path.dirname(canonicalViewName), match[1]));
|
||||||
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
|
|
||||||
|
|
||||||
const f = locals[key];
|
// Clear existing links from cache
|
||||||
if (typeof f !== 'function') throw new Error(key + ' is not a function.');
|
for (const dependency of Object.keys(this.reverseDependencyCache)) {
|
||||||
localMap[`'${key}', \`[${code.substring(code.indexOf('`[') + 2, code.length - 2)}]\``] = f.call(locals, ...args);
|
this.reverseDependencyCache[dependency].delete(canonicalViewName);
|
||||||
} else {
|
}
|
||||||
localMap[`'${key}'`] = locals[key];
|
|
||||||
|
// Add new links to cache
|
||||||
|
for (const dependency of dependencies) {
|
||||||
|
if (Object.keys(this.reverseDependencyCache).indexOf(dependency) < 0) {
|
||||||
|
this.reverseDependencyCache[dependency] = new Set<string>();
|
||||||
}
|
}
|
||||||
});
|
this.reverseDependencyCache[dependency].add(canonicalViewName);
|
||||||
return localMap;
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type PreprocessingCacheEntry = {
|
|
||||||
backendCalls: string[],
|
|
||||||
code: string,
|
|
||||||
};
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import {Express} from "express";
|
import {Express} from "express";
|
||||||
|
|
||||||
|
import LazyLocalsCoreComponent from "../components/core/LazyLocalsCoreComponent.js";
|
||||||
import AssetPreCompiler from "./AssetPreCompiler.js";
|
import AssetPreCompiler from "./AssetPreCompiler.js";
|
||||||
|
|
||||||
export default abstract class ViewEngine extends AssetPreCompiler {
|
export default abstract class ViewEngine extends AssetPreCompiler {
|
||||||
@ -18,10 +19,11 @@ export default abstract class ViewEngine extends AssetPreCompiler {
|
|||||||
locals: Record<string, unknown>,
|
locals: Record<string, unknown>,
|
||||||
): Promise<string>;
|
): Promise<string>;
|
||||||
|
|
||||||
public setup(app: Express, main: boolean): void {
|
public setup(app: Express, main: boolean, lazyLocalsComponent: LazyLocalsCoreComponent): void {
|
||||||
app.engine(this.extension, (path, options, callback) => {
|
app.engine(this.extension, (path, options, callback) => {
|
||||||
// Props (locals)
|
const locals = {...options};
|
||||||
const locals = Object.assign(this.getGlobals().get(), options);
|
|
||||||
|
lazyLocalsComponent.setupLazyLocals(locals);
|
||||||
|
|
||||||
this.render(path, locals)
|
this.render(path, locals)
|
||||||
.then(value => callback(null, value))
|
.then(value => callback(null, value))
|
||||||
|
4
src/types/Express.d.ts
vendored
4
src/types/Express.d.ts
vendored
@ -33,6 +33,10 @@ declare global {
|
|||||||
|
|
||||||
getIntendedUrl(): string | null;
|
getIntendedUrl(): string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Response {
|
||||||
|
setLazyLocal(key: string, valueProvider: () => unknown): void;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,7 +12,7 @@ useApp(async (addr, port) => {
|
|||||||
this.use(new class extends Controller {
|
this.use(new class extends Controller {
|
||||||
public routes(): void {
|
public routes(): void {
|
||||||
this.get('/', (req, res) => {
|
this.get('/', (req, res) => {
|
||||||
res.send(CsrfProtectionComponent.getCsrfToken(req.getSession()));
|
res.send(this.getApp().as(CsrfProtectionComponent).getSessionCsrfToken(req.getSession()));
|
||||||
}, 'csrf_test');
|
}, 'csrf_test');
|
||||||
|
|
||||||
this.post('/', (req, res) => {
|
this.post('/', (req, res) => {
|
||||||
|
@ -58,7 +58,7 @@ export function authAppProvider(withUsername: boolean = true, approvalMode: bool
|
|||||||
res.render('home');
|
res.render('home');
|
||||||
}, 'home');
|
}, 'home');
|
||||||
this.get('/csrf', (req, res) => {
|
this.get('/csrf', (req, res) => {
|
||||||
res.send(CsrfProtectionComponent.getCsrfToken(req.getSession()));
|
res.send(this.getApp().as(CsrfProtectionComponent).getSessionCsrfToken(req.getSession()));
|
||||||
}, 'csrf');
|
}, 'csrf');
|
||||||
this.get('/is-auth', async (req, res) => {
|
this.get('/is-auth', async (req, res) => {
|
||||||
const proofs = await this.getApp().as(AuthComponent).getAuthGuard().getProofs(req);
|
const proofs = await this.getApp().as(AuthComponent).getAuthGuard().getProofs(req);
|
||||||
|
Loading…
Reference in New Issue
Block a user