@hyperframes/lint 0.8.143 → 0.8.145

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 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 && (readDecodedAttr(bodyTag.raw, "data-composition-id") || readAttr(bodyTag.raw, "data-width") || readAttr(bodyTag.raw, "data-height"))) {
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 === "svg" && !readDecodedAttr(tag.raw, "data-composition-id") && !readAttr(tag.raw, "data-width") && !readAttr(tag.raw, "data-height")) {
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 — catches `../` traversal in src,
2919
- // href, inline-style url(), and <style> url() asset references on
2920
- // compositions. Sub-compositions live under compositions/ but are served
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 collect = (value) => {
2939
- if (!value) return;
2940
- const trimmed = value.trim();
2941
- if (!trimmed.startsWith("../") && trimmed !== "..") return;
2942
- offenders.push(trimmed);
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(readAttr(tag.raw, "src"));
2946
- collect(readAttr(tag.raw, "href"));
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
- for (const url of extractCssUrlReferences(style.content)) collect(url);
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}). Renders rewrite this against each sub-composition's source path, but Studio preview and other live consumers resolve against the project root and 404.`,
2967
- fixHint: 'Use plain root-relative paths (e.g. "assets/...", "capture/...", "fonts/...") \u2014 compositions are served with the project root as their base URL, so paths must be root-relative, not relative to the compositions/ directory.'
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
  },