@hyperframes/lint 0.8.40 → 0.8.41
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 +160 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -5379,7 +5379,7 @@ import { existsSync as existsSync2, readFileSync, readdirSync } from "fs";
|
|
|
5379
5379
|
import { dirname, extname, join as join2, relative, resolve } from "path";
|
|
5380
5380
|
import { rewriteAssetPath as rewriteAssetPath2 } from "@hyperframes/parsers/asset-paths";
|
|
5381
5381
|
import { checkSubCompositionUsability } from "@hyperframes/parsers/sub-composition-validity";
|
|
5382
|
-
import { parseHTML as
|
|
5382
|
+
import { parseHTML as parseHTML3 } from "linkedom";
|
|
5383
5383
|
import {
|
|
5384
5384
|
cleanAssetUrl as cleanAssetUrl2,
|
|
5385
5385
|
collectSubCompositionSrcs,
|
|
@@ -5396,6 +5396,7 @@ import { execFile } from "child_process";
|
|
|
5396
5396
|
import { existsSync } from "fs";
|
|
5397
5397
|
import { join } from "path";
|
|
5398
5398
|
import { rewriteAssetPath } from "@hyperframes/parsers/asset-paths";
|
|
5399
|
+
import { parseNumeric } from "@hyperframes/parsers/composition-contract";
|
|
5399
5400
|
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
5400
5401
|
import {
|
|
5401
5402
|
cleanAssetUrl,
|
|
@@ -5404,6 +5405,7 @@ import {
|
|
|
5404
5405
|
maskNonScannableRanges,
|
|
5405
5406
|
resolveExistingLocalAsset
|
|
5406
5407
|
} from "@hyperframes/parsers/asset-resolution";
|
|
5408
|
+
import { parseHTML as parseHTML2 } from "linkedom";
|
|
5407
5409
|
var PROBE_TIMEOUT_MS = 4e3;
|
|
5408
5410
|
var PROBE_CONCURRENCY = 8;
|
|
5409
5411
|
function execFileAsync(file, args) {
|
|
@@ -5414,6 +5416,29 @@ function execFileAsync(file, args) {
|
|
|
5414
5416
|
});
|
|
5415
5417
|
});
|
|
5416
5418
|
}
|
|
5419
|
+
async function mapWithProbeConcurrency(entries, probe) {
|
|
5420
|
+
const results = new Array(entries.length);
|
|
5421
|
+
let nextIndex = 0;
|
|
5422
|
+
const workerCount = Math.min(PROBE_CONCURRENCY, entries.length);
|
|
5423
|
+
const runWorker = async () => {
|
|
5424
|
+
for (; ; ) {
|
|
5425
|
+
const index = nextIndex++;
|
|
5426
|
+
if (index >= entries.length) return;
|
|
5427
|
+
const entry = entries[index];
|
|
5428
|
+
if (entry !== void 0) results[index] = await probe(entry);
|
|
5429
|
+
}
|
|
5430
|
+
};
|
|
5431
|
+
await Promise.all(Array.from({ length: workerCount }, runWorker));
|
|
5432
|
+
return results;
|
|
5433
|
+
}
|
|
5434
|
+
function resolveLocalVideoReference(projectDir, rawSrc, compSrcPath) {
|
|
5435
|
+
if (isUnresolvedAssetPlaceholder(rawSrc)) return null;
|
|
5436
|
+
const src = cleanAssetUrl(rawSrc);
|
|
5437
|
+
if (!src || isRemoteOrInlineUrl(src)) return null;
|
|
5438
|
+
const rootRelative = compSrcPath ? rewriteAssetPath(compSrcPath, src, (path) => existsSync(join(projectDir, path))) : src;
|
|
5439
|
+
const asset = resolveExistingLocalAsset(projectDir, rootRelative);
|
|
5440
|
+
return asset ? { resolved: asset.resolved, src } : null;
|
|
5441
|
+
}
|
|
5417
5442
|
function hasHevcStream(json) {
|
|
5418
5443
|
if (typeof json !== "object" || json === null) return false;
|
|
5419
5444
|
const streams = Reflect.get(json, "streams");
|
|
@@ -5451,35 +5476,143 @@ function collectLocalVideoCandidates(projectDir, htmlSources) {
|
|
|
5451
5476
|
let match;
|
|
5452
5477
|
while ((match = re.exec(scannable)) !== null) {
|
|
5453
5478
|
const rawSrc = match[2] ?? "";
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
if (isRemoteOrInlineUrl(src)) continue;
|
|
5458
|
-
const rootRelative = compSrcPath ? rewriteAssetPath(compSrcPath, src, (path) => existsSync(join(projectDir, path))) : src;
|
|
5459
|
-
const resolvedAsset = resolveExistingLocalAsset(projectDir, rootRelative);
|
|
5460
|
-
if (!resolvedAsset) continue;
|
|
5461
|
-
if (!candidates.has(resolvedAsset.resolved)) candidates.set(resolvedAsset.resolved, src);
|
|
5479
|
+
const reference = resolveLocalVideoReference(projectDir, rawSrc, compSrcPath);
|
|
5480
|
+
if (!reference || candidates.has(reference.resolved)) continue;
|
|
5481
|
+
candidates.set(reference.resolved, reference.src);
|
|
5462
5482
|
}
|
|
5463
5483
|
}
|
|
5464
5484
|
return candidates;
|
|
5465
5485
|
}
|
|
5486
|
+
function collectVideoElements(html) {
|
|
5487
|
+
const { document } = parseHTML2(html);
|
|
5488
|
+
const roots = [document];
|
|
5489
|
+
const videos = [];
|
|
5490
|
+
for (let index = 0; index < roots.length; index++) {
|
|
5491
|
+
const root = roots[index];
|
|
5492
|
+
if (!root) continue;
|
|
5493
|
+
videos.push(...root.querySelectorAll("video"));
|
|
5494
|
+
for (const template of root.querySelectorAll("template")) {
|
|
5495
|
+
roots.push(template.content);
|
|
5496
|
+
}
|
|
5497
|
+
}
|
|
5498
|
+
return videos;
|
|
5499
|
+
}
|
|
5500
|
+
function readPositiveNumber(raw) {
|
|
5501
|
+
const value = parseNumeric(raw);
|
|
5502
|
+
return value !== null && value > 0 ? value : null;
|
|
5503
|
+
}
|
|
5504
|
+
function readNonNegativeNumber(raw) {
|
|
5505
|
+
const value = parseNumeric(raw);
|
|
5506
|
+
return value !== null && value >= 0 ? value : null;
|
|
5507
|
+
}
|
|
5508
|
+
function readFiniteVideoSlot(video, file) {
|
|
5509
|
+
if (video.hasAttribute("loop")) return null;
|
|
5510
|
+
if (video.hasAttribute("data-var-src")) return null;
|
|
5511
|
+
if (video.hasAttribute("data-playback-start")) return null;
|
|
5512
|
+
if (readPositiveNumber(video.getAttribute("data-duration")) === null) return null;
|
|
5513
|
+
const mediaStart = readNonNegativeNumber(video.getAttribute("data-media-start"));
|
|
5514
|
+
if (mediaStart === null) return null;
|
|
5515
|
+
return {
|
|
5516
|
+
rawSrc: video.getAttribute("src") ?? "",
|
|
5517
|
+
file,
|
|
5518
|
+
...video.id ? { elementId: video.id } : {},
|
|
5519
|
+
mediaStart
|
|
5520
|
+
};
|
|
5521
|
+
}
|
|
5522
|
+
function collectLocalFiniteVideoSlots(projectDir, htmlSources) {
|
|
5523
|
+
const slotsByPath = /* @__PURE__ */ new Map();
|
|
5524
|
+
for (const { html, compSrcPath } of htmlSources) {
|
|
5525
|
+
for (const video of collectVideoElements(html)) {
|
|
5526
|
+
const slot = readFiniteVideoSlot(video, compSrcPath ?? "index.html");
|
|
5527
|
+
if (!slot) continue;
|
|
5528
|
+
const reference = resolveLocalVideoReference(projectDir, slot.rawSrc, compSrcPath);
|
|
5529
|
+
if (!reference) continue;
|
|
5530
|
+
const slots = slotsByPath.get(reference.resolved) ?? [];
|
|
5531
|
+
slots.push({
|
|
5532
|
+
src: reference.src,
|
|
5533
|
+
file: slot.file,
|
|
5534
|
+
...slot.elementId ? { elementId: slot.elementId } : {},
|
|
5535
|
+
mediaStart: slot.mediaStart
|
|
5536
|
+
});
|
|
5537
|
+
slotsByPath.set(reference.resolved, slots);
|
|
5538
|
+
}
|
|
5539
|
+
}
|
|
5540
|
+
return slotsByPath;
|
|
5541
|
+
}
|
|
5542
|
+
function parsePositiveDuration(value) {
|
|
5543
|
+
const duration = typeof value === "number" || typeof value === "string" ? Number(value) : NaN;
|
|
5544
|
+
return Number.isFinite(duration) && duration > 0 ? duration : null;
|
|
5545
|
+
}
|
|
5546
|
+
function readPlayableVideoDuration(stdout) {
|
|
5547
|
+
try {
|
|
5548
|
+
const metadata = JSON.parse(stdout);
|
|
5549
|
+
if (typeof metadata !== "object" || metadata === null) return null;
|
|
5550
|
+
const streams = Reflect.get(metadata, "streams");
|
|
5551
|
+
const stream = Array.isArray(streams) ? streams.find((candidate) => typeof candidate === "object" && candidate !== null) : null;
|
|
5552
|
+
const streamDuration = stream ? parsePositiveDuration(Reflect.get(stream, "duration")) : null;
|
|
5553
|
+
if (streamDuration !== null) return streamDuration;
|
|
5554
|
+
const format = Reflect.get(metadata, "format");
|
|
5555
|
+
return typeof format === "object" && format !== null ? parsePositiveDuration(Reflect.get(format, "duration")) : null;
|
|
5556
|
+
} catch {
|
|
5557
|
+
return null;
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
async function probePlayableVideoDuration(ffprobePath, filePath) {
|
|
5561
|
+
try {
|
|
5562
|
+
return readPlayableVideoDuration(
|
|
5563
|
+
await execFileAsync(ffprobePath, [
|
|
5564
|
+
"-v",
|
|
5565
|
+
"error",
|
|
5566
|
+
"-select_streams",
|
|
5567
|
+
"v:0",
|
|
5568
|
+
"-show_entries",
|
|
5569
|
+
"stream=duration:format=duration",
|
|
5570
|
+
"-of",
|
|
5571
|
+
"json",
|
|
5572
|
+
"--",
|
|
5573
|
+
filePath
|
|
5574
|
+
])
|
|
5575
|
+
);
|
|
5576
|
+
} catch {
|
|
5577
|
+
return null;
|
|
5578
|
+
}
|
|
5579
|
+
}
|
|
5580
|
+
async function lintVideoMediaStartPastEof(projectDir, htmlSources) {
|
|
5581
|
+
const slotsByPath = collectLocalFiniteVideoSlots(projectDir, htmlSources);
|
|
5582
|
+
if (slotsByPath.size === 0) return [];
|
|
5583
|
+
const ffprobePath = findFfBinary("ffprobe", { configuredMustExist: true });
|
|
5584
|
+
if (!ffprobePath) return [];
|
|
5585
|
+
const entries = [...slotsByPath.entries()];
|
|
5586
|
+
const durations = await mapWithProbeConcurrency(
|
|
5587
|
+
entries,
|
|
5588
|
+
(entry) => probePlayableVideoDuration(ffprobePath, entry[0])
|
|
5589
|
+
);
|
|
5590
|
+
const findings = [];
|
|
5591
|
+
for (const [index, [, slots]] of entries.entries()) {
|
|
5592
|
+
const sourceDuration = durations[index];
|
|
5593
|
+
if (sourceDuration == null) continue;
|
|
5594
|
+
for (const slot of slots) {
|
|
5595
|
+
if (slot.mediaStart < sourceDuration) continue;
|
|
5596
|
+
findings.push({
|
|
5597
|
+
code: "video_media_start_at_or_past_eof",
|
|
5598
|
+
severity: "warning",
|
|
5599
|
+
message: `Video "${slot.src}" starts at ${slot.mediaStart}s, at or past its ${sourceDuration}s playable source duration. The video will hold its final frame for the explicit slot.`,
|
|
5600
|
+
file: slot.file,
|
|
5601
|
+
...slot.elementId ? { elementId: slot.elementId } : {},
|
|
5602
|
+
fixHint: `Trim data-media-start below ${sourceDuration}s if this final-frame hold is unintended.`
|
|
5603
|
+
});
|
|
5604
|
+
}
|
|
5605
|
+
}
|
|
5606
|
+
return findings;
|
|
5607
|
+
}
|
|
5466
5608
|
async function lintHevcPreviewCodec(candidates) {
|
|
5467
5609
|
if (candidates.size === 0) return [];
|
|
5468
5610
|
const ffprobePath = findFfBinary("ffprobe", { configuredMustExist: true });
|
|
5469
5611
|
if (!ffprobePath) return [];
|
|
5470
5612
|
const entries = [...candidates.entries()];
|
|
5471
|
-
const isHevc =
|
|
5472
|
-
|
|
5473
|
-
|
|
5474
|
-
await Promise.all(
|
|
5475
|
-
Array.from({ length: workerCount }, async () => {
|
|
5476
|
-
while (nextIndex < entries.length) {
|
|
5477
|
-
const index = nextIndex++;
|
|
5478
|
-
const entry = entries[index];
|
|
5479
|
-
if (!entry) break;
|
|
5480
|
-
isHevc[index] = await probeIsHevc(ffprobePath, entry[0]);
|
|
5481
|
-
}
|
|
5482
|
-
})
|
|
5613
|
+
const isHevc = await mapWithProbeConcurrency(
|
|
5614
|
+
entries,
|
|
5615
|
+
(entry) => probeIsHevc(ffprobePath, entry[0])
|
|
5483
5616
|
);
|
|
5484
5617
|
const hevcSrcs = entries.filter((_, i) => isHevc[i]).map(([, src]) => src);
|
|
5485
5618
|
if (hevcSrcs.length === 0) return [];
|
|
@@ -5496,7 +5629,7 @@ async function lintHevcPreviewCodec(candidates) {
|
|
|
5496
5629
|
|
|
5497
5630
|
// src/project.ts
|
|
5498
5631
|
function parseSubCompHtml(html) {
|
|
5499
|
-
return
|
|
5632
|
+
return parseHTML3(html).document;
|
|
5500
5633
|
}
|
|
5501
5634
|
function querySelectorAllIncludingTemplates(root, selector) {
|
|
5502
5635
|
const matches = [...root.querySelectorAll(selector)];
|
|
@@ -5534,7 +5667,7 @@ function collectLocalStylesheets(projectDir, document, compSrcPath) {
|
|
|
5534
5667
|
}
|
|
5535
5668
|
function collectExternalStyles(projectDir, html, compSrcPath) {
|
|
5536
5669
|
const styles = [];
|
|
5537
|
-
const { document } =
|
|
5670
|
+
const { document } = parseHTML3(html);
|
|
5538
5671
|
for (const { href, content } of collectLocalStylesheets(projectDir, document, compSrcPath)) {
|
|
5539
5672
|
styles.push({ href, content });
|
|
5540
5673
|
}
|
|
@@ -5542,7 +5675,7 @@ function collectExternalStyles(projectDir, html, compSrcPath) {
|
|
|
5542
5675
|
}
|
|
5543
5676
|
function collectCssSources(projectDir, html, compSrcPath) {
|
|
5544
5677
|
const sources = [];
|
|
5545
|
-
const { document } =
|
|
5678
|
+
const { document } = parseHTML3(html);
|
|
5546
5679
|
for (const style of querySelectorAllIncludingTemplates(document, "style")) {
|
|
5547
5680
|
sources.push({ content: style.textContent ?? "" });
|
|
5548
5681
|
}
|
|
@@ -5640,6 +5773,7 @@ async function lintProject(projectDir, entryFile) {
|
|
|
5640
5773
|
...!entryFile ? lintBlankRootWithStandaloneComposition(rootHtml, allHtmlSources) : [],
|
|
5641
5774
|
...lintDuplicateAudioTracks(allHtmlSources),
|
|
5642
5775
|
...lintMissingOrEmptySubComposition(projectDir, rootHtml),
|
|
5776
|
+
...await lintVideoMediaStartPastEof(projectDir, allHtmlSources),
|
|
5643
5777
|
...await lintHevcPreviewCodec(collectLocalVideoCandidates(projectDir, allHtmlSources))
|
|
5644
5778
|
];
|
|
5645
5779
|
if (projectFindings.length > 0) {
|
|
@@ -5661,7 +5795,7 @@ async function lintProject(projectDir, entryFile) {
|
|
|
5661
5795
|
return { results, totalErrors, totalWarnings, totalInfos };
|
|
5662
5796
|
}
|
|
5663
5797
|
function lintBlankRootWithStandaloneComposition(rootHtml, htmlSources) {
|
|
5664
|
-
const { document: rootDocument } =
|
|
5798
|
+
const { document: rootDocument } = parseHTML3(rootHtml);
|
|
5665
5799
|
const root = rootDocument.querySelector("body [data-composition-id]");
|
|
5666
5800
|
if (!root || root.querySelector("*:not(script):not(style):not(link):not(meta):not(template)")) {
|
|
5667
5801
|
return [];
|
|
@@ -5669,7 +5803,7 @@ function lintBlankRootWithStandaloneComposition(rootHtml, htmlSources) {
|
|
|
5669
5803
|
const standaloneCandidates = [];
|
|
5670
5804
|
for (const source of htmlSources) {
|
|
5671
5805
|
if (!source.compSrcPath) continue;
|
|
5672
|
-
const { document } =
|
|
5806
|
+
const { document } = parseHTML3(source.html);
|
|
5673
5807
|
const composition = document.querySelector("body [data-composition-id]");
|
|
5674
5808
|
if (!composition) continue;
|
|
5675
5809
|
const authoredTimedContent = Array.from(
|