@hyperframes/lint 0.8.199 → 0.8.201

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/dist/browser.js CHANGED
@@ -1047,6 +1047,19 @@ function selectorAliasesRuntimeHiddenStyle(selector) {
1047
1047
  }
1048
1048
  return unsafe;
1049
1049
  }
1050
+ function selectsOnInlineStyle(selector) {
1051
+ let found = false;
1052
+ try {
1053
+ selectorParser((root) => {
1054
+ root.walkAttributes((node) => {
1055
+ if (node.attribute.toLowerCase() === "style") found = true;
1056
+ });
1057
+ }).processSync(selector);
1058
+ } catch {
1059
+ return false;
1060
+ }
1061
+ return found;
1062
+ }
1050
1063
  var POSITION_PROPERTIES = /* @__PURE__ */ new Set(["left", "top", "right", "bottom", "inset"]);
1051
1064
  function weakIdOf(node) {
1052
1065
  if (node.type === "attribute" && node.attribute.toLowerCase() === "id") {
@@ -1454,6 +1467,7 @@ var coreRules = [
1454
1467
  const findings = [];
1455
1468
  const reportedRepeatedIds = /* @__PURE__ */ new Set();
1456
1469
  const reportedHiddenStyleSelectors = /* @__PURE__ */ new Set();
1470
+ const reportedInlineStyleSelectors = /* @__PURE__ */ new Set();
1457
1471
  const reportedReducedIdSelectors = /* @__PURE__ */ new Set();
1458
1472
  for (const style of styles) {
1459
1473
  let root;
@@ -1486,6 +1500,17 @@ var coreRules = [
1486
1500
  fixHint: `Remove the duplicate ancestor: change \`#${repeatedId} #${repeatedId}\` to \`#${repeatedId}\`.`
1487
1501
  });
1488
1502
  }
1503
+ if (!reportedInlineStyleSelectors.has(selector) && selectsOnInlineStyle(selector)) {
1504
+ reportedInlineStyleSelectors.add(selector);
1505
+ findings.push({
1506
+ code: "inline_style_attribute_selector",
1507
+ severity: "error",
1508
+ message: `Selector "${selector}" reads the inline \`style\` attribute. GSAP writes inline styles every frame, and each write then restyles far more than the animated element, so the preview slows to a crawl.`,
1509
+ selector,
1510
+ fixHint: "Let the runtime hide hidden clips; it already hides their contents. For a state of your own, toggle a class or data attribute from your script and select on that.",
1511
+ snippet: truncateSnippet(rule.toString())
1512
+ });
1513
+ }
1489
1514
  if (!forcesOpacityZero || reportedHiddenStyleSelectors.has(selector) || !selectorAliasesRuntimeHiddenStyle(selector)) {
1490
1515
  continue;
1491
1516
  }
@@ -1495,7 +1520,7 @@ var coreRules = [
1495
1520
  severity: "error",
1496
1521
  message: `Selector "${selector}" observes HyperFrames' runtime-owned hidden style and forces opacity to zero. The renderer hides each native video before copying its computed opacity to the visible replacement frame, so this rule makes both transparent.`,
1497
1522
  selector,
1498
- fixHint: 'Restrict the guard to sub-composition hosts, for example `[data-composition-src][style*="visibility: hidden"]` and `[data-composition-file][style*="visibility: hidden"]`. Do not derive arbitrary element or media opacity from runtime-owned inline visibility.',
1523
+ fixHint: "Remove this rule; the runtime already hides a hidden clip and its contents. To fade or hide something yourself, animate it on the timeline, or toggle a class or data attribute and select on that.",
1499
1524
  snippet: truncateSnippet(rule.toString())
1500
1525
  });
1501
1526
  }
@@ -2478,7 +2503,7 @@ var mediaRules = [
2478
2503
  const source = { id: readAttr(tag.raw, "id") || void 0, raw: tag.raw };
2479
2504
  if (tag.name === "video") {
2480
2505
  if (isAudibleVideoTag(tag.raw)) videos.push([src, source]);
2481
- } else if (tag.name === "audio") {
2506
+ } else if (tag.name === "audio" && readAttr(tag.raw, "data-timeline-role") !== "room-tone") {
2482
2507
  audios.push([src, source]);
2483
2508
  }
2484
2509
  }
