|
| 1 | +import "reflect-metadata"; |
| 2 | + |
| 3 | +import chokidar from "chokidar"; |
| 4 | +import { readFileSync } from "fs"; |
| 5 | +import Koa from "koa"; |
| 6 | +import Stream from "stream"; |
| 7 | + |
| 8 | +import { ICompiler } from "../lib/classes/Compiler"; |
| 9 | +import { IFileSystemHelper } from "../lib/classes/FileSystemHelper"; |
| 10 | +import { Messages } from "../lib/classes/Messages"; |
| 11 | +import { Renderer } from "../lib/classes/Renderer"; |
| 12 | + |
| 13 | +import { initContainer } from "../container"; |
| 14 | +import { MessagesEnum } from "../messages"; |
| 15 | +import { pr } from "../path-registry"; |
| 16 | + |
| 17 | +import { DEFAULTS } from "../lib/constants"; |
| 18 | +import { Config, TemplateVariables } from "../lib/interfaces"; |
| 19 | +import { DI_TOKENS } from "../lib/tokens"; |
| 20 | + |
| 21 | +const STATUS_PATH_REGEX = /^\/([0-9]{3})$/i; |
| 22 | + |
| 23 | +const runContainer = initContainer(); |
| 24 | + |
| 25 | +const fsHelper = runContainer.get<IFileSystemHelper>(DI_TOKENS.FS_HELPER); |
| 26 | + |
| 27 | +fsHelper.readConfig(pr.get("config")).then(async (config) => { |
| 28 | + runContainer.bind<Config>(DI_TOKENS.CONFIG).toConstantValue(config); |
| 29 | + |
| 30 | + // Registry update with new paths, which depends on current config |
| 31 | + pr.update({ |
| 32 | + src: `${DEFAULTS.SRC}/${config.locale}`, |
| 33 | + theme: `${DEFAULTS.THEMES}/${config.theme}`, |
| 34 | + themeConfig: `${DEFAULTS.THEMES}/${config.theme}/theme.tailwind.config.js`, |
| 35 | + themeCss: `${DEFAULTS.THEMES}/${config.theme}/@assets/css/main.twnd.css`, |
| 36 | + }); |
| 37 | + |
| 38 | + const compiler = runContainer.get<ICompiler>(DI_TOKENS.COMPILER); |
| 39 | + const statusList = await compiler.getStatusList(); |
| 40 | + |
| 41 | + // Server setup |
| 42 | + const app = new Koa(); |
| 43 | + |
| 44 | + const watcher = chokidar.watch([`${pr.get("src")}/**`, `${pr.get("theme")}/**`], { |
| 45 | + persistent: true, |
| 46 | + interval: 300, |
| 47 | + }); |
| 48 | + |
| 49 | + // Hot-reload feature over Server-sent events (SSE) |
| 50 | + app.use((ctx, next) => { |
| 51 | + console.log(`requested ${ctx.path}`); |
| 52 | + if ("/events" == ctx.path) { |
| 53 | + ctx.set({ |
| 54 | + "Content-Type": "text/event-stream", |
| 55 | + "Cache-Control": "no-cache", |
| 56 | + Connection: "keep-alive", |
| 57 | + }); |
| 58 | + ctx.status = 200; |
| 59 | + |
| 60 | + const stream = new Stream.PassThrough(); |
| 61 | + ctx.body = stream; |
| 62 | + |
| 63 | + stream.write(`data: init\n\n`); |
| 64 | + |
| 65 | + const sseHandler = (path) => { |
| 66 | + stream.write(`data: reload\n\n`); |
| 67 | + console.log(`hot-reload on ${path}`); |
| 68 | + }; |
| 69 | + |
| 70 | + watcher.on("add", sseHandler).on("change", sseHandler).on("unlink", sseHandler).on("addDir", sseHandler).on("unlinkDir", sseHandler); |
| 71 | + |
| 72 | + ctx.req.on("close", () => { |
| 73 | + stream.end(); |
| 74 | + }); |
| 75 | + } else { |
| 76 | + return next(); |
| 77 | + } |
| 78 | + }); |
| 79 | + |
| 80 | + // URL processor |
| 81 | + app.use(async (ctx, next) => { |
| 82 | + if (ctx.path === "/") { |
| 83 | + // Redirect to first status in a list |
| 84 | + ctx.redirect(`/${[...statusList][0]}`); |
| 85 | + } else if (STATUS_PATH_REGEX.test(ctx.path)) { |
| 86 | + // Read template if path looks like status path |
| 87 | + try { |
| 88 | + ctx.body = await fsHelper.readFile(pr.join("theme", "template.html")); |
| 89 | + return next(); |
| 90 | + } catch (_) { |
| 91 | + ctx.status = 500; |
| 92 | + ctx.body = Messages.text(MessagesEnum.NO_TEMPLATE_CONTENT); |
| 93 | + } |
| 94 | + } else { |
| 95 | + // Overwise return status 301 |
| 96 | + ctx.status = 301; |
| 97 | + } |
| 98 | + }); |
| 99 | + |
| 100 | + // Inject development variables |
| 101 | + app.use(async (ctx, next) => { |
| 102 | + if (ctx.body) { |
| 103 | + ctx.body = ctx.body.replace(/<\/(head|body)>/gi, "{{ $1-injection }}</$1>"); |
| 104 | + await next(); |
| 105 | + } else { |
| 106 | + ctx.status = 204; |
| 107 | + } |
| 108 | + }); |
| 109 | + |
| 110 | + // Render variables in template |
| 111 | + app.use(async (ctx, next) => { |
| 112 | + if (ctx.body) { |
| 113 | + try { |
| 114 | + const matches = ctx.path.match(STATUS_PATH_REGEX); |
| 115 | + const code = Number(matches[1]); |
| 116 | + if (!statusList.has(code)) { |
| 117 | + throw new Error(`No source file with status code #${code}`); |
| 118 | + } |
| 119 | + |
| 120 | + const initVars = await compiler.initTemplateVariables(); |
| 121 | + const commonVars = await fsHelper.readJson<TemplateVariables>(pr.join("src", "common.json")); |
| 122 | + const statusVars = await fsHelper.readJson<TemplateVariables>(pr.join("src", `${code}.json`)); |
| 123 | + |
| 124 | + const devVars = { |
| 125 | + "head-injection": "", |
| 126 | + "body-injection": readFileSync("./dev/sse.html").toString(), |
| 127 | + }; |
| 128 | + |
| 129 | + if (config.tailwind) { |
| 130 | + devVars["head-injection"] += `<script src="https://cdn.tailwindcss.com/3.2.4"></script>`; |
| 131 | + |
| 132 | + if (await fsHelper.ensure(pr.get("themeConfig"))) { |
| 133 | + // eslint-disable-next-line @typescript-eslint/no-var-requires |
| 134 | + const tailwindConfig = require(pr.get("themeConfig")); |
| 135 | + devVars["head-injection"] += `<script>tailwind.config = ${JSON.stringify(tailwindConfig)};</script>`; |
| 136 | + } |
| 137 | + |
| 138 | + if (await fsHelper.ensure(pr.get("themeCss"))) { |
| 139 | + const mainCss = await fsHelper.readFile(pr.get("themeCss")); |
| 140 | + devVars["head-injection"] += `<style type="text/tailwindcss">${mainCss}</style>`; |
| 141 | + } |
| 142 | + } |
| 143 | + |
| 144 | + ctx.body = Renderer.renderTemplate(ctx.body, { ...initVars, ...commonVars, ...statusVars, ...devVars, code }); |
| 145 | + |
| 146 | + await next(); |
| 147 | + } catch (err) { |
| 148 | + ctx.status = 500; |
| 149 | + ctx.body = err.message; |
| 150 | + } |
| 151 | + } else { |
| 152 | + ctx.status = 204; |
| 153 | + } |
| 154 | + }); |
| 155 | + |
| 156 | + const port = 8080; |
| 157 | + app.listen(port); |
| 158 | + console.log(`hot-reload server was started on port ${port}`); |
| 159 | +}); |
0 commit comments