@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/index.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
+ }
203
220
  } else if (ch === "\\") {
204
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);
241
+ } else if (ch === "\\") {
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
  }
@@ -806,6 +1060,12 @@ var VIDEO_SRC_EXT = /* @__PURE__ */ new Set([
806
1060
  "mpg",
807
1061
  "mpeg"
808
1062
  ]);
1063
+ var AUDIO_SRC_EXT = /* @__PURE__ */ new Set(["mp3", "wav", "aac", "flac", "opus", "aiff", "wma"]);
1064
+ var SRC_KIND_NOUN = {
1065
+ image: "an image",
1066
+ video: "a video",
1067
+ audio: "an audio file"
1068
+ };
809
1069
  function srcKind(src) {
810
1070
  const stripped = src.trim();
811
1071
  if (!stripped) return null;
@@ -815,6 +1075,7 @@ function srcKind(src) {
815
1075
  if (!mime) return null;
816
1076
  if (mime.startsWith("image/")) return "image";
817
1077
  if (mime.startsWith("video/")) return "video";
1078
+ if (mime.startsWith("audio/")) return "audio";
818
1079
  return null;
819
1080
  }
820
1081
  if (lower.startsWith("blob:")) return null;
@@ -834,6 +1095,7 @@ function srcKind(src) {
834
1095
  const ext = base.slice(dot + 1).toLowerCase();
835
1096
  if (IMAGE_SRC_EXT.has(ext)) return "image";
836
1097
  if (VIDEO_SRC_EXT.has(ext)) return "video";
1098
+ if (AUDIO_SRC_EXT.has(ext)) return "audio";
837
1099
  return null;
838
1100
  }
839
1101
  function findMediaSrcKindMismatchFindings(ctx) {
@@ -844,16 +1106,15 @@ function findMediaSrcKindMismatchFindings(ctx) {
844
1106
  if (!src) continue;
845
1107
  const kind = srcKind(src);
846
1108
  if (kind === null) continue;
847
- if (tag.name === "video" && kind !== "image") continue;
848
- if (tag.name === "img" && kind !== "video") continue;
849
- const elementId = readAttr(tag.raw, "id") || void 0;
850
1109
  const expected = tag.name === "video" ? "video" : "image";
1110
+ if (kind === expected) continue;
1111
+ const elementId = readAttr(tag.raw, "id") || void 0;
851
1112
  findings.push({
852
1113
  code: "media_src_kind_mismatch",
853
1114
  severity: "error",
854
- message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> src is a ${kind}, not a ${expected}. The producer fail-closes when the tag and file kind disagree.`,
1115
+ 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.`,
855
1116
  elementId,
856
- 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).",
1117
+ 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).",
857
1118
  snippet: truncateSnippet(tag.raw)
858
1119
  });
859
1120
  }
@@ -1638,6 +1899,14 @@ function isHiddenGsapState(values) {
1638
1899
  const display = stringValue(values.display)?.toLowerCase();
1639
1900
  return zeroValue(values.opacity) || zeroValue(values.autoAlpha) || visibility === "hidden" || display === "none";
1640
1901
  }
