Fix frontend HMR so the page doesn't break upon saving TS files and restructure frontend architecture (#3871)

* Clean up component setup/tear-down side effects

* Clean up more component setup/tear-down side effects

* Remove nonfunctional debouncer

* Clean up even more component setup/tear-down side effects

* Reuse backend state

* Fix HMR for IO Managers and for State Providers

* Rename IO Managers -> Managers and State Providers -> Stores

* Restructure and partially flatten managers/stores

* Code review fixes

* Review fixes
This commit is contained in:
Keavon Chambers
2026-03-19 13:38:41 -07:00
committed by GitHub
parent eb562146ce
commit 124b17f609
54 changed files with 1385 additions and 931 deletions
@@ -0,0 +1,61 @@
import { browserVersion, operatingSystem } from "@graphite/utility-functions/platform";
import { stripIndents } from "@graphite/utility-functions/strip-indents";
export function crashReportUrl(panicDetails: string): string {
const url = new URL("https://github.com/GraphiteEditor/Graphite/issues/new");
const buildUrl = (includeCrashReport: boolean) => {
let body = stripIndents`
**Describe the Crash**
Explain clearly what you were doing when the crash occurred.
**Steps To Reproduce**
Describe precisely how the crash occurred, step by step, starting with a new editor window.
1. Open the Graphite editor at https://dev.graphite.art — IMPORTANT! Confirm you have tested in this development version. It may have already been fixed since the last stable release.
2.
3.
4.
5.
**Additional Details**
Provide any further information or context that you think would be helpful in fixing the issue. Screenshots or video can be linked or attached to this issue.
**Browser and OS**
${browserVersion()}, ${operatingSystem()}
**Stack Trace**
Copied from the crash dialog in the Graphite editor:
`;
const manualCopyStackTraceNotice = stripIndents`
Before submitting this bug, REPLACE THIS WITH THE LOG. Return to the editor and click "Copy Error Log" in the crash dialog and paste it in place of this text.
`;
body += "\n\n```\n";
body += includeCrashReport ? panicDetails.trimEnd() : manualCopyStackTraceNotice;
body += "\n```";
const fields = {
title: "[Crash Report] ",
body,
labels: ["Crash"].join(","),
projects: [].join(","),
milestone: "",
assignee: "",
template: "",
};
Object.entries(fields).forEach(([field, value]) => {
if (value) url.searchParams.set(field, value);
});
return String(url);
};
let urlString = buildUrl(true);
if (urlString.length >= 8192) {
// Fall back to a shorter version if it exceeds GitHub limits of 8192 total characters
urlString = buildUrl(false);
}
return urlString;
}
@@ -1,30 +0,0 @@
export type Debouncer = ReturnType<typeof debouncer>;
export type DebouncerOptions = {
debounceTime: number;
};
export function debouncer<T>(callFn: (value: T) => unknown, { debounceTime = 60 }: Partial<DebouncerOptions> = {}) {
let currentValue: T | undefined;
let recentlyUpdated: boolean = false;
const debounceEmitValue = () => {
recentlyUpdated = false;
if (currentValue === undefined) return;
debounceUpdateValue(currentValue);
};
const debounceUpdateValue = (newValue: T) => {
if (recentlyUpdated) {
currentValue = newValue;
return;
}
callFn(newValue);
recentlyUpdated = true;
currentValue = undefined;
setTimeout(debounceEmitValue, debounceTime);
};
return { debounceUpdateValue };
}
-3
View File
@@ -1,3 +0,0 @@
export function clamp(value: number, min = 0, max = 1): number {
return Math.max(min, Math.min(value, max));
}
+1 -5
View File
@@ -26,11 +26,7 @@ export function browserVersion(): string {
export type OperatingSystem = "Windows" | "Mac" | "Linux";
export function operatingSystem(): OperatingSystem {
const osTable: Record<string, OperatingSystem> = {
Windows: "Windows",
Mac: "Mac",
Linux: "Linux",
};
const osTable: Record<string, OperatingSystem> = { Windows: "Windows", Mac: "Mac", Linux: "Linux" };
const userAgentOS = Object.keys(osTable).find((key) => window.navigator.userAgent.includes(key));
return osTable[userAgentOS || "Windows"];
+6 -16
View File
@@ -1,20 +1,13 @@
import type { Editor } from "@graphite/editor";
let resizeObserver: ResizeObserver | undefined;
export function setupViewportResizeObserver(editor: Editor) {
// Clean up existing observer if any
if (resizeObserver) {
resizeObserver.disconnect();
}
export function setupViewportResizeObserver(editor: Editor): () => void {
const viewports = Array.from(window.document.querySelectorAll("[data-viewport-container]"));
if (viewports.length <= 0) return;
if (viewports.length <= 0) return () => {};
const viewport = viewports[0];
if (!(viewport instanceof HTMLElement)) return;
if (!(viewport instanceof HTMLElement)) return () => {};
resizeObserver = new ResizeObserver((entries) => {
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const devicePixelRatio = window.devicePixelRatio || 1;
@@ -52,11 +45,8 @@ export function setupViewportResizeObserver(editor: Editor) {
});
resizeObserver.observe(viewport);
}
export function cleanupViewportResizeObserver() {
if (resizeObserver) {
return () => {
resizeObserver.disconnect();
resizeObserver = undefined;
}
};
}