@hyperframes/studio 0.7.23 → 0.7.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio",
3
- "version": "0.7.23",
3
+ "version": "0.7.25",
4
4
  "description": "",
5
5
  "repository": {
6
6
  "type": "git",
@@ -46,11 +46,11 @@
46
46
  "gsap": "^3.13.0",
47
47
  "marked": "^14.1.4",
48
48
  "mediabunny": "^1.45.3",
49
- "@hyperframes/core": "0.7.23",
50
- "@hyperframes/player": "0.7.23",
51
- "@hyperframes/sdk": "0.7.23",
52
- "@hyperframes/studio-server": "0.7.23",
53
- "@hyperframes/parsers": "0.7.23"
49
+ "@hyperframes/core": "0.7.25",
50
+ "@hyperframes/parsers": "0.7.25",
51
+ "@hyperframes/player": "0.7.25",
52
+ "@hyperframes/sdk": "0.7.25",
53
+ "@hyperframes/studio-server": "0.7.25"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/react": "19",
@@ -65,7 +65,7 @@
65
65
  "vite": "^6.4.2",
66
66
  "vitest": "^3.2.4",
67
67
  "zustand": "^5.0.0",
68
- "@hyperframes/producer": "0.7.23"
68
+ "@hyperframes/producer": "0.7.25"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "react": "19",
@@ -294,7 +294,12 @@ export function useDomEditSession({
294
294
  // the SDK resolves each reordered element (the reorderElements op's targets).
295
295
  onReorderShadow: sdkSession
296
296
  ? (targets: string[]) => {
297
- const reorderSrc = activeCompPath ? () => readProjectFile(activeCompPath) : undefined;
297
+ // Single-flight: every target in one reorder batch shares the same file, so
298
+ // memoize the read instead of firing one fetch per unresolved target.
299
+ let reorderSrcPromise: Promise<string> | undefined;
300
+ const reorderSrc = activeCompPath
301
+ ? () => (reorderSrcPromise ??= readProjectFile(activeCompPath))
302
+ : undefined;
298
303
  for (const target of targets)
299
304
  void recordResolverParity(sdkSession, target, "reorderElements", reorderSrc);
300
305
  }
@@ -221,12 +221,13 @@ export function sdkGsapTweenPersist(
221
221
  "addGsapTween",
222
222
  gsapSrc ? () => gsapSrc(targetPath) : undefined,
223
223
  );
224
- } else
224
+ } else {
225
225
  recordAnimationResolverParity(
226
226
  sdkSession,
227
227
  op.animationId,
228
228
  op.kind === "set" ? "setGsapTween" : "removeGsapTween",
229
229
  );
230
+ }
230
231
  // Leading dark-launch gate so flag-off does no SDK touch (getElement) at all —
231
232
  // matches the other three chokepoints' discipline.
232
233
  if (!STUDIO_SDK_CUTOVER_ENABLED) return Promise.resolve(false);
@@ -273,6 +273,22 @@ describe("C. Resolver-parity detection", () => {
273
273
  expect(lastShadow()?.sourceHfIdCount).toBe(2);
274
274
  });
275
275
 
276
+ it("C8 sourceLooseMatchOnly: hfId matches source only as plain text, not a data-hf-id attribute", async () => {
277
+ mockFlags.STUDIO_SDK_RESOLVER_SHADOW_ENABLED = true;
278
+ const session = { getElement: () => null, getElements: () => [] } as unknown as Composition;
279
+ // "hf-widget" appears only inside a class name, never as data-hf-id="hf-widget".
280
+ const source = `<div class="hf-widget-container">no attribute match here</div>`;
281
+ runResolverShadow(
282
+ session,
283
+ "hf-widget",
284
+ [{ type: "inline-style", property: "color", value: "red" }],
285
+ source,
286
+ );
287
+ const ev = lastShadow();
288
+ expect(ev?.sourceHfIdCount).toBe(0);
289
+ expect(ev?.sourceLooseMatchOnly).toBe(true);
290
+ });
291
+
276
292
  it("C10: unmappable op type produces no mismatch (excluded, not flagged)", async () => {
277
293
  const session = await openComposition(BASE_HTML);
278
294
  // "unknown-op" is not in MAPPED_OP_TYPES, so it must be silently excluded.
@@ -423,6 +439,19 @@ describe("F. recordResolverParity", () => {
423
439
  expect(ev?.mismatchCount).toBe(1);
424
440
  expect(ev?.sourceHfIdCount).toBeUndefined();
425
441
  });
442
+
443
+ it("tags sourceLooseMatchOnly when hfId matches source only as plain text, not a data-hf-id attribute", async () => {
444
+ mockFlags.STUDIO_SDK_RESOLVER_SHADOW_ENABLED = true;
445
+ const session = await openComposition(BASE_HTML);
446
+ // "hf-widget" appears only inside a class name, never as data-hf-id="hf-widget".
447
+ await recordResolverParity(session, "hf-widget", "setTiming", () =>
448
+ Promise.resolve('<div class="hf-widget-container"></div>'),
449
+ );
450
+ const ev = lastShadow();
451
+ expect(ev?.mismatchCount).toBe(1);
452
+ expect(ev?.sourceHfIdCount).toBe(0);
453
+ expect(ev?.sourceLooseMatchOnly).toBe(true);
454
+ });
426
455
  });
427
456
 
428
457
  // ─── G. recordAnimationResolverParity (GSAP animationId ops) ──────────────────
@@ -272,6 +272,10 @@ export function runResolverShadow(
272
272
  // every style/text/attr edit (the editor's chattiest path) at default-ON.
273
273
  if (mismatches.length === 0) return;
274
274
  const isElementNotFound = mismatches.some((m) => m.kind === "element_not_found");
275
+ const strictCount =
276
+ isElementNotFound && sourceContent !== undefined
277
+ ? countHfIdInSource(sourceContent, hfId)
278
+ : undefined;
275
279
  trackStudioEvent("sdk_resolver_shadow", {
276
280
  hfId,
277
281
  // sessionElementCount > 0 + element_not_found = runtime-only element;
@@ -282,13 +286,11 @@ export function runResolverShadow(
282
286
  // instance; =1 = single static node the SDK parse dropped (foreign-content
283
287
  // exclusion / sub-comp inlining gap); =0 = the runtime-node filter above
284
288
  // uses a loose substring match (biased toward keeping signal) while this
285
- // count uses a strict attribute match — an emitted event with count 0
286
- // means hfId appeared in source as plain text (e.g. a class name, comment,
287
- // or script string) but never as a data-hf-id attribute. Treat 0 as "not
288
- // a genuine attribute occurrence," not as a contradiction.
289
- ...(isElementNotFound && sourceContent !== undefined
290
- ? { sourceHfIdCount: countHfIdInSource(sourceContent, hfId) }
291
- : {}),
289
+ // count uses a strict attribute match — see sourceLooseMatchOnly below.
290
+ ...(strictCount !== undefined ? { sourceHfIdCount: strictCount } : {}),
291
+ // Loose suppression check matched (kept this event) but the strict
292
+ // attribute count came back 0 see the sourceHfIdCount comment above.
293
+ ...(strictCount === 0 ? { sourceLooseMatchOnly: true } : {}),
292
294
  mismatchCount: mismatches.length,
293
295
  mismatches: JSON.stringify(redactMismatches(mismatches)),
294
296
  });
@@ -334,6 +336,7 @@ export async function recordResolverParity(
334
336
  }
335
337
  // Runtime-generated node the static parse can't model — suppress (mirrors the dom-edit path).
336
338
  if (source !== undefined && !source.includes(hfId)) return;
339
+ const strictCount = source !== undefined ? countHfIdInSource(source, hfId) : undefined;
337
340
  trackStudioEvent("sdk_resolver_shadow", {
338
341
  hfId,
339
342
  opLabel,
@@ -342,7 +345,13 @@ export async function recordResolverParity(
342
345
  // on an emitted (non-suppressed) event — the suppression check above is a
343
346
  // loose substring match (biased toward keeping signal); see the longer
344
347
  // comment on this field in runResolverShadow for the full explanation.
345
- ...(source !== undefined ? { sourceHfIdCount: countHfIdInSource(source, hfId) } : {}),
348
+ ...(strictCount !== undefined ? { sourceHfIdCount: strictCount } : {}),
349
+ // Loose suppression check matched (kept this event) but the strict
350
+ // attribute count came back 0 — hfId appeared as plain text (class name,
351
+ // comment, script string) but never as a data-hf-id="..." attribute.
352
+ // Lets telemetry consumers filter this cohort without parsing the
353
+ // sourceHfIdCount comment above.
354
+ ...(strictCount === 0 ? { sourceLooseMatchOnly: true } : {}),
346
355
  mismatchCount: 1,
347
356
  mismatches: JSON.stringify([
348
357
  { kind: "element_not_found", hfId } satisfies SdkResolverMismatch,