@hyperframes/lint 0.8.49 → 0.8.51
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.d.ts +2 -0
- package/dist/browser.js +210 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +216 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -45,6 +45,8 @@ type HyperframeLinterOptions = {
|
|
|
45
45
|
* render time — the font will silently fall back to whatever the OS provides.
|
|
46
46
|
*/
|
|
47
47
|
distributed?: boolean;
|
|
48
|
+
/** Who is running the lint: Studio raises the structure rules to errors, the CLI keeps them warnings. */
|
|
49
|
+
host?: "studio" | "cli";
|
|
48
50
|
};
|
|
49
51
|
|
|
50
52
|
/**
|
|
@@ -90,6 +92,6 @@ interface ProjectLintResult {
|
|
|
90
92
|
totalWarnings: number;
|
|
91
93
|
totalInfos: number;
|
|
92
94
|
}
|
|
93
|
-
declare function lintProject(projectDir: string, entryFile?: string): Promise<ProjectLintResult>;
|
|
95
|
+
declare function lintProject(projectDir: string, entryFile?: string, hostOptions?: Pick<HyperframeLinterOptions, "host">): Promise<ProjectLintResult>;
|
|
94
96
|
|
|
95
97
|
export { type HyperframeLintFinding, type HyperframeLintResult, type HyperframeLintSeverity, type HyperframeLinterOptions, LINT_RULE_COUNT, LINT_RULE_GROUP_COUNTS, type LintTimings, type ProjectLintResult, lintHyperframeHtml, lintMediaUrls, lintProject, shouldBlockRender };
|
package/dist/index.js
CHANGED
|
@@ -1541,6 +1541,7 @@ function findRuntimeMediaSrcMutationFindings(ctx) {
|
|
|
1541
1541
|
}
|
|
1542
1542
|
var mediaRules = [
|
|
1543
1543
|
findNestedMediaStartBasisFindings,
|
|
1544
|
+
findSpeedRampOnNonMediaFindings,
|
|
1544
1545
|
// duplicate_media_id + duplicate_media_discovery_risk
|
|
1545
1546
|
({ tags }) => {
|
|
1546
1547
|
const findings = [];
|
|
@@ -1962,6 +1963,18 @@ function findVolumeDoubleAutomationFindings(ctx) {
|
|
|
1962
1963
|
}
|
|
1963
1964
|
return findings;
|
|
1964
1965
|
}
|
|
1966
|
+
function findSpeedRampOnNonMediaFindings(ctx) {
|
|
1967
|
+
return ctx.tags.filter((tag) => tag.name !== "video" && tag.name !== "audio").filter(
|
|
1968
|
+
(tag) => /"target"\s*:\s*"rate"/.test(readDecodedAttr(tag.raw, "data-automation") ?? "")
|
|
1969
|
+
).map((tag) => ({
|
|
1970
|
+
code: "speed_ramp_on_non_media",
|
|
1971
|
+
severity: "warning",
|
|
1972
|
+
message: `<${tag.name}> has a speed-ramp lane, but only <video> and <audio> clips can be retimed. The lane does nothing here.`,
|
|
1973
|
+
elementId: readAttr(tag.raw, "id") || void 0,
|
|
1974
|
+
fixHint: "Move the rate lane onto the <video> or <audio> clip, or retime an animation with a GSAP timeline instead.",
|
|
1975
|
+
snippet: truncateSnippet(tag.raw)
|
|
1976
|
+
}));
|
|
1977
|
+
}
|
|
1965
1978
|
function findCarveUngroupedSourcesFindings(ctx) {
|
|
1966
1979
|
const groupIds = new Set(
|
|
1967
1980
|
ctx.tags.filter((tag) => tag.name === "hf-audio-group").map((tag) => readAttr(tag.raw, "id"))
|
|
@@ -2068,6 +2081,11 @@ function findAudioGroupCarveAttrFindings(ctx) {
|
|
|
2068
2081
|
// src/rules/composition.ts
|
|
2069
2082
|
import { COMPOSITION_VARIABLE_TYPES, isSafeMediaUrl } from "@hyperframes/parsers/composition";
|
|
2070
2083
|
import { COMPOSITION_ATTRIBUTES, readClipTiming } from "@hyperframes/parsers/composition-contract";
|
|
2084
|
+
import { resolveCompositionDuration } from "@hyperframes/parsers/composition-duration";
|
|
2085
|
+
import {
|
|
2086
|
+
readAuthoredDurationSeconds,
|
|
2087
|
+
resolveMediaDuration
|
|
2088
|
+
} from "@hyperframes/parsers/media-duration";
|
|
2071
2089
|
var MAX_COMPOSITION_LINES = 300;
|
|
2072
2090
|
var MAX_TIMED_ELEMENTS_PER_TRACK = 3;
|
|
2073
2091
|
var TRACK_DENSITY_EXEMPT_TAGS = /* @__PURE__ */ new Set(["audio", "script", "style", "video"]);
|
|
@@ -2843,6 +2861,18 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
2843
2861
|
const hasInfiniteCssAnimation = /\banimation(?:-iteration-count)?\s*:[^;{}]*(?<![\w-])infinite(?![\w-])/.test(combinedCss);
|
|
2844
2862
|
const hasAnyNonGsapSignal = usesLottie || usesThree || usesWaapi || hasCssAnimationName;
|
|
2845
2863
|
if (!hasAnyNonGsapSignal) {
|
|
2864
|
+
const derived = deriveDurationFromClips(tags, rootTag);
|
|
2865
|
+
if (derived.source === "derived") {
|
|
2866
|
+
return [
|
|
2867
|
+
{
|
|
2868
|
+
code: "root_composition_duration_derived",
|
|
2869
|
+
severity: "warning",
|
|
2870
|
+
message: `Root composition has no data-duration and no GSAP timeline, so its length is taken from its clips: at least ${derived.seconds}s` + (derived.pendingClips > 0 ? `, and ${derived.pendingClips} clip(s) whose length is only known at runtime` : "") + ".",
|
|
2871
|
+
fixHint: 'Add data-duration="<seconds>" to the root element to set the length yourself.',
|
|
2872
|
+
snippet: truncateSnippet(rootTag.raw)
|
|
2873
|
+
}
|
|
2874
|
+
];
|
|
2875
|
+
}
|
|
2846
2876
|
return [
|
|
2847
2877
|
{
|
|
2848
2878
|
code: "root_composition_missing_duration_source",
|
|
@@ -2927,6 +2957,35 @@ ${allInlineStyles}`.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
|
2927
2957
|
];
|
|
2928
2958
|
}
|
|
2929
2959
|
];
|
|
2960
|
+
var CLIP_MEDIA_TAGS = /* @__PURE__ */ new Set(["img", "video", "audio"]);
|
|
2961
|
+
function deriveDurationFromClips(tags, rootTag) {
|
|
2962
|
+
const clipEnds = [];
|
|
2963
|
+
for (const tag of tags) {
|
|
2964
|
+
if (tag === rootTag) continue;
|
|
2965
|
+
const startRaw = readAttr(tag.raw, "data-start");
|
|
2966
|
+
if (startRaw === null) continue;
|
|
2967
|
+
const start = Number(startRaw);
|
|
2968
|
+
if (!Number.isFinite(start)) {
|
|
2969
|
+
clipEnds.push(null);
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
const getAttr = (name) => readAttr(tag.raw, name);
|
|
2973
|
+
const authored = readAuthoredDurationSeconds(getAttr, start);
|
|
2974
|
+
if (CLIP_MEDIA_TAGS.has(tag.name)) {
|
|
2975
|
+
const { seconds } = resolveMediaDuration({
|
|
2976
|
+
tag: tag.name,
|
|
2977
|
+
authoredDurationSeconds: authored,
|
|
2978
|
+
sourceDurationSeconds: null,
|
|
2979
|
+
mediaStartSeconds: 0,
|
|
2980
|
+
playbackRate: 1
|
|
2981
|
+
});
|
|
2982
|
+
clipEnds.push(seconds === null ? null : start + seconds);
|
|
2983
|
+
} else if (authored !== null && authored > 0) {
|
|
2984
|
+
clipEnds.push(start + authored);
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
return resolveCompositionDuration({ authoredDurationSeconds: null, clipEndsSeconds: clipEnds });
|
|
2988
|
+
}
|
|
2930
2989
|
|
|
2931
2990
|
// src/rules/gsap.ts
|
|
2932
2991
|
async function loadParseGsapScript() {
|
|
@@ -5322,6 +5381,155 @@ var slideshowRules = [
|
|
|
5322
5381
|
}
|
|
5323
5382
|
];
|
|
5324
5383
|
|
|
5384
|
+
// src/rules/structure.ts
|
|
5385
|
+
import {
|
|
5386
|
+
isSubCompositionHost,
|
|
5387
|
+
topLevelElements,
|
|
5388
|
+
trackKindOf
|
|
5389
|
+
} from "@hyperframes/parsers/top-level-elements";
|
|
5390
|
+
var INLINE_TEXT_TAGS = /* @__PURE__ */ new Set([
|
|
5391
|
+
"br",
|
|
5392
|
+
"wbr",
|
|
5393
|
+
"b",
|
|
5394
|
+
"i",
|
|
5395
|
+
"u",
|
|
5396
|
+
"s",
|
|
5397
|
+
"em",
|
|
5398
|
+
"strong",
|
|
5399
|
+
"small",
|
|
5400
|
+
"sub",
|
|
5401
|
+
"sup",
|
|
5402
|
+
"mark",
|
|
5403
|
+
"span",
|
|
5404
|
+
"a",
|
|
5405
|
+
"code",
|
|
5406
|
+
"abbr"
|
|
5407
|
+
]);
|
|
5408
|
+
var OPAQUE_TAGS = /* @__PURE__ */ new Set([
|
|
5409
|
+
"svg",
|
|
5410
|
+
"canvas",
|
|
5411
|
+
"picture",
|
|
5412
|
+
"video",
|
|
5413
|
+
"audio",
|
|
5414
|
+
"select",
|
|
5415
|
+
"textarea",
|
|
5416
|
+
"iframe",
|
|
5417
|
+
"object"
|
|
5418
|
+
]);
|
|
5419
|
+
var NON_LAYOUT_TAGS = /* @__PURE__ */ new Set(["style", "script", "template", "noscript"]);
|
|
5420
|
+
var MEDIA_TAGS = /* @__PURE__ */ new Set(["video", "audio", "img"]);
|
|
5421
|
+
var NODE_ATTRS = [
|
|
5422
|
+
"id",
|
|
5423
|
+
"class",
|
|
5424
|
+
"data-start",
|
|
5425
|
+
"data-duration",
|
|
5426
|
+
"data-end",
|
|
5427
|
+
"data-track",
|
|
5428
|
+
"data-track-index",
|
|
5429
|
+
"data-track-kind",
|
|
5430
|
+
"data-composition-id",
|
|
5431
|
+
"data-composition-src"
|
|
5432
|
+
];
|
|
5433
|
+
function toNode(open) {
|
|
5434
|
+
const attrs = {};
|
|
5435
|
+
for (const name of NODE_ATTRS) attrs[name] = readDecodedAttr(open.raw, name) ?? void 0;
|
|
5436
|
+
return { tag: open.name, attrs, children: [], open };
|
|
5437
|
+
}
|
|
5438
|
+
function enclosing(stack, root, index) {
|
|
5439
|
+
let top = stack.at(-1);
|
|
5440
|
+
while (top && stack.length > 1 && index >= top.end) {
|
|
5441
|
+
stack.pop();
|
|
5442
|
+
top = stack.at(-1);
|
|
5443
|
+
}
|
|
5444
|
+
return top?.node ?? root;
|
|
5445
|
+
}
|
|
5446
|
+
function buildTree(ctx) {
|
|
5447
|
+
const { rootTag } = ctx;
|
|
5448
|
+
const rootEnd = rootTag?.closeIndex;
|
|
5449
|
+
if (!rootTag || rootEnd == null) return null;
|
|
5450
|
+
const root = toNode(rootTag);
|
|
5451
|
+
const stack = [{ node: root, end: rootEnd }];
|
|
5452
|
+
for (const tag of ctx.tags.filter((t) => t.index > rootTag.index && t.index < rootEnd)) {
|
|
5453
|
+
const node = toNode(tag);
|
|
5454
|
+
enclosing(stack, root, tag.index).children.push(node);
|
|
5455
|
+
if (tag.closeIndex != null) stack.push({ node, end: tag.closeIndex });
|
|
5456
|
+
}
|
|
5457
|
+
return root;
|
|
5458
|
+
}
|
|
5459
|
+
function hasNestedStructure(node) {
|
|
5460
|
+
if (OPAQUE_TAGS.has(node.tag)) return null;
|
|
5461
|
+
for (const child of node.children) {
|
|
5462
|
+
if (NON_LAYOUT_TAGS.has(child.tag)) continue;
|
|
5463
|
+
if (!INLINE_TEXT_TAGS.has(child.tag)) return child;
|
|
5464
|
+
const deeper = hasNestedStructure(child);
|
|
5465
|
+
if (deeper) return deeper;
|
|
5466
|
+
}
|
|
5467
|
+
return null;
|
|
5468
|
+
}
|
|
5469
|
+
var describe = (node) => `<${node.tag}${node.attrs.id ? ` id="${node.attrs.id}"` : ""}>`;
|
|
5470
|
+
function nestedStructureFindings(rows, severity) {
|
|
5471
|
+
return rows.flatMap((row) => {
|
|
5472
|
+
const nested = isSubCompositionHost(row) ? null : hasNestedStructure(row);
|
|
5473
|
+
if (!nested) return [];
|
|
5474
|
+
return [
|
|
5475
|
+
{
|
|
5476
|
+
code: "nested_structure_needs_subcomposition",
|
|
5477
|
+
severity,
|
|
5478
|
+
message: `${describe(row)} is a timeline element that contains nested ${describe(nested)}. The timeline shows one row per top-level element, and the root composition is built only from sub-compositions.`,
|
|
5479
|
+
elementId: row.attrs.id,
|
|
5480
|
+
fixHint: `Move ${describe(row)} and its contents into a sub-composition file and mount it with data-composition-src.`
|
|
5481
|
+
}
|
|
5482
|
+
];
|
|
5483
|
+
});
|
|
5484
|
+
}
|
|
5485
|
+
function missingDurationFindings(rows, severity) {
|
|
5486
|
+
return rows.filter(
|
|
5487
|
+
(row) => !MEDIA_TAGS.has(row.tag) && row.attrs["data-duration"] === void 0 && row.attrs["data-end"] === void 0 && !isSubCompositionHost(row)
|
|
5488
|
+
).map((row) => ({
|
|
5489
|
+
code: "timeline_element_missing_timing",
|
|
5490
|
+
severity,
|
|
5491
|
+
message: `${describe(row)} is a timeline element without data-duration, so the timeline cannot draw where it ends.`,
|
|
5492
|
+
elementId: row.attrs.id,
|
|
5493
|
+
fixHint: `Add data-duration (in seconds) to ${describe(row)}.`
|
|
5494
|
+
}));
|
|
5495
|
+
}
|
|
5496
|
+
function captionFindings(rows, severity) {
|
|
5497
|
+
const captionRows = rows.filter((row) => trackKindOf(row).kind === "captions");
|
|
5498
|
+
const findings = captionRows.filter((row) => trackKindOf(row).source === "legacy-captions").map((row) => ({
|
|
5499
|
+
code: "caption_track_kind_missing",
|
|
5500
|
+
severity,
|
|
5501
|
+
message: `${describe(row)} looks like captions but is not marked as such, so the timeline cannot group it on the caption track.`,
|
|
5502
|
+
elementId: row.attrs.id,
|
|
5503
|
+
fixHint: `Add data-track-kind="captions" to ${describe(row)}.`
|
|
5504
|
+
}));
|
|
5505
|
+
const lanes = new Set(
|
|
5506
|
+
captionRows.flatMap((row) => row.attrs["data-track-index"] ?? row.attrs["data-track"] ?? [])
|
|
5507
|
+
);
|
|
5508
|
+
if (lanes.size > 1) {
|
|
5509
|
+
findings.push({
|
|
5510
|
+
code: "multiple_caption_tracks",
|
|
5511
|
+
severity,
|
|
5512
|
+
message: `Captions are spread across ${lanes.size} timeline tracks. Keep all captions on one track.`,
|
|
5513
|
+
fixHint: "Give every caption element the same data-track-index."
|
|
5514
|
+
});
|
|
5515
|
+
}
|
|
5516
|
+
return findings;
|
|
5517
|
+
}
|
|
5518
|
+
var structureRules = [
|
|
5519
|
+
(ctx) => {
|
|
5520
|
+
if (ctx.options.isSubComposition) return [];
|
|
5521
|
+
const root = buildTree(ctx);
|
|
5522
|
+
if (!root) return [];
|
|
5523
|
+
const severity = ctx.options.host === "studio" ? "error" : "warning";
|
|
5524
|
+
const rows = topLevelElements(root);
|
|
5525
|
+
return [
|
|
5526
|
+
...nestedStructureFindings(rows, severity),
|
|
5527
|
+
...missingDurationFindings(rows, severity),
|
|
5528
|
+
...captionFindings(rows, severity)
|
|
5529
|
+
];
|
|
5530
|
+
}
|
|
5531
|
+
];
|
|
5532
|
+
|
|
5325
5533
|
// src/hyperframeLinter.ts
|
|
5326
5534
|
var RULE_GROUPS = [
|
|
5327
5535
|
{ group: "core", rules: coreRules },
|
|
@@ -5332,7 +5540,8 @@ var RULE_GROUPS = [
|
|
|
5332
5540
|
{ group: "adapters", rules: adapterRules },
|
|
5333
5541
|
{ group: "textures", rules: textureRules },
|
|
5334
5542
|
{ group: "fonts", rules: fontRules },
|
|
5335
|
-
{ group: "slideshow", rules: slideshowRules }
|
|
5543
|
+
{ group: "slideshow", rules: slideshowRules },
|
|
5544
|
+
{ group: "structure", rules: structureRules }
|
|
5336
5545
|
];
|
|
5337
5546
|
var LINT_RULE_COUNT = RULE_GROUPS.reduce((n, g) => n + g.rules.length, 0);
|
|
5338
5547
|
var LINT_RULE_GROUP_COUNTS = Object.fromEntries(
|
|
@@ -5476,7 +5685,8 @@ import {
|
|
|
5476
5685
|
isWithinProjectRoot,
|
|
5477
5686
|
maskNonScannableRanges as maskNonScannableRanges2,
|
|
5478
5687
|
resolveExistingLocalAsset as resolveExistingLocalAsset2,
|
|
5479
|
-
resolveLocalAssetCandidates
|
|
5688
|
+
resolveLocalAssetCandidates,
|
|
5689
|
+
resolveProjectRelativeSrc
|
|
5480
5690
|
} from "@hyperframes/parsers/asset-resolution";
|
|
5481
5691
|
|
|
5482
5692
|
// src/hevcPreviewLint.ts
|
|
@@ -5794,7 +6004,7 @@ function resolveCssAssetCandidates(projectDir, url, htmlCompSrcPath, cssRootRela
|
|
|
5794
6004
|
}
|
|
5795
6005
|
return resolveLocalAssetCandidates(projectDir, url);
|
|
5796
6006
|
}
|
|
5797
|
-
async function lintProject(projectDir, entryFile) {
|
|
6007
|
+
async function lintProject(projectDir, entryFile, hostOptions = {}) {
|
|
5798
6008
|
const indexPath = entryFile ? resolve(entryFile) : resolve(projectDir, "index.html");
|
|
5799
6009
|
if (entryFile && !isWithinProjectRoot(projectDir, indexPath)) {
|
|
5800
6010
|
throw new Error(`Explicit lint entry is outside the project directory: ${entryFile}`);
|
|
@@ -5807,6 +6017,7 @@ async function lintProject(projectDir, entryFile) {
|
|
|
5807
6017
|
let totalInfos = 0;
|
|
5808
6018
|
const rootHtml = readFileSync(indexPath, "utf-8");
|
|
5809
6019
|
const rootResult = await lintHyperframeHtml(rootHtml, {
|
|
6020
|
+
...hostOptions,
|
|
5810
6021
|
filePath: indexPath,
|
|
5811
6022
|
externalStyles: collectExternalStyles(projectDir, rootHtml, rootCompSrcPath)
|
|
5812
6023
|
});
|
|
@@ -5838,6 +6049,7 @@ async function lintProject(projectDir, entryFile) {
|
|
|
5838
6049
|
allHtmlSources.push({ html, compSrcPath });
|
|
5839
6050
|
if (isSnippetFragment(html)) continue;
|
|
5840
6051
|
const result = await lintHyperframeHtml(html, {
|
|
6052
|
+
...hostOptions,
|
|
5841
6053
|
filePath,
|
|
5842
6054
|
isSubComposition: true,
|
|
5843
6055
|
externalStyles: collectExternalStyles(projectDir, html, compSrcPath)
|
|
@@ -5945,7 +6157,7 @@ function lintAudioSrcNotFound(projectDir, htmlSources) {
|
|
|
5945
6157
|
if (/^(https?:|data:|blob:)/i.test(src)) continue;
|
|
5946
6158
|
if (isUnresolvedAssetPlaceholder2(src)) continue;
|
|
5947
6159
|
const rootRelative = compSrcPath ? rewriteAssetPath2(compSrcPath, src, (path) => existsSync2(join2(projectDir, path))) : src;
|
|
5948
|
-
if (!
|
|
6160
|
+
if (!existsSync2(resolveProjectRelativeSrc(rootRelative, projectDir))) {
|
|
5949
6161
|
missingSrcs.push(src);
|
|
5950
6162
|
}
|
|
5951
6163
|
}
|