Add ApplicationComponent init lifecycle step and unstatic globals

This renames ApplicationComponent (previous) init to initRoutes and handle to handleRoutes
This commit is contained in:
Alice Gaudon 2021-05-13 16:03:59 +02:00
parent cdf95c0c0b
commit a3ebf46b54
20 changed files with 112 additions and 72 deletions

View File

@ -94,8 +94,11 @@ export default abstract class Application implements Extendable<ApplicationCompo
// Register migrations // Register migrations
MysqlConnectionManager.registerMigrations(this.getMigrations()); MysqlConnectionManager.registerMigrations(this.getMigrations());
// Register all components and alike // Register and initialize all components and alike
await this.init(); await this.init();
for (const component of this.components) {
await component.init?.();
}
// Process command line // Process command line
if (!this.ignoreCommandLine) { if (!this.ignoreCommandLine) {
@ -218,14 +221,14 @@ export default abstract class Application implements Extendable<ApplicationCompo
// Components routes // Components routes
for (const component of this.components) { for (const component of this.components) {
if (component.init) { if (component.initRoutes) {
component.setCurrentRouter(initRouter); component.setCurrentRouter(initRouter);
await component.init(initRouter); await component.initRoutes(initRouter);
} }
if (component.handle) { if (component.handleRoutes) {
component.setCurrentRouter(handleRouter); component.setCurrentRouter(handleRouter);
await component.handle(handleRouter); await component.handleRoutes(handleRouter);
} }
component.setCurrentRouter(null); component.setCurrentRouter(null);
@ -292,7 +295,6 @@ export default abstract class Application implements Extendable<ApplicationCompo
} }
const frontendToolsComponent = this.as(FrontendToolsComponent); const frontendToolsComponent = this.as(FrontendToolsComponent);
frontendToolsComponent.setupGlobals();
await frontendToolsComponent.preCompileViews(flags.watch); await frontendToolsComponent.preCompileViews(flags.watch);
break; break;
} }

View File

