Skip to content

feat(studio): load GSAP from the CLI's bundled copy by default - #6256

Merged
jrusso1020 merged 4 commits into
mainfrom
jrusso/local-gsap-default
Oct 25, 2026
Merged

jrusso1020 merged 4 commits into
mainfrom
jrusso/local-gsap-default

Conversation

@jrusso1020

@jrusso1020 jrusso1020 commented Oct 24, 2026 •

Copy link
Copy Markdown
Collaborator

What

HyperFrames Studio now loads the GSAP scripts it adds to previews (GSAP core, CustomEase, MotionPathPlugin) from files the CLI ships, served by the preview server itself, instead of from jsDelivr. Opening a project in npx hyperframes preview no longer makes a jsDelivr request for these scripts.

This also picks up both follow-ups from the review of #6247: renders now honor HYPERFRAMES_GSAP_BASE_URL, and the setting accepts plain http:// only for loopback hosts.

Why

#6205: Studio requested MotionPathPlugin.min.js from jsDelivr every time the editor opened, which fails offline and on restricted networks and sends a third-party request from a local editor. #6247 added an opt-in mirror, but the default still went to jsDelivr.

Related work

Closes #6205

Builds on #6247, which added HYPERFRAMES_GSAP_BASE_URL and routed every Studio load site (preview document, sub-compositions, the soft-reload loader and the editor preload) through one setting. This PR changes the default behind that setting.

How

Lookup order (packages/studio-server/src/routes/preview.ts):

  1. HYPERFRAMES_GSAP_BASE_URL, when set. Unchanged, still no public fallback.
  2. Otherwise the host's bundled copy, when the adapter passes the new optional gsapLocalRoot (a same-origin path, passed like runtimeUrl).
  3. Otherwise jsDelivr, so other hosts embedding studio-server keep today's behavior.

Shipping the files. packages/cli/scripts/build-copy.mjs copies gsap.min.js, CustomEase.min.js and MotionPathPlugin.min.js from the workspace's installed gsap into dist/vendor/npm/[email protected]/dist/. The build fails if the installed gsap version differs from GSAP_CDN_VERSION, so the two cannot drift silently. No new runtime dependency.

Serving them. studioServer.ts serves /api/vendor/npm/:pkg/dist/:file, modeled on /api/runtime.js. It answers only the three files at the bundled version and returns 404 for anything else. When running from source it falls back to core's installed gsap, and if no files are found it passes no gsapLocalRoot, so preview keeps using jsDelivr instead of breaking.

Root-relative values (packages/core/src/gsapCdn.ts). normalizeGsapBaseUrl still rejects root-relative values, which is right for the env var. A new normalizeGsapLocalRoot validates the adapter's path (no //host, backslash, query or fragment). gsapCdnDist treats a root-relative base as the local copy, which holds only 3.15.0: any other version, such as an authored [email protected], resolves to jsDelivr. The server-written meta tag carries the root, so the browser loaders in gsapSoftReload.ts and the editor preload follow the same rule without changes.

