Skip to content

Commit 079bcae

Browse files
committed
refactor(core): type-safety hardening — double casts, magic numbers, globals
- Extract magic numbers: UHD_SQUARE_MIN/UHD_RECT_MIN in htmlParser, PROFILER_TIMEOUT_MS/FC_MATCH_TIMEOUT_MS in systemFontLocator, MAX_COPY_INDEX in files route - Replace 4 as-unknown-as-HTMLElement double casts with isHTMLElement type predicate (reusing existing structural guard in sourceMutation) - Create runtime/globals.ts with getDebugSurface typed accessor, eliminating double cast in diagnostics.ts - Extract applyMediaAttrs and lastKeyframeOpacity to reduce function complexity in htmlParser and files route - Remove dead SwallowedEvent interface from diagnostics
1 parent 2851fe3 commit 079bcae

6 files changed

Lines changed: 60 additions & 63 deletions

File tree

packages/core/src/fonts/systemFontLocator.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { homedir, platform } from "node:os";
44
import { join, resolve } from "node:path";
55

66
export const SYSTEM_FONT_SIZE_LIMIT = 5 * 1024 * 1024;
7+
const PROFILER_TIMEOUT_MS = 5000;
8+
const FC_MATCH_TIMEOUT_MS = 3000;
79

810
export type FontFileFormat = "ttf" | "otf" | "woff2" | "woff" | "ttc";
911

