Skip to content

fix: nested GSAP sub-composition lint and render handling#405

Merged
miguel-heygen merged 4 commits intomainfrom
codex/fix-nested-gsap-subcomposition-handling
Apr 22, 2026
Merged

fix: nested GSAP sub-composition lint and render handling#405
miguel-heygen merged 4 commits intomainfrom
codex/fix-nested-gsap-subcomposition-handling

Conversation

@miguel-heygen
Copy link
Copy Markdown
Collaborator

@miguel-heygen miguel-heygen commented Apr 22, 2026

Summary

  • allow nested sub-composition files to inherit GSAP from their host without tripping missing_gsap_script
  • keep nested render seeks stable for sub-compositions without regressing producer baselines
  • stop producer render-hint detection from treating the compiler's own nested mount retry wrapper as user-authored requestAnimationFrame() usage

Root Cause

  • the core linter treated template-based nested compositions like standalone root compositions, so it incorrectly required a local GSAP loader even when the host composition already provided GSAP
  • producer detectRenderModeHints() runs before CDN scripts are inlined, so nested GSAP exports were never failing because of the GSAP payload itself
  • the nested-only false positive came from the compiler-generated mount bootstrap that waits for the inlined sub-composition root with requestAnimationFrame() before running the hoisted inline script
  • preview and export seek paths also needed to stay split so the nested timeline re-arm behavior that stabilizes scrubbing does not collapse render baselines

What Changed

  • lint: keep the nested GSAP false-positive fix and regression coverage for template sub-compositions
  • runtime: keep the render-seek behavior that preserves nested child offsets during export without changing preview scrubbing behavior
  • producer: mark compiler-owned mount bootstrap blocks and strip only those blocks before scanning inline scripts for raw requestAnimationFrame()
  • producer tests now cover both cases: compiler-generated wrappers are ignored, but real user-authored nested requestAnimationFrame() still opts into screenshot mode

Validation

  • bun test packages/core/src/lint/rules/gsap.test.ts
  • bun test packages/producer/src/services/htmlCompiler.test.ts
  • bunx oxfmt packages/producer/src/services/htmlCompiler.ts packages/producer/src/services/htmlCompiler.test.ts
  • bunx oxlint packages/producer/src/services/htmlCompiler.ts packages/producer/src/services/htmlCompiler.test.ts
  • bun run --filter @hyperframes/producer test --sequential chat style-11-prod
    • style-11-prod passed locally
    • chat still shows local-only visual drift on this macOS/ARM workstation, but the render metadata now reports renderModeHints.recommendScreenshot=false, which is the concrete acceptance condition for #402
  • Docker CI-image repro is blocked locally by OrbStack x86/arm64 loader mismatch, so final regression confirmation is deferred to GitHub Actions

Closes #392
Closes #402

@miguel-heygen miguel-heygen changed the title [codex] Fix nested GSAP sub-composition lint and render hints fix: nested GSAP sub-composition lint and render hints Apr 22, 2026
@miguel-heygen miguel-heygen marked this pull request as ready for review April 22, 2026 04:51
Copy link
Copy Markdown
Collaborator

@jrusso1020 jrusso1020 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both fixes are at the right layer and the root-cause analysis is correct. Walking through each:

Fix 1 — missing_gsap_script lint rule for sub-compositions. canInheritGsapFromHost = options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template") is a good two-path shape. The explicit caller-supplied flag is the bulletproof path (programmatic callers who know they're linting a sub-composition pass isSubComposition: true); the <template> heuristic is the backstop for when the file is being linted in isolation from an IDE or CLI. Solid.

One small robustness note: the heuristic triggers on exact <template prefix after trimStart(). A composition file that starts with a DOCTYPE (<!DOCTYPE html>\n<template…), an HTML comment, or a BOM would fail the heuristic. Probably not real-world given the documented nested-composition pattern always starts with <template>, but if a user sees a false-positive after wrapping their template in a comment or adding a stray blank line, the explanation is there. Worth either a one-line comment on the heuristic noting the assumption, or switching to a lightweight scan ("first non-comment, non-doctype token is <template") later if users hit it.