Renders (review note 1). htmlCompiler.ts used to rewrite a missing local gsap/dist/* script to jsDelivr. It now uses HYPERFRAMES_GSAP_BASE_URL when set. The compiler already downloads and inlines remote scripts, so the render fetches GSAP from the mirror at compile time. The same rewrite runs on every sub-composition mounted with data-composition-src, at any depth, before it is compiled and hoisted. A sub-composition's script path is checked the way the inliner resolves it (a sibling file first, then the project root), so a copy that exists next to the scene is kept. An invalid value fails the render with the same configuration error Studio gives. The value is read only when a rewrite is needed, so a bad value cannot fail renders that never touch it. Renders do not use the CLI's bundled copy: they compile outside the preview server, so there is no same-origin root to point at.

Plain http (review note 2). normalizeGsapBaseUrl now accepts http:// only for localhost, 127.0.0.0/8 and ::1, and requires HTTPS for any other host. This covers both Studio and renders.

Not changed, on purpose. URLs authored in a composition stay as written, including the jsDelivr GSAP tag in the init templates. When Studio writes the first animation into a file that has no GSAP, it still writes the jsDelivr URL, because that file has to render outside Studio. The docs now say both.

Before

Studio on main with HYPERFRAMES_GSAP_BASE_URL unset, opened in headless Chromium against a project that vendors GSAP locally. Every request the page and its preview iframe made was recorded; opening the editor requests MotionPathPlugin.min.js from jsDelivr.

Request log on main: MotionPathPlugin from jsDelivr

Studio on main

After

Same project, same browser, this branch. The plugin comes from the preview server at /api/vendor/npm, and no request leaves the preview server's origin.

Request log on this branch: MotionPathPlugin from the preview server, no other origins

Studio on this branch

Test plan

  • Unit tests added/updated
  • Manual testing performed
  • Documentation updated (if applicable)
  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

New tests (19 of them fail when the source changes are reverted to main):

  • Default preview uses the local root, no jsDelivr. preview.test.ts covers the root document, sub-composition and MotionPathPlugin injection. studioServer.previewAssets.test.ts exercises the real CLI server, fetches all three files from the route, and checks the 404s.
  • Version mismatch falls back to jsDelivr. Covered in gsapCdn.test.ts, preview.test.ts and gsapSoftReload.test.ts (editor preload with a 3.14.2 runtime).
  • Env var precedence. Covered in preview.test.ts and studioServer.previewAssets.test.ts.
  • http rejected for remote hosts, allowed for loopback. Covered in gsapCdn.test.ts and preview.test.ts.
  • Render path. htmlCompiler.test.ts checks that the mirror is fetched and inlined, that jsDelivr is kept when no mirror is set, and that invalid values fail. It also covers a missing GSAP script in a sub-composition and in a sub-composition of a sub-composition (both fail without the fix), and a sub-composition whose GSAP exists next to it (kept, nothing fetched).
  • Cache. The preview ETag changes when the bundled copy replaces jsDelivr.

Run locally:

  • bun run build, bun run lint, bun run format:check, bun run --filter '*' typecheck, bun run typecheck:scripts, bun run verify:packed-manifests, bun run check:cli-lazy-deps: all pass.
  • Tests: core 5191, studio-server 1408 and studio 8023 pass. CLI: 5668 pass. 21 browser and process tests failed because Chrome timed out starting on a loaded machine. I reran five of those files on their own with chrome-headless-shell, and all 19 of their tests passed. Producer unit: 1022 pass. One integration suite, lateVisibleImageGrading, timed out because the local test sandbox blocks loopback listening; it does not touch this code.
  • Manual: the captures above, plus a check that the served files are the GSAP 3.15.0 builds with their license headers intact.

🤖 Generated with Claude Code

@mintlify

mintlify Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
hyperframes 🟢 Ready View Preview Oct 25, 2026, 2:30 AM

💡 Tip: Enable Automations to automatically generate PRs for you.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 271 (base branch 271), smooth 144 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (2)

  • seqplainkeys-none-center-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true
  • seqplainkeys-none-px-r0-root-z100: tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo false, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true / tracking 0.06, pressJump 0, drop 0, reload 0, render 0.03, renderKey -, undo true, teleport true

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The bundled-file route's exact filename/package allowlist and loopback HTTP restriction look sound, and the relevant hosted tests ran green. One render-path gap remains: the new mirror setting is applied only to the entry HTML, not to missing GSAP scripts in nested compositions. I left the reproducer and requested regression coverage inline. This head is also currently conflicted with main; that is an independent merge gate. — Jerrai

/** Renders follow the same `HYPERFRAMES_GSAP_BASE_URL` mirror as Studio preview. */
function renderGsapDist(): string {
try {
return gsapCdnDist(undefined, normalizeGsapBaseUrl(process.env.HYPERFRAMES_GSAP_BASE_URL));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The render mirror only applies when rewriteUnresolvableGsapToCdn is called on the entry document (compileForRender, line 2030). A supported data-composition-src scene is read by parseSubCompositions and passed straight to compileHtmlFile (lines 793–799), then its local <script src="node_modules/gsap/dist/gsap.min.js"> is hoisted into the assembled render. If that local path is missing, inlineExternalScripts only fetches HTTP URLs, so setting HYPERFRAMES_GSAP_BASE_URL never supplies GSAP for the nested scene. Please apply the same missing-local rewrite to nested composition HTML before compilation/hoisting, and cover a nested render with the mirror configured. — Jerrai

jrusso1020 and others added 4 commits October 25, 2026 02:09
Studio preview now loads the GSAP, CustomEase and MotionPathPlugin scripts
it adds from files the CLI ships and serves same-origin at /api/vendor/npm,
instead of jsDelivr. HYPERFRAMES_GSAP_BASE_URL still wins when set, and a
composition on another GSAP version gets its plugin from jsDelivr.

Renders now honor HYPERFRAMES_GSAP_BASE_URL when they replace a missing
local GSAP script, and the setting accepts plain http only for loopback
hosts.

Closes #6205

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
…source

The Windows CI lane installs with the hoisted linker, so gsap is not under
packages/core/node_modules there.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A sub-composition's missing local gsap/dist script was hoisted into the
render unchanged, so HYPERFRAMES_GSAP_BASE_URL (and the jsDelivr fallback)
only reached the entry document. Run the same rewrite on every
sub-composition before it is compiled, resolving its path the way the
inliner will hoist it.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>

@jerrai-bot-heygen jerrai-bot-heygen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The successor closes my nested-render gap: each mounted sub-composition is rewritten before compilation, including recursive scenes and the direct-disk fallback, while the sibling-first asset lookup preserves a local copy. The new one- and two-depth render tests exercise the mirror path. The rebase left the previously reviewed vendor route and loopback restriction intact. Exact-head producer unit tests, support tests, build, lint and typecheck ran green; the initial Windows browserManager.test.ts probe-timing failure passed on rerun and does not touch this diff. No CODEOWNER match. This is approval of the code, not merge or release authorization. — Jerrai

@jrusso1020
jrusso1020 added this pull request to the merge queue Oct 25, 2026
Merged via the queue into main with commit 7f19f23 Oct 25, 2026
133 of 142 checks passed
@jrusso1020
jrusso1020 deleted the jrusso/local-gsap-default branch October 25, 2026 03:28

This branch was successfully deployed

1 active deployment
staging - docs — 0bd8136a Deployed Oct 25, 2026 by mintlify[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

studio: GSAP plugins are loaded from cdn.jsdelivr.net at runtime (offline / no third-party requests)

2 participants