Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F1374881
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
19 KB
Referenced Files
None
Subscribers
None
View Options
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
Details
Attached
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)
Attached To
Mode
rSGEN sealgen
Attached
Detach File
Event Timeline
Log In to Comment