mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +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
132 lines
4.1 KiB
TypeScript
132 lines
4.1 KiB
TypeScript
// fallow-ignore-file dead-code
|
|
import { expect } from "vitest";
|
|
import {
|
|
parseGsapScript,
|
|
serializeGsapAnimations,
|
|
convertToKeyframesInScript,
|
|
} from "./gsapParser.js";
|
|
import type { GsapAnimation, GsapPercentageKeyframe } from "./gsapParser.js";
|
|
|
|
/**
|
|
* Parse a script and serialize the result, returning both the parsed output
|
|
* and the serialized string for assertion. Shared across gsapParser.test.ts
|
|
* and gsapParser.stress.test.ts.
|
|
*/
|
|
export function parseAndSerialize(script: string) {
|
|
const parsed = parseGsapScript(script);
|
|
const serialized = serializeGsapAnimations(parsed.animations, parsed.timelineVar, {
|
|
preamble: parsed.preamble,
|
|
postamble: parsed.postamble,
|
|
});
|
|
return { parsed, serialized };
|
|
}
|
|
|
|
/**
|
|
* Parse a script expecting exactly one animation, and return it directly.
|
|
*/
|
|
export function parseSingleAnimation(script: string): GsapAnimation {
|
|
const result = parseGsapScript(script);
|
|
expect(result.animations).toHaveLength(1);
|
|
return result.animations[0]!;
|
|
}
|
|
|
|
/**
|
|
* Assert that a parsed animation's stagger extra exists and contains
|
|
* the expected substrings (as a __raw: prefixed string).
|
|
*/
|
|
export function expectStaggerRaw(anim: GsapAnimation, ...expectedSubstrings: string[]): void {
|
|
expect(anim.extras).toBeDefined();
|
|
expect(anim.extras!.stagger).toBeDefined();
|
|
const stagger = String(anim.extras!.stagger);
|
|
expect(stagger.startsWith("__raw:")).toBe(true);
|
|
for (const sub of expectedSubstrings) {
|
|
expect(stagger).toContain(sub);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert a single keyframe's percentage, properties, and optional ease.
|
|
*/
|
|
export function expectKeyframe(
|
|
kf: GsapPercentageKeyframe,
|
|
percentage: number,
|
|
properties: Record<string, number | string>,
|
|
ease?: string,
|
|
): void {
|
|
expect(kf.percentage).toBe(percentage);
|
|
for (const [key, value] of Object.entries(properties)) {
|
|
expect(kf.properties[key]).toBe(value);
|
|
}
|
|
if (ease !== undefined) {
|
|
expect(kf.ease).toBe(ease);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assert that an animation has a defined keyframes block with the expected format
|
|
* and count, and return the keyframes array for further assertions.
|
|
*/
|
|
export function expectKeyframesFormat(
|
|
anim: GsapAnimation,
|
|
format: string,
|
|
count: number,
|
|
): GsapPercentageKeyframe[] {
|
|
expect(anim.keyframes).toBeDefined();
|
|
expect(anim.keyframes!.format).toBe(format);
|
|
expect(anim.keyframes!.keyframes).toHaveLength(count);
|
|
return anim.keyframes!.keyframes;
|
|
}
|
|
|
|
/**
|
|
* Parse a script expecting one animation, assert that `rawProp` is a __raw: string
|
|
* and `resolvableProp` has the expected value.
|
|
*/
|
|
export function expectRawWithResolvable(
|
|
script: string,
|
|
rawProp: string,
|
|
resolvableProp: string,
|
|
resolvableValue: number | string,
|
|
): void {
|
|
const anim = parseSingleAnimation(script);
|
|
const val = anim.properties[rawProp];
|
|
expect(typeof val === "string" && val.startsWith("__raw:")).toBe(true);
|
|
expect(anim.properties[resolvableProp]).toBe(resolvableValue);
|
|
}
|
|
|
|
/**
|
|
* Parse a script expecting one animation, assert that `position` matches the expected value.
|
|
*/
|
|
export function expectSingleAnimPosition(script: string, position: number): void {
|
|
const anim = parseSingleAnimation(script);
|
|
expect(anim.position).toBe(position);
|
|
}
|
|
|
|
/**
|
|
* Parse a script, get the first animation id, run convertToKeyframesInScript,
|
|
* reparse, and return the first animation for assertion.
|
|
*/
|
|
export function convertAndReparse(
|
|
script: string,
|
|
runtimeValues?: Record<string, number | string>,
|
|
): GsapAnimation {
|
|
const id = parseSingleAnimation(script).id;
|
|
const updated = convertToKeyframesInScript(script, id, runtimeValues);
|
|
return parseSingleAnimation(updated);
|
|
}
|
|
|
|
/**
|
|
* Parse a script, return the first animation and run a split-related reparse.
|
|
* Asserts the reparse result has exactly `expectedCount` animations and returns
|
|
* the selector of the first animation.
|
|
*/
|
|
export function parseSplitAndAssert(
|
|
script: string,
|
|
splitFn: (s: string) => string,
|
|
expectedCount: number,
|
|
): string[] {
|
|
const result = splitFn(script);
|
|
const parsed = parseGsapScript(result);
|
|
expect(parsed.animations).toHaveLength(expectedCount);
|
|
return parsed.animations.map((a) => a.targetSelector);
|
|
}
|