Fix 2 — compiler-inlined GSAP payloads no longer trigger screenshot mode. isCompilerInlinedGsapScript matches the comment marker the compiler writes (/* inlined: https://… gsap.min.js */) and the detector skips those scripts entirely. Correct fix at the right site, test coverage on both the isolated detector and the end-to-end compileForRender path is appropriate.

Two observations worth calling out for followup:

  1. Tight coupling between the compiler's comment format and the detector's regex. If the inliner ever changes its comment format — /* inlined from: …, // inlined: …, different field ordering — the detector silently stops skipping and the false-positive comes back. This coupling is hard to test against because it lives in two files. A more durable design: mark compiler-inlined scripts with a DOM attribute (e.g. <script data-hf-compiler-inlined="https://cdn…/gsap.min.js">) and have detectRenderModeHints check for data-hf-compiler-inlined attr-presence rather than regex-parsing the comment. Same intent, less fragile seam. Not blocking for this PR.

  2. GSAP core only. The regex \bgsap(?:\.min)?\.js\b matches gsap.js / gsap.min.js. GSAP plugin bundles (scrolltrigger.min.js, flip.min.js, etc.) inlined from the CDN would still trigger the false-positive. Probably rare today — the issue reporter was using GSAP core — but worth broadening the pattern to match the filename convention the compiler uses for all GSAP-family inlines. If the comment format already includes a path like /* inlined: https://…/gsap/3.14.2/plugins/scrolltrigger.min.js */, a more permissive \bgsap\b.*\.js\b would cover the whole family.

Fix 3 — sub-composition bootstrap renamed so requestAnimationFrame( substring no longer matches the detector. This works but it's a workaround, not a structural fix. By renaming __tryRun__hfCompilerRunWhenMounted and indirecting through window.requestAnimationFrame.bind(window) stored in __hfCompilerSchedule, the detector's /requestAnimationFrame\s*\(/ check no longer fires on the literal call site. It solves the issue cleanly but future edits to the bootstrap have to preserve the invariant "do not write requestAnimationFrame( literally in this function" — or the false-positive regresses silently.

Suggestion for a followup (same spirit as #1 above): wrap the inserted bootstrap in a <script data-hf-compiler-generated="sub-composition-bootstrap"> wrapper and have detectRenderModeHints skip any <script> with a data-hf-compiler-generated attribute. Then the indirection trick becomes unnecessary, the bootstrap can call requestAnimationFrame(callback) directly, and there's one clear "compiler-generated, not user code" boundary the detector respects. For now the rename is fine; just add a one-line comment above the bootstrap noting "do not use the literal requestAnimationFrame( — it trips detectRenderModeHints."

Test coverage. Lint rule covers both trigger paths (explicit flag and template-file heuristic). Render-mode detector has an isolated unit test for the inlined-GSAP skip plus a full compileForRender integration test that exercises the end-to-end nested-composition path with a mocked CDN fetch. The integration test asserts both the render-mode decision (recommendScreenshot: false, empty reasons) and that the bootstrap's rAF call survives compilation — good belt-and-braces.

Backwards compatibility. options.isSubComposition is additive. The heuristic and the inlined-GSAP skip are strictly new behaviors that reduce false positives. No existing call site changes semantics.

Approved.

Rames Jusso

@miguel-heygen miguel-heygen changed the title fix: nested GSAP sub-composition lint and render hints fix: nested GSAP linter false positive for sub-compositions Apr 22, 2026
@miguel-heygen miguel-heygen changed the title fix: nested GSAP linter false positive for sub-compositions fix: nested GSAP sub-composition lint and seek handling Apr 22, 2026
@miguel-heygen miguel-heygen changed the title fix: nested GSAP sub-composition lint and seek handling fix: nested GSAP sub-composition lint and render handling Apr 22, 2026
@miguel-heygen miguel-heygen merged commit d740f5c into main Apr 22, 2026
25 checks passed
Copy link
Copy Markdown
Collaborator Author

Merge activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants