Skip to content

Commit

Permalink
feat: add hot-reload server to template development (#25)
Browse files Browse the repository at this point in the history
* 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
sapachev authored Nov 3, 2023
1 parent 7247cf5 commit 54dc06d
Show file tree
Hide file tree
Showing 9 changed files with 1,007 additions and 38 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ The main configuration is stored in the `config.json` file in a root directory a
}
```

There possible to run hot-reload server to develop your own theme with custom markup, styles, and scripts. To start dev-server just run command `npm run dev`. This command will start server on 8080 port ([http://localhost:8080](http://localhost:8080). By default, this address will be opened with a first status code, defined in `src` directory, which corresponds to configured `locale` value. You can choose any other code to continue specific page development. Don't be surprised with injected parts of code in a rendered page, because this is a part of hot-reload mode. Any change of the main configuration will require dev-server restart. The only configured theme and locale directories are watching during development.


### Templates

All templates are located in the `themes` directory. You can change the existing `minimalistic` theme or add a new one. There are no special requirements to page templates: every template is a usual HTML document with injected variables for the text messages from locale files. The [mustache.js](https://www.npmjs.com/package/mustache) library was used to handle variables injection and compile templates. So if you want to have something specific around templates, you can refer to this library documentation to get more information about templating.
Expand Down
27 changes: 27 additions & 0 deletions container.ts
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;
}
159 changes: 159 additions & 0 deletions dev/server.ts
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}`);
});
9 changes: 9 additions & 0 deletions dev/sse.html
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>
34 changes: 6 additions & 28 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,20 @@
import "reflect-metadata";

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 { FileSystemHelper } from "./lib/classes/FileSystemHelper";
import { ILogger } from "./lib/classes/Logger";
import { Main } from "./lib/classes/Main";
import { IStyler, Styler } from "./lib/classes/Styler";

import { initContainer } from "./container";
import { pr } from "./path-registry";

import { Config } from "./lib/interfaces";
import { Messages } from "./lib/classes/Messages";
import { MessagesEnum } from "./messages";
import { PathRegistry } from "./lib/classes/PathRegistry";

import { DEFAULTS } from "./lib/constants";
import { DI_TOKENS } from "./lib/tokens";

// Resigstry of resolved paths to usage during the process
const pr = new PathRegistry({
assetsDist: `${DEFAULTS.DIST}/${DEFAULTS.ASSETS}`,
config: DEFAULTS.CONFIG,
dist: DEFAULTS.DIST,
package: DEFAULTS.PACKAGE,
snippets: DEFAULTS.SNIPPETS,
twndDist: `${DEFAULTS.DIST}/${DEFAULTS.ASSETS}/css/${DEFAULTS.TAILWIND_OUT}`,
});

// Register DI
const runContainer = new Container({ defaultScope: "Singleton" });
runContainer.bind<ICompiler>(DI_TOKENS.COMPILER).to(Compiler);
runContainer.bind<IChildProcessWrapper>(DI_TOKENS.CHILD_PROCESS).to(ChildProcessWrapper);
runContainer.bind<IFileSystemHelper>(DI_TOKENS.FS_HELPER).to(FileSystemHelper);
runContainer.bind<IFileSystemWrapper>(DI_TOKENS.FS).to(NodeFS);
runContainer.bind<ILogger>(DI_TOKENS.LOGGER).to(Logger);
runContainer.bind<IStyler>(DI_TOKENS.STYLER).to(Styler);
runContainer.bind<PathRegistry>(DI_TOKENS.PATH).toConstantValue(pr);
const runContainer = initContainer();

runContainer
.resolve(FileSystemHelper)
Expand Down
Loading

0 comments on commit 54dc06d

Please sign in to comment.