@@ -238,7 +240,7 @@ function getSystemProfilerIndex(): Map<string, SystemProfilerEntry[]> {
238240
const raw = execFileSync("system_profiler", ["SPFontsDataType", "-json"], {
239241
encoding: "utf8",
240242
maxBuffer: 12 * 1024 * 1024,
241-
timeout: 5000,
243+
timeout: PROFILER_TIMEOUT_MS,
242244
});
243245
const parsed = JSON.parse(raw);
244246
if (!parsed?.SPFontsDataType || !Array.isArray(parsed.SPFontsDataType)) return profilerCache;
@@ -289,7 +291,7 @@ function locateViaFcMatch(targetFamily: string): LocatedFont | null {
289291
try {
290292
const result = execFileSync("fc-match", [targetFamily, "--format=%{file}"], {
291293
encoding: "utf8",
292-
timeout: 3000,
294+
timeout: FC_MATCH_TIMEOUT_MS,
293295
}).trim();
294296
if (!result || !isRegularFile(result) || !isPathBounded(result)) return null;
295297
const fileName = result.split("/").pop() ?? "";

packages/core/src/parsers/htmlParser.ts

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -139,19 +139,16 @@ function parseResolutionFromHtml(doc: Document): CanvasResolution | null {
139139
return null;
140140
}
141141

142+
const UHD_SQUARE_MIN = 2160;
143+
const UHD_RECT_MIN = 3840;
144+
142145
function resolveResolutionFromDimensions(width: number, height: number): CanvasResolution {
143146
const longSide = Math.max(width, height);
144-
// UHD cutoff is the long side of the 4K presets (3840 for `landscape-4k` /
145-
// `portrait-4k`, 2160 for `square-4k`). A looser threshold (e.g. >= 2560)
146-
// would silently misclassify QHD/1440p (2560x1440) as 4K, which is the
147-
// wrong default for a common authoring resolution closer to 1080p than to
148-
// UHD. Authors who genuinely want the 4K preset can still set
149-
// `data-resolution="..."` explicitly.
150147
if (width === height) {
151-
return longSide >= 2160 ? "square-4k" : "square";
148+
return longSide >= UHD_SQUARE_MIN ? "square-4k" : "square";
152149
}
153150
const isLandscape = width > height;
154-
const isUhd = longSide >= 3840;
151+
const isUhd = longSide >= UHD_RECT_MIN;
155152
if (isLandscape) return isUhd ? "landscape-4k" : "landscape";
156153
return isUhd ? "portrait-4k" : "portrait";
157154
}
@@ -612,16 +609,20 @@ export function addElementToHtml(
612609

613610
let newEl: Element;
614611

612+
function applyMediaAttrs(el: Element, mediaEl: TimelineMediaElement): void {
613+
if (mediaEl.src) el.setAttribute("src", mediaEl.src);
614+
if (mediaEl.volume !== undefined && mediaEl.volume !== 1) {
615+
el.setAttribute("data-volume", String(mediaEl.volume));
616+
}
617+
}
618+
615619
switch (element.type) {
616620
case "video": {
617621
const mediaEl = element as TimelineMediaElement;
618622
newEl = doc.createElement("video");
619623
newEl.setAttribute("muted", "");
620624
newEl.setAttribute("playsinline", "");
621-
if (mediaEl.src) newEl.setAttribute("src", mediaEl.src);
622-
if (mediaEl.volume !== undefined && mediaEl.volume !== 1) {
623-
newEl.setAttribute("data-volume", String(mediaEl.volume));
624-
}
625+
applyMediaAttrs(newEl, mediaEl);
625626
if (mediaEl.hasAudio) {
626627
newEl.setAttribute("data-has-audio", "true");
627628
}
@@ -637,10 +638,7 @@ export function addElementToHtml(
637638
case "audio": {
638639
const mediaEl = element as TimelineMediaElement;
639640
newEl = doc.createElement("audio");
640-
if (mediaEl.src) newEl.setAttribute("src", mediaEl.src);
641-
if (mediaEl.volume !== undefined && mediaEl.volume !== 1) {
642-
newEl.setAttribute("data-volume", String(mediaEl.volume));
643-
}
641+
applyMediaAttrs(newEl, mediaEl);
644642
break;
645643
}
646644
case "text":

packages/core/src/runtime/diagnostics.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,11 @@
2727
* helper call is a real statement, so no `no-empty` warnings ship in the
2828
* inlined IIFE.
2929
*/
30-
export interface SwallowedEvent {
31-
/** Short, descriptive label naming the operation that failed. */
32-
label: string;
33-
/** The thrown value (often an Error, but JS allows anything). */
34-
error: unknown;
35-
}
36-
37-
interface HFDebugSurface {
38-
__hfDebug?: boolean;
39-
__HYPERFRAMES_DEBUG?: boolean;
40-
__hf?: {
41-
onSwallowed?: (event: SwallowedEvent) => void;
42-
};
43-
}
30+
import { getDebugSurface } from "./globals.js";
4431

4532
export function swallow(label: string, error?: unknown): void {
4633
if (typeof window === "undefined") return;
47-
const w = window as unknown as HFDebugSurface;
34+
const w = getDebugSurface();
4835

4936
const handler = w.__hf?.onSwallowed;
5037
if (handler) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export interface HFDebugSurface {
2+
__hfDebug?: boolean;
3+
__HYPERFRAMES_DEBUG?: boolean;
4+
__hf?: {
5+
onSwallowed?: (event: { label: string; error: unknown }) => void;
6+
};
7+
}
8+
9+
export function getDebugSurface(): HFDebugSurface {
10+
return globalThis as HFDebugSurface;
11+
}

packages/core/src/studio-api/helpers/sourceMutation.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export function removeElementFromHtml(source: string, target: SourceMutationTarg
122122
return wrappedFragment ? document.body.innerHTML || "" : document.toString();
123123
}
124124

125-
function isHTMLElement(el: Element): boolean {
125+
export function isHTMLElement(el: Element): el is HTMLElement {
126126
const HTMLEl = el.ownerDocument.defaultView?.HTMLElement;
127127
return HTMLEl ? el instanceof HTMLEl : "style" in el;
128128
}
@@ -283,7 +283,7 @@ export function patchElementInHtml(
283283
const { document, wrappedFragment } = parseSourceDocument(source);
284284
const el = findTargetElement(document, target);
285285
if (!el || !isHTMLElement(el)) return { html: source, matched: false };
286-
const htmlEl = el as unknown as HTMLElement;
286+
const htmlEl = el;
287287

288288
for (const op of operations) {
289289
switch (op.type) {
@@ -320,7 +320,7 @@ export function patchElementInHtml(
320320
case "text-content":
321321
if (op.value != null) {
322322
const inner = htmlEl.children.length === 1 ? htmlEl.firstElementChild : null;
323-
const textTarget = inner ? (inner as unknown as HTMLElement) : htmlEl;
323+
const textTarget = inner && isHTMLElement(inner) ? inner : htmlEl;
324324
textTarget.textContent = op.value;
325325
}
326326
break;

packages/core/src/studio-api/routes/files.ts

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
patchElementInHtml,
3030
probeElementInSource,
3131
splitElementInHtml,
32+
isHTMLElement,
3233
type PatchOperation,
3334
} from "../helpers/sourceMutation.js";
3435
import { parseHTML } from "linkedom";
@@ -265,7 +266,8 @@ function stripStudioEditsFromTarget(document: Document, selector: string): numbe
265266
try {
266267
for (const el of document.querySelectorAll(selector)) {
267268
if (!el.getAttribute("data-hf-studio-path-offset")) continue;
268-
const htmlEl = el as unknown as HTMLElement;
269+
if (!isHTMLElement(el)) continue;
270+
const htmlEl = el;
269271
const originalTranslate = el.getAttribute("data-hf-studio-original-inline-translate");
270272
htmlEl.style.removeProperty("--hf-studio-offset-x");
271273
htmlEl.style.removeProperty("--hf-studio-offset-y");
@@ -285,33 +287,29 @@ function stripStudioEditsFromTarget(document: Document, selector: string): numbe
285287
return stripped;
286288
}
287289

288-
function bakeVisibilityOnDelete(document: Document, anim: GsapAnimation): void {
289-
let finalOpacity: number | string | undefined;
290-
if (anim.method === "from") {
291-
return;
292-
}
293-
if (anim.keyframes) {
294-
const kfs = anim.keyframes.keyframes;
295-
for (let i = kfs.length - 1; i >= 0; i--) {
296-
if ("opacity" in kfs[i]!.properties) {
297-
finalOpacity = kfs[i]!.properties.opacity;
298-
break;
299-
}
300-
}
301-
} else if ("opacity" in anim.properties) {
302-
finalOpacity = anim.properties.opacity;
303-
}
304-
if (finalOpacity == null) {
305-
return;
306-
}
307-
if (typeof finalOpacity === "string" && /^[+\-*]=/.test(finalOpacity)) {
308-
return;
290+
function lastKeyframeOpacity(kfs: GsapAnimation["keyframes"]): number | string | undefined {
291+
if (!kfs) return undefined;
292+
for (let i = kfs.keyframes.length - 1; i >= 0; i--) {
293+
if ("opacity" in kfs.keyframes[i]!.properties) return kfs.keyframes[i]!.properties.opacity;
309294
}
310-
const numOpacity = Number(finalOpacity);
311-
if (!Number.isFinite(numOpacity) || numOpacity === 0) return;
295+
return undefined;
296+
}
297+
298+
function resolveFinalOpacity(anim: GsapAnimation): number | null {
299+
if (anim.method === "from") return null;
300+
const raw = anim.keyframes ? lastKeyframeOpacity(anim.keyframes) : anim.properties.opacity;
301+
if (raw == null) return null;
302+
if (typeof raw === "string" && /^[+\-*]=/.test(raw)) return null;
303+
const num = Number(raw);
304+
return Number.isFinite(num) && num !== 0 ? num : null;
305+
}
306+
307+
function bakeVisibilityOnDelete(document: Document, anim: GsapAnimation): void {
308+
const opacity = resolveFinalOpacity(anim);
309+
if (opacity === null) return;
312310
try {
313311
for (const el of document.querySelectorAll(anim.targetSelector)) {
314-
(el as unknown as HTMLElement).style.setProperty("opacity", String(numOpacity));
312+
if (isHTMLElement(el)) el.style.setProperty("opacity", String(opacity));
315313
}
316314
} catch {
317315
// Invalid selector — skip silently.
@@ -780,8 +778,9 @@ async function processUploadedFiles(
780778
const ext = dotIdx > 0 ? name.slice(dotIdx) : "";
781779
const base = dotIdx > 0 ? name.slice(0, dotIdx) : name;
782780
let n = 2;
783-
while (n < 10000 && existsSync(resolve(targetDir, `${base} (${n})${ext}`))) n++;
784-
if (n >= 10000) {
781+
const MAX_COPY_INDEX = 10000;
782+
while (n < MAX_COPY_INDEX && existsSync(resolve(targetDir, `${base} (${n})${ext}`))) n++;
783+
if (n >= MAX_COPY_INDEX) {
785784
skipped.push(name);
786785
continue;
787786
}

0 commit comments

Comments
 (0)