Skip to content

Commit 6f67729

Browse files
refactor(core): simplify packages/core — dead code, dedup, type safety (#1413)
- Delete unused mediaPreloader module, 5 dead RuntimeState fields, emitPerformanceMetric, lintScriptUrls, 5 variable type guards - Consolidate compiler utilities: unify CSS URL regex, relative URL predicate, MIME map, @import regex, bulk asset rewrite delegation - Cache extractGsapWindows per script (eliminates 2 redundant recast parses per lint run), share stripJsComments and script extraction - Deduplicate GSAP parser: share serializeValue/safeJsKey, centralize converted-id fallback (6 sites), keyframe codegen (3 sites), waypoint extraction, insert-after-anchor, script hoisting - Replace 88 bare any annotations with typed AstNode/AstPath interfaces - Derive RuntimeBridgeControlAction from HyperframeControlAction, alias RuntimePickerElementInfo, share macOS font profiler - Gate generateHyperframesStyles on includeStyles, collapse 4 GSAP property mutation cases into 2 - Extract magic numbers into named constants, replace 5 double casts with type guards and typed accessors (runtime/globals.ts), reduce function complexity in htmlParser and files route
1 parent fbc3cdf commit 6f67729

29 files changed

Lines changed: 551 additions & 1399 deletions

packages/core/src/compiler/htmlBundler.ts

Lines changed: 106 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { readFileSync, existsSync } from "fs";
22
import { join, resolve, relative, dirname, isAbsolute, sep } from "path";
3+
import { CSS_URL_RE, isNonRelativeUrl } from "./assetPaths.js";
34
import { transformSync } from "esbuild";
45
import { compileHtml, type MediaDurationProber } from "./htmlCompiler";
56
import {
@@ -72,14 +73,7 @@ function injectInterceptor(html: string, runtimeMode: "inline" | "placeholder" =
7273
}
7374

7475
function isRelativeUrl(url: string): boolean {
75-
if (!url) return false;
76-
return (
77-
!url.startsWith("http://") &&
78-
!url.startsWith("https://") &&
79-
!url.startsWith("//") &&
80-
!url.startsWith("data:") &&
81-
!isAbsolute(url)
82-
);
76+
return !isNonRelativeUrl(url) && !isAbsolute(url);
8377
}
8478

8579
function safeReadFile(filePath: string): string | null {
@@ -94,8 +88,6 @@ function safeReadFile(filePath: string): string | null {
9488
const CSS_IMPORT_RE =
9589
/@import\s+(?:url\(\s*(["']?)([^)"']+)\1\s*\)|(["'])([^"']+)\3)\s*([^;]*);\s*/g;
9690

97-
const REBASE_URL_RE = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
98-
9991
const CSS_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
10092

10193
function withCommentsStripped<T>(
@@ -123,7 +115,7 @@ function rebaseCssUrls(css: string, cssFileDir: string, projectDir: string): str
123115
const resolvedRoot = resolve(projectDir);
124116
const resolvedDir = resolve(cssFileDir);
125117
if (resolvedDir === resolvedRoot) return css;
126-
return css.replace(REBASE_URL_RE, (full, quote: string, urlValue: string) => {
118+
return css.replace(CSS_URL_RE, (full, quote: string, urlValue: string) => {
127119
if (!urlValue || !isRelativeUrl(urlValue)) return full;
128120
const { basePath, suffix } = splitUrlSuffix(urlValue.trim());
129121
if (!basePath) return full;
@@ -205,29 +197,24 @@ function appendSuffixToUrl(baseUrl: string, suffix: string): string {
205197
return baseUrl;
206198
}
207199

208-
function guessMimeType(filePath: string): string {
209-
const l = filePath.toLowerCase();
210-
if (l.endsWith(".svg")) return "image/svg+xml";
211-
if (l.endsWith(".json")) return "application/json";
212-
if (l.endsWith(".txt")) return "text/plain";
213-
if (l.endsWith(".xml")) return "application/xml";
214-
return "application/octet-stream";
215-
}
216-
217-
function shouldInlineAsDataUrl(filePath: string): boolean {
218-
const l = filePath.toLowerCase();
219-
return l.endsWith(".svg") || l.endsWith(".json") || l.endsWith(".txt") || l.endsWith(".xml");
220-
}
200+
const INLINE_MIME: Record<string, string> = {
201+
".svg": "image/svg+xml",
202+
".json": "application/json",
203+
".txt": "text/plain",
204+
".xml": "application/xml",
205+
};
221206

222207
function maybeInlineRelativeAssetUrl(urlValue: string, projectDir: string): string | null {
223208
if (!urlValue || !isRelativeUrl(urlValue)) return null;
224209
const { basePath, suffix } = splitUrlSuffix(urlValue.trim());
225210
if (!basePath) return null;
226211
const filePath = resolveWithinProject(projectDir, basePath);
227-
if (!filePath || !shouldInlineAsDataUrl(filePath)) return null;
212+
if (!filePath) return null;
213+
const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0] ?? "";
214+
const mimeType = INLINE_MIME[ext];
215+
if (!mimeType) return null;
228216
const content = safeReadFileBuffer(filePath);
229217
if (content == null) return null;
230-
const mimeType = guessMimeType(filePath);
231218
const dataUrl = `data:${mimeType};base64,${content.toString("base64")}`;
232219
return appendSuffixToUrl(dataUrl, suffix);
233220
}
@@ -479,14 +466,13 @@ function autoHealMissingCompositionIds(document: Document): void {
479466
function coalesceHeadStylesAndBodyScripts(document: Document): void {
480467
const headStyleEls = [...document.querySelectorAll("head style")];
481468
if (headStyleEls.length > 1) {
482-
const importRe = /@import\s+url\([^)]*\)\s*;|@import\s+["'][^"']+["']\s*;/gi;
483469
const imports: string[] = [];
484470
const cssParts: string[] = [];
485471
const seenImports = new Set<string>();
486472
for (const el of headStyleEls) {
487473
const raw = (el.textContent || "").trim();
488474
if (!raw) continue;
489-
const nonImportCss = raw.replace(importRe, (match) => {
475+
const nonImportCss = raw.replace(CSS_IMPORT_RE, (match) => {
490476
const cleaned = match.trim();
491477
if (!seenImports.has(cleaned)) {
492478
seenImports.add(cleaned);
@@ -607,6 +593,78 @@ export interface BundleOptions {
607593
* - Inlines sub-composition HTML fragments (data-composition-src)
608594
* - Inlines small textual assets as data URLs
609595
*/
596+
597+
function ensureExternalScriptTag(doc: Document, src: string): void {
598+
if (doc.querySelector(`script[src="${src}"]`)) return;
599+
const el = doc.createElement("script");
600+
el.setAttribute("src", src);
601+
doc.body.appendChild(el);
602+
}
603+
604+
function hoistExternalScript(
605+
src: string,
606+
projectDir: string,
607+
doc: Document,
608+
seenSrcs: Set<string>,
609+
chunks: string[],
610+
): void {
611+
if (seenSrcs.has(src)) return;
612+
seenSrcs.add(src);
613+
if (!isNonRelativeUrl(src) && !isAbsolute(src)) {
614+
const jsPath = resolveWithinProject(projectDir, src);
615+
const js = jsPath ? safeReadFile(jsPath) : null;
616+
if (js != null) {
617+
chunks.push(js);
618+
return;
619+
}
620+
}
621+
ensureExternalScriptTag(doc, src);
622+
}
623+
624+
function hoistCompositionScripts(
625+
container: { querySelectorAll: (sel: string) => NodeListOf<Element> },
626+
opts: {
627+
projectDir: string;
628+
document: Document;
629+
compId: string | null;
630+
runtimeScope: string | undefined;
631+
runtimeCompId: string | undefined;
632+
authoredRootId: string | undefined;
633+
seenCompScriptSrcs: Set<string>;
634+
compScriptChunks: string[];
635+
},
636+
): void {
637+
for (const scriptEl of [...container.querySelectorAll("script")]) {
638+
const externalSrc = (scriptEl.getAttribute("src") || "").trim();
639+
if (externalSrc) {
640+
hoistExternalScript(
641+
externalSrc,
642+
opts.projectDir,
643+
opts.document,
644+
opts.seenCompScriptSrcs,
645+
opts.compScriptChunks,
646+
);
647+
} else {
648+
opts.compScriptChunks.push(
649+
opts.compId
650+
? wrapScopedCompositionScript(
651+
scriptEl.textContent || "",
652+
opts.compId,
653+
"[HyperFrames] composition script error:",
654+
opts.runtimeScope,
655+
opts.runtimeCompId || opts.compId,
656+
opts.authoredRootId,
657+
)
658+
: wrapInlineScriptWithErrorBoundary(
659+
scriptEl.textContent || "",
660+
"[HyperFrames] composition script error:",
661+
),
662+
);
663+
}
664+
scriptEl.remove();
665+
}
666+
}
667+
610668
export async function bundleToSingleHtml(
611669
projectDir: string,
612670
options?: BundleOptions,
@@ -789,47 +847,16 @@ export async function bundleToSingleHtml(
789847
);
790848
styleEl.remove();
791849
}
792-
// Hoist scripts into the collected script chunks
793-
for (const scriptEl of [...innerRoot.querySelectorAll("script")]) {
794-
const externalSrc = (scriptEl.getAttribute("src") || "").trim();
795-
if (externalSrc) {
796-
if (!seenCompScriptSrcs.has(externalSrc)) {
797-
seenCompScriptSrcs.add(externalSrc);
798-
if (isRelativeUrl(externalSrc)) {
799-
const jsPath = resolveWithinProject(projectDir, externalSrc);
800-
const js = jsPath ? safeReadFile(jsPath) : null;
801-
if (js != null) {
802-
compScriptChunks.push(js);
803-
} else if (!document.querySelector(`script[src="${externalSrc}"]`)) {
804-
const extScript = document.createElement("script");
805-
extScript.setAttribute("src", externalSrc);
806-
document.body.appendChild(extScript);
807-
}
808-
} else if (!document.querySelector(`script[src="${externalSrc}"]`)) {
809-
const extScript = document.createElement("script");
810-
extScript.setAttribute("src", externalSrc);
811-
document.body.appendChild(extScript);
812-
}
813-
}
814-
} else {
815-
compScriptChunks.push(
816-
compId
817-
? wrapScopedCompositionScript(
818-
scriptEl.textContent || "",
819-
compId,
820-
"[HyperFrames] composition script error:",
821-
runtimeScope,
822-
runtimeCompId || compId,
823-
authoredRootId,
824-
)
825-
: wrapInlineScriptWithErrorBoundary(
826-
scriptEl.textContent || "",
827-
"[HyperFrames] composition script error:",
828-
),
829-
);
830-
}
831-
scriptEl.remove();
832-
}
850+
hoistCompositionScripts(innerRoot, {
851+
projectDir,
852+
document,
853+
compId,
854+
runtimeScope,
855+
runtimeCompId,
856+
authoredRootId: authoredRootId ?? undefined,
857+
seenCompScriptSrcs,
858+
compScriptChunks,
859+
});
833860

834861
// Copy dimension attributes from inner root to host if not already set
835862
const innerW = innerRoot.getAttribute("data-width");
@@ -845,45 +872,16 @@ export async function bundleToSingleHtml(
845872
compStyleChunks.push(compId ? scopeCssToComposition(css, compId, runtimeScope) : css);
846873
styleEl.remove();
847874
}
848-
for (const scriptEl of [...innerDoc.querySelectorAll("script")]) {
849-
const externalSrc = (scriptEl.getAttribute("src") || "").trim();
850-
if (externalSrc) {
851-
if (!seenCompScriptSrcs.has(externalSrc)) {
852-
seenCompScriptSrcs.add(externalSrc);
853-
if (isRelativeUrl(externalSrc)) {
854-
const jsPath = resolveWithinProject(projectDir, externalSrc);
855-
const js = jsPath ? safeReadFile(jsPath) : null;
856-
if (js != null) {
857-
compScriptChunks.push(js);
858-
} else if (!document.querySelector(`script[src="${externalSrc}"]`)) {
859-
const extScript = document.createElement("script");
860-
extScript.setAttribute("src", externalSrc);
861-
document.body.appendChild(extScript);
862-
}
863-
} else if (!document.querySelector(`script[src="${externalSrc}"]`)) {
864-
const extScript = document.createElement("script");
865-
extScript.setAttribute("src", externalSrc);
866-
document.body.appendChild(extScript);
867-
}
868-
}
869-
} else {
870-
compScriptChunks.push(
871-
compId
872-
? wrapScopedCompositionScript(
873-
scriptEl.textContent || "",
874-
compId,
875-
"[HyperFrames] composition script error:",
876-
runtimeScope,
877-
runtimeCompId || compId,
878-
)
879-
: wrapInlineScriptWithErrorBoundary(
880-
scriptEl.textContent || "",
881-
"[HyperFrames] composition script error:",
882-
),
883-
);
884-
}
885-
scriptEl.remove();
886-
}
875+
hoistCompositionScripts(innerDoc, {
876+
projectDir,
877+
document,
878+
compId,
879+
runtimeScope,
880+
runtimeCompId,
881+
authoredRootId: undefined,
882+
seenCompScriptSrcs,
883+
compScriptChunks,
884+
});
887885

888886
host.innerHTML = innerDoc.body.innerHTML || "";
889887
}

packages/core/src/compiler/rewriteSubCompPaths.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -67,18 +67,12 @@ export function rewriteAssetPaths<T>(
6767
getAttr: (el: T, attr: string) => string | null | undefined,
6868
setAttr: (el: T, attr: string, value: string) => void,
6969
): void {
70-
const compDir = dirname(compSrcPath);
71-
if (!compDir || compDir === ".") return;
72-
7370
for (const el of elements) {
7471
for (const attr of PATH_ATTRS) {
7572
const val = (getAttr(el, attr) || "").trim();
76-
if (isAbsoluteOrSpecial(val)) continue;
77-
if (!needsRewrite(val)) continue;
78-
const rewritten = join(compDir, val);
79-
const normalized = resolve("/", rewritten).slice(1);
80-
if (normalized !== val) {
81-
setAttr(el, attr, normalized);
73+
const rewritten = rewriteAssetPath(compSrcPath, val);
74+
if (rewritten !== val) {
75+
setAttr(el, attr, rewritten);
8276
}
8377
}
8478
}

packages/core/src/core.types.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -330,26 +330,6 @@ export interface CompositionSpec {
330330
variables: CompositionVariable[];
331331
}
332332

333-
export function isStringVariable(v: CompositionVariable): v is StringVariable {
334-
return v.type === "string";
335-
}
336-
337-
export function isNumberVariable(v: CompositionVariable): v is NumberVariable {
338-
return v.type === "number";
339-
}
340-
341-
export function isColorVariable(v: CompositionVariable): v is ColorVariable {
342-
return v.type === "color";
343-
}
344-
345-
export function isBooleanVariable(v: CompositionVariable): v is BooleanVariable {
346-
return v.type === "boolean";
347-
}
348-
349-
export function isEnumVariable(v: CompositionVariable): v is EnumVariable {
350-
return v.type === "enum";
351-
}
352-
353333
export type TimelineElement =
354334
| TimelineMediaElement
355335
| TimelineTextElement

packages/core/src/fonts/systemFontLocator.ts

Lines changed: 9 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() ?? "";
@@ -403,6 +405,11 @@ function dedupeVariants(variants: LocatedFontVariant[]): LocatedFontVariant[] {
403405
return Array.from(seen.values());
404406
}
405407

408+
export function getSystemProfilerFamilies(): string[] {
409+
const index = getSystemProfilerIndex();
410+
return Array.from(index.keys());
411+
}
412+
406413
export function clearSystemFontCache(): void {
407414
cache.clear();
408415
profilerCache = null;

0 commit comments

Comments
 (0)