Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core/src/parsers/gsapParserAcorn.full.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
* T6d: parse-parity suite — runs the full gsapParser.test.ts parse scenarios
* against parseGsapScriptAcorn. Write-path tests are it.skip'd; those live
* in gsapWriter.acorn.test.ts.
*
* Trust model: assertions here trust the recast-baseline outputs from
* gsapParser.test.ts as ground truth. T6b (gsapParser.acorn.test.ts) carries
* the real behavioral parity contract; this file widens coverage to the full
* corpus without duplicating the contract commentary.
* motionPath parity tests live in the Phase 3b commit (PR #1379) because that
* commit adds the acorn motionPath parser itself.
*/
import { describe, it, expect } from "vitest";
import { parseGsapScriptAcorn } from "./gsapParserAcorn.js";
Expand Down
54 changes: 28 additions & 26 deletions packages/core/src/parsers/gsapParserAcorn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ function lookupBindingFromAncestors(
const selector = bindings.get(scopeNode)?.get(name);
if (selector !== undefined) return selector;
}
return null;
// Program-scope bindings are stored under null (enclosingScopeNodeFromAncestors
// returns null when no function wrapper exists — the common case in HF scripts).
return bindings.get(null)?.get(name) ?? null;
}

function isFunctionNode(node: any): boolean {
Expand Down Expand Up @@ -470,31 +472,31 @@ function findAllTweenCalls(
) {
const method = callee.property.name;
const args = node.arguments;
if (args.length >= 2) {
const selectorValue =
resolveTargetSelector(args[0], nodeAncestors, scope, targetBindings) ??
"__unresolved__";

if (method === "fromTo") {
results.push({
node,
ancestors: nodeAncestors,
method: "fromTo",
selector: selectorValue,
fromArg: args[1],
varsArg: args[2],
positionArg: args[3],
});
} else {
results.push({
node,
ancestors: nodeAncestors,
method: method as GsapMethod,
selector: selectorValue,
varsArg: args[1],
positionArg: args[2],
});
}
const selectorValue =
args.length >= 1
? (resolveTargetSelector(args[0], nodeAncestors, scope, targetBindings) ??
"__unresolved__")
: "__unresolved__";

if (method === "fromTo" && args.length >= 3) {
results.push({
node,
ancestors: nodeAncestors,
method: "fromTo",
selector: selectorValue,
fromArg: args[1],
varsArg: args[2],
positionArg: args[3],
});
} else if (method !== "fromTo" && args.length >= 2) {
results.push({
node,
ancestors: nodeAncestors,
method: method as GsapMethod,
selector: selectorValue,
varsArg: args[1],
positionArg: args[2],
});
}
}
}
Expand Down
11 changes: 4 additions & 7 deletions packages/core/src/parsers/gsapWriterAcorn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ import * as acornWalk from "acorn-walk";
function valueToCode(value: unknown): string {
if (typeof value === "string" && value.startsWith("__raw:")) return value.slice(6);
if (typeof value === "string") return JSON.stringify(value);
if (typeof value === "number" || typeof value === "boolean") return String(value);
if (typeof value === "number") return Number.isNaN(value) ? "0" : String(value);
if (typeof value === "boolean") return String(value);
return JSON.stringify(value);
}

function safeKey(key: string): string {
return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key);
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
}

