You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
193 lines
5.3 KiB
TypeScript
193 lines
5.3 KiB
TypeScript
import { FlatTemplatable, Templatable, tempstream } from "tempstream";
|
|
import { Readable } from "stream";
|
|
import { BaseContext } from "koa";
|
|
import { default as default_navbar } from "./routes/common/navbar.js";
|
|
import { toKebabCase } from "js-convert-case";
|
|
|
|
export const defaultHead = (
|
|
ctx: BaseContext,
|
|
title: string,
|
|
options: HTMLOptions
|
|
) => /* HTML */ `<title>${title} · ${ctx.$app.manifest.name}</title>
|
|
<meta name="viewport" content="width=device-width" />
|
|
<script async src="/dist/bundle.js"></script>
|
|
<link
|
|
href="/dist/main.css${options.autoRefreshCSS
|
|
? `?${Math.random()}${Math.random()}`
|
|
: ""}"
|
|
rel="stylesheet"
|
|
type="text/css"
|
|
/>
|
|
${options.morphing ? `<meta name="turbo-refresh-method" content="morph" />` : ""}
|
|
${options.preserveScroll
|
|
? `<meta name="turbo-refresh-scroll" content="preserve">`
|
|
: ""}`;
|
|
|
|
export type HTMLOptions = {
|
|
preserveScroll?: boolean;
|
|
morphing?: boolean;
|
|
navbar?: (ctx: BaseContext) => FlatTemplatable;
|
|
autoRefreshCSS?: boolean;
|
|
};
|
|
|
|
export default function html(
|
|
ctx: BaseContext,
|
|
title: string,
|
|
body: Templatable,
|
|
htmlOptions: HTMLOptions = {},
|
|
makeHead: (
|
|
ctx: BaseContext,
|
|
title: string,
|
|
options: HTMLOptions
|
|
) => Templatable = defaultHead
|
|
): Readable {
|
|
ctx.set("content-type", "text/html;charset=utf-8");
|
|
return tempstream/* HTML */ ` <!DOCTYPE html>
|
|
<html lang="pl" class="title--${toKebabCase(title)}">
|
|
<head>
|
|
${makeHead(ctx, title, htmlOptions)}
|
|
</head>
|
|
<body>
|
|
${(htmlOptions.navbar || default_navbar)(ctx)} ${body}
|
|
${htmlOptions.autoRefreshCSS
|
|
? /* HTML */ `<script>
|
|
function make_new_link() {
|
|
const new_link = document.createElement("link");
|
|
new_link.rel = "stylesheet";
|
|
new_link.href = \`/dist/main.css?\${Math.random()}+\${Math.random()}\`;
|
|
new_link.type = "text/css";
|
|
return new_link;
|
|
}
|
|
|
|
function getStyles() {
|
|
return Array.from(
|
|
document.querySelectorAll("head link")
|
|
).filter(
|
|
(e) => new URL(e.href).pathname == "/dist/main.css"
|
|
);
|
|
}
|
|
|
|
function cleanup_css() {
|
|
console.log("clearing styles");
|
|
getStyles()
|
|
.slice(0, -1)
|
|
.forEach((style) => {
|
|
style.parentElement.removeChild(style);
|
|
});
|
|
}
|
|
document.documentElement.addEventListener(
|
|
"turbo:morph",
|
|
cleanup_css
|
|
);
|
|
|
|
const sleep = (time) =>
|
|
new Promise((resolve) => {
|
|
setTimeout(resolve, time);
|
|
});
|
|
|
|
const APP_DOWN_ERROR_MESSAGE = "App is currently down";
|
|
|
|
function get_status() {
|
|
return fetch("/status.json").then((r) => r.json());
|
|
}
|
|
|
|
async function wait_for_run_id_to_change() {
|
|
let first_timestamp;
|
|
try {
|
|
const { started_at, status } = await get_status();
|
|
first_timestamp = started_at;
|
|
} catch (e) {
|
|
await wait_for_app_to_be_stable();
|
|
return;
|
|
}
|
|
|
|
if (!first_timestamp) {
|
|
throw new Error(APP_DOWN_ERROR_MESSAGE);
|
|
}
|
|
|
|
while (true) {
|
|
const { started_at, status } =
|
|
await get_status().catch(() => ({
|
|
started_at: first_timestamp,
|
|
}));
|
|
if (started_at !== first_timestamp) {
|
|
return;
|
|
}
|
|
await sleep(100);
|
|
}
|
|
}
|
|
|
|
async function wait_for_app_to_be_stable(n = 3) {
|
|
console.log("Waiting for app to be stable....");
|
|
let counter = 0;
|
|
while (true) {
|
|
const { status } = await get_status().catch((e) => ({
|
|
status: "down",
|
|
}));
|
|
if (status == "running") {
|
|
console.log(counter);
|
|
counter++;
|
|
} else {
|
|
counter = 0;
|
|
}
|
|
if (counter == n) {
|
|
return;
|
|
}
|
|
await sleep(100);
|
|
}
|
|
}
|
|
|
|
async function wait_for_app_restart() {
|
|
try {
|
|
await wait_for_run_id_to_change();
|
|
} catch (e) {
|
|
if (e.message !== APP_DOWN_ERROR_MESSAGE) {
|
|
throw e;
|
|
}
|
|
}
|
|
await wait_for_app_to_be_stable();
|
|
}
|
|
|
|
(async function () {
|
|
const { started_at, status } = await fetch(
|
|
"/status.json"
|
|
).then((r) => r.json());
|
|
last_known_start_timestamp = started_at;
|
|
const { port, watch } = await fetch(
|
|
"/dist/notifier.json"
|
|
).then((r) => r.json());
|
|
if (!watch) {
|
|
console.warning(
|
|
"Not running auto refresh on watch because the build process is not running in watch mode"
|
|
);
|
|
return;
|
|
}
|
|
const socket = new WebSocket(\`ws://localhost:\${port}\`);
|
|
socket.onmessage = async (message) => {
|
|
if (message.data === "css") {
|
|
const new_link = make_new_link();
|
|
new_link.onload = cleanup_css;
|
|
document
|
|
.querySelector("head")
|
|
.appendChild(new_link);
|
|
}
|
|
if (message.data === "ts") {
|
|
document.documentElement.classList.add(
|
|
"restarting"
|
|
);
|
|
await wait_for_app_restart();
|
|
document.documentElement.dispatchEvent(
|
|
new Event("ts-rebuilt")
|
|
);
|
|
document.documentElement.classList.remove(
|
|
"restarting"
|
|
);
|
|
}
|
|
};
|
|
})();
|
|
</script>`
|
|
: ""}
|
|
</body>
|
|
</html>`;
|
|
}
|