@hyperframes/lint 0.8.48 → 0.8.50
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 +155 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +158 -2
- 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
|
@@ -5322,6 +5322,159 @@ var slideshowRules = [
|
|
|
5322
5322
|
}
|
|
5323
5323
|
];
|
|
5324
5324
|
|
|
5325
|
+
// src/rules/structure.ts
|
|
5326
|
+
import {
|
|
5327
|
+
isSubCompositionHost,
|
|
5328
|
+
topLevelElements,
|
|
5329
|
+
trackKindOf
|
|
5330
|
+
} from "@hyperframes/parsers/top-level-elements";
|
|
5331
|
+
var INLINE_TEXT_TAGS = /* @__PURE__ */ new Set([
|
|
5332
|
+
"br",
|
|
5333
|
+
"wbr",
|
|
5334
|
+
"b",
|
|
5335
|
+
"i",
|
|
5336
|
+
"u",
|
|
5337
|
+
"s",
|
|
5338
|
+
"em",
|
|
5339
|
+
"strong",
|
|
5340
|
+
"small",
|
|
5341
|
+
"sub",
|
|
5342
|
+
"sup",
|
|
5343
|
+
"mark",
|
|
5344
|
+
"span",
|
|
5345
|
+
"a",
|
|
5346
|
+
"code",
|
|
5347
|
+
"abbr"
|
|
5348
|
+
]);
|
|
5349
|
+
var OPAQUE_TAGS = /* @__PURE__ */ new Set([
|
|
5350
|
+
"svg",
|
|
5351
|
+
"canvas",
|
|
5352
|
+
"picture",
|
|
5353
|
+
"video",
|
|
5354
|
+
"audio",
|
|
5355
|
+
"select",
|
|
5356
|
+
"textarea",
|
|
5357
|
+
"iframe",
|
|
5358
|
+
"object"
|
|
5359
|
+
]);
|
|
5360
|
+
var NON_LAYOUT_TAGS = /* @__PURE__ */ new Set(["style", "script", "template", "noscript"]);
|
|
5361
|
+
var MEDIA_TAGS = /* @__PURE__ */ new Set(["img", "video", "audio"]);
|
|
5362
|
+
var NODE_ATTRS = [
|
|
5363
|
+
"id",
|
|
5364
|
+
"class",
|
|
5365
|
+
"data-start",
|
|
5366
|
+
"data-duration",
|
|
5367
|
+
"data-end",
|
|
5368
|
+
"data-track",
|
|
5369
|
+
"data-track-index",
|
|
5370
|
+
"data-track-kind",
|
|
5371
|
+
"data-composition-id",
|
|
5372
|
+
"data-composition-src"
|
|
5373
|
+
];
|
|
5374
|
+
function toNode(open) {
|
|
5375
|
+
const attrs = {};
|
|
5376
|
+
for (const name of NODE_ATTRS) attrs[name] = readDecodedAttr(open.raw, name) ?? void 0;
|
|
5377
|
+
return { tag: open.name, attrs, children: [], open };
|
|
5378
|
+
}
|
|
5379
|
+
function enclosing(stack, root, index) {
|
|
5380
|
+
let top = stack.at(-1);
|
|
5381
|
+
while (top && stack.length > 1 && index >= top.end) {
|
|
5382
|
+
stack.pop();
|
|
5383
|
+
top = stack.at(-1);
|
|
5384
|
+
}
|
|
5385
|
+
return top?.node ?? root;
|
|
5386
|
+
}
|
|
5387
|
+
function buildTree(ctx) {
|
|
5388
|
+
const { rootTag } = ctx;
|
|
5389
|
+
const rootEnd = rootTag?.closeIndex;
|
|
5390
|
+
if (!rootTag || rootEnd == null) return null;
|
|
5391
|
+
const root = toNode(rootTag);
|
|
5392
|
+
const stack = [{ node: root, end: rootEnd }];
|
|
5393
|
+
for (const tag of ctx.tags.filter((t) => t.index > rootTag.index && t.index < rootEnd)) {
|
|
5394
|
+
const node = toNode(tag);
|
|
5395
|
+
enclosing(stack, root, tag.index).children.push(node);
|
|
5396
|
+
if (tag.closeIndex != null) stack.push({ node, end: tag.closeIndex });
|
|
5397
|
+
}
|
|
5398
|
+
return root;
|
|
5399
|
+
}
|
|
5400
|
+
function hasNestedStructure(node) {
|
|
5401
|
+
if (OPAQUE_TAGS.has(node.tag)) return null;
|
|
5402
|
+
for (const child of node.children) {
|
|
5403
|
+
if (NON_LAYOUT_TAGS.has(child.tag)) continue;
|
|
5404
|
+
if (!INLINE_TEXT_TAGS.has(child.tag)) return child;
|
|
5405
|
+
const deeper = hasNestedStructure(child);
|
|
5406
|
+
if (deeper) return deeper;
|
|
5407
|
+
}
|
|
5408
|
+
return null;
|
|
5409
|
+
}
|
|
5410
|
+
var describe = (node) => `<${node.tag}${node.attrs.id ? ` id="${node.attrs.id}"` : ""}>`;
|
|
5411
|
+
function nestedStructureFindings(rows, severity) {
|
|
5412
|
+
return rows.flatMap((row) => {
|
|
5413
|
+
const nested = isSubCompositionHost(row) ? null : hasNestedStructure(row);
|
|
5414
|
+
if (!nested) return [];
|
|
5415
|
+
return [
|
|
5416
|
+
{
|
|
5417
|
+
code: "nested_structure_needs_subcomposition",
|
|
5418
|
+
severity,
|
|
5419
|
+
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.`,
|
|
5420
|
+
elementId: row.attrs.id,
|
|
5421
|
+
fixHint: `Move ${describe(row)} and its contents into a sub-composition file and mount it with data-composition-src.`
|
|
5422
|
+
}
|
|
5423
|
+
];
|
|
5424
|
+
});
|
|
5425
|
+
}
|
|
5426
|
+
function missingDurationFindings(rows, severity) {
|
|
5427
|
+
const intendedClip = (row) => !MEDIA_TAGS.has(row.tag) || row.attrs["data-start"] !== void 0 || row.attrs["data-track-index"] !== void 0;
|
|
5428
|
+
return rows.filter(
|
|
5429
|
+
(row) => row.attrs["data-duration"] === void 0 && row.attrs["data-end"] === void 0 && !isSubCompositionHost(row) && intendedClip(row)
|
|
5430
|
+
).map((row) => {
|
|
5431
|
+
const isMedia = MEDIA_TAGS.has(row.tag);
|
|
5432
|
+
return {
|
|
5433
|
+
code: isMedia ? "media_missing_duration" : "timeline_element_missing_timing",
|
|
5434
|
+
severity,
|
|
5435
|
+
message: isMedia ? `${describe(row)} is media on the timeline without data-duration, so its clip has no length.` : `${describe(row)} is a timeline element without data-duration, so the timeline cannot draw where it ends.`,
|
|
5436
|
+
elementId: row.attrs.id,
|
|
5437
|
+
fixHint: `Add data-duration (in seconds) to ${describe(row)}.`
|
|
5438
|
+
};
|
|
5439
|
+
});
|
|
5440
|
+
}
|
|
5441
|
+
function captionFindings(rows, severity) {
|
|
5442
|
+
const captionRows = rows.filter((row) => trackKindOf(row).kind === "captions");
|
|
5443
|
+
const findings = captionRows.filter((row) => trackKindOf(row).source === "legacy-captions").map((row) => ({
|
|
5444
|
+
code: "caption_track_kind_missing",
|
|
5445
|
+
severity,
|
|
5446
|
+
message: `${describe(row)} looks like captions but is not marked as such, so the timeline cannot group it on the caption track.`,
|
|
5447
|
+
elementId: row.attrs.id,
|
|
5448
|
+
fixHint: `Add data-track-kind="captions" to ${describe(row)}.`
|
|
5449
|
+
}));
|
|
5450
|
+
const lanes = new Set(
|
|
5451
|
+
captionRows.flatMap((row) => row.attrs["data-track-index"] ?? row.attrs["data-track"] ?? [])
|
|
5452
|
+
);
|
|
5453
|
+
if (lanes.size > 1) {
|
|
5454
|
+
findings.push({
|
|
5455
|
+
code: "multiple_caption_tracks",
|
|
5456
|
+
severity,
|
|
5457
|
+
message: `Captions are spread across ${lanes.size} timeline tracks. Keep all captions on one track.`,
|
|
5458
|
+
fixHint: "Give every caption element the same data-track-index."
|
|
5459
|
+
});
|
|
5460
|
+
}
|
|
5461
|
+
return findings;
|
|
5462
|
+
}
|
|
5463
|
+
var structureRules = [
|
|
5464
|
+
(ctx) => {
|
|
5465
|
+
if (ctx.options.isSubComposition) return [];
|
|
5466
|
+
const root = buildTree(ctx);
|
|
5467
|
+
if (!root) return [];
|
|
5468
|
+
const severity = ctx.options.host === "studio" ? "error" : "warning";
|
|
5469
|
+
const rows = topLevelElements(root);
|
|
5470
|
+
return [
|
|
5471
|
+
...nestedStructureFindings(rows, severity),
|
|
5472
|
+
...missingDurationFindings(rows, severity),
|
|
5473
|
+
...captionFindings(rows, severity)
|
|
5474
|
+
];
|
|
5475
|
+
}
|
|
5476
|
+
];
|
|
5477
|
+
|
|
5325
5478
|
// src/hyperframeLinter.ts
|
|
5326
5479
|
var RULE_GROUPS = [
|
|
5327
5480
|
{ group: "core", rules: coreRules },
|
|
@@ -5332,7 +5485,8 @@ var RULE_GROUPS = [
|
|
|
5332
5485
|
{ group: "adapters", rules: adapterRules },
|
|
5333
5486
|
{ group: "textures", rules: textureRules },
|
|
5334
5487
|
{ group: "fonts", rules: fontRules },
|
|
5335
|
-
{ group: "slideshow", rules: slideshowRules }
|
|
5488
|
+
{ group: "slideshow", rules: slideshowRules },
|
|
5489
|
+
{ group: "structure", rules: structureRules }
|
|
5336
5490
|
];
|
|
5337
5491
|
var LINT_RULE_COUNT = RULE_GROUPS.reduce((n, g) => n + g.rules.length, 0);
|
|
5338
5492
|
var LINT_RULE_GROUP_COUNTS = Object.fromEntries(
|
|
@@ -5794,7 +5948,7 @@ function resolveCssAssetCandidates(projectDir, url, htmlCompSrcPath, cssRootRela
|
|
|
5794
5948
|
}
|
|
5795
5949
|
return resolveLocalAssetCandidates(projectDir, url);
|
|
5796
5950
|
}
|
|
5797
|
-
async function lintProject(projectDir, entryFile) {
|
|
5951
|
+
async function lintProject(projectDir, entryFile, hostOptions = {}) {
|
|
5798
5952
|
const indexPath = entryFile ? resolve(entryFile) : resolve(projectDir, "index.html");
|
|
5799
5953
|
if (entryFile && !isWithinProjectRoot(projectDir, indexPath)) {
|
|
5800
5954
|
throw new Error(`Explicit lint entry is outside the project directory: ${entryFile}`);
|
|
@@ -5807,6 +5961,7 @@ async function lintProject(projectDir, entryFile) {
|
|
|
5807
5961
|
let totalInfos = 0;
|
|
5808
5962
|
const rootHtml = readFileSync(indexPath, "utf-8");
|
|
5809
5963
|
const rootResult = await lintHyperframeHtml(rootHtml, {
|
|
5964
|
+
...hostOptions,
|
|
5810
5965
|
filePath: indexPath,
|
|
5811
5966
|
externalStyles: collectExternalStyles(projectDir, rootHtml, rootCompSrcPath)
|
|
5812
5967
|
});
|
|
@@ -5838,6 +5993,7 @@ async function lintProject(projectDir, entryFile) {
|
|
|
5838
5993
|
allHtmlSources.push({ html, compSrcPath });
|
|
5839
5994
|
if (isSnippetFragment(html)) continue;
|
|
5840
5995
|
const result = await lintHyperframeHtml(html, {
|
|
5996
|
+
...hostOptions,
|
|
5841
5997
|
filePath,
|
|
5842
5998
|
isSubComposition: true,
|
|
5843
5999
|
externalStyles: collectExternalStyles(projectDir, html, compSrcPath)
|