@hyperframes/lint 0.8.145 → 0.8.147
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 +45 -46
- package/dist/index.js +60 -62
- package/package.json +2 -2
- package/dist/browser.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -599,7 +599,10 @@ function truncateSnippet(value, maxLength = 220) {
|
|
|
599
599
|
return `${normalized2.slice(0, maxLength - 3)}...`;
|
|
600
600
|
}
|
|
601
601
|
function mediaSrcTagRe(tagAlternation) {
|
|
602
|
-
return new RegExp(`<(${tagAlternation})\\b[^>]*\\ssrc\\s*=\\s*["'
|
|
602
|
+
return new RegExp(`<(${tagAlternation})\\b[^>]*\\ssrc\\s*=\\s*("[^"]*"|'[^']*')[^>]*>`, "gi");
|
|
603
|
+
}
|
|
604
|
+
function mediaSrcOf(match) {
|
|
605
|
+
return readDecodedAttr(match[0], "src") ?? "";
|
|
603
606
|
}
|
|
604
607
|
|
|
605
608
|
// src/sourceLocations.ts
|
|
@@ -1080,7 +1083,7 @@ function describeRootDimensionsDrift(source, dataWidth, dataHeight) {
|
|
|
1080
1083
|
const declared = `Root composition declares data-width="${dataWidth}" data-height="${dataHeight}"`;
|
|
1081
1084
|
if (!bodyCssMismatch) {
|
|
1082
1085
|
return {
|
|
1083
|
-
message: `${declared}, but ${viewportMismatch}. The viewport meta has no effect on capture
|
|
1086
|
+
message: `${declared}, but ${viewportMismatch}. The viewport meta has no effect on capture (the renderer sizes the viewport from the root's own data-width/data-height), so this is stale metadata, not a clipping risk.`,
|
|
1084
1087
|
fixHint: "update the meta viewport to match, or scaffold with `hyperframes init --resolution portrait`"
|
|
1085
1088
|
};
|
|
1086
1089
|
}
|
|
@@ -1352,7 +1355,7 @@ var coreRules = [
|
|
|
1352
1355
|
findings.push({
|
|
1353
1356
|
code: "unclosed_tag_swallowed_element",
|
|
1354
1357
|
severity: "error",
|
|
1355
|
-
message: `<${tag.name}> is missing its closing \`>\` before the next
|
|
1358
|
+
message: `<${tag.name}> is missing its closing \`>\` before the next \`<\`, so the following element is swallowed as bogus attribute text and never becomes a real node.`,
|
|
1356
1359
|
fixHint: "Close the previous tag's `>` before opening the next element.",
|
|
1357
1360
|
snippet: truncateSnippet(tag.raw)
|
|
1358
1361
|
});
|
|
@@ -2067,7 +2070,7 @@ var mediaRules = [
|
|
|
2067
2070
|
severity: "error",
|
|
2068
2071
|
message: `<video${elementId ? ` id="${elementId}"` : ""}> has data-start but declares neither muted nor data-has-audio. If the file has sound, add data-has-audio="true" (the sound stays on this clip). If it is silent, add muted.`,
|
|
2069
2072
|
elementId,
|
|
2070
|
-
fixHint: 'Add data-has-audio="true" when the file has sound (recommended
|
|
2073
|
+
fixHint: 'Add data-has-audio="true" when the file has sound (recommended: it keeps picture and sound on one clip), or add `muted` for silent footage.',
|
|
2071
2074
|
snippet: truncateSnippet(tag.raw)
|
|
2072
2075
|
});
|
|
2073
2076
|
}
|
|
@@ -2152,7 +2155,7 @@ var mediaRules = [
|
|
|
2152
2155
|
severity: "error",
|
|
2153
2156
|
message: `Self-closing <${tagName}/> is invalid HTML. The browser will leave the tag open, swallowing all subsequent elements as invisible fallback content. This makes compositions INVISIBLE.`,
|
|
2154
2157
|
elementId,
|
|
2155
|
-
fixHint: `Change <${tagName} .../> to <${tagName} ...></${tagName}
|
|
2158
|
+
fixHint: `Change <${tagName} .../> to <${tagName} ...></${tagName}>. Media elements MUST have explicit closing tags.`,
|
|
2156
2159
|
snippet: truncateSnippet(scMatch[0])
|
|
2157
2160
|
});
|
|
2158
2161
|
}
|
|
@@ -2195,7 +2198,7 @@ var mediaRules = [
|
|
|
2195
2198
|
findings.push({
|
|
2196
2199
|
code: "base64_media_prohibited",
|
|
2197
2200
|
severity: "error",
|
|
2198
|
-
message: `Inline base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB)${isSuspicious ? "
|
|
2201
|
+
message: `Inline base64 audio/video detected (${(dataSize / 1024).toFixed(0)} KB)${isSuspicious ? ", likely fabricated data" : ""}. Base64 media is prohibited: it bloats file size and breaks rendering.`,
|
|
2199
2202
|
fixHint: "Use a relative path (assets/music.mp3) or HTTPS URL for the audio/video src. Never embed media as base64.",
|
|
2200
2203
|
snippet: truncateSnippet((b64Match[1] ?? "").slice(0, 80) + "...")
|
|
2201
2204
|
});
|
|
@@ -2224,7 +2227,7 @@ var mediaRules = [
|
|
|
2224
2227
|
findings.push({
|
|
2225
2228
|
code: "media_missing_id",
|
|
2226
2229
|
severity: "error",
|
|
2227
|
-
message: `<${tag.name}> has data-start but no id attribute. The renderer requires id to discover media elements
|
|
2230
|
+
message: `<${tag.name}> has data-start but no id attribute. The renderer requires id to discover media elements, so this ${tag.name === "audio" ? "audio will be SILENT" : "video will be FROZEN"} in renders.`,
|
|
2228
2231
|
fixHint: `Add a unique id attribute: <${tag.name} id="my-${tag.name}" ...>`,
|
|
2229
2232
|
snippet: truncateSnippet(tag.raw)
|
|
2230
2233
|
});
|
|
@@ -2281,7 +2284,7 @@ var mediaRules = [
|
|
|
2281
2284
|
findings.push({
|
|
2282
2285
|
code: "media_crossorigin_breaks_preview",
|
|
2283
2286
|
severity: "error",
|
|
2284
|
-
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
|
|
2287
|
+
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, which hides the bug.`,
|
|
2285
2288
|
elementId,
|
|
2286
2289
|
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.",
|
|
2287
2290
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -2315,7 +2318,7 @@ var mediaRules = [
|
|
|
2315
2318
|
findings.push({
|
|
2316
2319
|
code: "video_audio_double_source",
|
|
2317
2320
|
severity: "error",
|
|
2318
|
-
message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source at the same time. The unmuted video already provides audio
|
|
2321
|
+
message: `<audio${audioInfo.id ? ` id="${audioInfo.id}"` : ""}> and <video${videoInfo.id ? ` id="${videoInfo.id}"` : ""}> both point to the same source at the same time. The unmuted video already provides audio, so the duplicate <audio> will cause double playback and echo.`,
|
|
2319
2322
|
elementId: audioInfo.id,
|
|
2320
2323
|
fixHint: "Remove the <audio> element and let the video carry its own sound (recommended), or mute the video (add `muted`) and keep the separate <audio> when picture and sound must be cut independently.",
|
|
2321
2324
|
snippet: truncateSnippet(audioInfo.raw)
|
|
@@ -2351,7 +2354,7 @@ function findVolumeTweenOverridesGainFindings(ctx) {
|
|
|
2351
2354
|
findings.push({
|
|
2352
2355
|
code: "audio_volume_tween_overrides_gain",
|
|
2353
2356
|
severity: "warning",
|
|
2354
|
-
message: `#${id} has data-volume="${volume}" (${db}) and a GSAP tween on \`volume\`. Tween values are absolute
|
|
2357
|
+
message: `#${id} has data-volume="${volume}" (${db}) and a GSAP tween on \`volume\`. Tween values are absolute: they REPLACE this gain rather than scale it, so wherever the tween names a value the clip plays at that value, not at ${db}.`,
|
|
2355
2358
|
elementId: id,
|
|
2356
2359
|
fixHint: "Write the tween's targets in the same absolute gain (e.g. `volume: 1.95`, not `volume: 1`), or reset data-volume to 1 and let the tween carry the level on its own.",
|
|
2357
2360
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -2370,7 +2373,7 @@ function findVolumeDoubleAutomationFindings(ctx) {
|
|
|
2370
2373
|
findings.push({
|
|
2371
2374
|
code: "audio_volume_double_automation",
|
|
2372
2375
|
severity: "warning",
|
|
2373
|
-
message: `#${id} has both a volume automation lane and a GSAP tween on \`volume\`. The lane wins
|
|
2376
|
+
message: `#${id} has both a volume automation lane and a GSAP tween on \`volume\`. The lane wins, and the tween is ignored in preview and in the render.`,
|
|
2374
2377
|
elementId: id,
|
|
2375
2378
|
fixHint: "Keep one of them: delete the volume lane to go back to tweening, or drop the tween and shape the level in the automation lane.",
|
|
2376
2379
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -2418,7 +2421,7 @@ function findCarveUngroupedSourcesFindings(ctx) {
|
|
|
2418
2421
|
severity: "warning",
|
|
2419
2422
|
message: `${elementId ? `#${elementId}'s` : "This"} carve names ${clipIds.length} voice clips directly (${clipIds.join(", ")}) instead of a group.`,
|
|
2420
2423
|
elementId,
|
|
2421
|
-
fixHint: "Group the voice clips and carve against the group
|
|
2424
|
+
fixHint: "Group the voice clips and carve against the group. A hand-rolled clip list silently rots when a clip is added.",
|
|
2422
2425
|
snippet: truncateSnippet(tag.raw)
|
|
2423
2426
|
});
|
|
2424
2427
|
}
|
|
@@ -2467,7 +2470,7 @@ function findAudioGroupTimingAttrFindings(ctx) {
|
|
|
2467
2470
|
findings.push({
|
|
2468
2471
|
code: "audio_group_timing_attrs",
|
|
2469
2472
|
severity: "warning",
|
|
2470
|
-
message: `${elementId ? `#${elementId}` : "This audio group"} carries ${present.map((attr) => `\`${attr}\``).join(", ")}, which a bus has no use for
|
|
2473
|
+
message: `${elementId ? `#${elementId}` : "This audio group"} carries ${present.map((attr) => `\`${attr}\``).join(", ")}, which a bus has no use for: its members carry the timing and its automation clock is composition time.`,
|
|
2471
2474
|
elementId,
|
|
2472
2475
|
fixHint: `Remove ${present.map((attr) => `\`${attr}\``).join(", ")} from the group element.`,
|
|
2473
2476
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -2484,7 +2487,7 @@ function findAudioGroupCarveAttrFindings(ctx) {
|
|
|
2484
2487
|
findings.push({
|
|
2485
2488
|
code: "audio_group_carve_attr",
|
|
2486
2489
|
severity: "warning",
|
|
2487
|
-
message: `${elementId ? `#${elementId}` : "This audio group"} carries \`data-fx-carve\`, which belongs on the clip being carved
|
|
2490
|
+
message: `${elementId ? `#${elementId}` : "This audio group"} carries \`data-fx-carve\`, which belongs on the clip being carved. A bus has no audio of its own to level-match against, and a carve here stacks with any its members already have.`,
|
|
2488
2491
|
elementId,
|
|
2489
2492
|
fixHint: "Remove `data-fx-carve` and the `fromCarve` nodes it wrote into this bus's `data-fx-chain`, and carve the bed clip instead.",
|
|
2490
2493
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -3162,7 +3165,7 @@ var compositionRules = [
|
|
|
3162
3165
|
findings.push({
|
|
3163
3166
|
code: "root_composition_missing_html_wrapper",
|
|
3164
3167
|
severity: "error",
|
|
3165
|
-
message: "Composition starts with a bare element instead of a proper HTML document. An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> is a fragment
|
|
3168
|
+
message: "Composition starts with a bare element instead of a proper HTML document. An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> is a fragment: browsers quirks-mode it, the preview server cannot load it, and the bundler will fail to inject runtime scripts.",
|
|
3166
3169
|
fixHint: 'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.',
|
|
3167
3170
|
snippet: rootTag ? truncateSnippet(rootTag.raw) : void 0
|
|
3168
3171
|
});
|
|
@@ -3317,7 +3320,7 @@ var compositionRules = [
|
|
|
3317
3320
|
findings.push({
|
|
3318
3321
|
code: "unknown_variable_binding",
|
|
3319
3322
|
severity: "warning",
|
|
3320
|
-
message: `<${tag.name}> binds ${attr}="${id}" but no variable "${id}" is declared in data-composition-variables
|
|
3323
|
+
message: `<${tag.name}> binds ${attr}="${id}" but no variable "${id}" is declared in data-composition-variables, so the binding will silently keep the authored fallback.`,
|
|
3321
3324
|
fixHint: `Declare the variable on the composition root (<html>, or the [data-composition-id] root element for a template/fragment comp): data-composition-variables='[{"id":"${id}","type":"${attr === "data-var-src" ? "image" : "string"}","label":"${id}","default":"..."}]', or fix the binding id.`,
|
|
3322
3325
|
elementId: readAttr(tag.raw, "id") || void 0,
|
|
3323
3326
|
snippet: truncateSnippet(tag.raw)
|
|
@@ -3358,8 +3361,8 @@ var compositionRules = [
|
|
|
3358
3361
|
{
|
|
3359
3362
|
code: "html_dir_attribute_breaks_render",
|
|
3360
3363
|
severity: "error",
|
|
3361
|
-
message: `<html dir="${dir}"> renders correctly in preview/snapshot but produces a fully blank/black video from render
|
|
3362
|
-
fixHint: `Remove dir="${dir}" from <html>. Keep lang, and scope ${scopedDirection} to individual text-containing elements instead
|
|
3364
|
+
message: `<html dir="${dir}"> renders correctly in preview/snapshot but produces a fully blank/black video from render. This is a confirmed, silent failure.`,
|
|
3365
|
+
fixHint: `Remove dir="${dir}" from <html>. Keep lang, and scope ${scopedDirection} to individual text-containing elements instead. Text still shapes correctly via the browser's own bidi algorithm.`,
|
|
3363
3366
|
snippet: truncateSnippet(htmlTag.raw)
|
|
3364
3367
|
}
|
|
3365
3368
|
];
|
|
@@ -3397,7 +3400,7 @@ var compositionRules = [
|
|
|
3397
3400
|
findings.push({
|
|
3398
3401
|
code: "subcomposition_blanks_before_host",
|
|
3399
3402
|
severity: "warning",
|
|
3400
|
-
message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s
|
|
3403
|
+
message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s, so its slot will be blank for ~${gap}s.`,
|
|
3401
3404
|
elementId,
|
|
3402
3405
|
fixHint: `data-duration is the slot's visible window. Set this sub-composition's data-duration to ${round3(rootDuration - t.start)} to fill the host window, or add another clip to cover the remaining ~${gap}s.`,
|
|
3403
3406
|
snippet: truncateSnippet(t.tag.raw)
|
|
@@ -3436,7 +3439,7 @@ var compositionRules = [
|
|
|
3436
3439
|
severity: "error",
|
|
3437
3440
|
message: `Root element has class="${rootClasses.join(" ")}" and is styled by ${offenders.length} rule(s) keyed off that class (e.g. ${example}). At render, every sub-composition rule is scoped to [data-composition-id="${rootCompositionId}"] <selector>, so a selector whose leftmost part is the ROOT's own class becomes a descendant selector that cannot match the scoped element itself. Since #1886 the producer preserves the authored root as an inner wrapper, so this no longer renders the scene unstyled, but #root is the shape the scoper special-cases and the registry blocks model. Use it so preview, render, and Studio agree.`,
|
|
3438
3441
|
selector: example,
|
|
3439
|
-
fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`)
|
|
3442
|
+
fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`). The runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant.`,
|
|
3440
3443
|
snippet: truncateSnippet(rootTag.raw)
|
|
3441
3444
|
}
|
|
3442
3445
|
];
|
|
@@ -3519,7 +3522,7 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
3519
3522
|
{
|
|
3520
3523
|
code: "root_composition_missing_duration_source",
|
|
3521
3524
|
severity: "error",
|
|
3522
|
-
message: `Root composition uses Three.js with no data-duration. The runtime cannot discover a Three.js scene's duration automatically (no AnimationClip/AnimationMixer inspection)
|
|
3525
|
+
message: `Root composition uses Three.js with no data-duration. The runtime cannot discover a Three.js scene's duration automatically (no AnimationClip/AnimationMixer inspection), so render will fail with "Composition has zero duration".`,
|
|
3523
3526
|
fixHint: 'Add data-duration="<seconds>" to the root element.',
|
|
3524
3527
|
snippet: truncateSnippet(rootTag.raw)
|
|
3525
3528
|
}
|
|
@@ -3582,8 +3585,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
3582
3585
|
{
|
|
3583
3586
|
code: "composition_heavy_overlay_count_high",
|
|
3584
3587
|
severity: "warning",
|
|
3585
|
-
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
|
|
3586
|
-
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
|
|
3588
|
+
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 (including opacity:0 / visibility:hidden ones) 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.`,
|
|
3589
|
+
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: 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).`
|
|
3587
3590
|
}
|
|
3588
3591
|
];
|
|
3589
3592
|
},
|
|
@@ -3626,8 +3629,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
3626
3629
|
{
|
|
3627
3630
|
code: "root_zoom_rescales_a_fixed_canvas",
|
|
3628
3631
|
severity: "error",
|
|
3629
|
-
message: `\`zoom: ${hit.value}\` on ${hit.where} rescales the painted composition inside a frame that does not rescale with it. The capture frame is sized from the root's data-width/data-height in LAYOUT pixels, and \`zoom\` changes only what is painted inside it, so ` + (enlarging ? `content past the frame's edge renders zero pixels
|
|
3630
|
-
fixHint: `Author the composition at its real size
|
|
3632
|
+
message: `\`zoom: ${hit.value}\` on ${hit.where} rescales the painted composition inside a frame that does not rescale with it. The capture frame is sized from the root's data-width/data-height in LAYOUT pixels, and \`zoom\` changes only what is painted inside it, so ` + (enlarging ? `content past the frame's edge renders zero pixels. It is inside the declared composition and absent from the output, with nothing else reporting it.` : `the composition paints into part of the frame and the remainder of every output frame is dead space.`),
|
|
3633
|
+
fixHint: `Author the composition at its real size: set data-width/data-height (and the root's width/height) to the dimensions you want, and drop the canvas-level \`zoom\`. To scale the OUTPUT without changing layout, pass \`--output-resolution\` to render, which supersamples. \`zoom\` on descendants is fine and is not flagged: it is honoured exactly as specified.`,
|
|
3631
3634
|
snippet: hit.snippet
|
|
3632
3635
|
}
|
|
3633
3636
|
];
|
|
@@ -3671,8 +3674,8 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
3671
3674
|
{
|
|
3672
3675
|
code: "composition_exceeds_inspection_viewport_cap",
|
|
3673
3676
|
severity: "warning",
|
|
3674
|
-
message: `${over.join(" and ")} exceeds the ${INSPECTION_VIEWPORT_CAP}px viewport cap that check, validate, snapshot, compare and layout clamp to. render is unaffected and produces the full size, so the video is correct
|
|
3675
|
-
fixHint: `Keep the authored size if the output needs it, and verify the region past ${INSPECTION_VIEWPORT_CAP}px from a render rather than from check/snapshot/compare
|
|
3677
|
+
message: `${over.join(" and ")} exceeds the ${INSPECTION_VIEWPORT_CAP}px viewport cap that check, validate, snapshot, compare and layout clamp to. render is unaffected and produces the full size, so the video is correct, but every inspection command captures a ${INSPECTION_VIEWPORT_CAP}px-wide frame, and anything beyond that is missing from what they report. Measured at 0.8.71: a 5000x400 composition renders 5000x400 with all content, while snapshot returns 4096x400 with the element at left:4500 absent.`,
|
|
3678
|
+
fixHint: `Keep the authored size if the output needs it, and verify the region past ${INSPECTION_VIEWPORT_CAP}px from a render rather than from check/snapshot/compare. A clean inspection result does not cover it. If the large canvas is only there to gain resolution, author at the layout size and pass \`--output-resolution\` to render instead, which supersamples without changing layout.`,
|
|
3676
3679
|
snippet: truncateSnippet(rootTag.raw)
|
|
3677
3680
|
}
|
|
3678
3681
|
];
|
|
@@ -4551,7 +4554,7 @@ ${right.raw}`)
|
|
|
4551
4554
|
findings.push({
|
|
4552
4555
|
code: "gsap_animates_clip_element",
|
|
4553
4556
|
severity: "error",
|
|
4554
|
-
message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility, and autoAlpha writes visibility as well as opacity
|
|
4557
|
+
message: `GSAP animation sets ${conflictingProps.join(", ")} on a clip element. Selector "${sel}" resolves to element ${elDesc}. The framework manages clip visibility, and autoAlpha writes visibility as well as opacity. Do not animate these properties on clip elements.`,
|
|
4555
4558
|
selector: sel,
|
|
4556
4559
|
elementId: clipInfo.id || void 0,
|
|
4557
4560
|
fixHint: "Remove the visibility/display/autoAlpha tween, or move the content into a child <div> and target that instead.",
|
|
@@ -4745,7 +4748,7 @@ ${right.raw}`)
|
|
|
4745
4748
|
findings.push({
|
|
4746
4749
|
code: "audio_reactive_single_tween_per_group",
|
|
4747
4750
|
severity: "warning",
|
|
4748
|
-
message: "Audio-reactive captions use a single tween per group based on peak values. This sets one static value at group.start
|
|
4751
|
+
message: "Audio-reactive captions use a single tween per group based on peak values. This sets one static value at group.start, which is not perceptible as audio reactivity.",
|
|
4749
4752
|
fixHint: "Sample audio data at 100-200ms intervals throughout each group's lifetime (for loop from group.start to group.end) and create a tween at each sample point for visible pulsing."
|
|
4750
4753
|
});
|
|
4751
4754
|
}
|
|
@@ -4830,7 +4833,7 @@ ${right.raw}`)
|
|
|
4830
4833
|
...locate(script, gsapCallOffset(script, "timeline")),
|
|
4831
4834
|
code: "gsap_timeline_not_registered",
|
|
4832
4835
|
severity: "error",
|
|
4833
|
-
message: "GSAP timeline is created but never registered in window.__timelines. The runtime discovers timelines from this registry
|
|
4836
|
+
message: "GSAP timeline is created but never registered in window.__timelines. The runtime discovers timelines from this registry. Without registration, animations will not play during preview or render.",
|
|
4834
4837
|
fixHint: 'Add `window.__timelines = window.__timelines || {};` and `window.__timelines["root"] = tl;` after creating the timeline (use the composition\'s data-composition-id as the key).'
|
|
4835
4838
|
});
|
|
4836
4839
|
}
|
|
@@ -4898,9 +4901,9 @@ ${right.raw}`)
|
|
|
4898
4901
|
findings.push({
|
|
4899
4902
|
code: "gsap_from_opacity_noop",
|
|
4900
4903
|
severity: "error",
|
|
4901
|
-
message: `"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. gsap.from() animates FROM the specified value TO the current CSS value
|
|
4904
|
+
message: `"${sel}" has CSS \`opacity: 0\` and a gsap.${win.method}() that also sets opacity to 0. gsap.from() animates FROM the specified value TO the current CSS value. Since CSS is already 0, the element animates from 0\u21920 and never becomes visible.`,
|
|
4902
4905
|
selector: sel,
|
|
4903
|
-
fixHint: `Remove \`opacity: 0\` from the CSS/inline style on "${sel}". Let gsap.from({opacity: 0}) handle the initial hidden state
|
|
4906
|
+
fixHint: `Remove \`opacity: 0\` from the CSS/inline style on "${sel}". Let gsap.from({opacity: 0}) handle the initial hidden state. It will animate FROM 0 TO the CSS value (1 by default).`,
|
|
4904
4907
|
snippet: truncateSnippet(win.raw)
|
|
4905
4908
|
});
|
|
4906
4909
|
}
|
|
@@ -4979,12 +4982,12 @@ ${right.raw}`)
|
|
|
4979
4982
|
if (layoutProps.length > 0 && allTargetsHtmlInCanvas(call.selector)) layoutProps = [];
|
|
4980
4983
|
if (layoutProps.length === 0 && reflowProps.length === 0 && !usesRoundProps) continue;
|
|
4981
4984
|
const flagged = [...layoutProps, ...reflowProps, ...usesRoundProps ? ["roundProps"] : []];
|
|
4982
|
-
const message = `GSAP tween on "${call.selector}" uses motion that snaps to integer device pixels: ${flagged.join(", ")}. Layout and text-reflow properties snap during browser layout; roundProps rounds the tween value. Slow motion or an ease-out tail then stutters under the seek-by-frame capture engine
|
|
4985
|
+
const message = `GSAP tween on "${call.selector}" uses motion that snaps to integer device pixels: ${flagged.join(", ")}. Layout and text-reflow properties snap during browser layout; roundProps rounds the tween value. Slow motion or an ease-out tail then stutters under the seek-by-frame capture engine. Animate transforms (x/y/scale/opacity) instead.`;
|
|
4983
4986
|
const fixes = [];
|
|
4984
4987
|
if (layoutProps.length > 0) {
|
|
4985
4988
|
const tokens = [...new Set(layoutProps.flatMap((p) => LAYOUT_FIX[p] ?? []))];
|
|
4986
4989
|
fixes.push(
|
|
4987
|
-
`replace ${layoutProps.join("/")} with the transform equivalent (${tokens.join(", ")})
|
|
4990
|
+
`replace ${layoutProps.join("/")} with the transform equivalent (${tokens.join(", ")}), e.g. tl.fromTo("${call.selector}", { x: -1300 }, { x: 0, ...yourAnimation })`
|
|
4988
4991
|
);
|
|
4989
4992
|
}
|
|
4990
4993
|
if (reflowProps.length > 0) {
|
|
@@ -4996,7 +4999,7 @@ ${right.raw}`)
|
|
|
4996
4999
|
}
|
|
4997
5000
|
if (spacing.length > 0) {
|
|
4998
5001
|
parts.push(
|
|
4999
|
-
`for ${spacing.join("/")}, split the text into per-character elements and animate each glyph's x (the spread)
|
|
5002
|
+
`for ${spacing.join("/")}, split the text into per-character elements and animate each glyph's x (the spread), since uniform scale is NOT equivalent, or hold the final value statically`
|
|
5000
5003
|
);
|
|
5001
5004
|
}
|
|
5002
5005
|
fixes.push(
|
|
@@ -5060,7 +5063,7 @@ ${right.raw}`)
|
|
|
5060
5063
|
findings.push({
|
|
5061
5064
|
code: "gsap_relative_value_second_writer",
|
|
5062
5065
|
severity: "error",
|
|
5063
|
-
message: `Relative value(s) ${values} on "${win.targetSelector}" start while another writer for the same propert${sharedProps.length > 1 ? "ies" : "y"} is active between ${formatTime(win.position)} and ${formatTime(overlapEnd)}. Relative tweens capture their base at tween init: the sequential path inits mid-flight of the other writer, a cold render worker landing later inits with its end state
|
|
5066
|
+
message: `Relative value(s) ${values} on "${win.targetSelector}" start while another writer for the same propert${sharedProps.length > 1 ? "ies" : "y"} is active between ${formatTime(win.position)} and ${formatTime(overlapEnd)}. Relative tweens capture their base at tween init: the sequential path inits mid-flight of the other writer, a cold render worker landing later inits with its end state, so the same frame renders at two different positions (snap at chunk boundaries).`,
|
|
5064
5067
|
selector: win.targetSelector,
|
|
5065
5068
|
fixHint: `Use absolute values for ${sharedProps.join(", ")}, or a fromTo() with explicit endpoints, so every seek path resolves the same state. Single-writer relative values are safe; the conflict is the second writer.`,
|
|
5066
5069
|
snippet: truncateSnippet(`${win.raw}
|
|
@@ -5129,13 +5132,13 @@ ${other.raw}`)
|
|
|
5129
5132
|
const readsInvariant = TRANSFORM_INVARIANT_READ.test(fn.body);
|
|
5130
5133
|
const badMember = firstParamMemberAccessHazard(fn);
|
|
5131
5134
|
if (!readsSensitive && !readsInvariant && !badMember) continue;
|
|
5132
|
-
const reason = readsSensitive ? "reads transform-sensitive geometry, so its result depends on the worker's own seek order" : badMember ? `accesses .${badMember} on its first parameter
|
|
5135
|
+
const reason = readsSensitive ? "reads transform-sensitive geometry, so its result depends on the worker's own seek order" : badMember ? `accesses .${badMember} on its first parameter. GSAP function values receive (index, target, targets), so the first parameter is a NUMBER and this throws at tween init` : "measures layout at tween init, which is deterministic across cold render workers only while the measured layout never animates";
|
|
5133
5136
|
findings.push({
|
|
5134
5137
|
code: "gsap_function_value_hazard",
|
|
5135
5138
|
severity: readsSensitive || badMember ? "error" : "warning",
|
|
5136
5139
|
message: `Function-valued tween var for ${prop} on "${anim.targetSelector}" ${reason}. Each render worker initializes tweens independently.`,
|
|
5137
5140
|
selector: anim.targetSelector,
|
|
5138
|
-
fixHint: badMember ? "Use the SECOND parameter for the element: (index, target) =>
|
|
5141
|
+
fixHint: badMember ? "Use the SECOND parameter for the element: (index, target) => ..., or index arithmetic like (i) => i * 20." : "Compute the value once at build time (before the timeline is registered) and pass a constant, or derive it from fixed composition coordinates.",
|
|
5139
5142
|
snippet: truncateSnippet(raw)
|
|
5140
5143
|
});
|
|
5141
5144
|
}
|
|
@@ -5171,9 +5174,9 @@ ${other.raw}`)
|
|
|
5171
5174
|
findings.push({
|
|
5172
5175
|
code: "gsap_callback_dom_measurement",
|
|
5173
5176
|
severity: "warning",
|
|
5174
|
-
message: "Timeline callback reaches DOM measurement (getBoundingClientRect/getTotalLength/getComputedStyle/...). The renderer seeks with suppressEvents=false, so callbacks re-fire on every seek
|
|
5177
|
+
message: "Timeline callback reaches DOM measurement (getBoundingClientRect/getTotalLength/getComputedStyle/...). The renderer seeks with suppressEvents=false, so callbacks re-fire on every seek, and a cold render worker runs them against whatever DOM state its own non-linear seek order produced. Measured geometry is seek-order-dependent, and values measured at build time (before the callback ran) are stale or zero.",
|
|
5175
5178
|
selector: truncateSnippet(site, 120),
|
|
5176
|
-
fixHint: "Do all measurement and DOM setup synchronously at build time, before registering the timeline
|
|
5179
|
+
fixHint: "Do all measurement and DOM setup synchronously at build time, before registering the timeline, or derive geometry from fixed composition coordinates instead of measuring.",
|
|
5177
5180
|
snippet: truncateSnippet(snippet)
|
|
5178
5181
|
});
|
|
5179
5182
|
};
|
|
@@ -5246,8 +5249,8 @@ ${other.raw}`)
|
|
|
5246
5249
|
findings.push({
|
|
5247
5250
|
code: "gsap_timeline_return_used_as_tween",
|
|
5248
5251
|
severity: "error",
|
|
5249
|
-
message: `\`${match[0].includes(".push") ? "push" : boundName}\` captures the return of \`.${method}()\` on timeline \`${timelineVars[0]}\`, but a timeline's \`.to()\`/\`.from()\`/\`.set()\` returns THE TIMELINE ITSELF, not the tween it created. Every captured value is the same master timeline, so a tween-scoped call on it
|
|
5250
|
-
fixHint: "To build a group of keyframes you can later discard, put them in a nested timeline: `const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0)
|
|
5252
|
+
message: `\`${match[0].includes(".push") ? "push" : boundName}\` captures the return of \`.${method}()\` on timeline \`${timelineVars[0]}\`, but a timeline's \`.to()\`/\`.from()\`/\`.set()\` returns THE TIMELINE ITSELF, not the tween it created. Every captured value is the same master timeline, so a tween-scoped call on it (\`.kill()\` above all) hits the whole composition: it interrupts the timeline and detaches it from its parent, which freezes a parent-driven seek while an explicit \`tl.progress()\` keeps working. Only \`gsap.to()\` returns a tween.`,
|
|
5253
|
+
fixHint: "To build a group of keyframes you can later discard, put them in a nested timeline: `const nested = gsap.timeline(); nested.to(...); tl.add(nested, 0);`. Then `nested.kill()` replaces exactly those keyframes and cannot reach `tl`. Children keep their absolute times when the nest is added at 0. To hold a single real tween, create it with `gsap.to(...)` and place it with `tl.add(tween, at)`.",
|
|
5251
5254
|
snippet: truncateSnippet(source.slice(contextStart, match.index + match[0].length + 60))
|
|
5252
5255
|
});
|
|
5253
5256
|
}
|
|
@@ -5342,7 +5345,7 @@ ${other.raw}`)
|
|
|
5342
5345
|
findings.push({
|
|
5343
5346
|
code: "svg_drawon_css_dasharray_conflict",
|
|
5344
5347
|
severity: "error",
|
|
5345
|
-
message: `GSAP writes strokeDasharray on "${targetLabel}", but its CSS ("${conflictToken}") declares a multi-component stroke-dasharray. GSAP merges dash lists per component, so the CSS gap survives (e.g. "641.4px, 10px")
|
|
5348
|
+
message: `GSAP writes strokeDasharray on "${targetLabel}", but its CSS ("${conflictToken}") declares a multi-component stroke-dasharray. GSAP merges dash lists per component, so the CSS gap survives (e.g. "641.4px, 10px"). The draw-on hide only hides one gap's worth and the line stays visible the whole scene.`,
|
|
5346
5349
|
selector: quotedSelector ?? void 0,
|
|
5347
5350
|
fixHint: `Remove the CSS stroke-dasharray from "${conflictToken}" (decorative dashes belong on a separate element), or set the full two-component value in GSAP: strokeDasharray: "\${len} \${len}".`,
|
|
5348
5351
|
snippet: truncateSnippet(match[0] + firstVars.slice(1))
|
|
@@ -5464,7 +5467,7 @@ ${other.raw}`)
|
|
|
5464
5467
|
findings.push({
|
|
5465
5468
|
code: "svg_measure_before_path_d",
|
|
5466
5469
|
severity: sameVarAssignmentExists ? "warning" : "error",
|
|
5467
|
-
message: sameVarAssignmentExists ? `getTotalLength() is called on "${tokenLabel}", whose \`d\` is only assigned inside a function body
|
|
5470
|
+
message: sameVarAssignmentExists ? `getTotalLength() is called on "${tokenLabel}", whose \`d\` is only assigned inside a function body. If the measure runs before that function (e.g. the function is a timeline callback), the length is 0 and the dash animation is dead.` : `getTotalLength() is called on "${tokenLabel}", but the path has no static \`d\` attribute and no d assignment exists anywhere, so getTotalLength() returns 0 in Chrome, silently killing dash animations.`,
|
|
5468
5471
|
selector: tokenLabel,
|
|
5469
5472
|
fixHint: "Assign the path's `d` synchronously at build time (top level, before measuring), or author a static d attribute in the HTML.",
|
|
5470
5473
|
snippet: truncateSnippet(match[0] + ")")
|
|
@@ -6012,7 +6015,7 @@ var fontRules = [
|
|
|
6012
6015
|
{
|
|
6013
6016
|
code: "system_font_will_alias",
|
|
6014
6017
|
severity: "warning",
|
|
6015
|
-
message: `Font ${aliased.length === 1 ? "family" : "families"} will be substituted at render time: ${aliased.join(", ")}. In distributed/Lambda rendering system-font capture is disabled
|
|
6018
|
+
message: `Font ${aliased.length === 1 ? "family" : "families"} will be substituted at render time: ${aliased.join(", ")}. In distributed/Lambda rendering system-font capture is disabled, so these fonts will fall back to OS defaults. Embed explicit @font-face declarations instead.`
|
|
6016
6019
|
}
|
|
6017
6020
|
];
|
|
6018
6021
|
},
|
|
@@ -6032,7 +6035,7 @@ var fontRules = [
|
|
|
6032
6035
|
code: "font_family_without_font_face",
|
|
6033
6036
|
severity: "error",
|
|
6034
6037
|
message: `Font ${undeclared.length === 1 ? "family" : "families"} used without @font-face declaration: ${undeclared.join(", ")}. These are not in the auto-resolved font list, so the renderer cannot supply them automatically. Text will fall back to a generic font, producing incorrect typography in the video.`,
|
|
6035
|
-
fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files. For an OS-bundled system font (e.g. Hiragino Sans, Microsoft YaHei) that has no downloadable file, use src: local('Exact Font Name') instead
|
|
6038
|
+
fixHint: "Add @font-face { font-family: '...'; src: url('capture/assets/fonts/...woff2'); } for each font family, pointing to the captured .woff2 files. For an OS-bundled system font (e.g. Hiragino Sans, Microsoft YaHei) that has no downloadable file, use src: local('Exact Font Name') instead. The declaration alone satisfies this check without needing a font file."
|
|
6036
6039
|
});
|
|
6037
6040
|
return findings;
|
|
6038
6041
|
}
|
|
@@ -6560,7 +6563,7 @@ function collectLocalVideoCandidates(projectDir, htmlSources) {
|
|
|
6560
6563
|
const re = new RegExp(videoSrcRe.source, videoSrcRe.flags);
|
|
6561
6564
|
let match;
|
|
6562
6565
|
while ((match = re.exec(scannable)) !== null) {
|
|
6563
|
-
const rawSrc = match
|
|
6566
|
+
const rawSrc = mediaSrcOf(match);
|
|
6564
6567
|
const reference = resolveLocalVideoReference(projectDir, rawSrc, compSrcPath);
|
|
6565
6568
|
if (!reference || candidates.has(reference.resolved)) continue;
|
|
6566
6569
|
candidates.set(reference.resolved, reference.src);
|
|
@@ -6948,9 +6951,10 @@ function lintAudioSrcNotFound(projectDir, htmlSources) {
|
|
|
6948
6951
|
const audioSrcRe = mediaSrcTagRe("audio");
|
|
6949
6952
|
const missingByFile = /* @__PURE__ */ new Map();
|
|
6950
6953
|
for (const { html, compSrcPath } of htmlSources) {
|
|
6954
|
+
const scannable = maskNonScannableRanges2(html);
|
|
6951
6955
|
let match;
|
|
6952
|
-
while ((match = audioSrcRe.exec(
|
|
6953
|
-
const src = match
|
|
6956
|
+
while ((match = audioSrcRe.exec(scannable)) !== null) {
|
|
6957
|
+
const src = mediaSrcOf(match);
|
|
6954
6958
|
if (/^(https?:|data:|blob:)/i.test(src)) continue;
|
|
6955
6959
|
if (isUnresolvedAssetPlaceholder2(src)) continue;
|
|
6956
6960
|
const rootRelative = compSrcPath ? rewriteAssetPath2(compSrcPath, src, (path) => existsSync2(join2(projectDir, path))) : src;
|
|
@@ -6982,7 +6986,7 @@ function lintMissingLocalAsset(projectDir, htmlSources) {
|
|
|
6982
6986
|
let match;
|
|
6983
6987
|
while ((match = re.exec(scannable)) !== null) {
|
|
6984
6988
|
const tagName = (match[1] ?? "").toLowerCase();
|
|
6985
|
-
const rawSrc = match
|
|
6989
|
+
const rawSrc = mediaSrcOf(match);
|
|
6986
6990
|
if (isUnresolvedAssetPlaceholder2(rawSrc)) continue;
|
|
6987
6991
|
const src = cleanAssetUrl2(rawSrc);
|
|
6988
6992
|
if (!src) continue;
|
|
@@ -7008,7 +7012,7 @@ function lintMissingLocalAsset(projectDir, htmlSources) {
|
|
|
7008
7012
|
severity: "error",
|
|
7009
7013
|
file,
|
|
7010
7014
|
message: `<${tagName}> element references local file(s) not found in the project: ${unique.join(", ")}. The renderer will silently skip these and produce a video with missing visuals.`,
|
|
7011
|
-
fixHint: unique.length === 1 ? `Add "${unique[0]}" to the project directory, or update the src attribute to point to an existing file. Common cause: captured asset filenames are unreliable (heygen-logo.svg often contains Google, nvidia-logo.svg may contain Autodesk, etc.). Open the contact sheets and verify the file actually exists at this path before referencing it.` : "Add the missing files to the project directory, or update the src attributes to point to existing files. Captured asset filenames are unreliable
|
|
7015
|
+
fixHint: unique.length === 1 ? `Add "${unique[0]}" to the project directory, or update the src attribute to point to an existing file. Common cause: captured asset filenames are unreliable (heygen-logo.svg often contains Google, nvidia-logo.svg may contain Autodesk, etc.). Open the contact sheets and verify the file actually exists at this path before referencing it.` : "Add the missing files to the project directory, or update the src attributes to point to existing files. Captured asset filenames are unreliable. Verify against capture/contact-sheets/ and capture/extracted/asset-descriptions.md."
|
|
7012
7016
|
});
|
|
7013
7017
|
}
|
|
7014
7018
|
return findings;
|
|
@@ -7075,11 +7079,6 @@ function lintMultipleRootCompositions(projectDir) {
|
|
|
7075
7079
|
}
|
|
7076
7080
|
function lintDuplicateAudioTracks(htmlSources) {
|
|
7077
7081
|
const findings = [];
|
|
7078
|
-
function extractAttr(tag, name) {
|
|
7079
|
-
const re = new RegExp(`\\b${name}\\s*=\\s*["']([^"']+)["']`, "i");
|
|
7080
|
-
const m = tag.match(re);
|
|
7081
|
-
return m?.[1] ?? null;
|
|
7082
|
-
}
|
|
7083
7082
|
const tracks = [];
|
|
7084
7083
|
const seen = /* @__PURE__ */ new Set();
|
|
7085
7084
|
for (const { html } of htmlSources) {
|
|
@@ -7088,10 +7087,10 @@ function lintDuplicateAudioTracks(htmlSources) {
|
|
|
7088
7087
|
while ((match = soundTagRe.exec(html)) !== null) {
|
|
7089
7088
|
const tag = match[0];
|
|
7090
7089
|
if (/^<video/i.test(tag) && !isAudibleVideoTag(tag)) continue;
|
|
7091
|
-
const trackStr =
|
|
7092
|
-
const startStr =
|
|
7093
|
-
const durStr =
|
|
7094
|
-
const src =
|
|
7090
|
+
const trackStr = readAttr(tag, "data-track-index");
|
|
7091
|
+
const startStr = readAttr(tag, "data-start");
|
|
7092
|
+
const durStr = readAttr(tag, "data-duration");
|
|
7093
|
+
const src = readDecodedAttr(tag, "src") || "unknown";
|
|
7095
7094
|
if (!trackStr || !startStr) continue;
|
|
7096
7095
|
const trackIndex = parseInt(trackStr, 10);
|
|
7097
7096
|
const start = parseFloat(startStr);
|
|
@@ -7165,7 +7164,7 @@ function lintMissingOrEmptySubComposition(projectDir, rootHtml) {
|
|
|
7165
7164
|
code: "missing_or_empty_sub_composition",
|
|
7166
7165
|
severity: "error",
|
|
7167
7166
|
message: `data-composition-src references "${srcPath}", but ${problem}.`,
|
|
7168
|
-
fixHint: folder ? `Point data-composition-src at the HTML file inside the folder, such as "${srcPath}/index.html".` : `Fix this before rendering
|
|
7167
|
+
fixHint: folder ? `Point data-composition-src at the HTML file inside the folder, such as "${srcPath}/index.html".` : `Fix this before rendering: the render pre-flight rejects unusable sub-compositions. Write valid HTML into "${srcPath}". It needs a <template> or <body> containing an element with data-composition-id, data-width, and data-height. Preview/studio still tolerates and skips the scene while you author it. If a scene-authoring step is still running, wait for it to finish before referencing the file, or re-run the step that generates it.`
|
|
7169
7168
|
});
|
|
7170
7169
|
}
|
|
7171
7170
|
return findings;
|
|
@@ -7183,4 +7182,3 @@ export {
|
|
|
7183
7182
|
lintProject,
|
|
7184
7183
|
shouldBlockRender
|
|
7185
7184
|
};
|
|
7186
|
-
//# sourceMappingURL=index.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/lint",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.147",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/heygen-com/hyperframes",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"linkedom": "^0.18.12",
|
|
36
36
|
"postcss": "^8.5.8",
|
|
37
37
|
"postcss-selector-parser": "^7.1.4",
|
|
38
|
-
"@hyperframes/parsers": "0.8.
|
|
38
|
+
"@hyperframes/parsers": "0.8.147"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/node": "^25.0.10",
|