@hyperframes/core 0.7.13 → 0.7.15

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.
Files changed (238) hide show
  1. package/dist/compiler/staticGuard.js +1 -1
  2. package/dist/compiler/staticGuard.js.map +1 -1
  3. package/dist/core.types.d.ts +2 -369
  4. package/dist/core.types.d.ts.map +1 -1
  5. package/dist/core.types.js +1 -84
  6. package/dist/core.types.js.map +1 -1
  7. package/dist/generators/hyperframes.d.ts +1 -1
  8. package/dist/generators/hyperframes.d.ts.map +1 -1
  9. package/dist/generators/hyperframes.js +1 -1
  10. package/dist/generators/hyperframes.js.map +1 -1
  11. package/dist/index.d.ts +5 -7
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +7 -4
  14. package/dist/index.js.map +1 -1
  15. package/dist/lint/index.d.ts +2 -2
  16. package/dist/lint/index.d.ts.map +1 -1
  17. package/dist/lint/index.js +2 -1
  18. package/dist/lint/index.js.map +1 -1
  19. package/dist/parsers/gsapConstants.d.ts +2 -12
  20. package/dist/parsers/gsapConstants.d.ts.map +1 -1
  21. package/dist/parsers/gsapConstants.js +2 -110
  22. package/dist/parsers/gsapConstants.js.map +1 -1
  23. package/dist/parsers/gsapParserAcorn.d.ts +2 -48
  24. package/dist/parsers/gsapParserAcorn.d.ts.map +1 -1
  25. package/dist/parsers/gsapParserAcorn.js +2 -1057
  26. package/dist/parsers/gsapParserAcorn.js.map +1 -1
  27. package/dist/parsers/gsapParserExports.d.ts +2 -20
  28. package/dist/parsers/gsapParserExports.d.ts.map +1 -1
  29. package/dist/parsers/gsapParserExports.js +2 -8
  30. package/dist/parsers/gsapParserExports.js.map +1 -1
  31. package/dist/parsers/gsapWriterAcorn.d.ts +2 -90
  32. package/dist/parsers/gsapWriterAcorn.d.ts.map +1 -1
  33. package/dist/parsers/gsapWriterAcorn.js +2 -2016
  34. package/dist/parsers/gsapWriterAcorn.js.map +1 -1
  35. package/dist/parsers/hfIds.d.ts +2 -16
  36. package/dist/parsers/hfIds.d.ts.map +1 -1
  37. package/dist/parsers/hfIds.js +2 -127
  38. package/dist/parsers/hfIds.js.map +1 -1
  39. package/dist/parsers/springEase.d.ts +2 -28
  40. package/dist/parsers/springEase.d.ts.map +1 -1
  41. package/dist/parsers/springEase.js +2 -69
  42. package/dist/parsers/springEase.js.map +1 -1
  43. package/dist/slideshow/index.d.ts +1 -0
  44. package/dist/slideshow/index.d.ts.map +1 -1
  45. package/dist/slideshow/index.js +1 -0
  46. package/dist/slideshow/index.js.map +1 -1
  47. package/dist/studio-api/helpers/draftMarkers.d.ts +2 -10
  48. package/dist/studio-api/helpers/draftMarkers.d.ts.map +1 -1
  49. package/dist/studio-api/helpers/draftMarkers.js +2 -10
  50. package/dist/studio-api/helpers/draftMarkers.js.map +1 -1
  51. package/dist/studio-api/helpers/finiteMutation.d.ts +2 -10
  52. package/dist/studio-api/helpers/finiteMutation.d.ts.map +1 -1
  53. package/dist/studio-api/helpers/finiteMutation.js +2 -20
  54. package/dist/studio-api/helpers/finiteMutation.js.map +1 -1
  55. package/dist/studio-api/helpers/manualEditsRenderScript.d.ts +2 -12
  56. package/dist/studio-api/helpers/manualEditsRenderScript.d.ts.map +1 -1
  57. package/dist/studio-api/helpers/manualEditsRenderScript.js +2 -632
  58. package/dist/studio-api/helpers/manualEditsRenderScript.js.map +1 -1
  59. package/dist/studio-api/helpers/screenshotClip.d.ts +2 -7
  60. package/dist/studio-api/helpers/screenshotClip.d.ts.map +1 -1
  61. package/dist/studio-api/helpers/screenshotClip.js +2 -21
  62. package/dist/studio-api/helpers/screenshotClip.js.map +1 -1
  63. package/dist/studio-api/helpers/studioMotionRenderScript.d.ts +2 -8
  64. package/dist/studio-api/helpers/studioMotionRenderScript.d.ts.map +1 -1
  65. package/dist/studio-api/helpers/studioMotionRenderScript.js +2 -207
  66. package/dist/studio-api/helpers/studioMotionRenderScript.js.map +1 -1
  67. package/dist/studio-api/index.d.ts +2 -9
  68. package/dist/studio-api/index.d.ts.map +1 -1
  69. package/dist/studio-api/index.js +2 -8
  70. package/dist/studio-api/index.js.map +1 -1
  71. package/package.json +16 -18
  72. package/dist/generated/runtime-inline.js +0 -11
  73. package/dist/hyperframe.manifest.json +0 -22
  74. package/dist/hyperframe.runtime.mjs +0 -325
  75. package/dist/lint/context.d.ts +0 -17
  76. package/dist/lint/context.d.ts.map +0 -1
  77. package/dist/lint/context.js +0 -37
  78. package/dist/lint/context.js.map +0 -1
  79. package/dist/lint/hyperframeLinter.d.ts +0 -14
  80. package/dist/lint/hyperframeLinter.d.ts.map +0 -1
  81. package/dist/lint/hyperframeLinter.js +0 -132
  82. package/dist/lint/hyperframeLinter.js.map +0 -1
  83. package/dist/lint/rules/adapters.d.ts +0 -3
  84. package/dist/lint/rules/adapters.d.ts.map +0 -1
  85. package/dist/lint/rules/adapters.js +0 -40
  86. package/dist/lint/rules/adapters.js.map +0 -1
  87. package/dist/lint/rules/captions.d.ts +0 -3
  88. package/dist/lint/rules/captions.d.ts.map +0 -1
  89. package/dist/lint/rules/captions.js +0 -242
  90. package/dist/lint/rules/captions.js.map +0 -1
  91. package/dist/lint/rules/composition.d.ts +0 -5
  92. package/dist/lint/rules/composition.d.ts.map +0 -1
  93. package/dist/lint/rules/composition.js +0 -694
  94. package/dist/lint/rules/composition.js.map +0 -1
  95. package/dist/lint/rules/core.d.ts +0 -3
  96. package/dist/lint/rules/core.d.ts.map +0 -1
  97. package/dist/lint/rules/core.js +0 -432
  98. package/dist/lint/rules/core.js.map +0 -1
  99. package/dist/lint/rules/fonts.d.ts +0 -3
  100. package/dist/lint/rules/fonts.d.ts.map +0 -1
  101. package/dist/lint/rules/fonts.js +0 -205
  102. package/dist/lint/rules/fonts.js.map +0 -1
  103. package/dist/lint/rules/gsap.d.ts +0 -4
  104. package/dist/lint/rules/gsap.d.ts.map +0 -1
  105. package/dist/lint/rules/gsap.js +0 -1039
  106. package/dist/lint/rules/gsap.js.map +0 -1
  107. package/dist/lint/rules/media.d.ts +0 -3
  108. package/dist/lint/rules/media.d.ts.map +0 -1
  109. package/dist/lint/rules/media.js +0 -491
  110. package/dist/lint/rules/media.js.map +0 -1
  111. package/dist/lint/rules/slideshow.d.ts +0 -4
  112. package/dist/lint/rules/slideshow.d.ts.map +0 -1
  113. package/dist/lint/rules/slideshow.js +0 -74
  114. package/dist/lint/rules/slideshow.js.map +0 -1
  115. package/dist/lint/rules/textures.d.ts +0 -3
  116. package/dist/lint/rules/textures.d.ts.map +0 -1
  117. package/dist/lint/rules/textures.js +0 -192
  118. package/dist/lint/rules/textures.js.map +0 -1
  119. package/dist/lint/types.d.ts +0 -36
  120. package/dist/lint/types.d.ts.map +0 -1
  121. package/dist/lint/types.js +0 -2
  122. package/dist/lint/types.js.map +0 -1
  123. package/dist/lint/utils.d.ts +0 -55
  124. package/dist/lint/utils.d.ts.map +0 -1
  125. package/dist/lint/utils.js +0 -264
  126. package/dist/lint/utils.js.map +0 -1
  127. package/dist/parsers/gsapInline.d.ts +0 -47
  128. package/dist/parsers/gsapInline.d.ts.map +0 -1
  129. package/dist/parsers/gsapInline.js +0 -513
  130. package/dist/parsers/gsapInline.js.map +0 -1
  131. package/dist/parsers/gsapParser.d.ts +0 -158
  132. package/dist/parsers/gsapParser.d.ts.map +0 -1
  133. package/dist/parsers/gsapParser.js +0 -2634
  134. package/dist/parsers/gsapParser.js.map +0 -1
  135. package/dist/parsers/gsapParser.test-helpers.d.ts +0 -49
  136. package/dist/parsers/gsapParser.test-helpers.d.ts.map +0 -1
  137. package/dist/parsers/gsapParser.test-helpers.js +0 -97
  138. package/dist/parsers/gsapParser.test-helpers.js.map +0 -1
  139. package/dist/parsers/gsapSerialize.d.ts +0 -191
  140. package/dist/parsers/gsapSerialize.d.ts.map +0 -1
  141. package/dist/parsers/gsapSerialize.js +0 -410
  142. package/dist/parsers/gsapSerialize.js.map +0 -1
  143. package/dist/parsers/gsapUnroll.d.ts +0 -7
  144. package/dist/parsers/gsapUnroll.d.ts.map +0 -1
  145. package/dist/parsers/gsapUnroll.js +0 -137
  146. package/dist/parsers/gsapUnroll.js.map +0 -1
  147. package/dist/parsers/htmlParser.d.ts +0 -27
  148. package/dist/parsers/htmlParser.d.ts.map +0 -1
  149. package/dist/parsers/htmlParser.js +0 -731
  150. package/dist/parsers/htmlParser.js.map +0 -1
  151. package/dist/studio-api/createStudioApi.d.ts +0 -10
  152. package/dist/studio-api/createStudioApi.d.ts.map +0 -1
  153. package/dist/studio-api/createStudioApi.js +0 -32
  154. package/dist/studio-api/createStudioApi.js.map +0 -1
  155. package/dist/studio-api/helpers/backupJournal.d.ts +0 -9
  156. package/dist/studio-api/helpers/backupJournal.d.ts.map +0 -1
  157. package/dist/studio-api/helpers/backupJournal.js +0 -81
  158. package/dist/studio-api/helpers/backupJournal.js.map +0 -1
  159. package/dist/studio-api/helpers/hfIdPersist.d.ts +0 -11
  160. package/dist/studio-api/helpers/hfIdPersist.d.ts.map +0 -1
  161. package/dist/studio-api/helpers/hfIdPersist.js +0 -39
  162. package/dist/studio-api/helpers/hfIdPersist.js.map +0 -1
  163. package/dist/studio-api/helpers/mediaValidation.d.ts +0 -20
  164. package/dist/studio-api/helpers/mediaValidation.d.ts.map +0 -1
  165. package/dist/studio-api/helpers/mediaValidation.js +0 -56
  166. package/dist/studio-api/helpers/mediaValidation.js.map +0 -1
  167. package/dist/studio-api/helpers/mime.d.ts +0 -4
  168. package/dist/studio-api/helpers/mime.d.ts.map +0 -1
  169. package/dist/studio-api/helpers/mime.js +0 -39
  170. package/dist/studio-api/helpers/mime.js.map +0 -1
  171. package/dist/studio-api/helpers/previewAdapter.d.ts +0 -43
  172. package/dist/studio-api/helpers/previewAdapter.d.ts.map +0 -1
  173. package/dist/studio-api/helpers/previewAdapter.js +0 -118
  174. package/dist/studio-api/helpers/previewAdapter.js.map +0 -1
  175. package/dist/studio-api/helpers/projectSignature.d.ts +0 -5
  176. package/dist/studio-api/helpers/projectSignature.d.ts.map +0 -1
  177. package/dist/studio-api/helpers/projectSignature.js +0 -154
  178. package/dist/studio-api/helpers/projectSignature.js.map +0 -1
  179. package/dist/studio-api/helpers/safePath.d.ts +0 -12
  180. package/dist/studio-api/helpers/safePath.d.ts.map +0 -1
  181. package/dist/studio-api/helpers/safePath.js +0 -38
  182. package/dist/studio-api/helpers/safePath.js.map +0 -1
  183. package/dist/studio-api/helpers/sourceMutation.d.ts +0 -28
  184. package/dist/studio-api/helpers/sourceMutation.d.ts.map +0 -1
  185. package/dist/studio-api/helpers/sourceMutation.js +0 -339
  186. package/dist/studio-api/helpers/sourceMutation.js.map +0 -1
  187. package/dist/studio-api/helpers/subComposition.d.ts +0 -20
  188. package/dist/studio-api/helpers/subComposition.d.ts.map +0 -1
  189. package/dist/studio-api/helpers/subComposition.js +0 -289
  190. package/dist/studio-api/helpers/subComposition.js.map +0 -1
  191. package/dist/studio-api/helpers/waveform.d.ts +0 -4
  192. package/dist/studio-api/helpers/waveform.d.ts.map +0 -1
  193. package/dist/studio-api/helpers/waveform.js +0 -80
  194. package/dist/studio-api/helpers/waveform.js.map +0 -1
  195. package/dist/studio-api/routes/files.d.ts +0 -4
  196. package/dist/studio-api/routes/files.d.ts.map +0 -1
  197. package/dist/studio-api/routes/files.js +0 -1200
  198. package/dist/studio-api/routes/files.js.map +0 -1
  199. package/dist/studio-api/routes/fonts.d.ts +0 -3
  200. package/dist/studio-api/routes/fonts.d.ts.map +0 -1
  201. package/dist/studio-api/routes/fonts.js +0 -157
  202. package/dist/studio-api/routes/fonts.js.map +0 -1
  203. package/dist/studio-api/routes/lint.d.ts +0 -4
  204. package/dist/studio-api/routes/lint.d.ts.map +0 -1
  205. package/dist/studio-api/routes/lint.js +0 -29
  206. package/dist/studio-api/routes/lint.js.map +0 -1
  207. package/dist/studio-api/routes/preview.d.ts +0 -4
  208. package/dist/studio-api/routes/preview.d.ts.map +0 -1
  209. package/dist/studio-api/routes/preview.js +0 -331
  210. package/dist/studio-api/routes/preview.js.map +0 -1
  211. package/dist/studio-api/routes/projects.d.ts +0 -4
  212. package/dist/studio-api/routes/projects.d.ts.map +0 -1
  213. package/dist/studio-api/routes/projects.js +0 -45
  214. package/dist/studio-api/routes/projects.js.map +0 -1
  215. package/dist/studio-api/routes/registry.d.ts +0 -4
  216. package/dist/studio-api/routes/registry.d.ts.map +0 -1
  217. package/dist/studio-api/routes/registry.js +0 -31
  218. package/dist/studio-api/routes/registry.js.map +0 -1
  219. package/dist/studio-api/routes/render.d.ts +0 -4
  220. package/dist/studio-api/routes/render.d.ts.map +0 -1
  221. package/dist/studio-api/routes/render.js +0 -269
  222. package/dist/studio-api/routes/render.js.map +0 -1
  223. package/dist/studio-api/routes/storyboard.d.ts +0 -4
  224. package/dist/studio-api/routes/storyboard.d.ts.map +0 -1
  225. package/dist/studio-api/routes/storyboard.js +0 -65
  226. package/dist/studio-api/routes/storyboard.js.map +0 -1
  227. package/dist/studio-api/routes/thumbnail.d.ts +0 -4
  228. package/dist/studio-api/routes/thumbnail.d.ts.map +0 -1
  229. package/dist/studio-api/routes/thumbnail.js +0 -107
  230. package/dist/studio-api/routes/thumbnail.js.map +0 -1
  231. package/dist/studio-api/routes/waveform.d.ts +0 -4
  232. package/dist/studio-api/routes/waveform.d.ts.map +0 -1
  233. package/dist/studio-api/routes/waveform.js +0 -41
  234. package/dist/studio-api/routes/waveform.js.map +0 -1
  235. package/dist/studio-api/types.d.ts +0 -120
  236. package/dist/studio-api/types.d.ts.map +0 -1
  237. package/dist/studio-api/types.js +0 -2
  238. package/dist/studio-api/types.js.map +0 -1
@@ -1,694 +0,0 @@
1
- import { findHtmlTag, readAttr, readJsonAttr, stripJsComments, truncateSnippet } from "../utils.js";
2
- import { COMPOSITION_VARIABLE_TYPES } from "../../core.types.js";
3
- // Agent guidance thresholds: warning-only nudges for files/tracks that become hard
4
- // to inspect and revise reliably in a single composition.
5
- const MAX_COMPOSITION_LINES = 300;
6
- const MAX_TIMED_ELEMENTS_PER_TRACK = 3;
7
- const TRACK_DENSITY_EXEMPT_TAGS = new Set(["audio", "script", "style", "video"]);
8
- function countPhysicalLines(source) {
9
- if (source.length === 0)
10
- return 0;
11
- const normalized = source.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
12
- const withoutFinalNewline = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
13
- return withoutFinalNewline.split("\n").length;
14
- }
15
- function countStructuralLines(source) {
16
- return countPhysicalLines(source.replace(/<style\b[^>]*>[\s\S]*?<\/style>/gi, "<style></style>"));
17
- }
18
- export function isRegistrySourceFile(filePath) {
19
- if (!filePath)
20
- return false;
21
- const normalized = filePath.replace(/\\/g, "/");
22
- return /(?:^|\/)registry\/blocks\/([^/]+)\/\1\.html$/i.test(normalized);
23
- }
24
- export function isRegistryInstalledFile(rawSource) {
25
- return /^\s*<!--\s*hyperframes-registry-item:[^>]*-->/i.test(rawSource.slice(0, 512));
26
- }
27
- function isCompositionRootOrMount(rawTag) {
28
- return Boolean(readAttr(rawTag, "data-composition-id") || readAttr(rawTag, "data-composition-src"));
29
- }
30
- // Asset references inside CSS `url(...)`/`url("...")`/`url('...')` functions.
31
- // Returns the inner path without quotes; comments are stripped first so
32
- // `/* url(foo) */` is ignored. Bare `url()` and `data:` are excluded by the
33
- // rules that consume this — the helper just yields raw URL values.
34
- function extractCssUrlReferences(css) {
35
- const out = [];
36
- const noComments = css.replace(/\/\*[\s\S]*?\*\//g, "");
37
- const urlPattern = /\burl\(\s*(["']?)([^)"']+)\1\s*\)/g;
38
- let m;
39
- while ((m = urlPattern.exec(noComments)) !== null) {
40
- const raw = (m[2] ?? "").trim();
41
- if (raw)
42
- out.push(raw);
43
- }
44
- return out;
45
- }
46
- // Top-level CSS selectors (comma-split) in a stylesheet, skipping at-rule headers
47
- // (@media/@keyframes/...) and keyframe stops. Heuristic — the lint layer has no
48
- // full CSS parser, and rules elsewhere in this file scan CSS the same way.
49
- function extractCssSelectors(css) {
50
- const out = [];
51
- const noComments = css.replace(/\/\*[\s\S]*?\*\//g, "");
52
- const ruleHeader = /([^{}]+)\{/g;
53
- let m;
54
- while ((m = ruleHeader.exec(noComments)) !== null) {
55
- const header = (m[1] ?? "").trim();
56
- if (!header || header.startsWith("@"))
57
- continue;
58
- for (const sel of header.split(",")) {
59
- const s = sel.trim();
60
- if (s)
61
- out.push(s);
62
- }
63
- }
64
- return out;
65
- }
66
- // Class tokens in a selector's leftmost compound (before the first descendant /
67
- // child / sibling combinator). `.frame .title` → ["frame"]; `.a.b > .c` → ["a","b"].
68
- function leftmostCompoundClasses(selector) {
69
- const leftmost = selector.trim().split(/[\s>+~]+/)[0] ?? "";
70
- return (leftmost.match(/\.([\w-]+)/g) ?? []).map((c) => c.slice(1));
71
- }
72
- // Distinct selectors across all <style> blocks whose leftmost compound keys off one
73
- // of the root element's own classes — the ones that break under id-scoping.
74
- function rootClassStyledSelectors(styles, rootClasses) {
75
- const offenders = [];
76
- for (const style of styles) {
77
- for (const selector of extractCssSelectors(style.content)) {
78
- const hitsRoot = leftmostCompoundClasses(selector).some((c) => rootClasses.includes(c));
79
- if (hitsRoot && !offenders.includes(selector))
80
- offenders.push(selector);
81
- }
82
- }
83
- return offenders;
84
- }
85
- export const compositionRules = [
86
- // invalid_parent_traversal_in_asset_path — catches `../` traversal in src,
87
- // href, inline-style url(), and <style> url() asset references on
88
- // compositions. Sub-compositions live under compositions/ but are served
89
- // with the project root as their base URL, so any `../`-traversing path
90
- // climbs above the project root and 404s in Studio preview. Renders
91
- // tolerate it because the server-side bundler rewrites `../foo` against
92
- // each sub-composition's source path; the runtime now mirrors that fallback
93
- // (see rewriteSubCompositionAssetPaths in runtime/compositionLoader.ts), but
94
- // the authoring-time signal is still wrong — flag it at lint time so the
95
- // baked path is plain root-relative and matches what the bundler emits.
96
- //
97
- // Mirrors the runtime fallback's surface: `[src]` / `[href]` attribute
98
- // values, `[style]` inline url(), and `<style>` block url() references.
99
- // Skips absolute URLs (http(s)://, //, data:, /-prefixed root-relative),
100
- // hash anchors, and plain relative paths (`assets/x.mp4`) — only `../`
101
- // traversal is flagged. Subsumes the older `../capture/`-specific rule.
102
- // fallow-ignore-next-line complexity
103
- ({ tags, styles, rawSource, options }) => {
104
- if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource))
105
- return [];
106
- const offenders = [];
107
- const collect = (value) => {
108
- if (!value)
109
- return;
110
- const trimmed = value.trim();
111
- if (!trimmed.startsWith("../") && trimmed !== "..")
112
- return;
113
- offenders.push(trimmed);
114
- };
115
- for (const tag of tags) {
116
- collect(readAttr(tag.raw, "src"));
117
- collect(readAttr(tag.raw, "href"));
118
- // Use readJsonAttr for `style` — inline url('...') values contain the
119
- // opposite quote, which readAttr's [^"']+ class would truncate.
120
- const styleAttr = readJsonAttr(tag.raw, "style");
121
- if (styleAttr) {
122
- for (const url of extractCssUrlReferences(styleAttr))
123
- collect(url);
124
- }
125
- }
126
- for (const style of styles) {
127
- for (const url of extractCssUrlReferences(style.content))
128
- collect(url);
129
- }
130
- if (offenders.length === 0)
131
- return [];
132
- // Group counts by leading path token (e.g. ../capture/, ../assets/, ../../assets/)
133
- // so the message names the offending prefixes instead of a bare count.
134
- const prefixCounts = new Map();
135
- for (const path of offenders) {
136
- const prefix = path.match(/^(?:\.\.\/)+[^/]+\//)?.[0] ?? path;
137
- prefixCounts.set(prefix, (prefixCounts.get(prefix) ?? 0) + 1);
138
- }
139
- const prefixSummary = Array.from(prefixCounts.entries())
140
- .sort(([, a], [, b]) => b - a)
141
- .map(([prefix, count]) => (count > 1 ? `${prefix} (${count})` : prefix))
142
- .join(", ");
143
- return [
144
- {
145
- code: "invalid_parent_traversal_in_asset_path",
146
- severity: "error",
147
- message: `Found ${offenders.length} asset path(s) traversing above the project root with "../" ` +
148
- `(${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.`,
149
- fixHint: 'Use plain root-relative paths (e.g. "assets/...", "capture/...", "fonts/...") — compositions are served with the project root as their base URL, so paths must be root-relative, not relative to the compositions/ directory.',
150
- },
151
- ];
152
- },
153
- // composition_file_too_large
154
- ({ rawSource, options }) => {
155
- if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource))
156
- return [];
157
- const lineCount = countStructuralLines(rawSource);
158
- if (lineCount <= MAX_COMPOSITION_LINES)
159
- return [];
160
- const splitTarget = options.isSubComposition
161
- ? "Split this sub-composition further into smaller .html files"
162
- : "Split coherent scenes or layers into separate .html files under compositions/";
163
- return [
164
- {
165
- code: "composition_file_too_large",
166
- severity: "warning",
167
- message: `This HTML composition file has ${lineCount} lines. Smaller sub-compositions are easier to read, iterate on, and diff.`,
168
- fixHint: `${splitTarget}, then mount them from the parent with data-composition-src so each file stays small enough to inspect, revise, and validate independently.`,
169
- },
170
- ];
171
- },
172
- // timeline_track_too_dense
173
- // fallow-ignore-next-line complexity
174
- ({ tags, options }) => {
175
- const trackCounts = new Map();
176
- for (const tag of tags) {
177
- if (TRACK_DENSITY_EXEMPT_TAGS.has(tag.name))
178
- continue;
179
- if (isCompositionRootOrMount(tag.raw))
180
- continue;
181
- if (!readAttr(tag.raw, "data-start"))
182
- continue;
183
- const track = readAttr(tag.raw, "data-track-index");
184
- if (!track)
185
- continue;
186
- trackCounts.set(track, (trackCounts.get(track) ?? 0) + 1);
187
- }
188
- const findings = [];
189
- for (const [track, count] of trackCounts) {
190
- if (count <= MAX_TIMED_ELEMENTS_PER_TRACK)
191
- continue;
192
- const splitTarget = options.isSubComposition
193
- ? "Move coherent scene groups into smaller .html files"
194
- : "Move coherent scene groups into separate .html files under compositions/";
195
- findings.push({
196
- code: "timeline_track_too_dense",
197
- severity: "warning",
198
- message: `Track ${track} has ${count} timed elements in this HTML file. Smaller sub-compositions keep timelines easier to read, iterate on, and diff.`,
199
- fixHint: `${splitTarget} and mount them from the parent with data-composition-src so the timeline stays easier to inspect, revise, and validate.`,
200
- });
201
- }
202
- return findings;
203
- },
204
- // timed_element_missing_visibility_hidden
205
- // fallow-ignore-next-line complexity
206
- ({ tags }) => {
207
- const findings = [];
208
- for (const tag of tags) {
209
- if (tag.name === "audio" || tag.name === "script" || tag.name === "style")
210
- continue;
211
- if (!readAttr(tag.raw, "data-start"))
212
- continue;
213
- if (readAttr(tag.raw, "data-composition-id"))
214
- continue;
215
- if (readAttr(tag.raw, "data-composition-src"))
216
- continue;
217
- const classAttr = readAttr(tag.raw, "class") || "";
218
- const styleAttr = readAttr(tag.raw, "style") || "";
219
- const hasClip = classAttr.split(/\s+/).includes("clip");
220
- const hasHiddenStyle = /visibility\s*:\s*hidden/i.test(styleAttr) || /opacity\s*:\s*0/i.test(styleAttr);
221
- if (!hasClip && !hasHiddenStyle) {
222
- const elementId = readAttr(tag.raw, "id") || undefined;
223
- findings.push({
224
- code: "timed_element_missing_visibility_hidden",
225
- severity: "info",
226
- message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has data-start but no class="clip", visibility:hidden, or opacity:0. Consider adding initial hidden state if the element should not be visible before its start time.`,
227
- elementId,
228
- fixHint: 'Add class="clip" (with CSS: .clip { visibility: hidden; }) or style="opacity:0" if the element should start hidden.',
229
- snippet: truncateSnippet(tag.raw),
230
- });
231
- }
232
- }
233
- return findings;
234
- },
235
- // deprecated_data_layer + deprecated_data_end
236
- // fallow-ignore-next-line complexity
237
- ({ tags }) => {
238
- const findings = [];
239
- for (const tag of tags) {
240
- if (readAttr(tag.raw, "data-layer") && !readAttr(tag.raw, "data-track-index")) {
241
- const elementId = readAttr(tag.raw, "id") || undefined;
242
- findings.push({
243
- code: "deprecated_data_layer",
244
- severity: "error",
245
- message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> uses data-layer instead of data-track-index.`,
246
- elementId,
247
- fixHint: "Replace data-layer with data-track-index. The runtime reads data-track-index.",
248
- snippet: truncateSnippet(tag.raw),
249
- });
250
- }
251
- if (readAttr(tag.raw, "data-end") && !readAttr(tag.raw, "data-duration")) {
252
- const elementId = readAttr(tag.raw, "id") || undefined;
253
- findings.push({
254
- code: "deprecated_data_end",
255
- severity: "error",
256
- message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> uses data-end without data-duration. Use data-duration in source HTML.`,
257
- elementId,
258
- fixHint: "Replace data-end with data-duration. The compiler generates data-end from data-duration automatically.",
259
- snippet: truncateSnippet(tag.raw),
260
- });
261
- }
262
- }
263
- return findings;
264
- },
265
- // split_data_attribute_selector
266
- ({ scripts, styles }) => {
267
- const findings = [];
268
- const splitDataAttrSelectorPattern = /\[data-composition-id=(["'])([^"'\]]+)\1\s+(data-[\w:-]+)=(["'])([^"'\]]*)\4\]/g;
269
- const scan = (content) => {
270
- splitDataAttrSelectorPattern.lastIndex = 0;
271
- let match;
272
- while ((match = splitDataAttrSelectorPattern.exec(content)) !== null) {
273
- const compId = match[2] ?? "";
274
- const attrName = match[3] ?? "";
275
- const attrValue = match[5] ?? "";
276
- findings.push({
277
- code: "split_data_attribute_selector",
278
- severity: "error",
279
- message: `Selector "${match[0]}" combines two attributes inside one CSS attribute selector. ` +
280
- "Browsers reject it, so GSAP timelines or querySelector calls will fail before registering.",
281
- selector: match[0],
282
- fixHint: `Use separate attribute selectors: [data-composition-id="${compId}"][${attrName}="${attrValue}"].`,
283
- snippet: truncateSnippet(match[0]),
284
- });
285
- }
286
- };
287
- for (const style of styles)
288
- scan(style.content);
289
- for (const script of scripts)
290
- scan(script.content);
291
- return findings;
292
- },
293
- // template_literal_selector
294
- ({ scripts }) => {
295
- const findings = [];
296
- for (const script of scripts) {
297
- const templateLiteralSelectorPattern = /(?:querySelector|querySelectorAll)\s*\(\s*`[^`]*\$\{[^}]+\}[^`]*`\s*\)/g;
298
- let tlMatch;
299
- while ((tlMatch = templateLiteralSelectorPattern.exec(script.content)) !== null) {
300
- findings.push({
301
- code: "template_literal_selector",
302
- severity: "error",
303
- message: "querySelector uses a template literal variable (e.g. `${compId}`). " +
304
- "The HTML bundler's CSS parser crashes on these. Use a hardcoded string instead.",
305
- fixHint: "Replace the template literal variable with a hardcoded string. The bundler's CSS parser cannot handle interpolated variables in script content.",
306
- snippet: truncateSnippet(tlMatch[0]),
307
- });
308
- }
309
- }
310
- return findings;
311
- },
312
- // timed_element_missing_clip_class
313
- // fallow-ignore-next-line complexity
314
- ({ tags }) => {
315
- const findings = [];
316
- const skipTags = new Set(["audio", "video", "script", "style", "template"]);
317
- for (const tag of tags) {
318
- if (skipTags.has(tag.name))
319
- continue;
320
- // Skip composition hosts
321
- if (readAttr(tag.raw, "data-composition-id"))
322
- continue;
323
- if (readAttr(tag.raw, "data-composition-src"))
324
- continue;
325
- const hasStart = readAttr(tag.raw, "data-start") !== null;
326
- const hasDuration = readAttr(tag.raw, "data-duration") !== null;
327
- // data-track-index alone marks a layer container, not a time-bounded clip
328
- if (!hasStart && !hasDuration)
329
- continue;
330
- const classAttr = readAttr(tag.raw, "class") || "";
331
- const hasClip = classAttr.split(/\s+/).includes("clip");
332
- if (hasClip)
333
- continue;
334
- const elementId = readAttr(tag.raw, "id") || undefined;
335
- findings.push({
336
- code: "timed_element_missing_clip_class",
337
- severity: "error",
338
- message: `<${tag.name}${elementId ? ` id="${elementId}"` : ""}> has timing attributes but no class="clip". The element will be visible for the entire composition instead of only during its scheduled time range.`,
339
- elementId,
340
- fixHint: 'Add class="clip" to the element. The HyperFrames runtime uses .clip to control visibility based on data-start/data-duration.',
341
- snippet: truncateSnippet(tag.raw),
342
- });
343
- }
344
- return findings;
345
- },
346
- // overlapping_clips_same_track
347
- // fallow-ignore-next-line complexity
348
- ({ tags }) => {
349
- const findings = [];
350
- const trackMap = new Map();
351
- for (const tag of tags) {
352
- const startStr = readAttr(tag.raw, "data-start");
353
- const durationStr = readAttr(tag.raw, "data-duration");
354
- const trackStr = readAttr(tag.raw, "data-track-index");
355
- if (!startStr || !durationStr || !trackStr)
356
- continue;
357
- const start = Number(startStr);
358
- const duration = Number(durationStr);
359
- const track = trackStr;
360
- // Skip non-numeric (relative timing references like "intro-comp")
361
- if (Number.isNaN(start) || Number.isNaN(duration))
362
- continue;
363
- const clips = trackMap.get(track) || [];
364
- clips.push({
365
- start,
366
- end: start + duration,
367
- elementId: readAttr(tag.raw, "id") || undefined,
368
- snippet: truncateSnippet(tag.raw) || "",
369
- });
370
- trackMap.set(track, clips);
371
- }
372
- for (const [track, clips] of trackMap) {
373
- clips.sort((a, b) => a.start - b.start);
374
- for (let i = 0; i < clips.length - 1; i++) {
375
- const current = clips[i];
376
- const next = clips[i + 1];
377
- if (!current || !next)
378
- continue;
379
- if (current.end > next.start) {
380
- findings.push({
381
- code: "overlapping_clips_same_track",
382
- severity: "error",
383
- message: `Track ${track}: clip ending at ${current.end}s overlaps with clip starting at ${next.start}s. Overlapping clips on the same track cause rendering conflicts.`,
384
- fixHint: "Adjust data-start or data-duration so clips on the same track do not overlap, or move one clip to a different data-track-index.",
385
- });
386
- }
387
- }
388
- }
389
- return findings;
390
- },
391
- // root_composition_missing_data_start
392
- ({ rootTag, options }) => {
393
- const findings = [];
394
- if (options.isSubComposition)
395
- return findings;
396
- if (!rootTag)
397
- return findings;
398
- const compId = readAttr(rootTag.raw, "data-composition-id");
399
- if (!compId)
400
- return findings;
401
- const hasStart = readAttr(rootTag.raw, "data-start") !== null;
402
- if (!hasStart) {
403
- findings.push({
404
- code: "root_composition_missing_data_start",
405
- severity: "error",
406
- message: `Root composition "${compId}" is missing data-start. The runtime needs data-start="0" on the root element to begin playback.`,
407
- fixHint: 'Add data-start="0" to the root composition element.',
408
- snippet: truncateSnippet(rootTag.raw),
409
- });
410
- }
411
- return findings;
412
- },
413
- // standalone_composition_wrapped_in_template
414
- ({ rawSource, options }) => {
415
- const findings = [];
416
- if (options.isSubComposition)
417
- return findings;
418
- const trimmed = rawSource.trimStart().toLowerCase();
419
- if (trimmed.startsWith("<template")) {
420
- findings.push({
421
- code: "standalone_composition_wrapped_in_template",
422
- severity: "error",
423
- message: "Root index.html is wrapped in a <template> tag. " +
424
- "Only sub-compositions loaded via data-composition-src should use <template> wrappers. " +
425
- "The runtime cannot play a standalone composition inside a template.",
426
- fixHint: "Remove the <template> wrapper. Use <!DOCTYPE html><html>...<div data-composition-id>...</div>...</html> instead.",
427
- });
428
- }
429
- return findings;
430
- },
431
- // root_composition_missing_html_wrapper
432
- ({ rawSource, rootTag, options }) => {
433
- const findings = [];
434
- if (options.isSubComposition)
435
- return findings;
436
- const trimmed = rawSource.trimStart().toLowerCase();
437
- // Compositions inside <template> are caught by standalone_composition_wrapped_in_template
438
- if (trimmed.startsWith("<template"))
439
- return findings;
440
- const hasDoctype = trimmed.startsWith("<!doctype") || trimmed.startsWith("<html");
441
- const hasComposition = rawSource.includes("data-composition-id");
442
- if (hasComposition && !hasDoctype) {
443
- findings.push({
444
- code: "root_composition_missing_html_wrapper",
445
- severity: "error",
446
- message: "Composition starts with a bare element instead of a proper HTML document. " +
447
- "An index.html that contains data-composition-id but no <!DOCTYPE html>, <html>, or <body> " +
448
- "is a fragment — browsers quirks-mode it, the preview server cannot load it, and " +
449
- "the bundler will fail to inject runtime scripts.",
450
- fixHint: 'Wrap the composition in <!DOCTYPE html><html><head><meta charset="UTF-8"></head><body>...</body></html>.',
451
- snippet: rootTag ? truncateSnippet(rootTag.raw) : undefined,
452
- });
453
- }
454
- return findings;
455
- },
456
- // requestanimationframe_in_composition
457
- ({ scripts, rawSource, options }) => {
458
- if (isRegistrySourceFile(options.filePath) || isRegistryInstalledFile(rawSource))
459
- return [];
460
- const findings = [];
461
- for (const script of scripts) {
462
- const stripped = stripJsComments(script.content);
463
- if (/requestAnimationFrame\s*\(/.test(stripped)) {
464
- findings.push({
465
- code: "requestanimationframe_in_composition",
466
- severity: "error",
467
- message: "`requestAnimationFrame` runs on wall-clock time, not the GSAP timeline. It will not sync with frame capture and may cause flickering or missed frames during rendering.",
468
- fixHint: "Use GSAP tweens or onUpdate callbacks instead of requestAnimationFrame for animation logic.",
469
- snippet: truncateSnippet(script.content),
470
- });
471
- }
472
- }
473
- return findings;
474
- },
475
- // invalid_variable_values_json
476
- // Host elements (`[data-composition-src]`) carry per-instance values via
477
- // `data-variable-values`. The runtime swallows JSON errors silently and
478
- // falls back to declared defaults, which masks typos. This rule surfaces
479
- // the parse failure so authors notice before render time.
480
- // fallow-ignore-next-line complexity
481
- ({ tags }) => {
482
- const findings = [];
483
- for (const tag of tags) {
484
- const raw = readJsonAttr(tag.raw, "data-variable-values");
485
- if (!raw)
486
- continue;
487
- let parsed;
488
- try {
489
- parsed = JSON.parse(raw);
490
- }
491
- catch (err) {
492
- const reason = err instanceof Error ? err.message : "unknown";
493
- findings.push({
494
- code: "invalid_variable_values_json",
495
- severity: "error",
496
- message: `data-variable-values is not valid JSON (${reason}).`,
497
- fixHint: 'Wrap the attribute value in single quotes and the JSON keys/values in double quotes, e.g. data-variable-values=\'{"title":"Hello"}\'.',
498
- elementId: readAttr(tag.raw, "id") || undefined,
499
- snippet: truncateSnippet(tag.raw),
500
- });
501
- continue;
502
- }
503
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
504
- findings.push({
505
- code: "invalid_variable_values_json",
506
- severity: "error",
507
- message: 'data-variable-values must be a JSON object keyed by variable id (e.g. {"title":"Hello"}).',
508
- fixHint: "Replace the value with a JSON object whose keys are variable ids declared in the sub-composition's data-composition-variables.",
509
- elementId: readAttr(tag.raw, "id") || undefined,
510
- snippet: truncateSnippet(tag.raw),
511
- });
512
- }
513
- }
514
- return findings;
515
- },
516
- // invalid_composition_variables_declaration
517
- // The runtime parses `data-composition-variables` and silently returns []
518
- // on any structural problem. Surface JSON / shape failures so authors
519
- // catch them at lint time rather than wondering why their `getVariables()`
520
- // defaults aren't applied.
521
- // fallow-ignore-next-line complexity
522
- ({ source }) => {
523
- const htmlTag = findHtmlTag(source);
524
- if (!htmlTag)
525
- return [];
526
- const raw = readJsonAttr(htmlTag.raw, "data-composition-variables");
527
- if (!raw)
528
- return [];
529
- let parsed;
530
- try {
531
- parsed = JSON.parse(raw);
532
- }
533
- catch (err) {
534
- const reason = err instanceof Error ? err.message : "unknown";
535
- return [
536
- {
537
- code: "invalid_composition_variables_declaration",
538
- severity: "error",
539
- message: `data-composition-variables is not valid JSON (${reason}).`,
540
- fixHint: 'Provide a JSON array of variable declarations: data-composition-variables=\'[{"id":"title","type":"string","label":"Title","default":"Hello"}]\'.',
541
- snippet: truncateSnippet(htmlTag.raw),
542
- },
543
- ];
544
- }
545
- if (!Array.isArray(parsed)) {
546
- return [
547
- {
548
- code: "invalid_composition_variables_declaration",
549
- severity: "error",
550
- message: "data-composition-variables must be a JSON array of variable declarations.",
551
- fixHint: 'Wrap declarations in [] and give each an id, type, label, and default: \'[{"id":"title","type":"string","label":"Title","default":"Hello"}]\'.',
552
- snippet: truncateSnippet(htmlTag.raw),
553
- },
554
- ];
555
- }
556
- const findings = [];
557
- const knownTypes = new Set(COMPOSITION_VARIABLE_TYPES);
558
- for (let i = 0; i < parsed.length; i += 1) {
559
- const entry = parsed[i];
560
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
561
- findings.push({
562
- code: "invalid_composition_variables_declaration",
563
- severity: "error",
564
- message: `data-composition-variables entry [${i}] must be an object with id, type, label, and default.`,
565
- snippet: truncateSnippet(htmlTag.raw),
566
- });
567
- continue;
568
- }
569
- const e = entry;
570
- const missing = [];
571
- if (typeof e.id !== "string")
572
- missing.push("id");
573
- if (typeof e.type !== "string" || !knownTypes.has(e.type))
574
- missing.push("type");
575
- if (typeof e.label !== "string")
576
- missing.push("label");
577
- if (!("default" in e))
578
- missing.push("default");
579
- if (missing.length > 0) {
580
- findings.push({
581
- code: "invalid_composition_variables_declaration",
582
- severity: "error",
583
- message: `data-composition-variables entry [${i}] is missing or has invalid: ${missing.join(", ")}. Type must be one of string, number, color, boolean, enum, font, image.`,
584
- snippet: truncateSnippet(htmlTag.raw),
585
- });
586
- }
587
- }
588
- return findings;
589
- },
590
- // subcomposition_blanks_before_host
591
- // Warns when a full-bleed sub-composition slot ends before the host composition
592
- // does, leaving the slot blank for the remainder (issue #1540). Scoped narrowly to
593
- // the high-signal shape — a sole/dominant external mount starting at ~0 — so it
594
- // stays silent on intentional short clips (an intro followed by other clips that
595
- // carry the timeline forward).
596
- // fallow-ignore-next-line complexity
597
- ({ tags, rootTag }) => {
598
- if (!rootTag)
599
- return [];
600
- const rootDuration = Number(readAttr(rootTag.raw, "data-duration"));
601
- if (!Number.isFinite(rootDuration) || rootDuration <= 0)
602
- return [];
603
- // Two independent knobs that happen to share a 0.5s magnitude. Tuned for
604
- // real hosts (tens to hundreds of seconds); on a very short host (~6s) the
605
- // EPSILON slack would let a ~10% blank tail pass unflagged — acceptable
606
- // because the silent-blank trap this rule targets only matters at scale.
607
- const EPSILON = 0.5; // seconds; tolerance for "ends/covers near the host end"
608
- const START_TOLERANCE = 0.5; // seconds; "starts at the composition start"
609
- const round3 = (n) => Math.round(n * 1000) / 1000;
610
- // Timed children of the root. An element with data-start but no usable
611
- // data-duration is treated as covering the tail (end = Infinity), so an
612
- // unknown-length sibling suppresses the warning rather than triggering it.
613
- const timed = tags
614
- .filter((tag) => tag.index !== rootTag.index && readAttr(tag.raw, "data-start") !== null)
615
- .map((tag) => {
616
- const start = Number(readAttr(tag.raw, "data-start")) || 0;
617
- const dur = Number(readAttr(tag.raw, "data-duration"));
618
- const end = Number.isFinite(dur) && dur > 0 ? start + dur : Infinity;
619
- return { tag, start, end };
620
- });
621
- // `tags` is a flat list (no nesting depth), so a timed element nested
622
- // *inside* a candidate slot is treated as a tail-covering sibling rather
623
- // than a descendant. Acceptable: external src mounts are empty by
624
- // convention (content is loaded from the linked file), so the only
625
- // false-negative path is rare and matches the flat-tag scope of the
626
- // sibling rules in this file.
627
- const tailCovered = (exceptIndex) => timed.some((t) => t.tag.index !== exceptIndex && t.end >= rootDuration - EPSILON);
628
- const findings = [];
629
- for (const t of timed) {
630
- if (readAttr(t.tag.raw, "data-composition-src") === null)
631
- continue; // external slot only
632
- if (t.start > START_TOLERANCE)
633
- continue; // must start at the composition start
634
- if (!Number.isFinite(t.end))
635
- continue; // known, finite slot length
636
- if (t.end >= rootDuration - EPSILON)
637
- continue; // already fills the host window
638
- if (tailCovered(t.tag.index))
639
- continue; // another clip covers the tail — not full-bleed
640
- const elementId = readAttr(t.tag.raw, "id") || undefined;
641
- const gap = round3(rootDuration - t.end);
642
- findings.push({
643
- code: "subcomposition_blanks_before_host",
644
- severity: "warning",
645
- message: `<${t.tag.name}${elementId ? ` id="${elementId}"` : ""}> sub-composition ends at ${round3(t.end)}s but the composition runs to ${round3(rootDuration)}s — its slot will be blank for ~${gap}s.`,
646
- elementId,
647
- fixHint: `data-duration is the slot's visible window. Set this sub-composition's data-duration to ${round3(rootDuration - t.start)} to fill the host window, or add another clip to cover the remaining ~${gap}s.`,
648
- snippet: truncateSnippet(t.tag.raw),
649
- });
650
- }
651
- return findings;
652
- },
653
- // subcomposition_root_styled_by_class
654
- // A sub-composition's <style> is scoped at render time to
655
- // `[data-composition-id="<id>"] <selector>` so scenes inlined into one document
656
- // can't leak styles into each other. A rule whose LEFTMOST selector is the ROOT
657
- // element's own class (e.g. `.frame { ... }` on the same element that carries
658
- // data-composition-id) therefore becomes a DESCENDANT selector that can never
659
- // match the root — the whole scene renders unstyled (tiny text top-left, images
660
- // at natural size). lint/validate/inspect evaluate the file in isolation (no
661
- // scoping) and Studio previews each scene in its own iframe (no scoping), so the
662
- // break is invisible until the composited MP4 render. Style the root via `#root`
663
- // (the scoper special-cases the root id) and descendants via plain selectors,
664
- // like the registry blocks — the runtime already scopes each scene by id, so a
665
- // class namespace on the root is redundant.
666
- ({ rootTag, rootCompositionId, styles, options }) => {
667
- if (!options.isSubComposition)
668
- return [];
669
- if (isRegistrySourceFile(options.filePath))
670
- return [];
671
- if (!rootTag || !rootCompositionId)
672
- return [];
673
- const rootClasses = (readAttr(rootTag.raw, "class") || "").split(/\s+/).filter(Boolean);
674
- if (rootClasses.length === 0)
675
- return [];
676
- const offenders = rootClassStyledSelectors(styles, rootClasses);
677
- if (offenders.length === 0)
678
- return [];
679
- const example = offenders.slice(0, 3).join(", ");
680
- return [
681
- {
682
- code: "subcomposition_root_styled_by_class",
683
- severity: "error",
684
- message: `Root element has class="${rootClasses.join(" ")}" and is styled by ${offenders.length} rule(s) keyed off that class (e.g. ${example}). ` +
685
- `At render, every sub-composition rule is scoped to [data-composition-id="${rootCompositionId}"] <selector>, so a selector whose leftmost part is the ROOT's own class becomes a descendant selector that cannot match the root — the scene renders unstyled (tiny text top-left, full-size images). ` +
686
- `lint/validate/inspect and Studio's per-frame iframe preview do not scope, so this passes every static check and looks correct in preview.`,
687
- selector: example,
688
- fixHint: `Give the root id="root" and style it with \`#root { ... }\` plus plain descendant selectors (\`.kicker\`, \`#hero\`) — the runtime already scopes each sub-composition by data-composition-id, so a class namespace on the root is redundant and breaks under scoping.`,
689
- snippet: truncateSnippet(rootTag.raw),
690
- },
691
- ];
692
- },
693
- ];
694
- //# sourceMappingURL=composition.js.map