@hyperframes/lint 0.8.50 → 0.8.53-desktop.0

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
@@ -1538,6 +1538,7 @@ function findRuntimeMediaSrcMutationFindings(ctx) {
1538
1538
  }
1539
1539
  var mediaRules = [
1540
1540
  findNestedMediaStartBasisFindings,
1541
+ findSpeedRampOnNonMediaFindings,
1541
1542
  // duplicate_media_id + duplicate_media_discovery_risk
1542
1543
  ({ tags }) => {
1543
1544
  const findings = [];
@@ -1959,6 +1960,18 @@ function findVolumeDoubleAutomationFindings(ctx) {
1959
1960
  }
1960
1961
  return findings;
1961
1962
  }
1963
+ function findSpeedRampOnNonMediaFindings(ctx) {
1964
+ return ctx.tags.filter((tag) => tag.name !== "video" && tag.name !== "audio").filter(
1965
+ (tag) => /"target"\s*:\s*"rate"/.test(readDecodedAttr(tag.raw, "data-automation") ?? "")
1966
+ ).map((tag) => ({
1967
+ code: "speed_ramp_on_non_media",
1968
+ severity: "warning",
1969
+ message: `<${tag.name}> has a speed-ramp lane, but only <video> and <audio> clips can be retimed. The lane does nothing here.`,
1970
+ elementId: readAttr(tag.raw, "id") || void 0,
1971
+ fixHint: "Move the rate lane onto the <video> or <audio> clip, or retime an animation with a GSAP timeline instead.",
1972
+ snippet: truncateSnippet(tag.raw)
1973
+ }));
1974
+ }
1962
1975
  function findCarveUngroupedSourcesFindings(ctx) {
1963
1976
  const groupIds = new Set(
1964
1977
  ctx.tags.filter((tag) => tag.name === "hf-audio-group").map((tag) => readAttr(tag.raw, "id"))
@@ -2065,6 +2078,11 @@ function findAudioGroupCarveAttrFindings(ctx) {
2065
2078
  // src/rules/composition.ts
2066
2079
  import { COMPOSITION_VARIABLE_TYPES, isSafeMediaUrl } from "@hyperframes/parsers/composition";
2067
2080
  import { COMPOSITION_ATTRIBUTES, readClipTiming } from "@hyperframes/parsers/composition-contract";
2081
+ import { resolveCompositionDuration } from "@hyperframes/parsers/composition-duration";
2082
+ import {
2083
+ readAuthoredDurationSeconds,
2084
+ resolveMediaDuration
2085
+ } from "@hyperframes/parsers/media-duration";
2068
2086
  var MAX_COMPOSITION_LINES = 300;
2069
2087
  var MAX_TIMED_ELEMENTS_PER_TRACK = 3;
2070
2088
  var TRACK_DENSITY_EXEMPT_TAGS = /* @__PURE__ */ new Set(["audio", "script", "style", "video"]);
@@ -2840,6 +2858,18 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
2840
2858
  const hasInfiniteCssAnimation = /\banimation(?:-iteration-count)?\s*:[^;{}]*(?<![\w-])infinite(?![\w-])/.test(combinedCss);
2841
2859
  const hasAnyNonGsapSignal = usesLottie || usesThree || usesWaapi || hasCssAnimationName;
2842
2860
  if (!hasAnyNonGsapSignal) {
2861
+ const derived = deriveDurationFromClips(tags, rootTag);
2862
+ if (derived.source === "derived") {
2863
+ return [
2864
+ {
2865
+ code: "root_composition_duration_derived",
2866
+ severity: "warning",
2867
+ message: `Root composition has no data-duration and no GSAP timeline, so its length is taken from its clips: at least ${derived.seconds}s` + (derived.pendingClips > 0 ? `, and ${derived.pendingClips} clip(s) whose length is only known at runtime` : "") + ".",
2868
+ fixHint: 'Add data-duration="<seconds>" to the root element to set the length yourself.',
2869
+ snippet: truncateSnippet(rootTag.raw)
2870
+ }
2871
+ ];
2872
+ }
2843
2873
  return [
2844
2874
  {
2845
2875
  code: "root_composition_missing_duration_source",
@@ -2924,6 +2954,35 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
2924
2954
  ];
2925
2955
  }
2926
2956
  ];
2957
+ var CLIP_MEDIA_TAGS = /* @__PURE__ */ new Set(["img", "video", "audio"]);
2958
+ function deriveDurationFromClips(tags, rootTag) {
2959
+ const clipEnds = [];
2960
+ for (const tag of tags) {
2961
+ if (tag === rootTag) continue;
2962
+ const startRaw = readAttr(tag.raw, "data-start");
2963
+ if (startRaw === null) continue;
2964
+ const start = Number(startRaw);
2965
+ if (!Number.isFinite(start)) {
2966
+ clipEnds.push(null);
2967
+ continue;
2968
+ }
2969
+ const getAttr = (name) => readAttr(tag.raw, name);
2970
+ const authored = readAuthoredDurationSeconds(getAttr, start);
2971
+ if (CLIP_MEDIA_TAGS.has(tag.name)) {
2972
+ const { seconds } = resolveMediaDuration({
2973
+ tag: tag.name,
2974
+ authoredDurationSeconds: authored,
2975
+ sourceDurationSeconds: null,
2976
+ mediaStartSeconds: 0,
2977
+ playbackRate: 1
2978
+ });
2979
+ clipEnds.push(seconds === null ? null : start + seconds);
2980
+ } else if (authored !== null && authored > 0) {
2981
+ clipEnds.push(start + authored);
2982
+ }
2983
+ }
2984
+ return resolveCompositionDuration({ authoredDurationSeconds: null, clipEndsSeconds: clipEnds });
2985
+ }
2927
2986
 
2928
2987
  // src/rules/gsap.ts
2929
2988
  async function loadParseGsapScript() {
@@ -5355,7 +5414,7 @@ var OPAQUE_TAGS = /* @__PURE__ */ new Set([
5355
5414
  "object"
5356
5415
  ]);
5357
5416
  var NON_LAYOUT_TAGS = /* @__PURE__ */ new Set(["style", "script", "template", "noscript"]);
5358
- var MEDIA_TAGS = /* @__PURE__ */ new Set(["img", "video", "audio"]);
5417
+ var MEDIA_TAGS = /* @__PURE__ */ new Set(["video", "audio", "img"]);
5359
5418
  var NODE_ATTRS = [
5360
5419
  "id",
5361
5420
  "class",
@@ -5421,19 +5480,15 @@ function nestedStructureFindings(rows, severity) {
5421
5480
  });
5422
5481
  }
5423
5482
  function missingDurationFindings(rows, severity) {
5424
- const intendedClip = (row) => !MEDIA_TAGS.has(row.tag) || row.attrs["data-start"] !== void 0 || row.attrs["data-track-index"] !== void 0;
5425
5483
  return rows.filter(
5426
- (row) => row.attrs["data-duration"] === void 0 && row.attrs["data-end"] === void 0 && !isSubCompositionHost(row) && intendedClip(row)
5427
- ).map((row) => {
5428
- const isMedia = MEDIA_TAGS.has(row.tag);
5429
- return {
5430
- code: isMedia ? "media_missing_duration" : "timeline_element_missing_timing",
5431
- severity,
5432
- message: isMedia ? `${describe(row)} is media on the timeline without data-duration, so its clip has no length.` : `${describe(row)} is a timeline element without data-duration, so the timeline cannot draw where it ends.`,
5433
- elementId: row.attrs.id,
5434
- fixHint: `Add data-duration (in seconds) to ${describe(row)}.`
5435
- };
5436
- });
5484
+ (row) => !MEDIA_TAGS.has(row.tag) && row.attrs["data-duration"] === void 0 && row.attrs["data-end"] === void 0 && !isSubCompositionHost(row)
5485
+ ).map((row) => ({
5486
+ code: "timeline_element_missing_timing",
5487
+ severity,
5488
+ message: `${describe(row)} is a timeline element without data-duration, so the timeline cannot draw where it ends.`,
5489
+ elementId: row.attrs.id,
5490
+ fixHint: `Add data-duration (in seconds) to ${describe(row)}.`
5491
+ }));
5437
5492
  }
5438
5493
  function captionFindings(rows, severity) {
5439
5494
  const captionRows = rows.filter((row) => trackKindOf(row).kind === "captions");