// fallow-ignore-next-line complexity
Expand Down Expand Up @@ -241,11 +242,7 @@ export function addAnimationToScript(
export function removeAnimationFromScript(script: string, animationId: string): string {
const parsed = parseGsapScriptAcornForWrite(script);
if (!parsed) return script;
let target = parsed.located.find((l) => l.id === animationId);
if (!target) {
const convertedId = animationId.replace(/-from-|-fromTo-/, "-to-");
target = parsed.located.find((l) => l.id === convertedId);
}
const target = parsed.located.find((l) => l.id === animationId);
if (!target) return script;

const ms = new MagicString(script);
Expand Down
20 changes: 12 additions & 8 deletions packages/core/src/studio-api/routes/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type UnsafeMutationValue,
} from "../helpers/finiteMutation.js";
import type { GsapAnimation } from "../../parsers/gsapSerialize.js";
import { parseGsapScriptAcorn } from "../../parsers/gsapParserAcorn.js";
import {
removeElementFromHtml,
patchElementInHtml,
Expand Down Expand Up @@ -316,7 +317,13 @@ function bakeVisibilityOnDelete(document: Document, anim: GsapAnimation): void {
}
}

/** Lazy-load gsapParser to avoid pulling recast into every file-route import. */
/**
* Lazy-load gsapParser for write ops (recast-backed) that are not yet ported to
* the acorn writer. The read path (`parseGsapScript`) has been replaced by the
* browser-safe `parseGsapScriptAcorn` — this loader is only needed for the write
* ops that remain: convertToKeyframesInScript, removeAllKeyframesFromScript,
* materializeKeyframesInScript, unrollDynamicAnimations, setArcPathInScript, etc.
*/
async function loadGsapParser() {
return import("../../parsers/gsapParser.js");
}
Expand Down Expand Up @@ -492,7 +499,6 @@ async function executeGsapMutation(
): Promise<GsapMutationResult | Response> {
const parser = await loadGsapParser();
const {
parseGsapScript,
updateAnimationInScript,
addAnimationToScript,
removeAnimationFromScript,
Expand All @@ -515,7 +521,7 @@ async function executeGsapMutation(
scriptText: string,
animationId: string,
): { anim: GsapAnimation } | { err: Response } {
const parsed = parseGsapScript(scriptText);
const parsed = parseGsapScriptAcorn(scriptText);
const anim = parsed.animations.find((a) => a.id === animationId);
if (!anim) return { err: respond({ error: "animation not found" }, 404) };
return { anim };
Expand Down Expand Up @@ -578,7 +584,7 @@ async function executeGsapMutation(
return removeAnimationFromScript(block.scriptText, body.animationId);
}
case "delete-all-for-selector": {
const parsed = parseGsapScript(block.scriptText);
const parsed = parseGsapScriptAcorn(block.scriptText);
const matching = parsed.animations.filter((a) => a.targetSelector === body.targetSelector);
if (matching.length === 0) return block.scriptText;
stripStudioEditsFromTarget(block.document, body.targetSelector);
Expand Down Expand Up @@ -1162,8 +1168,7 @@ export function registerFileRoutes(api: Hono, adapter: StudioApiAdapter): void {
});
}

const { parseGsapScript } = await loadGsapParser();
const parsed = parseGsapScript(block.scriptText);
const parsed = parseGsapScriptAcorn(block.scriptText);
return c.json(parsed);
});

Expand Down Expand Up @@ -1228,8 +1233,7 @@ export function registerFileRoutes(api: Hono, adapter: StudioApiAdapter): void {
writeFileSync(res.absPath, newHtml, "utf-8");
}

const { parseGsapScript } = await loadGsapParser();
const freshParsed = parseGsapScript(newScript);
const freshParsed = parseGsapScriptAcorn(newScript);
const responsePayload: Record<string, unknown> = {
ok: true,
changed,
Expand Down
4 changes: 3 additions & 1 deletion packages/sdk/src/engine/apply-patches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,9 @@ function applyOne(parsed: ParsedDocument, patch: JsonPatchOp, p: ParsedPath): vo
}

case "script": {
if (patch.op !== "remove") {
if (patch.op === "remove") {
setGsapScript(parsed.document, "");
} else {
setGsapScript(parsed.document, String(patch.value ?? ""));
}
break;
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/engine/mutate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ function handleSetGsapTween(
const extras: Record<string, unknown> = {};
if (properties.repeat !== undefined) extras.repeat = properties.repeat;
if (properties.yoyo !== undefined) extras.yoyo = properties.yoyo;
if (properties.stagger !== undefined) extras.stagger = properties.stagger;
if (Object.keys(extras).length > 0) updates.extras = extras;

const newScript = updateAnimationInScript(script, animationId, updates);
Expand Down
Loading