@@ -2514,6 +2539,8 @@ var mediaRules = [
2514
2539
  findAudioGroupTimingAttrFindings,
2515
2540
  // audio_group_carve_attr
2516
2541
  findAudioGroupCarveAttrFindings,
2542
+ // audio_group_holds_elements
2543
+ findAudioGroupHoldsElementsFindings,
2517
2544
  findLinkedClipFindings
2518
2545
  ];
2519
2546
  function findVolumeTweenOverridesGainFindings(ctx) {
@@ -2651,6 +2678,26 @@ function findAudioGroupTimingAttrFindings(ctx) {
2651
2678
  }
2652
2679
  return findings;
2653
2680
  }
2681
+ function findAudioGroupHoldsElementsFindings(ctx) {
2682
+ const findings = [];
2683
+ for (const group of ctx.tags) {
2684
+ if (group.name !== "hf-audio-group") continue;
2685
+ const end = group.closeIndex ?? Number.POSITIVE_INFINITY;
2686
+ const held = ctx.tags.filter((tag) => tag.index > group.index && tag.index < end);
2687
+ if (held.length === 0) continue;
2688
+ const elementId = readAttr(group.raw, "id") || void 0;
2689
+ const first = readAttr(held[0].raw, "id");
2690
+ findings.push({
2691
+ code: "audio_group_holds_elements",
2692
+ severity: "warning",
2693
+ message: `${elementId ? `#${elementId}` : "This audio group"} holds ${held.length} element(s), starting with <${held[0].name}${first ? `#${first}` : ""}>. Being inside it routes no audio through the bus (only \`data-audio-group\` does), and while the group has \`data-hidden\` they are hidden and silenced in preview and render.`,
2694
+ elementId,
2695
+ fixHint: "Close the group with `</hf-audio-group>` right after its opening tag (`/>` does not close a custom element), and give clips meant for this bus `data-audio-group`.",
2696
+ snippet: truncateSnippet(group.raw)
2697
+ });
2698
+ }
2699
+ return findings;
2700
+ }
2654
2701
  function findAudioGroupCarveAttrFindings(ctx) {
2655
2702
  const findings = [];
2656
2703
  for (const tag of ctx.tags) {
@@ -3174,13 +3221,14 @@ function variablesDeclarationFindings(tag, tags) {
3174
3221
  code: "invalid_composition_variables_declaration",
3175
3222
  severity: "error",
3176
3223
  message: `data-composition-variables entry [${i}] has a default that does not match its type, or an enum without an options array.`,
3177
- fixHint: "Use number defaults for number variables, boolean defaults for boolean variables, and string defaults for other types. Enum variables also require an options array.",
3224
+ fixHint: "Use number defaults for number variables, boolean defaults for boolean variables, and string defaults for other types. Font/image also accept font {name, source} or image {url, alt?, fit?} objects. Enum variables also require an options array.",
3178
3225
  snippet: truncateSnippet(tag.raw)
3179
3226
  });
3180
3227
  continue;
3181
3228
  }
