@hyperframes/lint 0.8.7 → 0.8.9

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
@@ -287,6 +287,9 @@ function truncateSnippet(value, maxLength = 220) {
287
287
  if (normalized.length <= maxLength) return normalized;
288
288
  return `${normalized.slice(0, maxLength - 3)}...`;
289
289
  }
290
+ function mediaSrcTagRe(tagAlternation) {
291
+ return new RegExp(`<(${tagAlternation})\\b[^>]*\\ssrc\\s*=\\s*["']([^"']+)["'][^>]*>`, "gi");
292
+ }
290
293
 
291
294
  // src/context.ts
292
295
  function buildLintContext(html, options = {}) {
@@ -3266,7 +3269,7 @@ var captionRules = [
3266
3269
  ];
3267
3270
 
3268
3271
  // src/rules/composition.ts
3269
- import { COMPOSITION_VARIABLE_TYPES } from "@hyperframes/parsers/composition";
3272
+ import { COMPOSITION_VARIABLE_TYPES, isSafeMediaUrl } from "@hyperframes/parsers/composition";
3270
3273
  import { COMPOSITION_ATTRIBUTES, readClipTiming } from "@hyperframes/parsers/composition-contract";
3271
3274
  var MAX_COMPOSITION_LINES = 300;
3272
3275
  var MAX_TIMED_ELEMENTS_PER_TRACK = 3;
@@ -3434,6 +3437,7 @@ var compositionRules = [
3434
3437
  const tagsByCompositionId = /* @__PURE__ */ new Map();
3435
3438
  for (const tag of tags) {
3436
3439
  if (isInsideInertTemplate(tag, tags)) continue;
3440
+ if (readAttr(tag.raw, "data-composition-src")) continue;
3437
3441
  const compositionId = readDecodedAttr(tag.raw, "data-composition-id");
3438
3442
  if (!compositionId || compositionId.trim().length === 0) continue;
3439
3443
  const matchingTags = tagsByCompositionId.get(compositionId) ?? [];
@@ -3836,6 +3840,11 @@ var compositionRules = [
3836
3840
  }
3837
3841
  const findings = [];
3838
3842
  const knownTypes = new Set(COMPOSITION_VARIABLE_TYPES);
3843
+ const varSrcIds = /* @__PURE__ */ new Set();
3844
+ for (const tag of tags) {
3845
+ const bound = readAttr(tag.raw, "data-var-src");
3846
+ if (bound) varSrcIds.add(bound);
3847
+ }
3839
3848
  for (let i = 0; i < parsed.length; i += 1) {
3840
3849
  const entry = parsed[i];
3841
3850
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
@@ -3860,6 +3869,17 @@ var compositionRules = [
3860
3869
  message: `data-composition-variables entry [${i}] is missing or has invalid: ${missing.join(", ")}. Type must be one of string, number, color, boolean, enum, font, image.`,
3861
3870
  snippet: truncateSnippet(htmlTag.raw)
3862
3871
  });
3872
+ continue;
3873
+ }
3874
+ const id = String(e.id);
3875
+ if ((e.type === "image" || varSrcIds.has(id)) && typeof e.default === "string" && e.default.length > 0 && !isSafeMediaUrl(e.default)) {
3876
+ findings.push({
3877
+ code: "unloadable_media_variable_default",
3878
+ severity: "error",
3879
+ message: `Variable "${id}" defaults to a URL the runtime will refuse to load, so any element bound to it renders its authored fallback src instead and the render still exits 0.`,
3880
+ fixHint: `Media URLs must be relative, http(s), blob:, or a data:image/* URI. Copy the file into the project and reference it relatively (e.g. "assets/bg.png") rather than by absolute path.`,
3881
+ snippet: truncateSnippet(htmlTag.raw)
3882
+ });
3863
3883
  }
3864
3884
  }
3865
3885
  return findings;
@@ -4771,13 +4791,13 @@ async function probeIsHevc(ffprobePath, filePath) {
4771
4791
  }
4772
4792
  function collectLocalVideoCandidates(projectDir, htmlSources) {
4773
4793
  const candidates = /* @__PURE__ */ new Map();
4774
- const videoSrcRe = /<video\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi;
4794
+ const videoSrcRe = mediaSrcTagRe("video");
4775
4795
  for (const { html, compSrcPath } of htmlSources) {
4776
4796
  const scannable = maskNonScannableRanges(html);
4777
4797
  const re = new RegExp(videoSrcRe.source, videoSrcRe.flags);
4778
4798
  let match;
4779
4799
  while ((match = re.exec(scannable)) !== null) {
4780
- const rawSrc = match[1] ?? "";
4800
+ const rawSrc = match[2] ?? "";
4781
4801
  if (isUnresolvedAssetPlaceholder(rawSrc)) continue;
4782
4802
  const src = cleanAssetUrl(rawSrc);
4783
4803
  if (!src) continue;
@@ -5041,12 +5061,12 @@ function lintProjectAudioFiles(projectDir, htmlSources) {
5041
5061
  }
5042
5062
  function lintAudioSrcNotFound(projectDir, htmlSources) {
5043
5063
  const findings = [];
5044
- const audioSrcRe = /<audio\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi;
5064
+ const audioSrcRe = mediaSrcTagRe("audio");
5045
5065
  const missingSrcs = [];
5046
5066
  for (const { html, compSrcPath } of htmlSources) {
5047
5067
  let match;
5048
5068
  while ((match = audioSrcRe.exec(html)) !== null) {
5049
- const src = match[1];
5069
+ const src = match[2];
5050
5070
  if (/^(https?:|data:|blob:)/i.test(src)) continue;
5051
5071
  if (isUnresolvedAssetPlaceholder2(src)) continue;
5052
5072
  const rootRelative = compSrcPath ? rewriteAssetPath2(compSrcPath, src, (path) => existsSync2(join2(projectDir, path))) : src;
@@ -5068,7 +5088,7 @@ function lintAudioSrcNotFound(projectDir, htmlSources) {
5068
5088
  }
5069
5089
  function lintMissingLocalAsset(projectDir, htmlSources) {
5070
5090
  const findings = [];
5071
- const localAssetSrcRe = /<(video|img|source)\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi;
5091
+ const localAssetSrcRe = mediaSrcTagRe("video|img|source");
5072
5092
  const missingByTag = /* @__PURE__ */ new Map();
5073
5093
  for (const { html, compSrcPath } of htmlSources) {
5074
5094
  const scannable = maskNonScannableRanges2(html);