Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 8x 1x 9x 9x 1x 8x 8x 8x 8x 7x 8x 1x 8x 1x 8x 8x 4x 4x 4x 4x 4x 4x 8x 8x 8x 1x 1x 1x 1x 2x 2x 2x 1x 1x 1x 1x 1x 1x 1x 1x 3x 1x 2x 3x 1x 1x 1x 1x 2x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 2x 1x 1x 1x 1x 1x 1x | import { HttpMethod } from "@xtaskjs/common";
import {
ExpressAdapterOptions,
HttpAdapter,
HttpRequestHandler,
HttpRequestLike,
HttpResponseLike,
HttpServerOptions,
HttpViewResult,
} from "./types";
import { readFile } from "fs/promises";
import path from "path";
const SUPPORTED_METHODS: HttpMethod[] = ["GET", "POST", "PATCH", "DELETE"];
const DEFAULT_VIEWS_FOLDER = "views";
const DEFAULT_PUBLIC_FOLDER = "public";
const DEFAULT_FILE_EXTENSION = ".html";
const interpolateTemplate = (template: string, model: Record<string, any>): string => {
return template.replace(/{{\s*([\w.]+)\s*}}/g, (_match, key: string) => {
const value = key.split(".").reduce<any>((acc, segment) => acc?.[segment], model);
return value === undefined || value === null ? "" : String(value);
});
};
const withLeadingDot = (value: string): string => (value.startsWith(".") ? value : `.${value}`);
export class ExpressAdapter implements HttpAdapter {
public readonly type = "express" as const;
private readonly app: any;
private readonly viewsPath: string;
private readonly fileExtension: string;
private readonly templateRenderer?: (
template: string,
model: Record<string, any>,
context: { req: HttpRequestLike; res: HttpResponseLike }
) => string | Promise<string>;
private readonly hasNativeViewEngine: boolean;
private closeServer?: { close: (cb?: (error?: Error) => void) => void };
constructor(app: any, options?: ExpressAdapterOptions) {
if (!app || typeof app.use !== "function" || typeof app.listen !== "function") {
throw new Error("ExpressAdapter requires a valid express app instance");
}
const templateEngine = options?.templateEngine;
this.viewsPath = templateEngine?.viewsPath || path.join(process.cwd(), DEFAULT_VIEWS_FOLDER);
this.fileExtension = withLeadingDot(templateEngine?.fileExtension || DEFAULT_FILE_EXTENSION);
if (typeof app.set === "function") {
app.set("views", this.viewsPath);
}
if (templateEngine?.engine && templateEngine.extension && typeof app.engine === "function") {
app.engine(templateEngine.extension, templateEngine.engine);
}
if (templateEngine?.viewEngine && typeof app.set === "function") {
app.set("view engine", templateEngine.viewEngine);
}
const staticFiles = options?.staticFiles;
if (staticFiles?.enabled !== false) {
const expressModule = require("express");
const publicPath = staticFiles?.publicPath || path.join(process.cwd(), DEFAULT_PUBLIC_FOLDER);
const urlPrefix = staticFiles?.urlPrefix || "/";
const staticMiddleware = expressModule.static(publicPath);
if (urlPrefix === "/") {
app.use(staticMiddleware);
} else E{
app.use(urlPrefix, staticMiddleware);
}
}
this.templateRenderer = templateEngine?.render;
this.hasNativeViewEngine = Boolean(templateEngine?.viewEngine || templateEngine?.engine);
this.app = app;
}
private async renderFileTemplate(template: string, model: Record<string, any>): Promise<string> {
const fullTemplateName = path.extname(template) ? template : `${template}${this.fileExtension}`;
const templatePath = path.join(this.viewsPath, fullTemplateName);
const templateFile = await readFile(templatePath, "utf-8");
return interpolateTemplate(templateFile, model);
}
registerRequestHandler(handler: HttpRequestHandler): void {
this.app.use(async (req: any, res: any) => {
const method = (req.method || "GET").toUpperCase() as HttpMethod;
if (!SUPPORTED_METHODS.includes(method)) {
res.status(405).send("Method Not Allowed");
return;
}
const path = req.path || req.url || "/";
await handler(method, path, req, res);
});
}
async listen(options: Required<HttpServerOptions>): Promise<void> {
await new Promise<void>((resolve, reject) => {
this.closeServer = this.app.listen(options.port, options.host, (error?: Error) => {
Iif (error) {
reject(error);
return;
}
resolve();
});
});
}
async renderView(req: HttpRequestLike, res: HttpResponseLike, payload: HttpViewResult): Promise<void> {
if (payload.statusCode && typeof res.status === "function") {
res.status(payload.statusCode);
} else Iif (payload.statusCode) {
res.statusCode = payload.statusCode;
}
if (this.templateRenderer) {
const html = await this.templateRenderer(payload.template, payload.model || {}, { req, res });
Eif (typeof res.send === "function") {
res.send(html);
return;
}
res.setHeader?.("content-type", "text/html; charset=utf-8");
res.end?.(html);
return;
}
if (this.hasNativeViewEngine && typeof res.render === "function") {
await new Promise<void>((resolve, reject) => {
res.render!(payload.template, payload.model || {}, (error, html) => {
Iif (error) {
reject(error);
return;
}
Eif (html !== undefined) {
if (typeof res.send === "function") {
res.send(html);
} else E{
res.setHeader?.("content-type", "text/html; charset=utf-8");
res.end?.(html);
}
}
resolve();
});
});
return;
}
const html = await this.renderFileTemplate(payload.template, payload.model || {});
Eif (typeof res.send === "function") {
res.send(html);
return;
}
res.setHeader?.("content-type", "text/html; charset=utf-8");
res.end?.(html);
return;
}
async close(): Promise<void> {
if (!this.closeServer) {
return;
}
await new Promise<void>((resolve, reject) => {
this.closeServer!.close((error?: Error) => {
Iif (error) {
reject(error);
return;
}
resolve();
});
});
this.closeServer = undefined;
}
}
|