@hyperframes/lint 0.7.67 → 0.7.69

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.js CHANGED
@@ -300,6 +300,7 @@ function buildLintContext(html, options = {}) {
300
300
 
301
301
  // src/rules/core.ts
302
302
  import postcss from "postcss";
303
+ import selectorParser from "postcss-selector-parser";
303
304
  function escapeRegExp(value) {
304
305
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
305
306
  }
@@ -309,6 +310,73 @@ function selectorTargetsCompositionId(selector, compositionId) {
309
310
  String.raw`\[\s*data-composition-id\s*=\s*(?:"${escaped}"|'${escaped}')\s*\]`
310
311
  ).test(selector);
311
312
  }
313
+ function repeatedDescendantId(selector) {
314
+ let repeated = null;
315
+ const requiredPseudoIds = (pseudo) => {
316
+ if (![":is", ":where"].includes(pseudo.value.toLowerCase()) || pseudo.nodes.length === 0) {
317
+ return /* @__PURE__ */ new Set();
318
+ }
319
+ const optionIdSets = [];
320
+ for (const option of pseudo.nodes) {
321
+ if (option.nodes.some((node) => node.type === "combinator")) return /* @__PURE__ */ new Set();
322
+ const optionIds = new Set(
323
+ option.nodes.filter((node) => node.type === "id").map((node) => node.value)
324
+ );
325
+ optionIdSets.push(optionIds);
326
+ }
327
+ const [firstOptionIds, ...remainingOptionIds] = optionIdSets;
328
+ return new Set(
329
+ [...firstOptionIds ?? []].filter(
330
+ (id) => remainingOptionIds.every((optionIds) => optionIds.has(id))
331
+ )
332
+ );
333
+ };
334
+ try {
335
+ selectorParser((root) => {
336
+ root.each((selectorNode) => {
337
+ const firstCompoundById = /* @__PURE__ */ new Map();
338
+ let compound = 0;
339
+ selectorNode.each((node) => {
340
+ if (repeated) return;
341
+ if (node.type === "combinator") {
342
+ compound += 1;
343
+ return;
344
+ }
345
+ const requiredIds = node.type === "id" ? [node.value] : node.type === "pseudo" ? [...requiredPseudoIds(node)] : [];
346
+ for (const id of requiredIds) {
347
+ const firstCompound = firstCompoundById.get(id);
348
+ if (firstCompound !== void 0 && firstCompound !== compound) {
349
+ repeated = id;
350
+ return;
351
+ }
352
+ firstCompoundById.set(id, compound);
353
+ }
354
+ });
355
+ });
356
+ }).processSync(selector);
357
+ } catch {
358
+ return null;
359
+ }
360
+ return repeated;
361
+ }
362
+ function resolvedRuleSelectors(rule) {
363
+ let ancestor = rule.parent;
364
+ while (ancestor && ancestor.type !== "rule") ancestor = ancestor.parent;
365
+ if (!ancestor || ancestor.type !== "rule") return rule.selectors;
366
+ const parentSelectors = resolvedRuleSelectors(ancestor);
367
+ return parentSelectors.flatMap(
368
+ (parentSelector) => rule.selectors.map((childSelector) => {
369
+ const nestingToken = /(^|[\s>+~,(])&/g;
370
+ if (nestingToken.test(childSelector)) {
371
+ return childSelector.replace(
372
+ nestingToken,
373
+ (_, separator) => separator + parentSelector
374
+ );
375
+ }
376
+ return `${parentSelector} ${childSelector}`;
377
+ })
378
+ );
379
+ }
312
380
  function isStudioTimelineElement(tag) {
313
381
  if (["script", "style", "link", "meta", "template", "noscript"].includes(tag.name)) {
314
382
  return false;
@@ -537,6 +605,34 @@ var coreRules = [
537
605
  }
538
606
  return findings;
539
607
  },
608
+ // repeated_id_descendant_selector
609
+ ({ styles }) => {
610
+ const findings = [];
611
+ const reported = /* @__PURE__ */ new Set();
612
+ for (const style of styles) {
613
+ let root;
614
+ try {
615
+ root = postcss.parse(style.content);
616
+ } catch {
617
+ continue;
618
+ }
619
+ root.walkRules((rule) => {
620
+ for (const selector of resolvedRuleSelectors(rule)) {
621
+ const repeatedId = repeatedDescendantId(selector);
622
+ if (!repeatedId || reported.has(repeatedId)) continue;
623
+ reported.add(repeatedId);
624
+ findings.push({
625
+ code: "repeated_id_descendant_selector",
626
+ severity: "error",
627
+ message: `Selector "${selector}" requires #${repeatedId} to be nested inside another #${repeatedId}. IDs must be unique, so this selector cannot match a valid composition.`,
628
+ selector,
629
+ fixHint: `Remove the duplicate ancestor: change \`#${repeatedId} #${repeatedId}\` to \`#${repeatedId}\`.`
630
+ });
631
+ }
632
+ });
633
+ }
634
+ return findings;
635
+ },
540
636
  // invalid_inline_script_syntax (malformed close tag)
541
637
  ({ source }) => {
542
638
  if (!INVALID_SCRIPT_CLOSE_PATTERN.test(source)) return [];
@@ -1265,6 +1361,10 @@ async function loadParseGsapScript() {
1265
1361
  const mod = await import("@hyperframes/parsers/gsap-parser-acorn");
1266
1362
  return mod.parseGsapScriptAcorn;
1267
1363
  }
1364
+ async function loadGsapScriptMotionPathFirstUseIndex() {
1365
+ const mod = await import("@hyperframes/parsers/gsap-parser-acorn");
1366
+ return mod.gsapScriptMotionPathFirstUseIndex;
1367
+ }
1268
1368
  var SCENE_BOUNDARY_EPSILON_SECONDS = 0.05;
1269
1369
  var UNRESOLVED_TARGET = "__unresolved__";
1270
1370
  function targetHasNoStableIdentity(selector, identity) {
@@ -2241,6 +2341,64 @@ ${right.raw}`)
2241
2341
  }
2242
2342
  ];
2243
2343
  },
2344
+ // missing_gsap_plugin
2345
+ async ({ scripts, rawSource, options }) => {
2346
+ const canInheritPluginFromHost = options.isSubComposition || rawSource.trimStart().toLowerCase().startsWith("<template");
2347
+ if (canInheritPluginFromHost) return [];
2348
+ const gsapScriptMotionPathFirstUseIndex = await loadGsapScriptMotionPathFirstUseIndex();
2349
+ const motionPathUseIndices = scripts.map(
2350
+ (script) => gsapScriptMotionPathFirstUseIndex(script.content)
2351
+ );
2352
+ const firstMotionPathScriptIndex = motionPathUseIndices.findIndex((index) => index !== null);
2353
+ const firstMotionPathUseIndex = motionPathUseIndices[firstMotionPathScriptIndex] ?? null;
2354
+ const firstUseScript = scripts[firstMotionPathScriptIndex];
2355
+ const executionMode = (attrs) => {
2356
+ const tag = `<script ${attrs}>`;
2357
+ const isModule = (readDecodedAttr(tag, "type") ?? "").toLowerCase() === "module";
2358
+ const hasSrc = readDecodedAttr(tag, "src") !== null;
2359
+ const hasAsync = readDecodedAttr(tag, "async") !== null;
2360
+ const hasDefer = readDecodedAttr(tag, "defer") !== null;
2361
+ if ((isModule || hasSrc) && hasAsync) return "async";
2362
+ if (isModule) return "module";
2363
+ if (hasSrc && hasDefer) return "defer";
2364
+ return "blocking";
2365
+ };
2366
+ const firstUseMode = firstUseScript ? executionMode(firstUseScript.attrs) : "blocking";
2367
+ const hasMotionPathPlugin = scripts.slice(0, firstMotionPathScriptIndex + 1).some((script, candidateIndex) => {
2368
+ const candidateMode = executionMode(script.attrs);
2369
+ const sameScript = candidateIndex === firstMotionPathScriptIndex;
2370
+ const candidateIsPostParse = candidateMode === "defer" || candidateMode === "module";
2371
+ const firstUseIsPostParse = firstUseMode === "defer" || firstUseMode === "module";
2372
+ const executesBeforeFirstUse = sameScript || candidateMode === "blocking" || candidateIsPostParse && firstUseIsPostParse;
2373
+ if (!executesBeforeFirstUse || !sameScript && candidateMode === "async") return false;
2374
+ const src = readAttr(`<script ${script.attrs}>`, "src") ?? "";
2375
+ const uncommented = stripJsComments(script.content);
2376
+ const hasStaticImport = /\bimport\s+(?:[\s\S]*?\sfrom\s*)?["'][^"']*\bMotionPathPlugin\b[^"']*["']/.test(
2377
+ uncommented
2378
+ ) || /\bimport\s+(?:[\w$]+\s*,\s*)?\{[^}]*\bMotionPathPlugin\b[^}]*\}\s+from\s*["'][^"']+["']/.test(
2379
+ uncommented
2380
+ ) || /\bimport\s+MotionPathPlugin\s+from\s*["'][^"']+["']/.test(uncommented);
2381
+ const inlinedMarkerIndex = script.content.search(/\/\*\s*inlined:.*MotionPathPlugin/i);
2382
+ const definitionIndex = uncommented.search(
2383
+ /\b(?:const|let|var|class|function)\s+MotionPathPlugin\b/
2384
+ );
2385
+ if (sameScript) {
2386
+ if (hasStaticImport) return true;
2387
+ if (firstMotionPathUseIndex === null) return false;
2388
+ return inlinedMarkerIndex >= 0 && inlinedMarkerIndex < firstMotionPathUseIndex || definitionIndex >= 0 && definitionIndex < firstMotionPathUseIndex;
2389
+ }
2390
+ return /MotionPathPlugin/i.test(src) || hasStaticImport || inlinedMarkerIndex >= 0 || definitionIndex >= 0;
2391
+ });
2392
+ if (firstMotionPathScriptIndex < 0 || hasMotionPathPlugin) return [];
2393
+ return [
2394
+ {
2395
+ code: "missing_gsap_plugin",
2396
+ severity: "error",
2397
+ message: "A GSAP tween uses motionPath, but MotionPathPlugin is not loaded. Core GSAP ignores this plugin-specific property, so the intended motion will not render.",
2398
+ fixHint: "Load MotionPathPlugin before the animation script and register it with gsap.registerPlugin(MotionPathPlugin), or replace motionPath with core GSAP x/y tweens."
2399
+ }
2400
+ ];
2401
+ },
2244
2402
  // audio_reactive_single_tween_per_group
2245
2403
  // fallow-ignore-next-line complexity
2246
2404
  ({ scripts, styles }) => {
@@ -4490,7 +4648,7 @@ var fontRules = [
4490
4648
  const used = extractUsedFontFamilies(styles);
4491
4649
  const googleFonts = collectGoogleFontFamilies(source, styles);
4492
4650
  const undeclared = used.filter(
4493
- (name) => !declared.has(name) && !FONT_ALIAS_KEYS.has(name) && !googleFonts.has(name)
4651
+ (name) => !declared.has(name) && !FONT_ALIAS_KEYS.has(name) && !googleFonts.has(name.replace(/\+/g, " "))
4494
4652
  );
4495
4653
  if (undeclared.length === 0) return findings;
4496
4654
  findings.push({