@hyperframes/lint 0.7.68 → 0.7.70
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.js +159 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.js +159 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
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({
|