-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add hot-reload server to template development (#25)
* feat: add dev server to template debug * fix: remove unused npm command * revert: default config locale * feat: add hot-reload over sse * feat: add ability to watch any status page instead of configured * fix: linter * docs: readme update * 1.10.0 * fix: typo
- Loading branch information
Showing
9 changed files
with
1,007 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Container } from "inversify"; | ||
|
||
import { Compiler, ICompiler } from "./lib/classes/Compiler"; | ||
import { ChildProcessWrapper, IChildProcessWrapper } from "./lib/classes/ChildProcessWrapper"; | ||
import { FileSystemHelper, IFileSystemHelper } from "./lib/classes/FileSystemHelper"; | ||
import { IFileSystemWrapper, NodeFS } from "./lib/classes/FileSystemWrapper"; | ||
import { ILogger, Logger } from "./lib/classes/Logger"; | ||
import { PathRegistry } from "./lib/classes/PathRegistry"; | ||
import { IStyler, Styler } from "./lib/classes/Styler"; | ||
|
||
import { pr } from "./path-registry"; | ||
|
||
import { DI_TOKENS } from "./lib/tokens"; | ||
|
||
// Register DI | ||
export function initContainer(): Container { | ||
const container = new Container({ defaultScope: "Singleton" }); | ||
container.bind<ICompiler>(DI_TOKENS.COMPILER).to(Compiler); | ||
container.bind<IChildProcessWrapper>(DI_TOKENS.CHILD_PROCESS).to(ChildProcessWrapper); | ||
container.bind<IFileSystemHelper>(DI_TOKENS.FS_HELPER).to(FileSystemHelper); | ||
container.bind<IFileSystemWrapper>(DI_TOKENS.FS).to(NodeFS); | ||
container.bind<ILogger>(DI_TOKENS.LOGGER).to(Logger); | ||
container.bind<IStyler>(DI_TOKENS.STYLER).to(Styler); | ||
container.bind<PathRegistry>(DI_TOKENS.PATH).toConstantValue(pr); | ||
|
||
return container; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,159 @@ | ||
import "reflect-metadata"; | ||
|
||
import chokidar from "chokidar"; | ||
import { readFileSync } from "fs"; | ||
import Koa from "koa"; | ||
import Stream from "stream"; | ||
|
||
import { ICompiler } from "../lib/classes/Compiler"; | ||
import { IFileSystemHelper } from "../lib/classes/FileSystemHelper"; | ||
import { Messages } from "../lib/classes/Messages"; | ||
import { Renderer } from "../lib/classes/Renderer"; | ||
|
||
import { initContainer } from "../container"; | ||
import { MessagesEnum } from "../messages"; | ||
import { pr } from "../path-registry"; | ||
|
||
import { DEFAULTS } from "../lib/constants"; | ||
import { Config, TemplateVariables } from "../lib/interfaces"; | ||
import { DI_TOKENS } from "../lib/tokens"; | ||
|
||
const STATUS_PATH_REGEX = /^\/([0-9]{3})$/i; | ||
|
||
const runContainer = initContainer(); | ||
|
||
const fsHelper = runContainer.get<IFileSystemHelper>(DI_TOKENS.FS_HELPER); | ||
|
||
fsHelper.readConfig(pr.get("config")).then(async (config) => { | ||
runContainer.bind<Config>(DI_TOKENS.CONFIG).toConstantValue(config); | ||
|
||
// Registry update with new paths, which depends on current config | ||
pr.update({ | ||
src: `${DEFAULTS.SRC}/${config.locale}`, | ||
theme: `${DEFAULTS.THEMES}/${config.theme}`, | ||
themeConfig: `${DEFAULTS.THEMES}/${config.theme}/theme.tailwind.config.js`, | ||
themeCss: `${DEFAULTS.THEMES}/${config.theme}/@assets/css/main.twnd.css`, | ||
}); | ||
|
||
const compiler = runContainer.get<ICompiler>(DI_TOKENS.COMPILER); | ||
const statusList = await compiler.getStatusList(); | ||
|
||
// Server setup | ||
const app = new Koa(); | ||
|
||
const watcher = chokidar.watch([`${pr.get("src")}/**`, `${pr.get("theme")}/**`], { | ||
persistent: true, | ||
interval: 300, | ||
}); | ||
|
||
// Hot-reload feature over Server-sent events (SSE) | ||
app.use((ctx, next) => { | ||
console.log(`requested ${ctx.path}`); | ||
if ("/events" == ctx.path) { | ||
ctx.set({ | ||
"Content-Type": "text/event-stream", | ||
"Cache-Control": "no-cache", | ||
Connection: "keep-alive", | ||
}); | ||
ctx.status = 200; | ||
|
||
const stream = new Stream.PassThrough(); | ||
ctx.body = stream; | ||
|
||
stream.write(`data: init\n\n`); | ||
|
||
const sseHandler = (path) => { | ||
stream.write(`data: reload\n\n`); | ||
console.log(`hot-reload on ${path}`); | ||
}; | ||
|
||
watcher.on("add", sseHandler).on("change", sseHandler).on("unlink", sseHandler).on("addDir", sseHandler).on("unlinkDir", sseHandler); | ||
|
||
ctx.req.on("close", () => { | ||
stream.end(); | ||
}); | ||
} else { | ||
return next(); | ||
} | ||
}); | ||
|
||
// URL processor | ||
app.use(async (ctx, next) => { | ||
if (ctx.path === "/") { | ||
// Redirect to first status in a list | ||
ctx.redirect(`/${[...statusList][0]}`); | ||
} else if (STATUS_PATH_REGEX.test(ctx.path)) { | ||
// Read template if path looks like status path | ||
try { | ||
ctx.body = await fsHelper.readFile(pr.join("theme", "template.html")); | ||
return next(); | ||
} catch (_) { | ||
ctx.status = 500; | ||
ctx.body = Messages.text(MessagesEnum.NO_TEMPLATE_CONTENT); | ||
} | ||
} else { | ||
// Overwise return status 301 | ||
ctx.status = 301; | ||
} | ||
}); | ||
|
||
// Inject development variables | ||
app.use(async (ctx, next) => { | ||
if (ctx.body) { | ||
ctx.body = ctx.body.replace(/<\/(head|body)>/gi, "{{ $1-injection }}</$1>"); | ||
await next(); | ||
} else { | ||
ctx.status = 204; | ||
} | ||
}); | ||
|
||
// Render variables in template | ||
app.use(async (ctx, next) => { | ||
if (ctx.body) { | ||
try { | ||
const matches = ctx.path.match(STATUS_PATH_REGEX); | ||
const code = Number(matches[1]); | ||
if (!statusList.has(code)) { | ||
throw new Error(`No source file with status code #${code}`); | ||
} | ||
|
||
const initVars = await compiler.initTemplateVariables(); | ||
const commonVars = await fsHelper.readJson<TemplateVariables>(pr.join("src", "common.json")); | ||
const statusVars = await fsHelper.readJson<TemplateVariables>(pr.join("src", `${code}.json`)); | ||
|
||
const devVars = { | ||
"head-injection": "", | ||
"body-injection": readFileSync("./dev/sse.html").toString(), | ||
}; | ||
|
||
if (config.tailwind) { | ||
devVars["head-injection"] += `<script src="https://cdn.tailwindcss.com/3.2.4"></script>`; | ||
|
||
if (await fsHelper.ensure(pr.get("themeConfig"))) { | ||
// eslint-disable-next-line @typescript-eslint/no-var-requires | ||
const tailwindConfig = require(pr.get("themeConfig")); | ||
devVars["head-injection"] += `<script>tailwind.config = ${JSON.stringify(tailwindConfig)};</script>`; | ||
} | ||
|
||
if (await fsHelper.ensure(pr.get("themeCss"))) { | ||
const mainCss = await fsHelper.readFile(pr.get("themeCss")); | ||
devVars["head-injection"] += `<style type="text/tailwindcss">${mainCss}</style>`; | ||
} | ||
} | ||
|
||
ctx.body = Renderer.renderTemplate(ctx.body, { ...initVars, ...commonVars, ...statusVars, ...devVars, code }); | ||
|
||
await next(); | ||
} catch (err) { | ||
ctx.status = 500; | ||
ctx.body = err.message; | ||
} | ||
} else { | ||
ctx.status = 204; | ||
} | ||
}); | ||
|
||
const port = 8080; | ||
app.listen(port); | ||
console.log(`hot-reload server was started on port ${port}`); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
<script type="text/javascript"> | ||
const src = new EventSource("/events"); | ||
|
||
src.onmessage = (event) => { | ||
if (event.data.indexOf("reload") !== -1) { | ||
window.location.reload(); | ||
} | ||
}; | ||
</script> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.