@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/browser.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/browser.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;
|
|
@@ -709,7 +711,8 @@ function buildLintContext(html, options = {}) {
|
|
|
709
711
|
content: style.content,
|
|
710
712
|
raw: style.content,
|
|
711
713
|
index: -1,
|
|
712
|
-
file: style.file ?? style.href
|
|
714
|
+
file: style.file ?? style.href,
|
|
715
|
+
rootRelativePath: style.rootRelativePath
|
|
713
716
|
}))
|
|
714
717
|
];
|
|
715
718
|
const scripts = structure.scripts;
|
|
@@ -745,6 +748,12 @@ import {
|
|
|
745
748
|
HTML_BODY_CSS_HEIGHT_FIRST_RE,
|
|
746
749
|
VIEWPORT_META_SIZE_RE
|
|
747
750
|
} from "@hyperframes/parsers/composition";
|
|
751
|
+
var SCRIPT_SYNTAX_EXEMPT_TYPES = /* @__PURE__ */ new Set([
|
|
752
|
+
"application/json",
|
|
753
|
+
"application/hyperframes-slideshow+json",
|
|
754
|
+
"importmap",
|
|
755
|
+
"module"
|
|
756
|
+
]);
|
|
748
757
|
function repeatedDescendantId(selector) {
|
|
749
758
|
let repeated = null;
|
|
750
759
|
const requiredPseudoIds = (pseudo) => {
|
|
@@ -1101,7 +1110,7 @@ var coreRules = [
|
|
|
1101
1110
|
},
|
|
1102
1111
|
// root_missing_composition_id + root_missing_dimensions
|
|
1103
1112
|
// fallow-ignore-next-line complexity
|
|
1104
|
-
({ rootTag }) => {
|
|
1113
|
+
({ rootTag, options }) => {
|
|
1105
1114
|
const findings = [];
|
|
1106
1115
|
if (!rootTag || !readDecodedAttr(rootTag.raw, "data-composition-id")) {
|
|
1107
1116
|
findings.push({
|
|
@@ -1113,7 +1122,7 @@ var coreRules = [
|
|
|
1113
1122
|
snippet: truncateSnippet(rootTag?.raw || "")
|
|
1114
1123
|
});
|
|
1115
1124
|
}
|
|
1116
|
-
if (!rootTag || !readAttr(rootTag.raw, "data-width") || !readAttr(rootTag.raw, "data-height")) {
|
|
1125
|
+
if (!options.isSubComposition && (!rootTag || !readAttr(rootTag.raw, "data-width") || !readAttr(rootTag.raw, "data-height"))) {
|
|
1117
1126
|
findings.push({
|
|
1118
1127
|
code: "root_missing_dimensions",
|
|
1119
1128
|
severity: "error",
|
|
@@ -1363,10 +1372,8 @@ var coreRules = [
|
|
|
1363
1372
|
({ scripts, locate }) => {
|
|
1364
1373
|
const findings = [];
|
|
1365
1374
|
for (const script of scripts) {
|
|
1366
|
-
const
|
|
1367
|
-
if (
|
|
1368
|
-
attrs
|
|
1369
|
-
))
|
|
1375
|
+
const type = readDecodedAttr(script.raw, "type")?.trim().toLowerCase() ?? "";
|
|
1376
|
+
if (readDecodedAttr(script.raw, "src") !== null || SCRIPT_SYNTAX_EXEMPT_TYPES.has(type))
|
|
1370
1377
|
continue;
|
|
1371
1378
|
const syntaxError = getInlineScriptSyntaxError(script.content);
|
|
1372
1379
|
if (!syntaxError) continue;
|
|
@@ -1783,6 +1790,7 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1783
1790
|
};
|
|
1784
1791
|
if (mediaTags.length === 0 || ctx.scripts.length === 0) return findings;
|
|
1785
1792
|
for (const script of ctx.scripts) {
|
|
1793
|
+
const code = stripJsCode(script.content);
|
|
1786
1794
|
const mediaVars = /* @__PURE__ */ new Map();
|
|
1787
1795
|
const assignmentPatterns = [
|
|
1788
1796
|
{
|
|
@@ -1799,6 +1807,7 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1799
1807
|
for (const { pattern, variableIndex, targetIndex } of assignmentPatterns) {
|
|
1800
1808
|
let match;
|
|
1801
1809
|
while ((match = pattern.exec(script.content)) !== null) {
|
|
1810
|
+
if (code[match.index] !== script.content[match.index]) continue;
|
|
1802
1811
|
const variableName = match[variableIndex];
|
|
1803
1812
|
const target = match[targetIndex];
|
|
1804
1813
|
if (!variableName || !target) continue;
|
|
@@ -1809,12 +1818,12 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1809
1818
|
}
|
|
1810
1819
|
const directIdPatterns = [
|
|
1811
1820
|
{
|
|
1812
|
-
pattern: /\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)
|
|
1821
|
+
pattern: /\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)\s*(?:\?\.|\.)\s*play\s*(?:\?\.\s*)?\(/g,
|
|
1813
1822
|
kind: "play()",
|
|
1814
1823
|
targetIndex: 1
|
|
1815
1824
|
},
|
|
1816
1825
|
{
|
|
1817
|
-
pattern: /\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)
|
|
1826
|
+
pattern: /\b(?:document|window\.document)\.getElementById\(\s*["']([^"']+)["']\s*\)\s*(?:\?\.|\.)\s*pause\s*(?:\?\.\s*)?\(/g,
|
|
1818
1827
|
kind: "pause()",
|
|
1819
1828
|
targetIndex: 1
|
|
1820
1829
|
},
|
|
@@ -1829,12 +1838,12 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1829
1838
|
targetIndex: 1
|
|
1830
1839
|
},
|
|
1831
1840
|
{
|
|
1832
|
-
pattern: /\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)
|
|
1841
|
+
pattern: /\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)\s*(?:\?\.|\.)\s*play\s*(?:\?\.\s*)?\(/g,
|
|
1833
1842
|
kind: "play()",
|
|
1834
1843
|
targetIndex: 2
|
|
1835
1844
|
},
|
|
1836
1845
|
{
|
|
1837
|
-
pattern: /\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)
|
|
1846
|
+
pattern: /\b(?:document|window\.document)\.querySelector\(\s*(["'])([\s\S]*?)\1\s*\)\s*(?:\?\.|\.)\s*pause\s*(?:\?\.\s*)?\(/g,
|
|
1838
1847
|
kind: "pause()",
|
|
1839
1848
|
targetIndex: 2
|
|
1840
1849
|
},
|
|
@@ -1852,6 +1861,7 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1852
1861
|
for (const { pattern, kind, targetIndex } of directIdPatterns) {
|
|
1853
1862
|
let match;
|
|
1854
1863
|
while ((match = pattern.exec(script.content)) !== null) {
|
|
1864
|
+
if (code[match.index] !== script.content[match.index]) continue;
|
|
1855
1865
|
const target = match[targetIndex];
|
|
1856
1866
|
if (!target) continue;
|
|
1857
1867
|
const elementId = mediaIndex.ids.has(target) ? target : selectorTargetsManagedMedia(target, mediaIndex) ? void 0 : null;
|
|
@@ -1869,8 +1879,20 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1869
1879
|
for (const [variableName, elementId] of mediaVars) {
|
|
1870
1880
|
const escapedVar = escapeRegExp(variableName);
|
|
1871
1881
|
const variablePatterns = [
|
|
1872
|
-
{
|
|
1873
|
-
|
|
1882
|
+
{
|
|
1883
|
+
pattern: new RegExp(
|
|
1884
|
+
`\\b${escapedVar}\\s*(?:\\?\\.|\\.)\\s*play\\s*(?:\\?\\.\\s*)?\\(`,
|
|
1885
|
+
"g"
|
|
1886
|
+
),
|
|
1887
|
+
kind: "play()"
|
|
1888
|
+
},
|
|
1889
|
+
{
|
|
1890
|
+
pattern: new RegExp(
|
|
1891
|
+
`\\b${escapedVar}\\s*(?:\\?\\.|\\.)\\s*pause\\s*(?:\\?\\.\\s*)?\\(`,
|
|
1892
|
+
"g"
|
|
1893
|
+
),
|
|
1894
|
+
kind: "pause()"
|
|
1895
|
+
},
|
|
1874
1896
|
{ pattern: new RegExp(`\\b${escapedVar}\\.currentTime\\s*=`, "g"), kind: "currentTime" },
|
|
1875
1897
|
{
|
|
1876
1898
|
pattern: new RegExp(`\\b${escapedVar}\\.muted\\s*=`, "g"),
|
|
@@ -1880,6 +1902,7 @@ function findImperativeMediaControlFindings(ctx) {
|
|
|
1880
1902
|
for (const { pattern, kind } of variablePatterns) {
|
|
1881
1903
|
let match;
|
|
1882
1904
|
while ((match = pattern.exec(script.content)) !== null) {
|
|
1905
|
+
if (code[match.index] !== script.content[match.index]) continue;
|
|
1883
1906
|
findings.push({
|
|
1884
1907
|
code: "imperative_media_control",
|
|
1885
1908
|
severity: "error",
|
|
@@ -2582,6 +2605,24 @@ function extractCssUrlReferences(css) {
|
|
|
2582
2605
|
}
|
|
2583
2606
|
return out;
|
|
2584
2607
|
}
|
|
2608
|
+
function folderDepth(filePath) {
|
|
2609
|
+
if (/^(?:[a-z]:)?[\\/]/i.test(filePath)) return 0;
|
|
2610
|
+
let depth = 0;
|
|
2611
|
+
for (const segment of filePath.split(/[\\/]/).slice(0, -1)) {
|
|
2612
|
+
if (segment === "..") depth = Math.max(0, depth - 1);
|
|
2613
|
+
else if (segment && segment !== ".") depth += 1;
|
|
2614
|
+
}
|
|
2615
|
+
return depth;
|
|
2616
|
+
}
|
|
2617
|
+
function climbsAboveRoot(fileDepth, path) {
|
|
2618
|
+
let depth = fileDepth;
|
|
2619
|
+
for (const segment of path.split("/")) {
|
|
2620
|
+
if (segment === "..") depth -= 1;
|
|
2621
|
+
else if (segment && segment !== ".") depth += 1;
|
|
2622
|
+
if (depth < 0) return true;
|
|
2623
|
+
}
|
|
2624
|
+
return false;
|
|
2625
|
+
}
|
|
2585
2626
|
function extractCssSelectors(css) {
|
|
2586
2627
|
const out = [];
|
|
2587
2628
|
const noComments = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -2895,42 +2936,31 @@ var compositionRules = [
|
|
|
2895
2936
|
}
|
|
2896
2937
|
return findings;
|
|
2897
2938
|
},
|
|
2898
|
-
// invalid_parent_traversal_in_asset_path —
|
|
2899
|
-
//
|
|
2900
|
-
//
|
|
2901
|
-
// with the project root as their base URL, so any `../`-traversing path
|
|
2902
|
-
// climbs above the project root and 404s in Studio preview. Renders
|
|
2903
|
-
// tolerate it because the server-side bundler rewrites `../foo` against
|
|
2904
|
-
// each sub-composition's source path; the runtime now mirrors that fallback
|
|
2905
|
-
// (see rewriteSubCompositionAssetPaths in runtime/compositionLoader.ts), but
|
|
2906
|
-
// the authoring-time signal is still wrong — flag it at lint time so the
|
|
2907
|
-
// baked path is plain root-relative and matches what the bundler emits.
|
|
2908
|
-
//
|
|
2909
|
-
// Mirrors the runtime fallback's surface: `[src]` / `[href]` attribute
|
|
2910
|
-
// values, `[style]` inline url(), and `<style>` block url() references.
|
|
2911
|
-
// Skips absolute URLs (http(s)://, //, data:, /-prefixed root-relative),
|
|
2912
|
-
// hash anchors, and plain relative paths (`assets/x.mp4`) — only `../`
|
|
2913
|
-
// traversal is flagged. Subsumes the older `../capture/`-specific rule.
|
|
2939
|
+
// invalid_parent_traversal_in_asset_path — a `../` path in src, href or a CSS url() that leaves
|
|
2940
|
+
// the project once resolved from the file's own folder, as the bundler, producer and runtime
|
|
2941
|
+
// resolve it. "../assets/x" from compositions/ stays inside and passes.
|
|
2914
2942
|
// fallow-ignore-next-line complexity
|
|
2915
2943
|
({ tags, styles, rawSource, options }) => {
|
|
2916
2944
|
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
|
|
2917
2945
|
const offenders = [];
|
|
2918
|
-
const
|
|
2919
|
-
|
|
2920
|
-
const trimmed = value
|
|
2921
|
-
if (
|
|
2922
|
-
|
|
2946
|
+
const fileDepth = folderDepth(options.compSrcPath ?? "index.html");
|
|
2947
|
+
const collect = (value, depth = fileDepth) => {
|
|
2948
|
+
const trimmed = value?.trim() ?? "";
|
|
2949
|
+
if (/^(?:[a-z][a-z0-9+.-]*:|[\\/#])/i.test(trimmed)) return;
|
|
2950
|
+
const path = (trimmed.split(/[?#]/, 1)[0] ?? "").replace(/\\|%2f/gi, "/").replace(/%2e/gi, ".");
|
|
2951
|
+
if (climbsAboveRoot(depth, path)) offenders.push(trimmed);
|
|
2923
2952
|
};
|
|
2924
2953
|
for (const tag of tags) {
|
|
2925
|
-
collect(
|
|
2926
|
-
collect(
|
|
2954
|
+
collect(readDecodedAttr(tag.raw, "src"));
|
|
2955
|
+
collect(readDecodedAttr(tag.raw, "href"));
|
|
2927
2956
|
const styleAttr = readJsonAttr(tag.raw, "style");
|
|
2928
2957
|
if (styleAttr) {
|
|
2929
2958
|
for (const url of extractCssUrlReferences(styleAttr)) collect(url);
|
|
2930
2959
|
}
|
|
2931
2960
|
}
|
|
2932
2961
|
for (const style of styles) {
|
|
2933
|
-
|
|
2962
|
+
const depth = style.rootRelativePath ? folderDepth(style.rootRelativePath) : fileDepth;
|
|
2963
|
+
for (const url of extractCssUrlReferences(style.content)) collect(url, depth);
|
|
2934
2964
|
}
|
|
2935
2965
|
if (offenders.length === 0) return [];
|
|
2936
2966
|
const prefixCounts = /* @__PURE__ */ new Map();
|
|
@@ -2943,8 +2973,8 @@ var compositionRules = [
|
|
|
2943
2973
|
{
|
|
2944
2974
|
code: "invalid_parent_traversal_in_asset_path",
|
|
2945
2975
|
severity: "error",
|
|
2946
|
-
message: `Found ${offenders.length} asset path(s) traversing above the project root with "../" (${prefixSummary})
|
|
2947
|
-
fixHint:
|
|
2976
|
+
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.`,
|
|
2977
|
+
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.`
|
|
2948
2978
|
}
|
|
2949
2979
|
];
|
|
2950
2980
|
},
|