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"; 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, ...assetPreCompilers: AssetPreCompiler[] ) { super(); this.assetPreCompilers = assetPreCompilers; this.publicDir = this.assetCompiler.targetDir; for (const assetPreCompiler of this.assetPreCompilers) { 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); main = false; } } } public async stop(): Promise { for (const assetPreCompiler of this.assetPreCompilers) { await assetPreCompiler.stop(); } } 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 router.use((req, res, next) => { res.locals.url = req.url; res.locals.params = req.params; res.locals.query = req.query; res.locals.body = req.body; next(); }); } public hookPreCompilers(): void { for (const assetPreCompiler of this.assetPreCompilers) { assetPreCompiler.onPreCompile(async watch => { await this.assetCompiler.compile(watch); }); assetPreCompiler.onInputChange(async restart => { await this.assetCompiler.stopWatching(restart); }); } } public async preCompileViews(watch: boolean): Promise { for (const viewEngine of this.assetPreCompilers) { await viewEngine.preCompileAll(watch); } await this.assetCompiler.compile(watch); if (watch) { this.hookPreCompilers(); } } public getGlobals(): Globals { return this.globals; } }