Page MenuHomeSealhub

No OneTemporary

diff --git a/src/build.ts b/src/build.ts
index 1440352..723427c 100644
--- a/src/build.ts
+++ b/src/build.ts
@@ -1,133 +1,133 @@
import { promises as fs } from "fs";
import * as chokidar from "chokidar";
import { make_notifier } from "./notifier.js";
import getPort from "get-port";
import { resolve } from "node:path";
import _locreq from "locreq";
import { hasShape, predicates } from "@sealcode/ts-predicates";
import { FontsBuilder } from "./builders/fonts-builder.js";
import { BackendTSBuilder } from "./builders/backend-ts-builder.js";
import { FrontendTSBuilder } from "./builders/frontend-ts-builder.js";
import { CSSBuilder } from "./builders/css-builder.js";
import { ColorsBuilder } from "./builders/colors-builder.js";
const target_locreq = _locreq(process.cwd());
async function get_notifier_port() {
return getPort({ port: 4000 });
}
async function write_notifier_config(watch: boolean, port?: number) {
const config = { watch } as Record<string, boolean | number>;
if (port) {
config.port = port;
}
await fs.writeFile(
target_locreq.resolve("public/dist/notifier.json"),
JSON.stringify(config)
);
}
const package_json_shape = {
sealgen: predicates.maybe(
predicates.shape({
styleDirs: predicates.maybe(predicates.array(predicates.string)),
controllerDirs: predicates.maybe(
predicates.array(predicates.string)
),
copyToPublic: predicates.maybe(
predicates.array(
predicates.shape({
from: predicates.string,
to: predicates.string,
})
)
),
})
),
};
async function build(watch: boolean): Promise<void> {
const project_dir = target_locreq.resolve("");
const package_json = JSON.parse(
await fs.readFile(resolve(project_dir, "package.json"), "utf-8")
) as Record<string, unknown>;
if (!hasShape(package_json_shape, package_json)) {
throw new Error("Misshaped package.json props");
}
const style_dirs = package_json.sealgen?.styleDirs || [];
const controller_dirs = package_json.sealgen?.controllerDirs || [];
const copy_to_public = package_json.sealgen?.copyToPublic || [];
await Promise.all(
copy_to_public.map(async ({ from, to }) => {
try {
await fs.stat(resolve(project_dir, "public", to));
} catch (e) {
await fs.mkdir(resolve(project_dir, "public", to, "../"), {
recursive: true,
});
await fs.symlink(
resolve(project_dir, from),
resolve(project_dir, "public", to)
);
}
})
);
const fonts_builder = new FontsBuilder(project_dir, style_dirs);
const backend_ts_builder = new BackendTSBuilder(project_dir, style_dirs);
const frontend_ts_builder = new FrontendTSBuilder(
project_dir,
style_dirs,
controller_dirs
);
const css_builder = new CSSBuilder(project_dir, style_dirs);
const colors_builder = new ColorsBuilder(project_dir, style_dirs);
const builders = [
fonts_builder,
backend_ts_builder,
colors_builder,
css_builder,
frontend_ts_builder,
];
await Promise.all(builders.map((b) => b.build()));
if (watch) {
const watcher = chokidar.watch("src", { ignoreInitial: true });
const port = await get_notifier_port();
- const notifier = make_notifier(port);
+ const notifier = make_notifier(port, 8080); // TODO: parse the app config to get the actual port
await write_notifier_config(watch, port);
watcher.on("all", (_, file_path) => {
console.log("Detected a change!", file_path);
builders.forEach((builder) => {
const owns_a_file = builder.ownsFile(file_path);
if (owns_a_file) {
void builder.build(notifier);
}
});
});
} else {
await write_notifier_config(watch);
builders.forEach((builder) => {
void builder.dispose();
});
}
}
export async function buildProject({
watch,
}: {
watch: boolean;
}): Promise<void> {
try {
await build(watch);
} catch (e) {
console.log("CAUGHT!");
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
console.error(e.message);
if (!watch) {
process.exit(1);
}
}
}
diff --git a/src/builders/colors-builder.ts b/src/builders/colors-builder.ts
index e54afb0..95f7da7 100644
--- a/src/builders/colors-builder.ts
+++ b/src/builders/colors-builder.ts
@@ -1,270 +1,439 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import { build } from "esbuild";
import tempfile from "tempfile";
import { promises as fs } from "fs";
import { resolve } from "path";
import { colord } from "colord";
import { formatWithPrettier } from "../utils/prettier.js";
import { Builder } from "./builder.js";
import { exec } from "../utils/exec.js";
+import { APP_BACK_ALIVE_SIGNAL } from "../notifier.js";
+
export const COLORS_TS_PATH = "src/back/colors.ts"; // keeping it in src/back instead of just src to make typescript hints work there
export const COLORS_CSS_PATH = "src/colors.css";
-export const COLORS_HTML_PATH = "src/colors.html";
+export const COLORS_HTML_PATH = "public/dist/colors.html";
export class ColorsBuilder extends Builder {
getName(): string {
return "colors";
}
ownsFile(file_path: string) {
return file_path == COLORS_TS_PATH;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
async dispose(): Promise<void> {}
async _build() {
const outfile = tempfile({ extension: "mjs" });
await build({
entryPoints: [COLORS_TS_PATH],
bundle: true,
format: "esm",
outfile,
});
await fs.appendFile(
outfile,
/* HTML */ `<script>
console.log(
JSON.stringify(
Object.fromEntries(
Object.entries(colors).map(
([group_name, group]) => [
group_name,
Object.fromEntries(
Object.entries(group).map(
([color_name, color]) => {
const shades = [];
for (let i = 0; i <= 9; i++) {
shades.push(
shade(color, i)
);
}
return [
color_name,
{ color, shades },
];
}
)
),
]
)
)
)
);
</script>`.replaceAll(/<\/?script>/g, "") // <script> helps Prettier format this snippet
);
const output = await exec("node", [outfile]);
const color_groups = JSON.parse(output.stdout) as Record<
string,
Record<string, { color: string; shades: string[] }>
>;
let css_colors = "";
let html_colors = "";
function html_color_box({
color,
shade_no,
name,
group_name,
is_main = false,
}: {
color: string;
shade_no: string;
name: string;
group_name: string;
is_main?: boolean;
}) {
+ const c = colord(color);
return /* HTML */ `<div
- class="color-box ${colord(color).isDark()
- ? "is-dark"
- : ""} ${is_main ? "is-main" : ""}"
+ class="color-box ${c.isDark() ? "is-dark" : ""} ${is_main
+ ? "is-main"
+ : ""} ${c.toHsl().l > 90 ? "is-almost-white" : ""}"
style="background-color: ${color}; order: ${parseInt(shade_no)}"
data-color-name="${name}"
data-color-group-name="${group_name}"
data-color-shade-number="${shade_no}"
- ></div>`;
+ >
+ <span class="hex">${c.toHex()}</span>
+ </div>`;
}
for (const group_name of Object.keys(color_groups).sort((key) =>
key == "brand" ? -1 : 11
)) {
html_colors += `<h2>${group_name}</h2><div class="group">`;
const colors = color_groups[group_name];
for (const [color_name, color_info] of Object.entries(colors)) {
html_colors += `<h3>${color_name}</h3><div class="shades">`;
const main_color = color_info.color;
css_colors += `--color-${group_name}-${color_name}: ${main_color};`;
const main_l = colord(main_color).toHsl().l;
html_colors += html_color_box({
color: main_color,
name: color_name,
shade_no: Math.round((main_l - (main_l % 10)) / 10)
.toString()
.padStart(2, "0"),
is_main: true,
group_name,
});
color_info.shades.forEach((shaded_color, index) => {
const shade_no = index.toString().padStart(2, "0");
css_colors += `--color-${group_name}-${color_name}-${shade_no}: ${shaded_color};`;
if (
Math.abs(
colord(shaded_color).toHsl().l -
colord(main_color).toHsl().l
) < 10
) {
// this color is already in HTML as the main color, skipping adding it to html to avoid duplication
return;
}
html_colors += html_color_box({
color: shaded_color,
shade_no,
name: color_name,
group_name,
});
});
html_colors += `</div>`;
}
html_colors += "</div>";
}
const css = await formatWithPrettier(
`/* DO NOT EDIT! This file is automatically generated by sealgen */
:root {
${css_colors}
}`,
"css"
);
+ function makeDemoPair(
+ fg: string,
+ bg: string,
+ text: string,
+ classname = ""
+ ) {
+ return /* HTML */ ` <div
+ class="pair ${classname}"
+ data-fg="${fg}"
+ data-bg="${bg}"
+ style="background-color: ${bg}; color: ${fg}"
+ >
+ <span>${text}</span>
+ </div>`;
+ }
+
const html = await formatWithPrettier(
/* HTML */ `<!DOCTYPE html>
<html>
<head>
+ <title>Color palettes</title>
+ <style>
+ ${css}
+ </style>
<style>
* {
font-family: sans-serif;
}
.shades {
display: flex;
}
.color-box {
width: 100px;
height: 100px;
transition: transform 50ms;
transition-timing-function: ease-in-out;
transform: scale(1);
cursor: pointer;
&:hover {
transform: scale(1.1);
z-index: 2;
+
+ .hex {
+ visibility: visible;
+ }
}
&.is-main {
height: 116px;
width: 116px;
margin-top: -8px;
box-shadow: 0px 0px 6px 2px white;
z-index: 1;
+
+ &:before {
+ font-weight: bold;
+ }
+ }
+
+ &.is-almost-white.is-main {
+ box-shadow: 0px 0px 6px 1px #00000078;
+ }
+
+ .hex {
+ padding: 8px;
+ font-family: Menlo, Consolas, Monaco,
+ Liberation Mono, Lucida Console,
+ monospace;
+ opacity: 0.5;
+ visibility: hidden;
+ }
+
+ &.is-dark .hex {
+ color: white;
}
}
- .color-box:after {
+ .color-box:before {
content: attr(data-color-shade-number);
box-sizing: border-box;
display: block;
color: black;
font-size: 14px;
width: 100%;
- height: 100%;
padding: 10px;
opacity: 0.3;
}
- .color-box.is-dark:after {
+ .color-box.is-dark:before {
color: white;
}
@keyframes float-up {
from {
transform: translateY(0);
opacity: 1;
}
to {
transform: translateY(-20px);
opacity: 0;
}
}
.toast {
background-color: black;
position: absolute;
color: white;
border-radius: 8px;
padding: 8px;
animation: float-up 800ms;
animation-timing-function: ease-out;
z-index: 3;
}
+
+ .container {
+ width: 100vw;
+ display: flex;
+ flex-flow: row wrap;
+ gap: 16px;
+
+ @media (max-width: 1400px) {
+ .demo {
+ order: 1;
+ }
+ .pallete {
+ order: 2;
+ }
+ }
+
+ .canvas {
+ background-color: var(--color-brand-canvas);
+ display: flex;
+ flex-flow: column;
+ border: 1px dashed gray;
+ color: gray;
+
+ gap: 8px;
+ padding: 8px;
+
+ & > * {
+ padding: 8px;
+ color: black;
+ }
+
+ .pair.single span {
+ opacity: 0.5;
+ }
+ }
+ }
</style>
</head>
<body>
- ${html_colors}
+ <div class="container">
+ <div class="pallete">${html_colors}</div>
+ <div class="demo">
+ <div class="canvas">
+ <span>canvas</span>
+ ${makeDemoPair(
+ "var(--color-brand-text-fg)",
+ "var(--color-brand-text-bg)",
+ "text-fg on text-bg"
+ )}
+ ${makeDemoPair(
+ "var(--color-brand-text-accent)",
+ "var(--color-brand-text-bg)",
+ "text-accent on text-bg"
+ )}
+ ${makeDemoPair(
+ "",
+ "var(--color-brand-accent)",
+ "accent",
+ "single"
+ )}
+ ${makeDemoPair(
+ "",
+ "var(--color-brand-accent2)",
+ "accent2",
+ "single"
+ )}
+ ${makeDemoPair(
+ "var(--color-brand-text-on-accent)",
+ "var(--color-brand-accent)",
+ "text-on-accent on accent"
+ )}
+ ${makeDemoPair(
+ "var(--color-brand-text-on-accent2)",
+ "var(--color-brand-accent2)",
+ "text-on-accent2 on accent2"
+ )}
+ </div>
+ </div>
+ </div>
</body>
<script>
- document.addEventListener("click", (event) => {
- navigator.clipboard.writeText(
- \`var(--color-\${event.target.getAttribute(
- "data-color-group-name"
- )}-\${event.target.getAttribute(
- "data-color-name"
- )}-\${event.target.getAttribute(
- "data-color-shade-number"
- )})\`
- );
+ function pop_toast() {
const toast = document.createElement("div");
document.body.appendChild(toast);
toast.classList.add("toast");
toast.textContent = "Copied!";
toast.style.setProperty(
"top",
event.clientY +
document.scrollingElement.scrollTop -
40 +
"px"
);
toast.style.setProperty(
"left",
event.clientX + "px"
);
toast.addEventListener("animationend", () => {
toast.remove();
});
+ }
+
+ document.addEventListener("click", (event) => {
+ const box = event.target.closest(".color-box");
+ if (box) {
+ const css_var = box.classList.contains(
+ "is-main"
+ )
+ ? \`var(--color-\${box.getAttribute(
+ "data-color-group-name"
+ )}-\${box.getAttribute(
+ "data-color-name"
+ )})\`
+ : \`var(--color-\${box.getAttribute(
+ "data-color-group-name"
+ )}-\${box.getAttribute(
+ "data-color-name"
+ )}-\${box.getAttribute(
+ "data-color-shade-number"
+ )})\`;
+ navigator.clipboard.writeText(css_var);
+ pop_toast();
+ }
+ const pair = event.target.closest(".pair");
+ if (pair) {
+ let to_copy = "";
+ if (pair.classList.contains("single")) {
+ to_copy = pair.getAttribute("data-bg");
+ } else {
+ to_copy = \`background-color: \${pair.getAttribute(
+ "data-bg"
+ )};\\ncolor: \${pair.getAttribute(
+ "data-fg"
+ )};\`;
+ }
+ navigator.clipboard.writeText(to_copy);
+ pop_toast();
+ }
});
</script>
+ <script>
+ (async function () {
+ const response = await (
+ await fetch("/dist/notifier.json")
+ ).json();
+ const ws = new WebSocket(
+ \`http://localhost:\${response.port}\`
+ );
+ ws.addEventListener("message", (event) => {
+ console.log(event);
+ if (event.data == "${APP_BACK_ALIVE_SIGNAL}") {
+ document.location = document.location; // refresh
+ }
+ });
+ })();
+ </script>
</html>`,
"html"
);
await Promise.all([
fs.writeFile(resolve(this.project_dir, COLORS_CSS_PATH), css),
fs.writeFile(resolve(this.project_dir, COLORS_HTML_PATH), html),
]);
}
}
diff --git a/src/notifier.ts b/src/notifier.ts
index 99fd236..2dcef5c 100644
--- a/src/notifier.ts
+++ b/src/notifier.ts
@@ -1,24 +1,116 @@
import { WebSocketServer, WebSocket } from "ws";
-export function make_notifier(port: number) {
+const APP_DOWN_ERROR_MESSAGE = "App is currently down";
+export const APP_BACK_ALIVE_SIGNAL = "app-back-alive";
+
+const sleep = (time: number) =>
+ new Promise((resolve) => {
+ setTimeout(resolve, time);
+ });
+
+async function get_status(
+ app_port: number
+): Promise<{ started_at: number; status: string }> {
+ const r = await fetch(`http://127.0.0.1:${app_port}/status.json`);
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
+ return (await r.json()) as { started_at: number; status: string };
+}
+
+async function wait_for_run_id_to_change(app_port: number) {
+ let first_timestamp: number;
+ try {
+ const { started_at } = await get_status(app_port);
+ first_timestamp = started_at;
+ } catch (e) {
+ await wait_for_app_to_be_stable(app_port);
+ return;
+ }
+
+ if (!first_timestamp) {
+ throw new Error(APP_DOWN_ERROR_MESSAGE);
+ }
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ // eslint-disable-next-line no-await-in-loop
+ const { started_at } = await get_status(app_port).catch(() => ({
+ started_at: first_timestamp,
+ }));
+ if (started_at !== first_timestamp) {
+ return;
+ }
+ // eslint-disable-next-line no-await-in-loop
+ await sleep(100);
+ }
+}
+
+async function wait_for_app_to_be_stable(app_port: number, n = 3) {
+ // eslint-disable-next-line no-console
+ console.log("Waiting for app to be stable....");
+ let counter = 0;
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ console.debug("notifier.ts:53");
+ // eslint-disable-next-line no-await-in-loop
+ const { status } = await get_status(app_port).catch(() => ({
+ status: "down",
+ }));
+ if (status == "running") {
+ // eslint-disable-next-line no-console
+ console.log(counter);
+ counter++;
+ } else {
+ counter = 0;
+ }
+ if (counter == n) {
+ return;
+ }
+ // eslint-disable-next-line no-await-in-loop
+ await sleep(100);
+ }
+}
+
+let restart_promise: Promise<void> | null = null;
+
+async function wait_for_app_restart(app_port: number) {
+ if (restart_promise) {
+ return restart_promise;
+ }
+ try {
+ restart_promise = wait_for_run_id_to_change(app_port);
+ await restart_promise;
+ } catch (e) {
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+ if (e.message !== APP_DOWN_ERROR_MESSAGE) {
+ throw e;
+ }
+ }
+ restart_promise = null;
+ await wait_for_app_to_be_stable(app_port);
+}
+
+export function make_notifier(port: number, app_port: number) {
const server = new WebSocketServer({
port,
});
let sockets: WebSocket[] = [];
server.on("connection", function (socket) {
sockets.push(socket);
// When a socket closes, or disconnects, remove it from the array.
socket.on("close", function () {
sockets = sockets.filter((s) => s !== socket);
});
});
console.log(
"build notifier listening on websocket at port " + port.toString()
);
return function notify(message: string) {
sockets.forEach((s) => s.send(message));
+ void wait_for_app_restart(app_port).then(() => {
+ sockets.forEach((s) => s.send(APP_BACK_ALIVE_SIGNAL));
+ });
};
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Feb 25, 17:23 (1 d, 13 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
610600
Default Alt Text
(19 KB)

Event Timeline