mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 09:28:08 +00:00
## Summary Extracts the GSAP parser/writer suite, HTML parser, hf-ids, spring-ease, and the shared composition data types out of `@hyperframes/core/src/parsers/` into a new, independently-publishable **`@hyperframes/parsers`** package. This is the foundation of the [#1749](https://github.com/heygen-com/hyperframes/issues/1749) effort: make HyperFrames' parsing/linting/validation usable as plain libraries in a Node app, without shelling out to the CLI. Parsers is the standalone base every other extracted package builds on. **Part 1 of 3** — splits #1754 into independently-reviewable pieces. Parts 2 (lint) and 3 (studio-server) stack on this branch. ## What moves | | | |---|---| | Source moved out of core | **~9,900 LOC** (`src/parsers/` → `packages/parsers/src/`) | | Total lines removed from core (incl. tests + goldens) | ~19,600 | | Files relocated | 39 | | Tests carried over | **660 passing** (5 skipped, 3 todo) | The big movers: `gsapParser` / `gsapParserAcorn` (the recast + acorn dual parsers), `gsapWriterAcorn`, `gsapSerialize`, `gsapUnroll`, `htmlParser`, `hfIds`, `springEase`, `stableIds`, plus the `__goldens__` corpus. ## Bundle footprint of the new package | Artifact | Size | |---|---| | `dist/` (unpacked) | 1.7 MB | | npm tarball (packed) | 409 KB | | `dist/index.js` | 90 KB (**~21 KB gzipped**) | | Heaviest entries | `gsapWriterAcorn.js` 93 KB · `gsapParser.js` 91 KB | Most of the weight is the GSAP AST machinery (recast/babel/acorn). It's tree-shakeable via subpath entries (`@hyperframes/parsers/hf-ids`, `/gsap-constants`, etc.) so a consumer that only needs `hf-ids` (2 KB) doesn't pull the parsers. ## How `@hyperframes/core` changes The interesting part: **core sheds its entire AST toolchain.** | core `dependencies` | before | after | |---|---|---| | count | 9 | 6 | | removed | — | `@babel/parser`, `acorn`, `acorn-walk`, `magic-string`, `recast` | | added | — | `@hyperframes/parsers`, `linkedom` | Before this PR, importing `@hyperframes/core` at all dragged in babel + recast + acorn just to construct types. Now those live behind `@hyperframes/parsers`, and a consumer that only wants core's runtime/compiler types never resolves the parser stack. Core keeps thin `@deprecated` re-export stubs at the old subpaths (`@hyperframes/core/gsap-parser`, `/gsap-constants`, …) so nothing downstream breaks. ## Design notes - **`"bun"` export condition before `"node"`** in every package export. Bun resolves the TypeScript source directly (no pre-built `dist/`), while Node/tsx/Docker contexts fall through to `"node"` → `dist/`. This keeps the dev loop zero-build while published artifacts stay Node-consumable. - `@hyperframes/parsers` is **standalone** — zero `@hyperframes/*` dependencies — so it can be the base of the stack. ## Test plan - [x] `bun run --filter @hyperframes/parsers test` — 660 tests pass - [x] `bun run --filter @hyperframes/sdk test` — 382 tests pass - [x] `bun run build` — full monorepo build succeeds - [x] Fallow audit passes on CI
131 lines
4.4 KiB
TypeScript
131 lines
4.4 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
|
|
import { editableAnimationId } from "./motionPathSelection";
|
|
import {
|
|
commitNode,
|
|
commitAddWaypoint,
|
|
commitAddKeyframe,
|
|
commitRemoveWaypoint,
|
|
commitCreatePath,
|
|
} from "./motionPathCommit";
|
|
|
|
const anim = (over: Partial<GsapAnimation>): GsapAnimation =>
|
|
({
|
|
id: "a1",
|
|
targetSelector: "#el",
|
|
method: "to",
|
|
position: 0,
|
|
properties: {},
|
|
...over,
|
|
}) as GsapAnimation;
|
|
|
|
describe("editableAnimationId", () => {
|
|
it("picks the arc animation for an arc path", () => {
|
|
const arc = anim({ id: "arc1", arcPath: { enabled: true, autoRotate: false, segments: [] } });
|
|
expect(editableAnimationId([anim({ id: "other" }), arc], "arc")).toBe("arc1");
|
|
});
|
|
|
|
it("picks a position-keyframe animation for a linear path", () => {
|
|
const kf = anim({
|
|
id: "kf1",
|
|
propertyGroup: "position",
|
|
keyframes: {
|
|
format: "percentage",
|
|
keyframes: [{ percentage: 0, properties: { x: 0, y: 0 } }],
|
|
} as never,
|
|
});
|
|
expect(editableAnimationId([kf], "linear")).toBe("kf1");
|
|
});
|
|
|
|
it("returns null for dynamic (unresolved) tweens — read-only", () => {
|
|
const dyn = anim({
|
|
id: "dyn",
|
|
arcPath: { enabled: true, autoRotate: false, segments: [] },
|
|
hasUnresolvedKeyframes: true,
|
|
});
|
|
expect(editableAnimationId([dyn], "arc")).toBeNull();
|
|
});
|
|
|
|
it("returns null for non-literal (helper) provenance — read-only", () => {
|
|
const helper = anim({
|
|
id: "h",
|
|
arcPath: { enabled: true, autoRotate: false, segments: [] },
|
|
provenance: { kind: "helper" } as never,
|
|
});
|
|
expect(editableAnimationId([helper], "arc")).toBeNull();
|
|
});
|
|
|
|
it("returns null when nothing matches", () => {
|
|
expect(editableAnimationId([anim({ id: "x" })], "linear")).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("commitNode", () => {
|
|
it("routes a keyframe node to update-keyframe by percentage", async () => {
|
|
const commit = vi.fn().mockResolvedValue(undefined);
|
|
await commitNode({ type: "keyframe", pct: 50 }, 120, 30, "a1", commit);
|
|
expect(commit).toHaveBeenCalledWith(
|
|
{ type: "update-keyframe", animationId: "a1", percentage: 50, properties: { x: 120, y: 30 } },
|
|
expect.objectContaining({ softReload: true }),
|
|
);
|
|
});
|
|
|
|
it("routes a waypoint node to update-motion-path-point by index", async () => {
|
|
const commit = vi.fn().mockResolvedValue(undefined);
|
|
await commitNode({ type: "waypoint", index: 2 }, 80, 40, "a1", commit);
|
|
expect(commit).toHaveBeenCalledWith(
|
|
{ type: "update-motion-path-point", animationId: "a1", pointIndex: 2, x: 80, y: 40 },
|
|
expect.objectContaining({ softReload: true }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("commitAddWaypoint / commitRemoveWaypoint", () => {
|
|
it("adds a waypoint at an index with coordinates", async () => {
|
|
const commit = vi.fn().mockResolvedValue(undefined);
|
|
await commitAddWaypoint("a1", 1, 120, -40, commit);
|
|
expect(commit).toHaveBeenCalledWith(
|
|
{ type: "add-motion-path-point", animationId: "a1", index: 1, x: 120, y: -40 },
|
|
expect.objectContaining({ softReload: true }),
|
|
);
|
|
});
|
|
|
|
it("removes a waypoint by index", async () => {
|
|
const commit = vi.fn().mockResolvedValue(undefined);
|
|
await commitRemoveWaypoint("a1", 2, commit);
|
|
expect(commit).toHaveBeenCalledWith(
|
|
{ type: "remove-motion-path-point", animationId: "a1", index: 2 },
|
|
expect.objectContaining({ softReload: true }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("commitAddKeyframe", () => {
|
|
it("inserts an x/y keyframe at a tween-relative percentage", async () => {
|
|
const commit = vi.fn().mockResolvedValue(undefined);
|
|
await commitAddKeyframe("a1", 42.5, 80, -20, commit);
|
|
expect(commit).toHaveBeenCalledWith(
|
|
{ type: "add-keyframe", animationId: "a1", percentage: 42.5, properties: { x: 80, y: -20 } },
|
|
expect.objectContaining({ softReload: true }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("commitCreatePath", () => {
|
|
it("authors a new motionPath to a destination at a given time", async () => {
|
|
const commit = vi.fn().mockResolvedValue(undefined);
|
|
await commitCreatePath("#title", 2.0, 300, -120, commit);
|
|
expect(commit).toHaveBeenCalledWith(
|
|
{
|
|
type: "add-motion-path",
|
|
targetSelector: "#title",
|
|
position: 2.0,
|
|
duration: 1.5,
|
|
x: 300,
|
|
y: -120,
|
|
},
|
|
expect.objectContaining({ softReload: true }),
|
|
);
|
|
});
|
|
});
|