@hyperframes/lint 0.7.20 → 0.7.22

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
@@ -1058,6 +1058,31 @@ var mediaRules = [
1058
1058
  }
1059
1059
  return findings;
1060
1060
  },
1061
+ // media_crossorigin_breaks_preview — `crossorigin` on <video>/<audio> forces a
1062
+ // CORS-checked fetch. The server-side renderer downloads media directly (no CORS),
1063
+ // so it always works there; but Studio preview runs in the browser, where a media
1064
+ // host that omits Access-Control-Allow-Origin silently fails the load — the media
1065
+ // shows BLANK/black in preview while renders look fine, hiding the bug. Plain
1066
+ // displayed media never needs crossorigin; it's only required to read pixels/samples
1067
+ // back (canvas/WebGL texture, WebAudio createMediaElementSource) AND only when the
1068
+ // host is known CORS-enabled.
1069
+ ({ tags }) => {
1070
+ const findings = [];
1071
+ for (const tag of tags) {
1072
+ if (tag.name !== "video" && tag.name !== "audio") continue;
1073
+ if (!hasAttrName(tag.raw, "crossorigin")) continue;
1074
+ const elementId = readAttr(tag.raw, "id") || void 0;
1075
+ findings.push({
1076
+ code: "media_crossorigin_breaks_preview",
1077
+ severity: "error",
1078
+ message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has crossorigin, which forces a CORS-checked fetch. If the media host omits Access-Control-Allow-Origin, the load silently fails in Studio preview (media shows BLANK/black) while server-side renders still work \u2014 hiding the bug.`,
1079
+ elementId,
1080
+ fixHint: "Remove the crossorigin attribute unless you read the media back via canvas/WebGL/WebAudio AND the host is known to send CORS headers. Plain displayed media never needs it.",
1081
+ snippet: truncateSnippet(tag.raw)
1082
+ });
1083
+ }
1084
+ return findings;
1085
+ },
1061
1086
  // video_audio_double_source — catches audible <video> paired with a separate
1062
1087
  // <audio> pointing to the same file, which causes double playback at runtime
1063
1088
  ({ tags }) => {
@@ -1102,6 +1127,7 @@ async function loadParseGsapScript() {
1102
1127
  return mod.parseGsapScriptAcorn;
1103
1128
  }
1104
1129
  var SCENE_BOUNDARY_EPSILON_SECONDS = 0.05;
1130
+ var UNRESOLVED_TARGET = "__unresolved__";
1105
1131
  function countClassUsage(tags) {
1106
1132
  const counts = /* @__PURE__ */ new Map();
1107
1133
  for (const tag of tags) {
@@ -1464,6 +1490,7 @@ var gsapRules = [
1464
1490
  const left = gsapWindows[i];
1465
1491
  if (!left) continue;
1466
1492
  if (left.end <= left.position) continue;
1493
+ if (left.targetSelector === UNRESOLVED_TARGET) continue;
1467
1494
  for (let j = i + 1; j < gsapWindows.length; j++) {
1468
1495
  const right = gsapWindows[j];
1469
1496
  if (!right) continue;
@@ -1490,6 +1517,7 @@ ${right.raw}`)
1490
1517
  }
1491
1518
  if (clipStartBoundaries.length > 0) {
1492
1519
  for (const win of gsapWindows) {
1520
+ if (win.targetSelector === UNRESOLVED_TARGET) continue;
1493
1521
  if (!isSceneBoundaryExit(win)) continue;
1494
1522
  const boundary = findMatchingSceneBoundary(win.end, clipStartBoundaries);
1495
1523
  if (boundary == null) continue;
@@ -2726,7 +2754,7 @@ var adapterRules = [
2726
2754
  (t) => /["']three["']/.test(t) && /importmap/.test(scripts.find((s) => s.content === t)?.attrs || "")
2727
2755
  );
2728
2756
  const hasThreeModuleImport = texts.some(
2729
- (t) => /\bimport\b.*['"]three['"]/.test(t) || /\bfrom\s+['"]three['"]/.test(t)
2757
+ (t) => /\b(?:import|from)\s*[^;\n]*['"][^'"]*three[^'"]*['"]/i.test(t)
2730
2758
  );
2731
2759
  if (!usesThree || hasThreeScript || hasThreeImportMap || hasThreeModuleImport) return [];
2732
2760
  return [
@@ -2933,6 +2961,12 @@ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
2933
2961
  "math",
2934
2962
  "emoji",
2935
2963
  "fangsong",
2964
+ // Vendor-prefixed system-font keywords. Like `system-ui`, the engine resolves
2965
+ // these to the OS UI font — they are never installable files and must not be
2966
+ // flagged as a missing @font-face, even when a generic fallback follows them
2967
+ // (e.g. `-apple-system, system-ui, sans-serif`).
2968
+ "-apple-system",
2969
+ "blinkmacsystemfont",
2936
2970
  "inherit",
2937
2971
  "initial",
2938
2972
  "unset",
@@ -2957,6 +2991,11 @@ function extractFontFaceFamilies(styles) {
2957
2991
  }
2958
2992
  return families;
2959
2993
  }
2994
+ function normalizeUsedFontName(part) {
2995
+ const name = part.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase();
2996
+ if (!name || name.includes("(") || name.includes(")")) return null;
2997
+ return name;
2998
+ }
2960
2999
  function extractUsedFontFamilies(styles) {
2961
3000
  const used = [];
2962
3001
  const seen = /* @__PURE__ */ new Set();
@@ -2965,9 +3004,8 @@ function extractUsedFontFamilies(styles) {
2965
3004
  const withoutFontFace = stripCssComments(style.content).replace(/@font-face\s*\{[^}]*\}/gi, "");
2966
3005
  let match;
2967
3006
  while ((match = propRe.exec(withoutFontFace)) !== null) {
2968
- const stack = match[1];
2969
- for (const part of stack.split(",")) {
2970
- const name = part.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase();
3007
+ for (const part of match[1].split(",")) {
3008
+ const name = normalizeUsedFontName(part);
2971
3009
  if (name && !GENERIC_FAMILIES.has(name) && !seen.has(name)) {
2972
3010
  seen.add(name);
2973
3011
  used.push(name);