@hyperframes/lint 0.7.59 → 0.7.60
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 +97 -0
- package/dist/browser.js.map +1 -1
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/browser.js
CHANGED
|
@@ -2421,6 +2421,27 @@ var MAX_COMPOSITION_LINES = 300;
|
|
|
2421
2421
|
var MAX_TIMED_ELEMENTS_PER_TRACK = 3;
|
|
2422
2422
|
var TRACK_DENSITY_EXEMPT_TAGS = /* @__PURE__ */ new Set(["audio", "script", "style", "video"]);
|
|
2423
2423
|
var CAPTION_CUE_TOKEN = /^(?:caption(?:[-_](?:group|word|line|block|cue|text))?|subtitle(?:[-_](?:group|line|cue|text))?|cg-.+)$/i;
|
|
2424
|
+
var HEAVY_OVERLAY_ELEMENT_COUNT_WARN = 25;
|
|
2425
|
+
var HEAVY_OVERLAY_EXEMPT_TAGS = /* @__PURE__ */ new Set([
|
|
2426
|
+
"audio",
|
|
2427
|
+
"body",
|
|
2428
|
+
"br",
|
|
2429
|
+
"defs",
|
|
2430
|
+
"head",
|
|
2431
|
+
"hr",
|
|
2432
|
+
"html",
|
|
2433
|
+
"link",
|
|
2434
|
+
"meta",
|
|
2435
|
+
"script",
|
|
2436
|
+
"source",
|
|
2437
|
+
"style",
|
|
2438
|
+
"template",
|
|
2439
|
+
"title",
|
|
2440
|
+
"use",
|
|
2441
|
+
"video"
|
|
2442
|
+
]);
|
|
2443
|
+
var HEAVY_OVERLAY_CSS_PATTERN = /(?:filter\s*:[^;}]*\bblur\s*\()|(?:clip-path\s*:(?!\s*(?:none|inherit|initial|unset)\b)\s*[^;}]+)|(?:radial-gradient\s*\()/i;
|
|
2444
|
+
var INLINE_STYLE_DISPLAY_NONE_PATTERN = /(?:^|;)\s*display\s*:\s*none\b/i;
|
|
2424
2445
|
var OVERLAP_EPSILON_SECONDS = 1e-6;
|
|
2425
2446
|
function countPhysicalLines(source) {
|
|
2426
2447
|
if (source.length === 0) return 0;
|
|
@@ -2479,6 +2500,33 @@ function leftmostCompoundClasses(selector) {
|
|
|
2479
2500
|
const leftmost = selector.trim().split(/[\s>+~]+/)[0] ?? "";
|
|
2480
2501
|
return (leftmost.match(/\.([\w-]+)/g) ?? []).map((c) => c.slice(1));
|
|
2481
2502
|
}
|
|
2503
|
+
function leftmostCompoundId(selector) {
|
|
2504
|
+
const leftmost = selector.trim().split(/[\s>+~]+/)[0] ?? "";
|
|
2505
|
+
return leftmost.match(/#([\w-]+)/)?.[1] ?? null;
|
|
2506
|
+
}
|
|
2507
|
+
function collectHeavyOverlayHooks(styles) {
|
|
2508
|
+
const classes = /* @__PURE__ */ new Set();
|
|
2509
|
+
const ids = /* @__PURE__ */ new Set();
|
|
2510
|
+
for (const style of styles) {
|
|
2511
|
+
const noComments = style.content.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
2512
|
+
const ruleWithBody = /([^{}]+)\{([^{}]*)\}/g;
|
|
2513
|
+
let m;
|
|
2514
|
+
while ((m = ruleWithBody.exec(noComments)) !== null) {
|
|
2515
|
+
const header = (m[1] ?? "").trim();
|
|
2516
|
+
const body = m[2] ?? "";
|
|
2517
|
+
if (!header || header.startsWith("@")) continue;
|
|
2518
|
+
if (!HEAVY_OVERLAY_CSS_PATTERN.test(body)) continue;
|
|
2519
|
+
for (const sel of header.split(",")) {
|
|
2520
|
+
const trimmed = sel.trim();
|
|
2521
|
+
if (!trimmed) continue;
|
|
2522
|
+
for (const cls of leftmostCompoundClasses(trimmed)) classes.add(cls);
|
|
2523
|
+
const idToken = leftmostCompoundId(trimmed);
|
|
2524
|
+
if (idToken) ids.add(idToken);
|
|
2525
|
+
}
|
|
2526
|
+
}
|
|
2527
|
+
}
|
|
2528
|
+
return { classes, ids };
|
|
2529
|
+
}
|
|
2482
2530
|
function rootClassStyledSelectors(styles, rootClasses) {
|
|
2483
2531
|
const offenders = [];
|
|
2484
2532
|
for (const style of styles) {
|
|
@@ -3229,6 +3277,55 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
3229
3277
|
];
|
|
3230
3278
|
}
|
|
3231
3279
|
return [];
|
|
3280
|
+
},
|
|
3281
|
+
// composition_heavy_overlay_count_high
|
|
3282
|
+
// Field signal ts=1784040753 (#hyperframes-cli-feedback): a composition
|
|
3283
|
+
// with ~40 heavy overlay DOM elements — `filter:blur`, oversized
|
|
3284
|
+
// `radial-gradient`, and `clip-path` animations — captures solid-black for
|
|
3285
|
+
// the first ~half of the render, recovering near the end. Reproduces
|
|
3286
|
+
// identically via drawElement AND forced --no-browser-gpu screenshot
|
|
3287
|
+
// capture AND `snapshot`, so the offender is the capture layer itself, not
|
|
3288
|
+
// encoder/mux. Independent of duration (padding the timeline grows the bad
|
|
3289
|
+
// zone proportionally, doesn't shift it). Reporter's workaround was to
|
|
3290
|
+
// split into per-transition mini compositions + FFmpeg concat.
|
|
3291
|
+
//
|
|
3292
|
+
// Presence alone matters: opacity:0 and visibility:hidden overlays still
|
|
3293
|
+
// contribute to the capture-layer regression, so they're counted-in. The
|
|
3294
|
+
// only escape hatch is `display: none` — an element removed from the render
|
|
3295
|
+
// tree can't feed the compositor. Warn at 25, well below the observed
|
|
3296
|
+
// 40-element repro, to give authors lead time before hitting the bug.
|
|
3297
|
+
// fallow-ignore-next-line complexity
|
|
3298
|
+
({ tags, styles, rawSource, options }) => {
|
|
3299
|
+
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
|
|
3300
|
+
const { classes: heavyClassTokens, ids: heavyIds } = collectHeavyOverlayHooks(styles);
|
|
3301
|
+
let heavyCount = 0;
|
|
3302
|
+
for (const tag of tags) {
|
|
3303
|
+
if (HEAVY_OVERLAY_EXEMPT_TAGS.has(tag.name)) continue;
|
|
3304
|
+
if (isCompositionRootOrMount(tag.raw)) continue;
|
|
3305
|
+
const styleAttr = readJsonAttr(tag.raw, "style") ?? "";
|
|
3306
|
+
if (styleAttr && INLINE_STYLE_DISPLAY_NONE_PATTERN.test(styleAttr)) continue;
|
|
3307
|
+
let heavy = false;
|
|
3308
|
+
if (styleAttr && HEAVY_OVERLAY_CSS_PATTERN.test(styleAttr)) heavy = true;
|
|
3309
|
+
if (!heavy && (heavyClassTokens.size > 0 || heavyIds.size > 0)) {
|
|
3310
|
+
const classList = (readAttr(tag.raw, "class") || "").split(/\s+/).filter(Boolean);
|
|
3311
|
+
if (classList.some((cls) => heavyClassTokens.has(cls))) heavy = true;
|
|
3312
|
+
if (!heavy) {
|
|
3313
|
+
const idValue = readAttr(tag.raw, "id");
|
|
3314
|
+
if (idValue && heavyIds.has(idValue)) heavy = true;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
if (heavy) heavyCount += 1;
|
|
3318
|
+
}
|
|
3319
|
+
if (heavyCount < HEAVY_OVERLAY_ELEMENT_COUNT_WARN) return [];
|
|
3320
|
+
const splitTarget = options.isSubComposition ? "Split this sub-composition further into per-transition mini-compositions" : "Split coherent scenes / transitions into separate .html files under compositions/";
|
|
3321
|
+
return [
|
|
3322
|
+
{
|
|
3323
|
+
code: "composition_heavy_overlay_count_high",
|
|
3324
|
+
severity: "warning",
|
|
3325
|
+
message: `This composition has ${heavyCount} elements carrying "heavy overlay" CSS (filter:blur, radial-gradient, or clip-path). Field signal: a composition with ~40 such elements \u2014 including opacity:0 / visibility:hidden ones \u2014 captures solid-black for the first ~half of the render, recovering near the end. Reproduces identically via drawElement, forced screenshot capture, and snapshot, so the capture layer itself is the offender (not encoder/mux). Independent of duration. Presence alone matters; only display:none elements are excluded here.`,
|
|
3326
|
+
fixHint: `${splitTarget} and concat the pieces (FFmpeg or the runtime's slideshow) so each capture only sees a small subset of heavy overlays at once. Even hidden overlays (opacity:0 / visibility:hidden) contribute \u2014 either remove truly unused ones from the source or scope them into their own per-transition sub-composition. If an overlay is genuinely inert for the whole clip, use display:none so it never enters the render tree. Field ref ts=1784040753 (#hyperframes-cli-feedback).`
|
|
3327
|
+
}
|
|
3328
|
+
];
|
|
3232
3329
|
}
|
|
3233
3330
|
];
|
|
3234
3331
|
|