@hyperframes/lint 0.8.50 → 0.8.52

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/index.js CHANGED
@@ -1541,6 +1541,7 @@ function findRuntimeMediaSrcMutationFindings(ctx) {
1541
1541
  }
1542
1542
  var mediaRules = [
1543
1543
  findNestedMediaStartBasisFindings,
1544
+ findSpeedRampOnNonMediaFindings,
1544
1545
  // duplicate_media_id + duplicate_media_discovery_risk
1545
1546
  ({ tags }) => {
1546
1547
  const findings = [];
@@ -1962,6 +1963,18 @@ function findVolumeDoubleAutomationFindings(ctx) {
1962
1963
  }
1963
1964
  return findings;
1964
1965
  }
1966
+ function findSpeedRampOnNonMediaFindings(ctx) {
1967
+ return ctx.tags.filter((tag) => tag.name !== "video" && tag.name !== "audio").filter(
1968
+ (tag) => /"target"\s*:\s*"rate"/.test(readDecodedAttr(tag.raw, "data-automation") ?? "")
1969
+ ).map((tag) => ({
1970
+ code: "speed_ramp_on_non_media",
1971
+ severity: "warning",
1972
+ message: `<${tag.name}> has a speed-ramp lane, but only <video> and <audio> clips can be retimed. The lane does nothing here.`,
1973
+ elementId: readAttr(tag.raw, "id") || void 0,
1974
+ fixHint: "Move the rate lane onto the <video> or <audio> clip, or retime an animation with a GSAP timeline instead.",
1975
+ snippet: truncateSnippet(tag.raw)
1976
+ }));
1977
+ }
1965
1978
  function findCarveUngroupedSourcesFindings(ctx) {
1966
1979
  const groupIds = new Set(
1967
1980
  ctx.tags.filter((tag) => tag.name === "hf-audio-group").map((tag) => readAttr(tag.raw, "id"))
@@ -2068,6 +2081,11 @@ function findAudioGroupCarveAttrFindings(ctx) {
2068
2081
  // src/rules/composition.ts
2069
2082
  import { COMPOSITION_VARIABLE_TYPES, isSafeMediaUrl } from "@hyperframes/parsers/composition";
2070
2083
  import { COMPOSITION_ATTRIBUTES, readClipTiming } from "@hyperframes/parsers/composition-contract";
2084
+ import { resolveCompositionDuration } from "@hyperframes/parsers/composition-duration";
2085
+ import {
2086
+ readAuthoredDurationSeconds,
2087
+ resolveMediaDuration
2088
+ } from "@hyperframes/parsers/media-duration";
2071
2089
  var MAX_COMPOSITION_LINES = 300;
2072
2090
  var MAX_TIMED_ELEMENTS_PER_TRACK = 3;
2073
2091
  var TRACK_DENSITY_EXEMPT_TAGS = /* @__PURE__ */ new Set(["audio", "script", "style", "video"]);
@@ -2843,6 +2861,18 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
2843
2861
  const hasInfiniteCssAnimation = /\banimation(?:-iteration-count)?\s*:[^;{}]*(?<![\w-])infinite(?![\w-])/.test(combinedCss);
2844
2862
  const hasAnyNonGsapSignal = usesLottie || usesThree || usesWaapi || hasCssAnimationName;
2845
2863
  if (!hasAnyNonGsapSignal) {
2864
+ const derived = deriveDurationFromClips(tags, rootTag);
2865
+ if (derived.source === "derived") {
2866
+ return [
2867
+ {
2868
+ code: "root_composition_duration_derived",
2869
+ severity: "warning",
2870
+ 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` : "") + ".",
2871
+ fixHint: 'Add data-duration="<seconds>" to the root element to set the length yourself.',
2872
+ snippet: truncateSnippet(rootTag.raw)
2873
+ }
2874
+ ];
2875
+ }
2846
2876
  return [
2847
2877
  {
2848
2878
  code: "root_composition_missing_duration_source",
@@ -2927,6 +2957,35 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
2927
2957
  ];
2928
2958
  }
2929
2959
  ];
2960
+ var CLIP_MEDIA_TAGS = /* @__PURE__ */ new Set(["img", "video", "audio"]);
2961
+ function deriveDurationFromClips(tags, rootTag) {
2962
+ const clipEnds = [];
2963
+ for (const tag of tags) {
2964
+ if (tag === rootTag) continue;
2965
+ const startRaw = readAttr(tag.raw, "data-start");
2966
+ if (startRaw === null) continue;
2967
+ const start = Number(startRaw);
2968
+ if (!Number.isFinite(start)) {
2969
+ clipEnds.push(null);
2970
+ continue;
2971
+ }
2972
+ const getAttr = (name) => readAttr(tag.raw, name);
2973
+ const authored = readAuthoredDurationSeconds(getAttr, start);
2974
+ if (CLIP_MEDIA_TAGS.has(tag.name)) {
2975
+ const { seconds } = resolveMediaDuration({
2976
+ tag: tag.name,
2977
+ authoredDurationSeconds: authored,
2978
+ sourceDurationSeconds: null,
2979
+ mediaStartSeconds: 0,
2980
+ playbackRate: 1
2981
+ });
2982
+ clipEnds.push(seconds === null ? null : start + seconds);
2983
+ } else if (authored !== null && authored > 0) {
2984
+ clipEnds.push(start + authored);
2985
+ }
2986
+ }
2987
+ return resolveCompositionDuration({ authoredDurationSeconds: null, clipEndsSeconds: clipEnds });
2988
+ }
2930
2989
 
2931
2990
  // src/rules/gsap.ts
2932
2991
  async function loadParseGsapScript() {
@@ -5358,7 +5417,7 @@ var OPAQUE_TAGS = /* @__PURE__ */ new Set([
5358
5417
  "object"
5359
5418
  ]);
5360
5419
  var NON_LAYOUT_TAGS = /* @__PURE__ */ new Set(["style", "script", "template", "noscript"]);
5361
- var MEDIA_TAGS = /* @__PURE__ */ new Set(["img", "video", "audio"]);
5420
+ var MEDIA_TAGS = /* @__PURE__ */ new Set(["video", "audio", "img"]);
5362
5421
  var NODE_ATTRS = [
5363
5422
  "id",
5364
5423
  "class",
@@ -5424,19 +5483,15 @@ function nestedStructureFindings(rows, severity) {
5424
5483
  });
5425
5484
  }
5426
5485
  function missingDurationFindings(rows, severity) {
5427
- const intendedClip = (row) => !MEDIA_TAGS.has(row.tag) || row.attrs["data-start"] !== void 0 || row.attrs["data-track-index"] !== void 0;
5428
5486
  return rows.filter(
5429
- (row) => row.attrs["data-duration"] === void 0 && row.attrs["data-end"] === void 0 && !isSubCompositionHost(row) && intendedClip(row)
5430
- ).map((row) => {
5431
- const isMedia = MEDIA_TAGS.has(row.tag);
5432
- return {
5433
- code: isMedia ? "media_missing_duration" : "timeline_element_missing_timing",
5434
- severity,
5435
- 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.`,
5436
- elementId: row.attrs.id,
5437
- fixHint: `Add data-duration (in seconds) to ${describe(row)}.`
5438
- };
5439
- });
5487
+ (row) => !MEDIA_TAGS.has(row.tag) && row.attrs["data-duration"] === void 0 && row.attrs["data-end"] === void 0 && !isSubCompositionHost(row)
5488
+ ).map((row) => ({
5489
+ code: "timeline_element_missing_timing",
5490
+ severity,
5491
+ message: `${describe(row)} is a timeline element without data-duration, so the timeline cannot draw where it ends.`,
5492
+ elementId: row.attrs.id,
5493
+ fixHint: `Add data-duration (in seconds) to ${describe(row)}.`
5494
+ }));
5440
5495
  }
5441
5496
  function captionFindings(rows, severity) {
5442
5497
  const captionRows = rows.filter((row) => trackKindOf(row).kind === "captions");
@@ -5630,7 +5685,8 @@ import {
5630
5685
  isWithinProjectRoot,
5631
5686
  maskNonScannableRanges as maskNonScannableRanges2,
5632
5687
  resolveExistingLocalAsset as resolveExistingLocalAsset2,
5633
- resolveLocalAssetCandidates
5688
+ resolveLocalAssetCandidates,
5689
+ resolveProjectRelativeSrc
5634
5690
  } from "@hyperframes/parsers/asset-resolution";
5635
5691
 
5636
5692
  // src/hevcPreviewLint.ts
@@ -6101,7 +6157,7 @@ function lintAudioSrcNotFound(projectDir, htmlSources) {
6101
6157
  if (/^(https?:|data:|blob:)/i.test(src)) continue;
6102
6158
  if (isUnresolvedAssetPlaceholder2(src)) continue;
6103
6159
  const rootRelative = compSrcPath ? rewriteAssetPath2(compSrcPath, src, (path) => existsSync2(join2(projectDir, path))) : src;
6104
- if (!resolveLocalAssetCandidates(projectDir, rootRelative).some(existsSync2)) {
6160
+ if (!existsSync2(resolveProjectRelativeSrc(rootRelative, projectDir))) {
6105
6161
  missingSrcs.push(src);
6106
6162
  }
6107
6163
  }