🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

css-dedup

Package Overview
Dependencies
Maintainers
1
Versions
16
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

css-dedup - npm Package Compare versions

Comparing version
1.7.0
to
1.8.0
+2
-1
package.json

@@ -55,2 +55,3 @@ {

"scripts": {
"benchmark": "node test/benchmark.js",
"lint": "eslint .",

@@ -65,3 +66,3 @@ "lint:fix": "eslint . --fix",

"types": "src/index.d.ts",
"version": "1.7.0"
"version": "1.8.0"
}
import postcss from 'postcss';
import { normalizeProp, declarationKey } from './normalization.js';
import { normalizeProp, declarationKey, resetPropertyCache } from './normalization.js';
import { isIgnoredSelector, resolveIgnorePatterns } from './hacks.js';
import { splitSelectors, hasSpacedTopLevelComma, selectorsAreMutuallyExclusive, selectorsLikelyDisjoint, resetSubjectIdentities } from './selectors.js';
import { splitSelectors, hasSpacedTopLevelComma, selectorsAreMutuallyExclusive, selectorsLikelyDisjoint, resetSelectorCaches } from './selectors.js';
import { propertiesOverlap } from './shorthands.js';

@@ -11,2 +11,20 @@

// The memoization the hot paths rely on is all per run: reused across one
// style sheet’s passes, never carried over to the next (see
// `resetSelectorCaches()` for why unbounded growth matters here). Called at
// the start of both top-level entry points.
function resetCaches() {
resetSelectorCaches();
resetPropertyCache();
separatorCache = new WeakMap();
}
// A rule’s own `splitSelectors()` result is a shared, cached array (see
// `selectors.js`)—anything that ends up in the public `Occurrence.selectors`
// or `AppliedChange.selectors` gets a copy, so a consumer mutating what it
// was handed can’t corrupt the cache for the rest of the run
function ownSelectors(selector) {
return [...splitSelectors(selector)];
}
// An anonymous `@layer {}` block is its own, distinct cascade layer—unlike

@@ -186,2 +204,4 @@ // two same-name `@layer x {}` blocks, which share one layer—so each block

export function analyzeRoot(root, options = {}) {
resetCaches();
const ignorePatterns = resolveIgnorePatterns(options);

@@ -255,3 +275,3 @@ const aggressive = options.aggressive ?? false;

selector: rule.selector,
selectors: splitSelectors(rule.selector),
selectors: ownSelectors(rule.selector),
line: rule.source?.start?.line,

@@ -296,3 +316,3 @@ })),

selector: rule.selector,
selectors: splitSelectors(rule.selector),
selectors: ownSelectors(rule.selector),
prop: decl.prop,

@@ -340,3 +360,19 @@ value: decl.value,

