@hyperframes/lint 0.8.29 → 0.8.31

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
@@ -188,57 +188,311 @@ function stripStringLiterals(source) {
188
188
  (literal) => literal[0] + " ".repeat(Math.max(0, literal.length - 1))
189
189
  );
190
190
  }
191
- function stripJsComments(source) {
191
+ function scanJsComments(source) {
192
192
  let out = "";
193
193
  let i = 0;
194
194
  let quote = null;
195
195
  let escaped = false;
196
+ let inRegex = false;
197
+ let inRegexClass = false;
198
+ let regexMisread = false;
199
+ const ctx = new CodeContext();
200
+ const emitCode = (ch) => {
201
+ out += ch;
202
+ ctx.push(ch);
203
+ };
204
+ const emitOpaque = (ch) => {
205
+ out += ch;
206
+ ctx.push(" ");
207
+ };
196
208
  while (i < source.length) {
197
209
  const ch = source[i] ?? "";
198
210
  const next = source[i + 1] ?? "";
199
- if (quote) {
211
+ if (inRegex) {
200
212
  out += ch;
201
213
  if (escaped) {
202
214
  escaped = false;
215
+ if (ch === "\n" || ch === "\r") {
216
+ inRegex = false;
217
+ inRegexClass = false;
218
+ regexMisread = true;
219
+ }
220
+ } else if (ch === "\\") {
221
+ escaped = true;
222
+ } else if (ch === "[") {
223
+ inRegexClass = true;
224
+ } else if (ch === "]") {
225
+ inRegexClass = false;
226
+ } else if (ch === "/" && !inRegexClass) {
227
+ inRegex = false;
228
+ ctx.push(ch);
229
+ } else if (ch === "\n" || ch === "\r") {
230
+ inRegex = false;
231
+ inRegexClass = false;
232
+ regexMisread = true;
233
+ }
234
+ i += 1;
235
+ continue;
236
+ }
237
+ if (quote) {
238
+ if (escaped) {
239
+ escaped = false;
240
+ emitOpaque(ch);
203
241
  } else if (ch === "\\") {
204
242
  escaped = true;
243
+ emitOpaque(ch);
205
244
  } else if (ch === quote) {
206
245
  quote = null;
246
+ emitCode(ch);
247
+ } else {
248
+ emitOpaque(ch);
207
249
  }
208
250
  i += 1;
209
251
  continue;
210
252
  }
211
253
  if (ch === "'" || ch === '"' || ch === "`") {
212
254
  quote = ch;
213
- out += ch;
255
+ emitCode(ch);
214
256
  i += 1;
215
257
  continue;
216
258
  }
217
259
  if (ch === "/" && next === "/") {
218
260
  out += " ";
261
+ ctx.push(" ");
262
+ ctx.push(" ");
219
263
  i += 2;
220
264
  while (i < source.length && source[i] !== "\n" && source[i] !== "\r") {
221
265
  out += " ";
266
+ ctx.push(" ");
222
267
  i += 1;
223
268
  }
224
269
  continue;
225
270
  }
226
271
  if (ch === "/" && next === "*") {
227
272
  out += " ";
273
+ ctx.push(" ");
274
+ ctx.push(" ");
228
275
  i += 2;
229
276
  while (i < source.length) {
230
277
  const blockCh = source[i] ?? "";
231
278
  const blockNext = source[i + 1] ?? "";
232
279
  if (blockCh === "*" && blockNext === "/") {
233
280
  out += " ";
281
+ ctx.push(" ");
282
+ ctx.push(" ");
234
283
  i += 2;
235
284
  break;
236
285
  }
237
- out += blockCh === "\n" || blockCh === "\r" ? blockCh : " ";
286
+ const kept = blockCh === "\n" || blockCh === "\r" ? blockCh : " ";
287
+ out += kept;
288
+ ctx.push(kept);
238
289
  i += 1;
239
290
  }
240
291
  continue;
241
292
  }
293
+ if (ch === "/" && ctx.startsRegexLiteral()) {
294
+ inRegex = true;
295
+ emitCode(ch);
296
+ i += 1;
297
+ continue;
298
+ }
299
+ emitCode(ch);
300
+ i += 1;
301
+ }
302
+ return { out, balanced: quote === null && !inRegex && !regexMisread };
303
+ }
304
+ function stripJsComments(source) {
305
+ return scanJsComments(source).out;
306
+ }
307
+ function stripJsCode(source) {
308
+ const { out, balanced } = scanJsComments(source);
309
+ return balanced ? stripJsStringLiterals(out) : source;
310
+ }
311
+ var REGEX_ALLOWED_BEFORE = new Set("=(,:[!&|?{};+-*%^~<>");
312
+ var REGEX_ALLOWED_KEYWORDS = /* @__PURE__ */ new Set([
313
+ "return",
314
+ "typeof",
315
+ "instanceof",
316
+ "in",
317
+ "of",
318
+ "new",
319
+ "delete",
320
+ "void",
321
+ "case",
322
+ "do",
323
+ "else",
324
+ "yield",
325
+ "await"
326
+ ]);
327
+ var WORD_CHAR = /[A-Za-z0-9_$]/;
328
+ var CodeContext = class {
329
+ last = "";
330
+ prev = "";
331
+ word = "";
332
+ wordEnded = false;
333
+ wordAfterDot = false;
334
+ push(ch) {
335
+ if (ch === " " || ch === " " || ch === "\n" || ch === "\r") {
336
+ this.wordEnded = true;
337
+ return;
338
+ }
339
+ if (WORD_CHAR.test(ch)) {
340
+ if (this.wordEnded || this.word === "") this.wordAfterDot = this.last === ".";
341
+ this.word = this.wordEnded ? ch : this.word + ch;
342
+ } else {
343
+ this.word = "";
344
+ this.wordAfterDot = false;
345
+ }
346
+ this.wordEnded = false;
347
+ this.prev = this.last;
348
+ this.last = ch;
349
+ }
350
+ startsRegexLiteral() {
351
+ if (this.last === "") return true;
352
+ if (WORD_CHAR.test(this.last))
353
+ return !this.wordAfterDot && REGEX_ALLOWED_KEYWORDS.has(this.word);
354
+ if ((this.last === "+" || this.last === "-") && this.prev === this.last) return false;
355
+ return REGEX_ALLOWED_BEFORE.has(this.last);
356
+ }
357
+ };
358
+ function stripJsStringLiterals(source) {
359
+ let out = "";
360
+ let i = 0;
361
+ const templateBraces = [];
362
+ const ctx = new CodeContext();
363
+ let quote = null;
364
+ let escaped = false;
365
+ let inRegex = false;
366
+ let inRegexClass = false;
367
+ let regexMisread = false;
368
+ const blank = (ch) => ch === "\n" || ch === "\r" ? ch : " ";
369
+ const emit = (text) => {
370
+ out += text;
371
+ for (const ch of text) ctx.push(ch);
372
+ };
373
+ while (i < source.length) {
374
+ const ch = source[i] ?? "";
375
+ const next = source[i + 1] ?? "";
376
+ if (inRegex) {
377
+ if (escaped) {
378
+ escaped = false;
379
+ if (ch === "\n" || ch === "\r") {
380
+ inRegex = false;
381
+ inRegexClass = false;
382
+ regexMisread = true;
383
+ }
384
+ emit(blank(ch));
385
+ } else if (ch === "\\") {
386
+ escaped = true;
387
+ emit(" ");
388
+ } else if (ch === "[") {
389
+ inRegexClass = true;
390
+ emit(" ");
391
+ } else if (ch === "]") {
392
+ inRegexClass = false;
393
+ emit(" ");
394
+ } else if (ch === "/" && !inRegexClass) {
395
+ inRegex = false;
396
+ emit(ch);
397
+ } else if (ch === "\n" || ch === "\r") {
398
+ inRegex = false;
399
+ inRegexClass = false;
400
+ escaped = false;
401
+ regexMisread = true;
402
+ emit(ch);
403
+ } else {
404
+ emit(" ");
405
+ }
406
+ i += 1;
407
+ continue;
408
+ }
409
+ if (quote) {
410
+ if (escaped) {
411
+ escaped = false;
412
+ emit(blank(ch));
413
+ } else if (ch === "\\") {
414
+ escaped = true;
415
+ emit(" ");
416
+ } else if (ch === quote) {
417
+ quote = null;
418
+ emit(ch);
419
+ } else if (ch === "`" || quote !== "`" || ch !== "$" || next !== "{") {
420
+ emit(blank(ch));
421
+ } else {
422
+ templateBraces.push(0);
423
+ quote = null;
424
+ emit("${");
425
+ i += 2;
426
+ continue;
427
+ }
428
+ i += 1;
429
+ continue;
430
+ }
431
+ if (ch === "'" || ch === '"' || ch === "`") {
432
+ quote = ch;
433
+ emit(ch);
434
+ i += 1;
435
+ continue;
436
+ }
437
+ if (ch === "/" && next !== "/" && next !== "*" && ctx.startsRegexLiteral()) {
438
+ inRegex = true;
439
+ emit(ch);
440
+ i += 1;
441
+ continue;
442
+ }
443
+ if (templateBraces.length > 0) {
444
+ const depth = templateBraces[templateBraces.length - 1] ?? 0;
445
+ if (ch === "{") templateBraces[templateBraces.length - 1] = depth + 1;
446
+ else if (ch === "}") {
447
+ if (depth === 0) {
448
+ templateBraces.pop();
449
+ quote = "`";
450
+ emit(ch);
451
+ i += 1;
452
+ continue;
453
+ }
454
+ templateBraces[templateBraces.length - 1] = depth - 1;
455
+ }
456
+ }
457
+ emit(ch);
458
+ i += 1;
459
+ }
460
+ if (quote !== null || templateBraces.length > 0 || inRegex || regexMisread) return source;
461
+ return out;
462
+ }
463
+ function stripCssComments(source) {
464
+ let out = "";
465
+ let i = 0;
466
+ let quote = null;
467
+ while (i < source.length) {
468
+ const ch = source[i] ?? "";
469
+ if (quote) {
470
+ out += ch;
471
+ if (ch === "\\") {
472
+ out += source[i + 1] ?? "";
473
+ i += 2;
474
+ continue;
475
+ }
476
+ if (ch === quote) quote = null;
477
+ i += 1;
478
+ continue;
479
+ }
480
+ if (ch === '"' || ch === "'") {
481
+ quote = ch;
482
+ out += ch;
483
+ i += 1;
484
+ continue;
485
+ }
486
+ if (ch === "/" && source[i + 1] === "*") {
487
+ const end = source.indexOf("*/", i + 2);
488
+ const stop = end === -1 ? source.length : end + 2;
489
+ for (let j = i; j < stop; j += 1) {
490
+ const c = source[j] ?? "";
491
+ out += c === "\n" || c === "\r" ? c : " ";
492
+ }
493
+ i = stop;
494
+ continue;
495
+ }
242
496
  out += ch;
243
497
  i += 1;
244
498
  }
@@ -803,6 +1057,12 @@ var VIDEO_SRC_EXT = /* @__PURE__ */ new Set([
803
1057
  "mpg",
804
1058
  "mpeg"
805
1059
  ]);
1060
+ var AUDIO_SRC_EXT = /* @__PURE__ */ new Set(["mp3", "wav", "aac", "flac", "opus", "aiff", "wma"]);
1061
+ var SRC_KIND_NOUN = {
1062
+ image: "an image",
1063
+ video: "a video",
1064
+ audio: "an audio file"
1065
+ };
806
1066
  function srcKind(src) {
807
1067
  const stripped = src.trim();
808
1068
  if (!stripped) return null;
@@ -812,6 +1072,7 @@ function srcKind(src) {
812
1072
  if (!mime) return null;
813
1073
  if (mime.startsWith("image/")) return "image";
814
1074
  if (mime.startsWith("video/")) return "video";
1075
+ if (mime.startsWith("audio/")) return "audio";
815
1076
  return null;
816
1077
  }
817
1078
  if (lower.startsWith("blob:")) return null;
@@ -831,6 +1092,7 @@ function srcKind(src) {
831
1092
  const ext = base.slice(dot + 1).toLowerCase();
832
1093
  if (IMAGE_SRC_EXT.has(ext)) return "image";
833
1094
  if (VIDEO_SRC_EXT.has(ext)) return "video";
1095
+ if (AUDIO_SRC_EXT.has(ext)) return "audio";
834
1096
  return null;
835
1097
  }
836
1098
  function findMediaSrcKindMismatchFindings(ctx) {
@@ -841,16 +1103,15 @@ function findMediaSrcKindMismatchFindings(ctx) {
841
1103
  if (!src) continue;
842
1104
  const kind = srcKind(src);
843
1105
  if (kind === null) continue;
844
- if (tag.name === "video" && kind !== "image") continue;
845
- if (tag.name === "img" && kind !== "video") continue;
846
- const elementId = readAttr(tag.raw, "id") || void 0;
847
1106
  const expected = tag.name === "video" ? "video" : "image";
1107
+ if (kind === expected) continue;
1108
+ const elementId = readAttr(tag.raw, "id") || void 0;
848
1109
  findings.push({
849
1110
  code: "media_src_kind_mismatch",
850
1111
  severity: "error",
851
- message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> src is a ${kind}, not a ${expected}. The producer fail-closes when the tag and file kind disagree.`,
1112
+ message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> src is ${SRC_KIND_NOUN[kind]}, not ${SRC_KIND_NOUN[expected]}. The producer fail-closes when the tag and file kind disagree.`,
852
1113
  elementId,
853
- fixHint: tag.name === "video" ? "Use <img> for a still, or point <video> at a video URL (mp4/webm/mov/\u2026)." : "Use <video> for a video URL, or point <img> at a still (png/jpg/webp/\u2026).",
1114
+ fixHint: tag.name === "video" ? "Use <img> for a still, <audio> for sound, or point <video> at a video URL (mp4/webm/mov/\u2026)." : "Use <video> for a video URL, <audio> for sound, or point <img> at a still (png/jpg/webp/\u2026).",
854
1115
  snippet: truncateSnippet(tag.raw)
855
1116
  });
856
1117
  }
@@ -1635,6 +1896,14 @@ function isHiddenGsapState(values) {
1635
1896
  const display = stringValue(values.display)?.toLowerCase();
1636
1897
  return zeroValue(values.opacity) || zeroValue(values.autoAlpha) || visibility === "hidden" || display === "none";
1637
1898
  }
1899
+ function hiddenSetTargetSelectors(target, aliases) {
1900
+ const parts = target.startsWith("[") && target.endsWith("]") ? target.slice(1, -1).split(",") : [target];
1901
+ return parts.flatMap((part) => {
1902
+ const trimmed = part.trim();
1903
+ const resolved = /^(["'`])([^"'`]+)\1$/.exec(trimmed)?.[2] ?? aliases.get(trimmed);
1904
+ return resolved === void 0 ? [] : resolved.split(",");
1905
+ }).map((selector) => selector.trim()).filter((selector) => selector.length > 0);
1906
+ }
1638
1907
  function extractStandaloneHiddenSelectors(script) {
1639
1908
  const selectors = /* @__PURE__ */ new Set();
1640
1909
  const source = stripJsComments(script);
@@ -1645,16 +1914,15 @@ function extractStandaloneHiddenSelectors(script) {
1645
1914
  )) {
1646
1915
  aliases.set(match2[1] ?? "", match2[3] ?? "");
1647
1916
  }
1648
- const pattern = /gsap\.set\s*\(\s*([^,]+?)\s*,\s*\{([\s\S]*?)\}\s*\)/g;
1917
+ const pattern = /gsap\.set\s*\(\s*(\[[^[\]]*\]|"[^"]*"|'[^']*'|`[^`]*`|[^,()[\]]+?)\s*,\s*\{([\s\S]*?)\}\s*\)/g;
1649
1918
  let match;
1650
1919
  while ((match = pattern.exec(source)) !== null) {
1651
1920
  if (indexInsideNonIifeRange(match.index, source, functionRanges)) continue;
1652
- const target = (match[1] ?? "").trim();
1653
- const selector = /^(["'`])([^"'`]+)\1$/.exec(target)?.[2] ?? aliases.get(target);
1654
- if (!selector) continue;
1921
+ const targets = hiddenSetTargetSelectors((match[1] ?? "").trim(), aliases);
1922
+ if (targets.length === 0) continue;
1655
1923
  const body = match[2] ?? "";
1656
1924
  if (/(?:opacity|autoAlpha)\s*:\s*0(?:\.0+)?\s*(?:,|$)/.test(body)) {
1657
- selectors.add(selector);
1925
+ for (const selector of targets) selectors.add(selector);
1658
1926
  }
1659
1927
  }
1660
1928
  return selectors;
@@ -2234,6 +2502,9 @@ var gsapRules = [
2234
2502
  // fallow-ignore-next-line complexity
2235
2503
  async ({ source, tags, scripts, styles, rootCompositionId }) => {
2236
2504
  const findings = [];
2505
+ const authoredHiddenSelectors = new Set(
2506
+ scripts.flatMap((script) => [...extractStandaloneHiddenSelectors(script.content)])
2507
+ );
2237
2508
  const clipIds = /* @__PURE__ */ new Map();
2238
2509
  const clipClasses = /* @__PURE__ */ new Map();
2239
2510
  for (const tag of tags) {
@@ -2326,9 +2597,10 @@ ${right.raw}`)
2326
2597
  );
2327
2598
  }).sort((a, b) => a.position - b.position);
2328
2599
  const startsHiddenAtZero = visibilityWindows.some(
2329
- (win) => win.position <= SCENE_BOUNDARY_EPSILON_SECONDS && isHiddenGsapState(win.propertyValues)
2600
+ (win) => win.position <= SCENE_BOUNDARY_EPSILON_SECONDS && (isHiddenGsapState(win.propertyValues) || win.fromPropertyValues !== void 0 && isHiddenGsapState(win.fromPropertyValues))
2330
2601
  );
2331
2602
  if (startsHiddenAtZero) continue;
2603
+ if (selectors.some((selector2) => authoredHiddenSelectors.has(selector2))) continue;
2332
2604
  const firstVisible = visibilityWindows.find((win) => makesOverlayVisible(win));
2333
2605
  if (!firstVisible) continue;
2334
2606
  const selector = selectors.find(
@@ -3796,8 +4068,8 @@ var compositionRules = [
3796
4068
  });
3797
4069
  }
3798
4070
  };
3799
- for (const style of styles) scan(style.content);
3800
- for (const script of scripts) scan(script.content);
4071
+ for (const style of styles) scan(stripCssComments(style.content));
4072
+ for (const script of scripts) scan(stripJsComments(script.content));
3801
4073
  return findings;
3802
4074
  },
3803
4075
  // template_literal_selector
@@ -3805,14 +4077,17 @@ var compositionRules = [
3805
4077
  const findings = [];
3806
4078
  for (const script of scripts) {
3807
4079
  const templateLiteralSelectorPattern = /(?:querySelector|querySelectorAll)\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*`\s*\)/g;
4080
+ const scanned = stripJsCode(script.content);
3808
4081
  let tlMatch;
3809
- while ((tlMatch = templateLiteralSelectorPattern.exec(script.content)) !== null) {
4082
+ while ((tlMatch = templateLiteralSelectorPattern.exec(scanned)) !== null) {
3810
4083
  findings.push({
3811
4084
  code: "template_literal_selector",
3812
4085
  severity: "error",
3813
4086
  message: "querySelector uses a template literal variable (e.g. `${compId}`). The HTML bundler's CSS parser crashes on these. Use a hardcoded string instead.",
3814
4087
  fixHint: "Replace the template literal variable with a hardcoded string. The bundler's CSS parser cannot handle interpolated variables in script content.",
3815
- snippet: truncateSnippet(tlMatch[0])
4088
+ snippet: truncateSnippet(
4089
+ script.content.slice(tlMatch.index, tlMatch.index + tlMatch[0].length)
4090
+ )
3816
4091
  });
3817
4092
  }
3818
4093
  }
@@ -3914,7 +4189,7 @@ var compositionRules = [
3914
4189
  if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
3915
4190
  const findings = [];
3916
4191
  for (const script of scripts) {
3917
- const stripped = stripJsComments(script.content);
4192
+ const stripped = stripJsCode(script.content);
3918
4193
  if (/requestAnimationFrame\s*\(/.test(stripped)) {
3919
4194
  findings.push({
3920
4195
  code: "requestanimationframe_in_composition",
@@ -4566,7 +4841,7 @@ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
4566
4841
  "unset",
4567
4842
  "revert"
4568
4843
  ]);
4569
- function stripCssComments(css) {
4844
+ function stripCssComments2(css) {
4570
4845
  return css.replace(/\/\*[\s\S]*?\*\//g, " ");
4571
4846
  }
4572
4847
  function extractFontFaceFamilies(styles) {
@@ -4574,7 +4849,7 @@ function extractFontFaceFamilies(styles) {
4574
4849
  const fontFaceRe = /@font-face\s*\{[^}]*\}/gi;
4575
4850
  const familyRe = /font-family\s*:\s*(['"]?)([^;'"]+)\1/i;
4576
4851
  for (const style of styles) {
4577
- const content = stripCssComments(style.content);
4852
+ const content = stripCssComments2(style.content);
4578
4853
  let match;
4579
4854
  while ((match = fontFaceRe.exec(content)) !== null) {
4580
4855
  const familyMatch = match[0].match(familyRe);
@@ -4595,7 +4870,7 @@ function extractUsedFontFamilies(styles) {
4595
4870
  const seen = /* @__PURE__ */ new Set();
4596
4871
  const propRe = /font-family\s*:\s*([^;}{]+)/gi;
4597
4872
  for (const style of styles) {
4598
- const withoutFontFace = stripCssComments(style.content).replace(/@font-face\s*\{[^}]*\}/gi, "");
4873
+ const withoutFontFace = stripCssComments2(style.content).replace(/@font-face\s*\{[^}]*\}/gi, "");
4599
4874
  let match;
4600
4875
  while ((match = propRe.exec(withoutFontFace)) !== null) {
4601
4876
  for (const part of match[1].split(",")) {