Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add hot-reload server to template development #25

Merged
merged 9 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading