@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/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 && (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
  }
@@ -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;
@@ -1110,7 +1113,7 @@ var coreRules = [
1110
1113
  },
1111
1114
  // root_missing_composition_id + root_missing_dimensions
1112
1115
  // fallow-ignore-next-line complexity
1113
- ({ rootTag }) => {
1116
+ ({ rootTag, options }) => {
1114
1117
  const findings = [];
1115
1118
  if (!rootTag || !readDecodedAttr(rootTag.raw, "data-composition-id")) {
1116
1119
  findings.push({
@@ -1122,7 +1125,7 @@ var coreRules = [
1122
1125
  snippet: truncateSnippet(rootTag?.raw || "")
1123
1126
  });
1124
1127
  }
1125
- 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"))) {
1126
1129
  findings.push({
1127
1130
  code: "root_missing_dimensions",
1128
1131
  severity: "error",
@@ -2605,6 +2608,24 @@ function extractCssUrlReferences(css) {
2605
2608
  }
2606
2609
  return out;
2607
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
+ }
2608
2629
  function extractCssSelectors(css) {
2609
2630
  const out = [];
2610
2631
  const noComments = css.replace(/\/\*[\s\S]*?\*\//g, "");
@@ -2918,42 +2939,31 @@ var compositionRules = [
2918
2939
  }
2919
2940
  return findings;
2920
2941
  },
2921
- // invalid_parent_traversal_in_asset_path — catches `../` traversal in src,
2922
- // href, inline-style url(), and <style> url() asset references on
2923
- // compositions. Sub-compositions live under compositions/ but are served
2924
- // with the project root as their base URL, so any `../`-traversing path
2925
- // climbs above the project root and 404s in Studio preview. Renders
2926
- // tolerate it because the server-side bundler rewrites `../foo` against
2927
- // each sub-composition's source path; the runtime now mirrors that fallback
2928
- // (see rewriteSubCompositionAssetPaths in runtime/compositionLoader.ts), but
2929
- // the authoring-time signal is still wrong — flag it at lint time so the
2930
- // baked path is plain root-relative and matches what the bundler emits.
2931
- //
2932
- // Mirrors the runtime fallback's surface: `[src]` / `[href]` attribute
2933
- // values, `[style]` inline url(), and `<style>` block url() references.
2934
- // Skips absolute URLs (http(s)://, //, data:, /-prefixed root-relative),
2935
- // hash anchors, and plain relative paths (`assets/x.mp4`) — only `../`
2936
- // 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.
2937
2945
  // fallow-ignore-next-line complexity
2938
2946
  ({ tags, styles, rawSource, options }) => {
2939
2947
  if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource)) return [];
2940
2948
  const offenders = [];
2941
- const collect = (value) => {
2942
- if (!value) return;
2943
- const trimmed = value.trim();
2944
- if (!trimmed.startsWith("../") && trimmed !== "..") return;
2945
- offenders.push(trimmed);
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);
2946
2955
  };
2947
2956
  for (const tag of tags) {
2948
- collect(readAttr(tag.raw, "src"));
2949
- collect(readAttr(tag.raw, "href"));
2957
+ collect(readDecodedAttr(tag.raw, "src"));
2958
+ collect(readDecodedAttr(tag.raw, "href"));
2950
2959
  const styleAttr = readJsonAttr(tag.raw, "style");
2951
2960
  if (styleAttr) {
2952
2961
  for (const url of extractCssUrlReferences(styleAttr)) collect(url);
2953
2962
  }
2954
2963
  }
2955
2964
  for (const style of styles) {
2956
- for (const url of extractCssUrlReferences(style.content)) collect(url);
2965
+ const depth = style.rootRelativePath ? folderDepth(style.rootRelativePath) : fileDepth;
2966
+ for (const url of extractCssUrlReferences(style.content)) collect(url, depth);
2957
2967
  }
2958
2968
  if (offenders.length === 0) return [];
2959
2969
  const prefixCounts = /* @__PURE__ */ new Map();
@@ -2966,8 +2976,8 @@ var compositionRules = [
2966
2976
  {
2967
2977
  code: "invalid_parent_traversal_in_asset_path",
2968
2978
  severity: "error",
2969
- 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.`,
2970
- 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.'
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.`
2971
2981
  }
2972
2982
  ];
2973
2983
  },
@@ -6749,7 +6759,7 @@ function collectExternalStyles(projectDir, html, compSrcPath) {
6749
6759
  document,
6750
6760
  compSrcPath
6751
6761
  )) {
6752
- styles.push({ href, content, file: join2(projectDir, rootRelativePath) });
6762
+ styles.push({ href, content, file: join2(projectDir, rootRelativePath), rootRelativePath });
6753
6763
  }
6754
6764
  return styles;
6755
6765
  }
@@ -6801,6 +6811,7 @@ async function lintProject(projectDir, entryFile, hostOptions = {}) {
6801
6811
  const rootResult = await lintHyperframeHtml(rootHtml, {
6802
6812
  ...hostOptions,
6803
6813
  filePath: indexPath,
6814
+ compSrcPath: rootFile,
6804
6815
  externalStyles: collectExternalStyles(projectDir, rootHtml, rootCompSrcPath)
6805
6816
  });
6806
6817
  results.push({ file: rootFile, result: rootResult, contentHash: contentDigest(rootHtml) });
@@ -6834,6 +6845,7 @@ async function lintProject(projectDir, entryFile, hostOptions = {}) {
6834
6845
  ...hostOptions,
6835
6846
  filePath,
6836
6847
  isSubComposition: true,
6848
+ compSrcPath,
6837
6849
  externalStyles: collectExternalStyles(projectDir, html, compSrcPath)
6838
6850
  });
6839
6851
  results.push({