// rules outvote that convention.
//
// Memoized per container for one run, by node identity. By the time a second
// residual asks, the container already holds the residuals this run inserted
// (each carrying this very separator), so a fresh tally would be counting its
// own output.
let separatorCache = new WeakMap();
function typicalSeparator(container) {
const cached = separatorCache.get(container);
if (cached !== undefined) return cached;
const computed = computeTypicalSeparator(container);
separatorCache.set(container, computed);
return computed;
}
function computeTypicalSeparator(container) {
const counts = new Map();

@@ -464,5 +500,5 @@ for (let i = 1; i < container.nodes.length; i++) {

function consolidateRoot(root, options = {}) {
// The subject-identity memoization is per run: fresh here, reused across
// this run’s fixed-point passes, never carried over to the next style sheet
resetSubjectIdentities();
// The memoization is per run: fresh here, reused across this run’s
// fixed-point passes, never carried over to the next style sheet
resetCaches();

@@ -573,8 +609,14 @@ // Taken before any mutation, so it reflects the file as it stood on disk—

function findBlockingRule(scope, distinctRules, exemptRules, firstIndex, lastIndex, propNormalized) {
const groupSelectors = distinctRules.flatMap(rule => splitSelectors(rule.selector));
for (const [index, rule] of scope.rules.entries()) {
if (index <= firstIndex || index >= lastIndex || exemptRules.has(rule)) continue;
// Only the rules actually inside the span can block—walking the whole
// scope to skip everything outside it made this quadratic in the scope’s
// rule count. The group’s own selector list is likewise only needed once
// something in that span conflicts, which is the rare case.
let groupSelectors = null;
for (let index = firstIndex + 1; index < lastIndex; index++) {
const rule = scope.rules[index];
if (exemptRules.has(rule)) continue;
const conflict = rule.nodes.find(node => node.type === 'decl' && propertiesOverlap(propOf(node.prop), propNormalized));
if (!conflict) continue;
groupSelectors ??= distinctRules.flatMap(groupRule => splitSelectors(groupRule.selector));
const candidateSelectors = splitSelectors(rule.selector);

@@ -655,3 +697,3 @@ // The (memoized, cheap) heuristic goes first: In aggressive mode it

key: splitSelectors(target.selector).join(', '),
selectors: splitSelectors(target.selector),
selectors: ownSelectors(target.selector),
folded: true,

@@ -663,3 +705,3 @@ });

if (merged) {
applied.push(...removeRedundantDuplicates(target, scope.label, splitSelectors(target.selector)));
applied.push(...removeRedundantDuplicates(target, scope.label, ownSelectors(target.selector)));
}

@@ -943,2 +985,6 @@ }

if (run.length) {
// Note: Replacing these scans with a rule → position `Map` is
// twice as slow. `indexOf()` stops at the hit; building the map is
// always a full pass, and each rebuild covers only a handful of
// lookups (`scope.rules` changes as merges land).
const previousIndex = scope.rules.indexOf(run.at(-1));

@@ -1308,3 +1354,3 @@ const nextIndex = scope.rules.indexOf(rule);

for (const rule of eligibleRules(scope, ignorePatterns)) {
applied.push(...removeRedundantDuplicates(rule, scope.label, splitSelectors(rule.selector)));
applied.push(...removeRedundantDuplicates(rule, scope.label, ownSelectors(rule.selector)));
}

@@ -1311,0 +1357,0 @@ }

@@ -316,3 +316,27 @@ import { normalizeColors } from './colors.js';

// Memoized, and scoped to one run (see `resetPropertyCache()`): The
// merge-safety scan normalizes the same property names over and over within
// a run, but a long-lived process (a PostCSS watch build) must not
// accumulate every custom property name—think generated or hashed ones—it
// has ever seen. One cache per mode, rather than one keyed by mode plus
// name: Building that composite key would allocate a string per lookup.
const propCacheDefault = new Map();
const propCacheAggressive = new Map();
export function resetPropertyCache() {
propCacheDefault.clear();
propCacheAggressive.clear();
}
export function normalizeProp(prop, aggressive = false) {
const cache = aggressive ? propCacheAggressive : propCacheDefault;
const cached = cache.get(prop);
if (cached !== undefined) return cached;
const computed = computeNormalizeProp(prop, aggressive);
cache.set(prop, computed);
return computed;
}
function computeNormalizeProp(prop, aggressive) {
const trimmed = prop.trim();

@@ -319,0 +343,0 @@ // Custom property names are case-sensitive (`--Foo` !== `--foo`); every

// Splits a selector list on top-level commas only, respecting commas nested
// inside `:is(a, b)`, `[attr="a,b"]`, and similar constructs
// inside `:is(a, b)`, `[attr="a,b"]`, and similar constructs. Memoized on the
// same per-run terms as `subjectIdentities` below—the same handful of
// selector strings get split repeatedly within a run (eligibility filtering,
// merge-safety scans, occurrence reporting), but selector text is unbounded
// across runs.
//
// The cached array is shared between callers, so anything handing it to the
// outside world (`Occurrence.selectors`, `AppliedChange.selectors`) copies
// it first—see `index.js`.
const splitCache = new Map();
export function splitSelectors(selectorList) {
const cached = splitCache.get(selectorList);
if (cached) return cached;
const computed = computeSplitSelectors(selectorList);
splitCache.set(selectorList, computed);
return computed;
}
function computeSplitSelectors(selectorList) {
const selectors = [];

@@ -194,3 +213,3 @@ let depth = 0;

// Memoized, and scoped to one consolidation run (see
// `resetSubjectIdentities()`): The merge-safety scan asks about the same
// `resetSelectorCaches()`): The merge-safety scan asks about the same
// selectors over and over within a run, but a long-lived process (a PostCSS

@@ -201,6 +220,8 @@ // watch build, say) must not accumulate every selector—think generated or

// Called at the start of each top-level `dedupRoot()` run—the only flow that
// reaches `subjectIdentity()`
export function resetSubjectIdentities() {
// Called at the start of each top-level `analyzeRoot()`/`dedupRoot()` run—
// `subjectIdentity()` is reached only from the latter, but `splitSelectors()`
// from both, so both have to bound their caches
export function resetSelectorCaches() {
subjectIdentities.clear();
splitCache.clear();
}

@@ -207,0 +228,0 @@

@@ -91,4 +91,38 @@ // Maps each shorthand property to the longhands it sets—physical and

function expandProperty(prop) {
return new Set([prop, ...(SHORTHAND_LONGHANDS[prop] ?? [])]);
// Each shorthand’s expansion, including its own name—built once at module
// load rather than per call, since `propertiesOverlap()` below sits on the
// merge-safety hot path and ran once per declaration per scanned rule
const EXPANSIONS = new Map(
Object.entries(SHORTHAND_LONGHANDS).map(([prop, longhands]) => [prop, new Set([prop, ...longhands])]),
);
// Every property name taking part in any shorthand/longhand relation, on
// either side. A name outside this set expands to just itself, so it can
// only ever overlap a property it’s equal to—which lets the overwhelmingly
// common case (`color`, `display`, …) answer from one lookup.
const RELATED_PROPS = new Set([
...Object.keys(SHORTHAND_LONGHANDS),
...Object.values(SHORTHAND_LONGHANDS).flat(),
]);
// Keyed by first property, then second—nested rather than by a joined
// string, so a lookup allocates nothing. Bounded by `RELATED_PROPS` (a fixed
// vocabulary), so unlike the per-run caches elsewhere this one never needs
// clearing.
const overlapCache = new Map();
function computeOverlap(a, b) {
const expandedA = EXPANSIONS.get(a);
const expandedB = EXPANSIONS.get(b);
// Neither is a shorthand, so both expand to just themselves—and equality
// was already ruled out by the caller
if (!expandedA && !expandedB) return false;
if (!expandedA) return expandedB.has(a);
if (!expandedB) return expandedA.has(b);
const [small, large] = expandedA.size <= expandedB.size ? [expandedA, expandedB] : [expandedB, expandedA];
for (const prop of small) {
if (large.has(prop)) return true;
}
return false;
}

@@ -104,8 +138,15 @@

if (a === b) return true;
const expandedA = expandProperty(a);
const expandedB = expandProperty(b);
for (const prop of expandedA) {
if (expandedB.has(prop)) return true;
if (!RELATED_PROPS.has(a) || !RELATED_PROPS.has(b)) return false;
let row = overlapCache.get(a);
if (!row) {
row = new Map();
overlapCache.set(a, row);
}
return false;
const cached = row.get(b);
if (cached !== undefined) return cached;
const overlap = computeOverlap(a, b);
row.set(b, overlap);
return overlap;
}