1902
+ function hiddenSetTargetSelectors(target, aliases) {
1903
+ const parts = target.startsWith("[") && target.endsWith("]") ? target.slice(1, -1).split(",") : [target];
1904
+ return parts.flatMap((part) => {
1905
+ const trimmed = part.trim();
1906
+ const resolved = /^(["'`])([^"'`]+)\1$/.exec(trimmed)?.[2] ?? aliases.get(trimmed);
1907
+ return resolved === void 0 ? [] : resolved.split(",");
1908
+ }).map((selector) => selector.trim()).filter((selector) => selector.length > 0);
1909
+ }
1641
1910
  function extractStandaloneHiddenSelectors(script) {
1642
1911
  const selectors = /* @__PURE__ */ new Set();
1643
1912
  const source = stripJsComments(script);
@@ -1648,16 +1917,15 @@ function extractStandaloneHiddenSelectors(script) {
1648
1917
  )) {
1649
1918
  aliases.set(match2[1] ?? "", match2[3] ?? "");
1650
1919
  }
1651
- const pattern = /gsap\.set\s*\(\s*([^,]+?)\s*,\s*\{([\s\S]*?)\}\s*\)/g;
1920
+ const pattern = /gsap\.set\s*\(\s*(\[[^[\]]*\]|"[^"]*"|'[^']*'|`[^`]*`|[^,()[\]]+?)\s*,\s*\{([\s\S]*?)\}\s*\)/g;
1652
1921
  let match;
1653
1922
  while ((match = pattern.exec(source)) !== null) {
1654
1923
  if (indexInsideNonIifeRange(match.index, source, functionRanges)) continue;
1655
- const target = (match[1] ?? "").trim();
1656
- const selector = /^(["'`])([^"'`]+)\1$/.exec(target)?.[2] ?? aliases.get(target);
1657
- if (!selector) continue;
1924
+ const targets = hiddenSetTargetSelectors((match[1] ?? "").trim(), aliases);
1925
+ if (targets.length === 0) continue;
1658
1926
  const body = match[2] ?? "";
1659
1927
  if (/(?:opacity|autoAlpha)\s*:\s*0(?:\.0+)?\s*(?:,|$)/.test(body)) {
1660
- selectors.add(selector);
1928
+ for (const selector of targets) selectors.add(selector);
1661
1929
  }
1662
1930
  }
1663
1931
  return selectors;
@@ -2237,6 +2505,9 @@ var gsapRules = [
2237
2505
  // fallow-ignore-next-line complexity
2238
2506
  async ({ source, tags, scripts, styles, rootCompositionId }) => {
2239
2507
  const findings = [];
2508
+ const authoredHiddenSelectors = new Set(
2509
+ scripts.flatMap((script) => [...extractStandaloneHiddenSelectors(script.content)])
2510
+ );
2240
2511
  const clipIds = /* @__PURE__ */ new Map();
2241
2512
  const clipClasses = /* @__PURE__ */ new Map();
2242
2513
  for (const tag of tags) {
@@ -2329,9 +2600,10 @@ ${right.raw}`)
2329
2600
  );
2330
2601
  }).sort((a, b) => a.position - b.position);
2331
2602
  const startsHiddenAtZero = visibilityWindows.some(
2332
- (win) => win.position <= SCENE_BOUNDARY_EPSILON_SECONDS && isHiddenGsapState(win.propertyValues)
2603
+ (win) => win.position <= SCENE_BOUNDARY_EPSILON_SECONDS && (isHiddenGsapState(win.propertyValues) || win.fromPropertyValues !== void 0 && isHiddenGsapState(win.fromPropertyValues))
2333
2604
  );
2334
2605
  if (startsHiddenAtZero) continue;
2606
+ if (selectors.some((selector2) => authoredHiddenSelectors.has(selector2))) continue;
2335
2607
  const firstVisible = visibilityWindows.find((win) => makesOverlayVisible(win));
2336
2608
  if (!firstVisible) continue;
2337
2609
  const selector = selectors.find(
@@ -3799,8 +4071,8 @@ var compositionRules = [
3799
4071
  });
3800
4072
  }
3801
4073
  };
3802
- for (const style of styles) scan(style.content);
3803
- for (const script of scripts) scan(script.content);
4074
+ for (const style of styles) scan(stripCssComments(style.content));
4075
+ for (const script of scripts) scan(stripJsComments(script.content));
3804
4076
  return findings;
3805
4077
  },
3806
4078
  // template_literal_selector
@@ -3808,14 +4080,17 @@ var compositionRules = [
3808
4080
  const findings = [];
3809
4081
  for (const script of scripts) {
3810
4082
  const templateLiteralSelectorPattern = /(?:querySelector|querySelectorAll)\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*`\s*\)/g;
4083
+ const scanned = stripJsCode(script.content);
3811
4084
  let tlMatch;
3812
- while ((tlMatch = templateLiteralSelectorPattern.exec(script.content)) !== null) {
4085
+ while ((tlMatch = templateLiteralSelectorPattern.exec(scanned)) !== null) {
3813
4086
  findings.push({
3814
4087
  code: "template_literal_selector",
3815
4088
  severity: "error",
3816
4089
  message: "querySelector uses a template literal variable (e.g. `${compId}`). The HTML bundler's CSS parser crashes on these. Use a hardcoded string instead.",
3817
4090
  fixHint: "Replace the template literal variable with a hardcoded string. The bundler's CSS parser cannot handle interpolated variables in script content.",
3818
- snippet: truncateSnippet(tlMatch[0])
4091
+ snippet: truncateSnippet(
4092
+ script.content.slice(tlMatch.index, tlMatch.index + tlMatch[0].length)
4093
+ )
3819
4094
  });
3820
4095
  }
3821
4096
  }
@@ -3917,7 +4192,7 @@ var compositionRules = [
3917
4192
  if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
3918
4193
  const findings = [];
3919
4194
  for (const script of scripts) {
3920
- const stripped = stripJsComments(script.content);
4195
+ const stripped = stripJsCode(script.content);
3921
4196
  if (/requestAnimationFrame\s*\(/.test(stripped)) {
3922
4197
  findings.push({
3923
4198
  code: "requestanimationframe_in_composition",
@@ -4569,7 +4844,7 @@ var GENERIC_FAMILIES = /* @__PURE__ */ new Set([
4569
4844
  "unset",
4570
4845
  "revert"
4571
4846
  ]);
4572
- function stripCssComments(css) {
4847
+ function stripCssComments2(css) {
4573
4848
  return css.replace(/\/\*[\s\S]*?\*\//g, " ");
4574
4849
  }
4575
4850
  function extractFontFaceFamilies(styles) {
@@ -4577,7 +4852,7 @@ function extractFontFaceFamilies(styles) {
4577
4852
  const fontFaceRe = /@font-face\s*\{[^}]*\}/gi;
4578
4853
  const familyRe = /font-family\s*:\s*(['"]?)([^;'"]+)\1/i;
4579
4854
  for (const style of styles) {
4580
- const content = stripCssComments(style.content);
4855
+ const content = stripCssComments2(style.content);
4581
4856
  let match;
4582
4857
  while ((match = fontFaceRe.exec(content)) !== null) {
4583
4858
  const familyMatch = match[0].match(familyRe);
@@ -4598,7 +4873,7 @@ function extractUsedFontFamilies(styles) {
4598
4873
  const seen = /* @__PURE__ */ new Set();
4599
4874
  const propRe = /font-family\s*:\s*([^;}{]+)/gi;
4600
4875
  for (const style of styles) {
4601
- const withoutFontFace = stripCssComments(style.content).replace(/@font-face\s*\{[^}]*\}/gi, "");
4876
+ const withoutFontFace = stripCssComments2(style.content).replace(/@font-face\s*\{[^}]*\}/gi, "");
4602
4877
  let match;
4603
4878
  while ((match = propRe.exec(withoutFontFace)) !== null) {
4604
4879
  for (const part of match[1].split(",")) {