@ -13,11 +13,13 @@ export default abstract class ApplicationComponent {
public async checkSecuritySettings?(): Promise<void>; public async checkSecuritySettings?(): Promise<void>;
public async init?(): Promise<void>;
public async start?(expressApp: Express): Promise<void>; public async start?(expressApp: Express): Promise<void>;
public async init?(router: Router): Promise<void>; public async initRoutes?(router: Router): Promise<void>;
public async handle?(router: Router): Promise<void>; public async handleRoutes?(router: Router): Promise<void>;
public async stop?(): Promise<void>; public async stop?(): Promise<void>;

View File

@ -108,7 +108,7 @@ export default class TestApp extends Application {
this.use(new AuthComponent(this, new MagicLinkAuthMethod(this, MAGIC_LINK_MAIL), new PasswordAuthMethod(this))); this.use(new AuthComponent(this, new MagicLinkAuthMethod(this, MAGIC_LINK_MAIL), new PasswordAuthMethod(this)));
// WebSocket server // WebSocket server
this.use(new WebSocketServerComponent(this, this.as(ExpressAppComponent), this.as(RedisComponent))); this.use(new WebSocketServerComponent());
} }
protected registerWebSocketListeners(): void { protected registerWebSocketListeners(): void {

View File

@ -18,7 +18,7 @@ export default class AuthComponent extends ApplicationComponent {
this.authGuard = new AuthGuard(app, ...authMethods); this.authGuard = new AuthGuard(app, ...authMethods);
} }
public async init(): Promise<void> { public async initRoutes(): Promise<void> {
this.use(AuthMiddleware); this.use(AuthMiddleware);
} }

View File

@ -16,7 +16,7 @@ export default class AutoUpdateComponent extends ApplicationComponent {
this.checkSecurityConfigField('gitlab_webhook_token'); this.checkSecurityConfigField('gitlab_webhook_token');
} }
public async init(router: Router): Promise<void> { public async initRoutes(router: Router): Promise<void> {
router.post('/update/push.json', (req, res) => { router.post('/update/push.json', (req, res) => {
const token = req.header('X-Gitlab-Token'); const token = req.header('X-Gitlab-Token');
if (!token || token !== config.get<string>('gitlab_webhook_token')) if (!token || token !== config.get<string>('gitlab_webhook_token'))

View File

@ -20,7 +20,7 @@ export default class CsrfProtectionComponent extends ApplicationComponent {
this.excluders.push(excluder); this.excluders.push(excluder);
} }
public async handle(router: Router): Promise<void> { public async handleRoutes(router: Router): Promise<void> {
router.use(async (req, res, next) => { router.use(async (req, res, next) => {
for (const excluder of CsrfProtectionComponent.excluders) { for (const excluder of CsrfProtectionComponent.excluders) {
if (excluder(req)) return next(); if (excluder(req)) return next();

View File

@ -30,7 +30,7 @@ export default class ExpressAppComponent extends ApplicationComponent {
this.expressApp = app; this.expressApp = app;
} }
public async init(router: Router): Promise<void> { public async initRoutes(router: Router): Promise<void> {
router.use(preventContextCorruptionMiddleware(express.json({ router.use(preventContextCorruptionMiddleware(express.json({
type: req => req.headers['content-type']?.match(/^application\/(.+\+)?json$/), type: req => req.headers['content-type']?.match(/^application\/(.+\+)?json$/),
}))); })));

View File

@ -3,7 +3,7 @@ import {Router} from "express";
import ApplicationComponent from "../ApplicationComponent.js"; import ApplicationComponent from "../ApplicationComponent.js";
export default class FormHelperComponent extends ApplicationComponent { export default class FormHelperComponent extends ApplicationComponent {
public async init(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; let _validation: unknown | null;
res.locals.validation = () => { res.locals.validation = () => {

View File

@ -1,7 +1,6 @@
import config from "config"; import config from "config";
import {Express, Router} from "express"; import {Express, Router} from "express";
import path from "path"; import path from "path";
import * as querystring from "querystring";
import {ParsedUrlQueryInput} from "querystring"; import {ParsedUrlQueryInput} from "querystring";
import util from "util"; import util from "util";
@ -9,6 +8,7 @@ import ApplicationComponent from "../ApplicationComponent.js";
import Controller, {RouteParams} from "../Controller.js"; import Controller, {RouteParams} from "../Controller.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 {logger} from "../Logger.js";
import {listFilesRecursively} from "../Utils.js"; import {listFilesRecursively} from "../Utils.js";
@ -18,6 +18,7 @@ export default class FrontendToolsComponent extends ApplicationComponent {
private readonly publicDir: string; private readonly publicDir: string;
private readonly publicAssetsCache: FileCache = new FileCache(); 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,9 +32,30 @@ 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', (
route: string,
params: RouteParams = [],
query: ParsedUrlQueryInput = {},
absolute: boolean = false,
) => Controller.route(route, 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 // Cache public assets
if (config.get<boolean>('asset_cache')) { if (config.get<boolean>('asset_cache')) {
@ -55,9 +77,6 @@ export default class FrontendToolsComponent extends ApplicationComponent {
main = false; main = false;
} }
} }
// Add util globals
this.setupGlobals();
} }
public async stop(): Promise<void> { public async stop(): Promise<void> {
@ -66,7 +85,7 @@ export default class FrontendToolsComponent extends ApplicationComponent {
} }
} }
public async handle(router: Router): Promise<void> { public async handleRoutes(router: Router): Promise<void> {
router.use((req, res, next) => { router.use((req, res, next) => {
res.locals.inlineAsset = (urlPath: string) => { res.locals.inlineAsset = (urlPath: string) => {
return this.publicAssetsCache.getOrFail(path.join(this.publicDir, urlPath)); return this.publicAssetsCache.getOrFail(path.join(this.publicDir, urlPath));
@ -86,25 +105,6 @@ export default class FrontendToolsComponent extends ApplicationComponent {
}); });
} }
public setupGlobals(): void {
ViewEngine.setGlobal('route', (
route: string,
params: RouteParams = [],
query: ParsedUrlQueryInput = {},
absolute: boolean = false,
) => Controller.route(route, params, query, absolute));
ViewEngine.setGlobal('app_version', this.getApp().getVersion());
ViewEngine.setGlobal('core_version', this.getApp().getCoreVersion());
ViewEngine.setGlobal('querystring', querystring);
ViewEngine.setGlobal('app', config.get('app'));
ViewEngine.setGlobal('dump', (val: unknown) => {
return util.inspect(val);
});
ViewEngine.setGlobal('hex', (v: number) => {
return v.toString(16);
});
}
public hookPreCompilers(): void { public hookPreCompilers(): void {
for (const assetPreCompiler of this.assetPreCompilers) { for (const assetPreCompiler of this.assetPreCompilers) {
assetPreCompiler.onPreCompile(async watch => { assetPreCompiler.onPreCompile(async watch => {
@ -127,4 +127,8 @@ export default class FrontendToolsComponent extends ApplicationComponent {
this.hookPreCompilers(); this.hookPreCompilers();
} }
} }
public getGlobals(): Globals {
return this.globals;
}
} }

View File

@ -75,7 +75,7 @@ export default class LogRequestsComponent extends ApplicationComponent {
return ''; return '';
} }
public async init(router: Router): Promise<void> { public async initRoutes(router: Router): Promise<void> {
router.use((req, res, next) => { router.use((req, res, next) => {
onFinished(res, (err) => { onFinished(res, (err) => {
if (!err) { if (!err) {

View File

@ -11,6 +11,7 @@ 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";
export default class MailComponent extends ApplicationComponent { export default class MailComponent extends ApplicationComponent {
private transporter?: Transporter; private transporter?: Transporter;
@ -91,7 +92,7 @@ export default class MailComponent extends ApplicationComponent {
locals.mail_to = options.to; locals.mail_to = options.to;
locals.mail_link = config.get<string>('public_url') + locals.mail_link = config.get<string>('public_url') +
Controller.route('mail', [template.template], locals); Controller.route('mail', [template.template], locals);
Object.assign(locals, ViewEngine.getGlobals()); Object.assign(locals, this.getApp().as(FrontendToolsComponent).getGlobals().get());
// Log // Log
logger.debug(`Send mail from ${options.from.address} to ${options.to}`); logger.debug(`Send mail from ${options.from.address} to ${options.to}`);

View File

@ -15,7 +15,7 @@ export default class MaintenanceComponent extends ApplicationComponent {
this.canServe = canServe; this.canServe = canServe;
} }
public async handle(router: Router): Promise<void> { public async handleRoutes(router: Router): Promise<void> {
router.use((req: Request, res: Response, next: NextFunction) => { router.use((req: Request, res: Response, next: NextFunction) => {
if (res.headersSent) { if (res.headersSent) {
return next(); return next();

View File

@ -3,11 +3,18 @@ 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 handle(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 = () => {
let url = req.header('referer'); let url = req.header('referer');

View File

@ -15,7 +15,7 @@ export default class ServeStaticDirectoryComponent extends ApplicationComponent
this.path = routePath; this.path = routePath;
} }
public async init(router: Router): Promise<void> { public async initRoutes(router: Router): Promise<void> {
const resolvedRoot = path.resolve(this.root); const resolvedRoot = path.resolve(this.root);
if (this.path) { if (this.path) {

View File

@ -4,7 +4,9 @@ import {Router} from "express";
import session from "express-session"; import session from "express-session";
import ApplicationComponent from "../ApplicationComponent.js"; import ApplicationComponent from "../ApplicationComponent.js";
import ViewEngine from "../frontend/ViewEngine.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 {
@ -15,6 +17,13 @@ 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')) {
@ -22,7 +31,7 @@ export default class SessionComponent extends ApplicationComponent {
} }
} }
public async init(router: Router): Promise<void> { public async initRoutes(router: Router): Promise<void> {
router.use(session({ router.use(session({
saveUninitialized: true, saveUninitialized: true,
secret: config.get('session.secret'), secret: config.get('session.secret'),

View File

@ -7,29 +7,33 @@ import WebSocket from "ws";
import Application from "../Application.js"; import Application from "../Application.js";
import ApplicationComponent from "../ApplicationComponent.js"; import ApplicationComponent from "../ApplicationComponent.js";
import ViewEngine from "../frontend/ViewEngine.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 {
private wss?: WebSocket.Server; private wss?: WebSocket.Server;
public constructor( public async init(): Promise<void> {
private readonly application: Application, const app = this.getApp();
private readonly expressAppComponent: ExpressAppComponent,
private readonly storeComponent: RedisComponent,
) {
super();
ViewEngine.setGlobal('websocketUrl', config.get('public_websocket_url')); app.require(ExpressAppComponent);
app.require(RedisComponent);
const globals = app.asOptional(FrontendToolsComponent)?.getGlobals();
if (globals) {
globals.set('websocketUrl', config.get('public_websocket_url'));
}
} }
public async start(_app: Express): Promise<void> { public async start(_app: Express): Promise<void> {
const listeners: { [p: string]: WebSocketListener<Application> } = this.application.getWebSocketListeners(); const app = this.getApp();
const listeners: { [p: string]: WebSocketListener<Application> } = app.getWebSocketListeners();
this.wss = new WebSocket.Server({ this.wss = new WebSocket.Server({
server: this.expressAppComponent.getServer(), server: app.as(ExpressAppComponent).getServer(),
}, () => { }, () => {
logger.info(`Websocket server started over webserver.`); logger.info(`Websocket server started over webserver.`);
}).on('error', (err) => { }).on('error', (err) => {
@ -57,7 +61,7 @@ export default class WebSocketServerComponent extends ApplicationComponent {
return; return;
} }
const store = this.storeComponent.getStore(); const store = app.as(RedisComponent).getStore();
store.get(sid, (err, session) => { store.get(sid, (err, session) => {
if (err || !session) { if (err || !session) {
logger.error(err, 'Error while initializing session in websocket.'); logger.error(err, 'Error while initializing session in websocket.');

View File

@ -4,9 +4,11 @@ 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>;
@ -51,6 +53,19 @@ export default abstract class AssetPreCompiler {
return this.outputToPublicDir; return this.outputToPublicDir;
} }
public getViewPaths(): string[] {
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();
@ -58,10 +73,6 @@ export default abstract class AssetPreCompiler {
} }
} }
public getViewPaths(): string[] {
return this.assetPaths;
}
public abstract preCompile(canonicalName: string, alsoCompileDependents: boolean): Promise<void>; public abstract preCompile(canonicalName: string, alsoCompileDependents: boolean): Promise<void>;
public onPreCompile(afterPreCompileHandler: (watch: boolean) => Promise<void>): void { public onPreCompile(afterPreCompileHandler: (watch: boolean) => Promise<void>): void {

11
src/frontend/Globals.ts Normal file
View File

@ -0,0 +1,11 @@
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;
}
}

View File

@ -278,7 +278,7 @@ export default class SvelteViewEngine extends ViewEngine {
// Load locals into locals store // Load locals into locals store
const localsModulePath = "../../build/ts/stores.js"; const localsModulePath = "../../build/ts/stores.js";
const localsModule = await import(localsModulePath); const localsModule = await import(localsModulePath);
const locals = ViewEngine.getGlobals(); const locals = this.getGlobals().get();
const localMap = this.compileBackendCalls(backendCalls, locals, true); const localMap = this.compileBackendCalls(backendCalls, locals, true);
localsModule.locals.set((key: string, args: string) => { localsModule.locals.set((key: string, args: string) => {
return localMap[args ? return localMap[args ?

View File

@ -3,17 +3,6 @@ import {Express} from "express";
import AssetPreCompiler from "./AssetPreCompiler.js"; import AssetPreCompiler from "./AssetPreCompiler.js";
export default abstract class ViewEngine extends AssetPreCompiler { export default abstract class ViewEngine extends AssetPreCompiler {
private static readonly globals: Record<string, unknown> = {};
public static getGlobals(): Record<string, unknown> {
return {...this.globals};
}
public static setGlobal(key: string, value: unknown): void {
this.globals[key] = value;
}
protected constructor( protected constructor(
targetDir: string, targetDir: string,
assetType: string, assetType: string,
@ -32,7 +21,7 @@ export default abstract class ViewEngine extends AssetPreCompiler {
public setup(app: Express, main: boolean): void { public setup(app: Express, main: boolean): void {
app.engine(this.extension, (path, options, callback) => { app.engine(this.extension, (path, options, callback) => {
// Props (locals) // Props (locals)
const locals = Object.assign(options, ViewEngine.getGlobals()); const locals = Object.assign(options, this.getGlobals().get());
this.render(path, locals) this.render(path, locals)
.then(value => callback(null, value)) .then(value => callback(null, value))