3182
- const id = String(e.id);
3183
- if ((e.type === "image" || varSrcIds.has(id)) && typeof e.default === "string" && e.default.length > 0 && !isSafeMediaUrl(e.default)) {
3229
+ const id = e.id;
3230
+ const mediaUrl = e.type === "image" && typeof e.default !== "string" ? e.default.url : e.default;
3231
+ if ((e.type === "image" || varSrcIds.has(id)) && typeof mediaUrl === "string" && mediaUrl.length > 0 && !isSafeMediaUrl(mediaUrl)) {
3184
3232
  findings.push({
3185
3233
  code: "unloadable_media_variable_default",
3186
3234
  severity: "error",
package/dist/index.js CHANGED
@@ -1053,6 +1053,19 @@ function selectorAliasesRuntimeHiddenStyle(selector) {
1053
1053
  }
1054
1054
  return unsafe;
1055
1055
  }
1056
+ function selectsOnInlineStyle(selector) {
1057
+ let found = false;
1058
+ try {
1059
+ selectorParser((root) => {
1060
+ root.walkAttributes((node) => {
1061
+ if (node.attribute.toLowerCase() === "style") found = true;
1062
+ });
1063
+ }).processSync(selector);
1064
+ } catch {
1065
+ return false;
1066
+ }
1067
+ return found;
1068
+ }
1056
1069
  var POSITION_PROPERTIES = /* @__PURE__ */ new Set(["left", "top", "right", "bottom", "inset"]);
1057
1070
  function weakIdOf(node) {
1058
1071
  if (node.type === "attribute" && node.attribute.toLowerCase() === "id") {
@@ -1460,6 +1473,7 @@ var coreRules = [
1460
1473
  const findings = [];
1461
1474
  const reportedRepeatedIds = /* @__PURE__ */ new Set();
1462
1475
  const reportedHiddenStyleSelectors = /* @__PURE__ */ new Set();
1476
+ const reportedInlineStyleSelectors = /* @__PURE__ */ new Set();
1463
1477
  const reportedReducedIdSelectors = /* @__PURE__ */ new Set();
1464
1478
  for (const style of styles) {
1465
1479
  let root;
@@ -1492,6 +1506,17 @@ var coreRules = [
1492
1506
  fixHint: `Remove the duplicate ancestor: change \`#${repeatedId} #${repeatedId}\` to \`#${repeatedId}\`.`
1493
1507
  });
1494
1508
  }
1509
+ if (!reportedInlineStyleSelectors.has(selector) && selectsOnInlineStyle(selector)) {
1510
+ reportedInlineStyleSelectors.add(selector);
1511
+ findings.push({
1512
+ code: "inline_style_attribute_selector",
1513
+ severity: "error",
1514
+ message: `Selector "${selector}" reads the inline \`style\` attribute. GSAP writes inline styles every frame, and each write then restyles far more than the animated element, so the preview slows to a crawl.`,
1515
+ selector,
1516
+ fixHint: "Let the runtime hide hidden clips; it already hides their contents. For a state of your own, toggle a class or data attribute from your script and select on that.",
1517
+ snippet: truncateSnippet(rule.toString())
1518
+ });
1519
+ }
1495
1520
  if (!forcesOpacityZero || reportedHiddenStyleSelectors.has(selector) || !selectorAliasesRuntimeHiddenStyle(selector)) {
1496
1521
  continue;
1497
1522
  }
@@ -1501,7 +1526,7 @@ var coreRules = [
1501
1526
  severity: "error",
1502
1527
  message: `Selector "${selector}" observes HyperFrames' runtime-owned hidden style and forces opacity to zero. The renderer hides each native video before copying its computed opacity to the visible replacement frame, so this rule makes both transparent.`,
1503
1528
  selector,
1504
- fixHint: 'Restrict the guard to sub-composition hosts, for example `[data-composition-src][style*="visibility: hidden"]` and `[data-composition-file][style*="visibility: hidden"]`. Do not derive arbitrary element or media opacity from runtime-owned inline visibility.',
1529
+ fixHint: "Remove this rule; the runtime already hides a hidden clip and its contents. To fade or hide something yourself, animate it on the timeline, or toggle a class or data attribute and select on that.",
1505
1530
  snippet: truncateSnippet(rule.toString())
1506
1531
  });
1507
1532
  }
@@ -2484,7 +2509,7 @@ var mediaRules = [
2484
2509
  const source = { id: readAttr(tag.raw, "id") || void 0, raw: tag.raw };
2485
2510
  if (tag.name === "video") {
2486
2511
  if (isAudibleVideoTag(tag.raw)) videos.push([src, source]);
2487
- } else if (tag.name === "audio") {
2512
+ } else if (tag.name === "audio" && readAttr(tag.raw, "data-timeline-role") !== "room-tone") {
2488
2513
  audios.push([src, source]);
2489
2514
  }
2490
2515
  }
@@ -2520,6 +2545,8 @@ var mediaRules = [
2520
2545
  findAudioGroupTimingAttrFindings,
2521
2546
  // audio_group_carve_attr
2522
2547
  findAudioGroupCarveAttrFindings,
2548
+ // audio_group_holds_elements
2549
+ findAudioGroupHoldsElementsFindings,
2523
2550
  findLinkedClipFindings
2524
2551
  ];
2525
2552
  function findVolumeTweenOverridesGainFindings(ctx) {
@@ -2657,6 +2684,26 @@ function findAudioGroupTimingAttrFindings(ctx) {
2657
2684
  }
2658
2685
  return findings;
2659
2686
  }
2687
+ function findAudioGroupHoldsElementsFindings(ctx) {
2688
+ const findings = [];
2689
+ for (const group of ctx.tags) {
2690
+ if (group.name !== "hf-audio-group") continue;
2691
+ const end = group.closeIndex ?? Number.POSITIVE_INFINITY;
2692
+ const held = ctx.tags.filter((tag) => tag.index > group.index && tag.index < end);
2693
+ if (held.length === 0) continue;
2694
+ const elementId = readAttr(group.raw, "id") || void 0;
2695
+ const first = readAttr(held[0].raw, "id");
2696
+ findings.push({
2697
+ code: "audio_group_holds_elements",
2698
+ severity: "warning",
2699
+ message: `${elementId ? `#${elementId}` : "This audio group"} holds ${held.length} element(s), starting with <${held[0].name}${first ? `#${first}` : ""}>. Being inside it routes no audio through the bus (only \`data-audio-group\` does), and while the group has \`data-hidden\` they are hidden and silenced in preview and render.`,
2700
+ elementId,
2701
+ fixHint: "Close the group with `</hf-audio-group>` right after its opening tag (`/>` does not close a custom element), and give clips meant for this bus `data-audio-group`.",
2702
+ snippet: truncateSnippet(group.raw)
2703
+ });
2704
+ }
2705
+ return findings;
2706
+ }
2660
2707
  function findAudioGroupCarveAttrFindings(ctx) {
2661
2708
  const findings = [];
2662
2709
  for (const tag of ctx.tags) {
@@ -3180,13 +3227,14 @@ function variablesDeclarationFindings(tag, tags) {
3180
3227
  code: "invalid_composition_variables_declaration",
3181
3228
  severity: "error",
3182
3229
  message: `data-composition-variables entry [${i}] has a default that does not match its type, or an enum without an options array.`,
3183
- fixHint: "Use number defaults for number variables, boolean defaults for boolean variables, and string defaults for other types. Enum variables also require an options array.",
3230
+ fixHint: "Use number defaults for number variables, boolean defaults for boolean variables, and string defaults for other types. Font/image also accept font {name, source} or image {url, alt?, fit?} objects. Enum variables also require an options array.",
3184
3231
  snippet: truncateSnippet(tag.raw)
3185
3232
  });
3186
3233
  continue;
3187
3234
  }
3188
- const id = String(e.id);
3189
- if ((e.type === "image" || varSrcIds.has(id)) && typeof e.default === "string" && e.default.length > 0 && !isSafeMediaUrl(e.default)) {
3235
+ const id = e.id;
3236
+ const mediaUrl = e.type === "image" && typeof e.default !== "string" ? e.default.url : e.default;
3237
+ if ((e.type === "image" || varSrcIds.has(id)) && typeof mediaUrl === "string" && mediaUrl.length > 0 && !isSafeMediaUrl(mediaUrl)) {
3190
3238
  findings.push({
3191
3239
  code: "unloadable_media_variable_default",
3192
3240
  severity: "error",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/lint",
3
- "version": "0.8.199",
3
+ "version": "0.8.201",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/heygen-com/hyperframes",
@@ -35,7 +35,7 @@
35
35
  "linkedom": "^0.18.12",
36
36
  "postcss": "^8.5.8",
37
37
  "postcss-selector-parser": "^7.1.4",
38
- "@hyperframes/parsers": "0.8.199"
38
+ "@hyperframes/parsers": "0.8.201"
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^25.0.10",