@hyperframes/lint 0.8.142 → 0.8.144
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 +80 -50
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +83 -51
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -36,10 +36,12 @@ type HyperframeLintResult = {
|
|
|
36
36
|
type HyperframeLinterOptions = {
|
|
37
37
|
filePath?: string;
|
|
38
38
|
isSubComposition?: boolean;
|
|
39
|
+
compSrcPath?: string;
|
|
39
40
|
externalStyles?: Array<{
|
|
40
41
|
href: string;
|
|
41
42
|
content: string;
|
|
42
43
|
file?: string;
|
|
44
|
+
rootRelativePath?: string;
|
|
43
45
|
}>;
|
|
44
46
|
/**
|
|
45
47
|
* Set to `true` when linting compositions destined for distributed / Lambda
|
package/dist/index.js
CHANGED
|
@@ -71,12 +71,13 @@ function parseHtmlStructure(source) {
|
|
|
71
71
|
function findHtmlTag(tags) {
|
|
72
72
|
return tags.find((tag) => tag.name === "html") ?? null;
|
|
73
73
|
}
|
|
74
|
+
var hasCompositionMarker = (tag) => Boolean(
|
|
75
|
+
readDecodedAttr(tag.raw, "data-composition-id") || readAttr(tag.raw, "data-width") || readAttr(tag.raw, "data-height")
|
|
76
|
+
);
|
|
74
77
|
function findRootTag(source, parsedTags) {
|
|
75
78
|
const tags = parsedTags ?? parseHtmlStructure(source).tags;
|
|
76
79
|
const bodyTag = tags.find((tag) => tag.name === "body");
|
|
77
|
-
if (bodyTag && (
|
|
78
|
-
return bodyTag;
|
|
79
|
-
}
|
|
80
|
+
if (bodyTag && hasCompositionMarker(bodyTag)) return bodyTag;
|
|
80
81
|
const bodyStart = bodyTag ? bodyTag.index + bodyTag.raw.length : 0;
|
|
81
82
|
const bodyEnd = bodyTag?.closeIndex ?? source.length;
|
|
82
83
|
const bodyTags = tags.filter((tag) => tag.index >= bodyStart && tag.index < bodyEnd);
|
|
@@ -84,7 +85,8 @@ function findRootTag(source, parsedTags) {
|
|
|
84
85
|
for (const tag of bodyTags) {
|
|
85
86
|
if (tag.index < skipBefore) continue;
|
|
86
87
|
if (["script", "style", "meta", "link", "title"].includes(tag.name)) continue;
|
|
87
|
-
if (tag.name === "
|
|
88
|
+
if ((tag.name === "html" || tag.name === "head") && !hasCompositionMarker(tag)) continue;
|
|
89
|
+
if (tag.name === "svg" && !hasCompositionMarker(tag)) {
|
|
88
90
|
skipBefore = tag.endIndex ?? Infinity;
|
|
89
91
|
continue;
|
|
90
92
|
}
|
|
@@ -108,10 +110,7 @@ function hasUnquotedLessThan(attrs) {
|
|
|
108
110
|
return false;
|
|
109
111
|
}
|
|
110
112
|
function readAttr(tagSource, attr) {
|
|
111
|
-
|
|
112
|
-
const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
113
|
-
const match = tagSource.match(new RegExp(`(?<![\\w-])${escaped}\\s*=\\s*["']([^"']+)["']`, "i"));
|
|
114
|
-
return match?.[1] || null;
|
|
113
|
+
return readAttributeValue(tagSource, attr, false) || null;
|
|
115
114
|
}
|
|
116
115
|
function hasAttrName(tagSource, attr) {
|
|
117
116
|
return readDecodedAttr(tagSource, attr) !== null;
|
|
@@ -134,15 +133,18 @@ function mediaWindowsOverlap(a, b) {
|
|
|
134
133
|
return a.start < b.end && b.start < a.end;
|
|
135
134
|
}
|
|
136
135
|
function readDecodedAttr(tagSource, attr) {
|
|
136
|
+
return readAttributeValue(tagSource, attr, true);
|
|
137
|
+
}
|
|
138
|
+
function readAttributeValue(tagSource, attr, decodeEntities) {
|
|
137
139
|
if (!tagSource) return null;
|
|
138
140
|
let value = null;
|
|
139
141
|
const parser = new Parser(
|
|
140
142
|
{
|
|
141
|
-
onattribute(name,
|
|
142
|
-
if (value === null && name.toLowerCase() === attr.toLowerCase()) value =
|
|
143
|
+
onattribute(name, attributeValue) {
|
|
144
|
+
if (value === null && name.toLowerCase() === attr.toLowerCase()) value = attributeValue;
|
|
143
145
|
}
|
|
144
146
|
},
|
|
145
|
-
{ decodeEntities
|
|
147
|
+
{ decodeEntities, lowerCaseAttributeNames: false, lowerCaseTags: true }
|
|
146
148
|
);
|
|
147
149
|
parser.end(tagSource);
|
|
148
150
|
return value;
|
|
@@ -712,7 +714,8 @@ function buildLintContext(html, options = {}) {
|
|
|
712
714
|
content: style.content,
|
|
713
715
|
raw: style.content,
|
|
714
716
|
index: -1,
|
|
715
|
-
file: style.file ?? style.href
|
|
717
|
+
file: style.file ?? style.href,
|
|
718
|
+
rootRelativePath: style.rootRelativePath
|
|
716
719
|
}))
|
|
717
720
|
];
|
|
718
721
|
const scripts = structure.scripts;
|
|
@@ -748,6 +751,12 @@ import {
|
|
|
748
751
|
HTML_BODY_CSS_HEIGHT_FIRST_RE,
|
|
749
752
|
VIEWPORT_META_SIZE_RE
|
|
750
753
|
} from "@hyperframes/parsers/composition";
|
|
754
|
+
var SCRIPT_SYNTAX_EXEMPT_TYPES = /* @__PURE__ */ new Set([
|
|
755
|
+
"application/json",
|
|
756
|
+
"application/hyperframes-slideshow+json",
|
|
757
|
+
"importmap",
|
|
758
|
+
"module"
|
|
759
|
+
]);
|
|
751
760
|
function repeatedDescendantId(selector) {
|
|
752
761
|
let repeated = null;
|
|
753
762
|
const requiredPseudoIds = (pseudo) => {
|
|
@@ -1104,7 +1113,7 @@ var coreRules = [
|
|
|
1104
1113
|
},
|
|
1105
1114
|
// root_missing_composition_id + root_missing_dimensions
|
|
1106
1115
|
// fallow-ignore-next-line complexity
|
|
1107
|
-
({ rootTag }) => {
|
|
1116
|
+
({ rootTag, options }) => {
|
|
1108
1117
|
const findings = [];
|
|
1109
1118
|
if (!rootTag || !readDecodedAttr(rootTag.raw, "data-composition-id")) {
|
|
1110
1119
|
findings.push({
|
|
@@ -1116,7 +1125,7 @@ var coreRules = [
|
|
|
1116
1125
|
snippet: truncateSnippet(rootTag?.raw || "")
|
|
1117
1126
|
});
|
|
1118
1127
|
}
|
|
1119
|
-
if (!rootTag || !readAttr(rootTag.raw, "data-width") || !readAttr(rootTag.raw, "data-height")) {
|
|
1128
|
+
if (!options.isSubComposition && (!rootTag || !readAttr(rootTag.raw, "data-width") || !readAttr(rootTag.raw, "data-height"))) {
|
|
1120
1129
|
findings.push({
|
|
1121
1130
|
code: "root_missing_dimensions",
|
|
1122
1131
|
severity: "error",
|
|
@@ -1366,10 +1375,8 @@ var coreRules = [
|
|
|
1366
1375
|
({ scripts, locate }) => {
|
|
1367
1376
|
const findings = [];
|
|
1368
1377
|
for (const script of scripts) {
|
|
1369
|
-
const
|
|
1370
|
-
if (
|
|
1371
|
-
attrs
|
|
1372
|
-
))
|
|
1378
|
+
const type = readDecodedAttr(script.raw, "type")?.trim().toLowerCase() ?? "";
|
|
1379
|
+
if (readDecodedAttr(script.raw, "src") !== null || SCRIPT_SYNTAX_EXEMPT_TYPES.has(type))
|
|
1373
1380
|
continue;
|
|
1374
1381
|
const syntaxError = getInlineScriptSyntaxError(script.content);
|
|
1375
1382
|
if (!syntaxError) continue;
|
|
@@ -1786,6 +1793,7 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1786
1793
|
};
|
|
1787
1794
|
if (mediaTags.length === 0 || ctx.scripts.length === 0) return findings;
|
|
1788
1795
|
for (const script of ctx.scripts) {
|
|
1796
|
+
const code = stripJsCode(script.content);
|
|
1789
1797
|
const mediaVars = /* @__PURE__ */ new Map();
|
|
1790
1798
|
const assignmentPatterns = [
|
|
1791
1799
|
{
|
|
@@ -1802,6 +1810,7 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1802
1810
|
for (const { pattern, variableIndex, targetIndex } of assignmentPatterns) {
|
|
1803
1811
|
let match;
|
|
1804
1812
|
while ((match = pattern.exec(script.content)) !== null) {
|
|
1813
|
+
if (code[match.index] !== script.content[match.index]) continue;
|
|
1805
1814
|
const variableName = match[variableIndex];
|
|
1806
1815
|
const target = match[targetIndex];
|
|
1807
1816
|
if (!variableName || !target) continue;
|
|
@@ -1812,12 +1821,12 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1812
1821
|
}
|
|
1813
1822
|
const directIdPatterns = [
|
|
1814
1823
|
{
|
|
1815
|
-
pattern: /\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)
|
|
1824
|
+
pattern: /\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)\s*(?:\?\.|\.)\s*play\s*(?:\?\.\s*)?\(/g,
|
|
1816
1825
|
kind: "play()",
|
|
1817
1826
|
targetIndex: 1
|
|
1818
1827
|
},
|
|
1819
1828
|
{
|
|
1820
|
-
pattern: /\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)
|
|
1829
|
+
pattern: /\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)\s*(?:\?\.|\.)\s*pause\s*(?:\?\.\s*)?\(/g,
|
|
1821
1830
|
kind: "pause()",
|
|
1822
1831
|
targetIndex: 1
|
|
1823
1832
|
},
|
|
@@ -1832,12 +1841,12 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1832
1841
|
targetIndex: 1
|
|
1833
1842
|
},
|
|
1834
1843
|
{
|
|
1835
|
-
pattern: /\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)
|
|
1844
|
+
pattern: /\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)\s*(?:\?\.|\.)\s*play\s*(?:\?\.\s*)?\(/g,
|
|
1836
1845
|
kind: "play()",
|
|
1837
1846
|
targetIndex: 2
|
|
1838
1847
|
},
|
|
1839
1848
|
{
|
|
1840
|
-
pattern: /\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)
|
|
1849
|
+
pattern: /\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)\s*(?:\?\.|\.)\s*pause\s*(?:\?\.\s*)?\(/g,
|
|
1841
1850
|
kind: "pause()",
|
|
1842
1851
|
targetIndex: 2
|
|
1843
1852
|
},
|
|
@@ -1855,6 +1864,7 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1855
1864
|
for (const { pattern, kind, targetIndex } of directIdPatterns) {
|
|
1856
1865
|
let match;
|
|
1857
1866
|
while ((match = pattern.exec(script.content)) !== null) {
|
|
1867
|
+
if (code[match.index] !== script.content[match.index]) continue;
|
|
1858
1868
|
const target = match[targetIndex];
|
|
1859
1869
|
if (!target) continue;
|
|
1860
1870
|
const elementId = mediaIndex.ids.has(target) ? target : selectorTargetsManagedMedia(target, mediaIndex) ? void 0 : null;
|
|
@@ -1872,8 +1882,20 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1872
1882
|
for (const [variableName, elementId] of mediaVars) {
|
|
1873
1883
|
const escapedVar = escapeRegExp(variableName);
|
|
1874
1884
|
const variablePatterns = [
|
|
1875
|
-
{
|
|
1876
|
-
|
|
1885
|
+
{
|
|
1886
|
+
pattern: new RegExp(
|
|
1887
|
+
`\\b${escapedVar}\\s*(?:\\?\\.|\\.)\\s*play\\s*(?:\\?\\.\\s*)?\\(`,
|
|
1888
|
+
"g"
|
|
1889
|
+
),
|
|
1890
|
+
kind: "play()"
|
|
1891
|
+
},
|
|
1892
|
+
{
|
|
1893
|
+
pattern: new RegExp(
|
|
1894
|
+
`\\b${escapedVar}\\s*(?:\\?\\.|\\.)\\s*pause\\s*(?:\\?\\.\\s*)?\\(`,
|
|
1895
|
+
"g"
|
|
1896
|
+
),
|
|
1897
|
+
kind: "pause()"
|
|
1898
|
+
},
|
|
1877
1899
|
{ pattern: new RegExp(`\\b${escapedVar}\\.currentTime\\s*=`, "g"), kind: "currentTime" },
|
|
1878
1900
|
{
|
|
1879
1901
|
pattern: new RegExp(`\\b${escapedVar}\\.muted\\s*=`, "g"),
|
|
@@ -1883,6 +1905,7 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1883
1905
|
for (const { pattern, kind } of variablePatterns) {
|
|
1884
1906
|
let match;
|
|
1885
1907
|
while ((match = pattern.exec(script.content)) !== null) {
|
|
1908
|
+
if (code[match.index] !== script.content[match.index]) continue;
|
|
1886
1909
|
findings.push({
|
|
1887
1910
|
code: "imperative_media_control",
|
|
1888
1911
|
severity: "error",
|
|
@@ -2585,6 +2608,24 @@ function extractCssUrlReferences(css) {
|
|
|
2585
2608
|
}
|
|
2586
2609
|
return out;
|
|
2587
2610
|
}
|
|
2611
|
+
function folderDepth(filePath) {
|
|
2612
|
+
if (/^(?:[a-z]:)?[\\/]/i.test(filePath)) return 0;
|
|
2613
|
+
let depth = 0;
|
|
2614
|
+
for (const segment of filePath.split(/[\\/]/).slice(0, -1)) {
|
|
2615
|
+
if (segment === "..") depth = Math.max(0, depth - 1);
|
|
2616
|
+
else if (segment && segment !== ".") depth += 1;
|
|
2617
|
+
}
|
|
2618
|
+
return depth;
|
|
2619
|
+
}
|
|
2620
|
+
function climbsAboveRoot(fileDepth, path) {
|
|
2621
|
+
let depth = fileDepth;
|
|
2622
|
+
for (const segment of path.split("/")) {
|
|
2623
|
+
if (segment === "..") depth -= 1;
|
|
2624
|
+
else if (segment && segment !== ".") depth += 1;
|
|
2625
|
+
if (depth < 0) return true;
|
|
2626
|
+
}
|
|
2627
|
+
return false;
|
|
2628
|
+
}
|
|
2588
2629
|
function extractCssSelectors(css) {
|
|
2589
2630
|
const out = [];
|
|
2590
2631
|
const noComments = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -2898,42 +2939,31 @@ var compositionRules = [
|
|
|
2898
2939
|
}
|
|
2899
2940
|
return findings;
|
|
2900
2941
|
},
|
|
2901
|
-
// invalid_parent_traversal_in_asset_path —
|
|
2902
|
-
//
|
|
2903
|
-
//
|
|
2904
|
-
// with the project root as their base URL, so any `../`-traversing path
|
|
2905
|
-
// climbs above the project root and 404s in Studio preview. Renders
|
|
2906
|
-
// tolerate it because the server-side bundler rewrites `../foo` against
|
|
2907
|
-
// each sub-composition's source path; the runtime now mirrors that fallback
|
|
2908
|
-
// (see rewriteSubCompositionAssetPaths in runtime/compositionLoader.ts), but
|
|
2909
|
-
// the authoring-time signal is still wrong — flag it at lint time so the
|
|
2910
|
-
// baked path is plain root-relative and matches what the bundler emits.
|
|
2911
|
-
//
|
|
2912
|
-
// Mirrors the runtime fallback's surface: `[src]` / `[href]` attribute
|
|
2913
|
-
// values, `[style]` inline url(), and `<style>` block url() references.
|
|
2914
|
-
// Skips absolute URLs (http(s)://, //, data:, /-prefixed root-relative),
|
|
2915
|
-
// hash anchors, and plain relative paths (`assets/x.mp4`) — only `../`
|
|
2916
|
-
// traversal is flagged. Subsumes the older `../capture/`-specific rule.
|
|
2942
|
+
// invalid_parent_traversal_in_asset_path — a `../` path in src, href or a CSS url() that leaves
|
|
2943
|
+
// the project once resolved from the file's own folder, as the bundler, producer and runtime
|
|
2944
|
+
// resolve it. "../assets/x" from compositions/ stays inside and passes.
|
|
2917
2945
|
// fallow-ignore-next-line complexity
|
|
2918
2946
|
({ tags, styles, rawSource, options }) => {
|
|
2919
2947
|
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
|
|
2920
2948
|
const offenders = [];
|
|
2921
|
-
const
|
|
2922
|
-
|
|
2923
|
-
const trimmed = value
|
|
2924
|
-
if (
|
|
2925
|
-
|
|
2949
|
+
const fileDepth = folderDepth(options.compSrcPath ?? "index.html");
|
|
2950
|
+
const collect = (value, depth = fileDepth) => {
|
|
2951
|
+
const trimmed = value?.trim() ?? "";
|
|
2952
|
+
if (/^(?:[a-z][a-z0-9+.-]*:|[\\/#])/i.test(trimmed)) return;
|
|
2953
|
+
const path = (trimmed.split(/[?#]/, 1)[0] ?? "").replace(/\\|%2f/gi, "/").replace(/%2e/gi, ".");
|
|
2954
|
+
if (climbsAboveRoot(depth, path)) offenders.push(trimmed);
|
|
2926
2955
|
};
|
|
2927
2956
|
for (const tag of tags) {
|
|
2928
|
-
collect(
|
|
2929
|
-
collect(
|
|
2957
|
+
collect(readDecodedAttr(tag.raw, "src"));
|
|
2958
|
+
collect(readDecodedAttr(tag.raw, "href"));
|
|
2930
2959
|
const styleAttr = readJsonAttr(tag.raw, "style");
|
|
2931
2960
|
if (styleAttr) {
|
|
2932
2961
|
for (const url of extractCssUrlReferences(styleAttr)) collect(url);
|
|
2933
2962
|
}
|
|
2934
2963
|
}
|
|
2935
2964
|
for (const style of styles) {
|
|
2936
|
-
|
|
2965
|
+
const depth = style.rootRelativePath ? folderDepth(style.rootRelativePath) : fileDepth;
|
|
2966
|
+
for (const url of extractCssUrlReferences(style.content)) collect(url, depth);
|
|
2937
2967
|
}
|
|
2938
2968
|
if (offenders.length === 0) return [];
|
|
2939
2969
|
const prefixCounts = /* @__PURE__ */ new Map();
|
|
@@ -2946,8 +2976,8 @@ var compositionRules = [
|
|
|
2946
2976
|
{
|
|
2947
2977
|
code: "invalid_parent_traversal_in_asset_path",
|
|
2948
2978
|
severity: "error",
|
|
2949
|
-
message: `Found ${offenders.length} asset path(s) traversing above the project root with "../" (${prefixSummary})
|
|
2950
|
-
fixHint:
|
|
2979
|
+
message: `Found ${offenders.length} asset path(s) traversing above the project root with "../" (${prefixSummary}), resolved from the folder of the file that holds them. Render and preview clamp them to the project root or resolve them outside it, so they load a different file than written, or none.`,
|
|
2980
|
+
fixHint: `Point the path at a file inside the project: from compositions/scene.html, "../assets/x.png" and "assets/x.png" both reach the project's assets folder.`
|
|
2951
2981
|
}
|
|
2952
2982
|
];
|
|
2953
2983
|
},
|
|
@@ -6729,7 +6759,7 @@ function collectExternalStyles(projectDir, html, compSrcPath) {
|
|
|
6729
6759
|
document,
|
|
6730
6760
|
compSrcPath
|
|
6731
6761
|
)) {
|
|
6732
|
-
styles.push({ href, content, file: join2(projectDir, rootRelativePath) });
|
|
6762
|
+
styles.push({ href, content, file: join2(projectDir, rootRelativePath), rootRelativePath });
|
|
6733
6763
|
}
|
|
6734
6764
|
return styles;
|
|
6735
6765
|
}
|
|
@@ -6781,6 +6811,7 @@ async function lintProject(projectDir, entryFile, hostOptions = {}) {
|
|
|
6781
6811
|
const rootResult = await lintHyperframeHtml(rootHtml, {
|
|
6782
6812
|
...hostOptions,
|
|
6783
6813
|
filePath: indexPath,
|
|
6814
|
+
compSrcPath: rootFile,
|
|
6784
6815
|
externalStyles: collectExternalStyles(projectDir, rootHtml, rootCompSrcPath)
|
|
6785
6816
|
});
|
|
6786
6817
|
results.push({ file: rootFile, result: rootResult, contentHash: contentDigest(rootHtml) });
|
|
@@ -6814,6 +6845,7 @@ async function lintProject(projectDir, entryFile, hostOptions = {}) {
|
|
|
6814
6845
|
...hostOptions,
|
|
6815
6846
|
filePath,
|
|
6816
6847
|
isSubComposition: true,
|
|
6848
|
+
compSrcPath,
|
|
6817
6849
|
externalStyles: collectExternalStyles(projectDir, html, compSrcPath)
|
|
6818
6850
|
});
|
|
6819
6851
|
results.push({
|