@hyperframes/lint 0.8.143 → 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 +43 -33
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +46 -34
- 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
|
}
|
|
@@ -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;
|
|
@@ -1107,7 +1110,7 @@ var coreRules = [
|
|
|
1107
1110
|
},
|
|
1108
1111
|
// root_missing_composition_id + root_missing_dimensions
|
|
1109
1112
|
// fallow-ignore-next-line complexity
|
|
1110
|
-
({ rootTag }) => {
|
|
1113
|
+
({ rootTag, options }) => {
|
|
1111
1114
|
const findings = [];
|
|
1112
1115
|
if (!rootTag || !readDecodedAttr(rootTag.raw, "data-composition-id")) {
|
|
1113
1116
|
findings.push({
|
|
@@ -1119,7 +1122,7 @@ var coreRules = [
|
|
|
1119
1122
|
snippet: truncateSnippet(rootTag?.raw || "")
|
|
1120
1123
|
});
|
|
1121
1124
|
}
|
|
1122
|
-
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"))) {
|
|
1123
1126
|
findings.push({
|
|
1124
1127
|
code: "root_missing_dimensions",
|
|
1125
1128
|
severity: "error",
|
|
@@ -2602,6 +2605,24 @@ function extractCssUrlReferences(css) {
|
|
|
2602
2605
|
}
|
|
2603
2606
|
return out;
|
|
2604
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
|
+
}
|
|
2605
2626
|
function extractCssSelectors(css) {
|
|
2606
2627
|
const out = [];
|
|
2607
2628
|
const noComments = css.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -2915,42 +2936,31 @@ var compositionRules = [
|
|
|
2915
2936
|
}
|
|
2916
2937
|
return findings;
|
|
2917
2938
|
},
|
|
2918
|
-
// invalid_parent_traversal_in_asset_path —
|
|
2919
|
-
//
|
|
2920
|
-
//
|
|
2921
|
-
// with the project root as their base URL, so any `../`-traversing path
|
|
2922
|
-
// climbs above the project root and 404s in Studio preview. Renders
|
|
2923
|
-
// tolerate it because the server-side bundler rewrites `../foo` against
|
|
2924
|
-
// each sub-composition's source path; the runtime now mirrors that fallback
|
|
2925
|
-
// (see rewriteSubCompositionAssetPaths in runtime/compositionLoader.ts), but
|
|
2926
|
-
// the authoring-time signal is still wrong — flag it at lint time so the
|
|
2927
|
-
// baked path is plain root-relative and matches what the bundler emits.
|
|
2928
|
-
//
|
|
2929
|
-
// Mirrors the runtime fallback's surface: `[src]` / `[href]` attribute
|
|
2930
|
-
// values, `[style]` inline url(), and `<style>` block url() references.
|
|
2931
|
-
// Skips absolute URLs (http(s)://, //, data:, /-prefixed root-relative),
|
|
2932
|
-
// hash anchors, and plain relative paths (`assets/x.mp4`) — only `../`
|
|
2933
|
-
// 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.
|
|
2934
2942
|
// fallow-ignore-next-line complexity
|
|
2935
2943
|
({ tags, styles, rawSource, options }) => {
|
|
2936
2944
|
if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
|
|
2937
2945
|
const offenders = [];
|
|
2938
|
-
const
|
|
2939
|
-
|
|
2940
|
-
const trimmed = value
|
|
2941
|
-
if (
|
|
2942
|
-
|
|
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);
|
|
2943
2952
|
};
|
|
2944
2953
|
for (const tag of tags) {
|
|
2945
|
-
collect(
|
|
2946
|
-
collect(
|
|
2954
|
+
collect(readDecodedAttr(tag.raw, "src"));
|
|
2955
|
+
collect(readDecodedAttr(tag.raw, "href"));
|
|
2947
2956
|
const styleAttr = readJsonAttr(tag.raw, "style");
|
|
2948
2957
|
if (styleAttr) {
|
|
2949
2958
|
for (const url of extractCssUrlReferences(styleAttr)) collect(url);
|
|
2950
2959
|
}
|
|
2951
2960
|
}
|
|
2952
2961
|
for (const style of styles) {
|
|
2953
|
-
|
|
2962
|
+
const depth = style.rootRelativePath ? folderDepth(style.rootRelativePath) : fileDepth;
|
|
2963
|
+
for (const url of extractCssUrlReferences(style.content)) collect(url, depth);
|
|
2954
2964
|
}
|
|
2955
2965
|
if (offenders.length === 0) return [];
|
|
2956
2966
|
const prefixCounts = /* @__PURE__ */ new Map();
|
|
@@ -2963,8 +2973,8 @@ var compositionRules = [
|
|
|
2963
2973
|
{
|
|
2964
2974
|
code: "invalid_parent_traversal_in_asset_path",
|
|
2965
2975
|
severity: "error",
|
|
2966
|
-
message: `Found ${offenders.length} asset path(s) traversing above the project root with "../" (${prefixSummary})
|
|
2967
|
-
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.`
|
|
2968
2978
|
}
|
|
2969
2979
|
];
|
|
2970
2980
|
},
|