mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(core): catch PostCSS parse errors instead of dropping compositions (#3589)
* fix(core): catch PostCSS parse errors instead of dropping compositions
Invalid CSS in a sub-composition style block made postcss.parse throw
inside scopeCssToComposition. The throw propagated to the composition
loader's catch block, which emptied the host — silently dropping the
entire scene. Lint swallowed the same error via catch { continue },
reporting 0 warnings.
Two fixes:
- Runtime: wrap postcss.parse in try/catch and return the original
(unscoped) CSS on failure, so the composition still mounts
- Lint: emit a css_parse_error finding instead of silently continuing
Fixes #3585.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: drop unparseable CSS instead of leaking it unscoped
Return "" on PostCSS parse failure so sub-composition stylesheets
that cannot be scoped are dropped rather than injected unscoped into
the parent document. Updates test fixture to use valid+malformed CSS
that demonstrates the leak risk.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
7dc31bd2cc
commit
1a788d62e7
@@ -1054,4 +1054,13 @@ describe("wrapInlineScriptWithErrorBoundary — <script> breakout", () => {
|
||||
expect(body).not.toContain("</script");
|
||||
expect(scriptsAfterRoundTrip(body)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("drops the entire stylesheet when PostCSS cannot parse it", () => {
|
||||
const malformedCss = `body { margin: 0; overflow: hidden; }
|
||||
:root { --accent: #5ef17c; }
|
||||
.stage { position: absolute; inset: 0; }
|
||||
.broken { transform: xPercent: -10; }`;
|
||||
const result = scopeCssToComposition(malformedCss, "scene-bad");
|
||||
expect(result).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -230,7 +230,12 @@ export function scopeCssToComposition(
|
||||
const scope =
|
||||
scopeSelectorOverride ||
|
||||
`[data-composition-id="${escapeCssAttributeValue(trimmedCompositionId)}"]`;
|
||||
const root = postcss.parse(css);
|
||||
let root: postcss.Root;
|
||||
try {
|
||||
root = postcss.parse(css);
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
|
||||
root.walkRules((rule) => {
|
||||
if (isInsideGlobalAtRule(rule)) return;
|
||||
|
||||
@@ -777,4 +777,20 @@ describe("core rules", () => {
|
||||
expect(result.findings.find((f) => f.code === "timeline_id_mismatch")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("css_parse_error — malformed CSS is reported instead of silently swallowed", () => {
|
||||
it("reports a css_parse_error finding for unparseable CSS", async () => {
|
||||
const html = `<html><body>
|
||||
<style>.stage { transform: xPercent: -10; }</style>
|
||||
<div data-composition-id="main" data-width="1920" data-height="1080" data-start="0" data-duration="5"></div>
|
||||
<script src="gsap.min.js"></script>
|
||||
<script>window.__timelines = { main: gsap.timeline({ paused: true }) };</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const finding = result.findings.find((f) => f.code === "css_parse_error");
|
||||
expect(finding).toBeDefined();
|
||||
expect(finding?.severity).toBe("error");
|
||||
expect(finding?.message).toContain("Missed semicolon");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -312,7 +312,12 @@ export const coreRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> = [
|
||||
let root: postcss.Root;
|
||||
try {
|
||||
root = postcss.parse(style.content);
|
||||
} catch {
|
||||
} catch (error) {
|
||||
findings.push({
|
||||
code: "css_parse_error",
|
||||
severity: "error",
|
||||
message: `CSS parse error: ${error instanceof Error ? error.message : "unknown"}`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
root.walkRules((rule) => {
|
||||
|
||||
Reference in New Issue
Block a user