Fix async lifecycle ordering, add _spa_init boot phase, update to jqhtml _load_only/_load_render_only flags

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
root
2026-03-06 22:33:38 +00:00
parent 11c95a2886
commit d1ac456279
2718 changed files with 70593 additions and 6320 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "postcss-merge-rules",
"version": "7.0.7",
"version": "7.0.8",
"description": "Merge CSS rules with PostCSS.",
"types": "types/index.d.ts",
"main": "src/index.js",
@@ -24,9 +24,9 @@
},
"repository": "cssnano/cssnano",
"dependencies": {
"browserslist": "^4.27.0",
"browserslist": "^4.28.1",
"caniuse-api": "^3.0.0",
"postcss-selector-parser": "^7.1.0",
"postcss-selector-parser": "^7.1.1",
"cssnano-utils": "^5.0.1"
},
"bugs": {
@@ -39,7 +39,7 @@
"@types/caniuse-api": "^3.0.6",
"postcss": "^8.5.6",
"postcss-simple-vars": "^7.0.1",
"postcss-discard-comments": "^7.0.5"
"postcss-discard-comments": "^7.0.6"
},
"peerDependencies": {
"postcss": "^8.4.32"

View File

@@ -54,20 +54,51 @@ function sameDeclarationsAndOrder(a, b) {
return a.every((d, index) => declarationIsEqual(d, b[index]));
}
/**
* RuleMeta stores metadata about a `Rule` during the merging process.
* It tracks selectors and declarations without re-parsing the AST many times.
*
* @typedef {Object} RuleMeta
* @property {string[]} selectors - Array of selector strings for the rule
* @property {import('postcss').Declaration[]} declarations - Array of declaration nodes for the rule
* @property {boolean} dirty - Whether the selectors have been modified and need flushing
*/
/**
* @param {import('postcss').Rule} ruleA
* @param {import('postcss').Rule} ruleB
* @param {string[]=} browsers
* @param {Map<string, boolean>=} compatibilityCache
* @param {string[]} browsers
* @param {Map<string, boolean>} compatibilityCache
* @param {WeakSet<import('postcss').Rule>} ruleCache
* @param {WeakMap<import('postcss').Rule, RuleMeta>} ruleMeta
* @return {boolean}
*/
function canMerge(ruleA, ruleB, browsers, compatibilityCache) {
const a = ruleA.selectors;
const b = ruleB.selectors;
function canMerge(
ruleA,
ruleB,
browsers,
compatibilityCache,
ruleCache,
ruleMeta
) {
const metaA = getMeta(ruleA, ruleMeta);
const metaB = getMeta(ruleB, ruleMeta);
const a = metaA.selectors;
const b = metaB.selectors;
const selectors = a.concat(b);
if (!ensureCompatibility(selectors, browsers, compatibilityCache)) {
if (ruleCache.has(ruleA) && ruleCache.has(ruleB)) {
// Both already validated
} else if (ruleCache.has(ruleA)) {
if (!ensureCompatibility(b, browsers, compatibilityCache)) {
return false;
}
} else if (ruleCache.has(ruleB)) {
if (!ensureCompatibility(a, browsers, compatibilityCache)) {
return false;
}
} else if (!ensureCompatibility(selectors, browsers, compatibilityCache)) {
return false;
}
@@ -105,6 +136,51 @@ function isRuleOrAtRule(node) {
function isDeclaration(node) {
return node.type === 'decl';
}
/**
* Retrieves or initializes virtual metadata for a PostCSS rule.
*
* This metadata caches selectors and declarations to avoid expensive AST
* re-parsing, especially for the selectors.
*
* @param {import('postcss').Rule} rule The PostCSS rule to get metadata for.
* @param {WeakMap<import('postcss').Rule, RuleMeta>} [ruleMeta] The metadata cache.
* @return {RuleMeta} The rule's virtual metadata.
*/
function getMeta(rule, ruleMeta) {
if (ruleMeta && rule) {
let meta = ruleMeta.get(rule);
if (!meta && rule.nodes) {
meta = {
selectors: rule.selectors,
declarations: rule.nodes.filter(isDeclaration),
dirty: false,
};
ruleMeta.set(rule, meta);
}
return meta ?? { selectors: [], declarations: [], dirty: false };
}
return {
selectors: rule?.selectors ?? [],
declarations: rule?.nodes?.filter(isDeclaration) ?? [],
dirty: false,
};
}
/**
* Commits virtual metadata changes back to the actual PostCSS rule.
*
* @param {import('postcss').Rule} rule The PostCSS rule to flush.
* @param {WeakMap<import('postcss').Rule, RuleMeta>} ruleMeta The metadata cache.
*/
function flush(rule, ruleMeta) {
const meta = ruleMeta.get(rule);
if (meta && meta.dirty) {
rule.selector = meta.selectors.join(',');
meta.dirty = false;
}
}
/**
* @param {import('postcss').Rule} rule
* @return {import('postcss').Declaration[]}
@@ -113,9 +189,6 @@ function getDecls(rule) {
return rule.nodes.filter(isDeclaration);
}
/** @type {(...rules: import('postcss').Rule[]) => string} */
const joinSelectors = (...rules) => rules.map((s) => s.selector).join();
/**
* @param {...import('postcss').Rule} rules
* @return {number}
@@ -227,10 +300,26 @@ function mergeParents(first, second) {
/**
* @param {import('postcss').Rule} first
* @param {import('postcss').Rule} second
* @param {string[]} browsers
* @param {Map<string, boolean>} compatibilityCache
* @param {WeakSet<import('postcss').Rule>} ruleCache
* @param {WeakMap<import('postcss').Rule, RuleMeta>} ruleMeta
* @return {import('postcss').Rule} mergedRule
*/
function partialMerge(first, second) {
let intersection = intersect(getDecls(first), getDecls(second));
function partialMerge(
first,
second,
browsers,
compatibilityCache,
ruleCache,
ruleMeta
) {
if (ruleMeta) {
flush(first, ruleMeta);
}
const metaFirst = getMeta(first, ruleMeta);
const metaSecond = getMeta(second, ruleMeta);
let intersection = intersect(metaFirst.declarations, metaSecond.declarations);
if (intersection.length === 0) {
return second;
}
@@ -244,8 +333,22 @@ function partialMerge(first, second) {
).next();
nextRule = parentSibling && parentSibling.nodes && parentSibling.nodes[0];
}
if (nextRule && nextRule.type === 'rule' && canMerge(second, nextRule)) {
let nextIntersection = intersect(getDecls(second), getDecls(nextRule));
if (
nextRule?.type === 'rule' &&
canMerge(
second,
nextRule,
browsers,
compatibilityCache,
ruleCache,
ruleMeta
)
) {
const metaNext = getMeta(nextRule, ruleMeta);
let nextIntersection = intersect(
metaSecond.declarations,
metaNext.declarations
);
if (nextIntersection.length > intersection.length) {
mergeParents(second, nextRule);
first = second;
@@ -254,7 +357,11 @@ function partialMerge(first, second) {
}
}
const firstDecls = getDecls(first);
const metaFirstActual = getMeta(first, ruleMeta);
const metaSecondActual = getMeta(second, ruleMeta);
let firstDecls = [...metaFirstActual.declarations];
let secondDecls = [...metaSecondActual.declarations];
// Filter out intersections with later conflicts in First
intersection = intersection.filter((decl, intersectIndex) => {
const indexOfDecl = indexOfDeclaration(firstDecls, decl);
@@ -276,7 +383,6 @@ function partialMerge(first, second) {
});
// Filter out intersections with previous conflicts in Second
const secondDecls = getDecls(second);
intersection = intersection.filter((decl) => {
const nextConflictIndex = secondDecls.findIndex((d) =>
isConflictingProp(d.prop, decl.prop)
@@ -306,15 +412,18 @@ function partialMerge(first, second) {
}
const receivingBlock = second.clone();
receivingBlock.selector = joinSelectors(first, second);
const firstSelectors = metaFirstActual.selectors;
const secondSelectors = metaSecondActual.selectors;
receivingBlock.selector = [...firstSelectors, ...secondSelectors].join();
receivingBlock.nodes = [];
/** @type {import('postcss').Container<import('postcss').ChildNode>} */ (
second.parent
).insertBefore(second, receivingBlock);
const firstClone = first.clone();
const secondClone = second.clone();
const firstClone = first.clone({ selectors: firstSelectors });
const secondClone = second.clone({ selectors: secondSelectors });
/**
* @param {function(import('postcss').Declaration):void} callback
@@ -335,6 +444,13 @@ function partialMerge(first, second) {
})
);
secondClone.walkDecls(moveDecl((decl) => decl.remove()));
// Ensure original rules are flushed for accurate length comparison
if (ruleMeta) {
flush(first, ruleMeta);
flush(second, ruleMeta);
}
const merged = ruleLength(firstClone, receivingBlock, secondClone);
const original = ruleLength(first, second);
if (merged < original) {
@@ -346,8 +462,13 @@ function partialMerge(first, second) {
}
});
if (!secondClone.parent) {
ruleCache?.add(receivingBlock);
return receivingBlock;
}
ruleCache?.add(receivingBlock);
ruleCache?.add(secondClone);
ruleMeta?.delete(first);
ruleMeta?.delete(second);
return secondClone;
} else {
receivingBlock.remove();
@@ -358,53 +479,101 @@ function partialMerge(first, second) {
/**
* @param {string[]} browsers
* @param {Map<string, boolean>} compatibilityCache
* @return {function(import('postcss').Rule)}
* @param {WeakSet<import('postcss').Rule>} ruleCache
* @param {WeakMap<import('postcss').Rule, RuleMeta>} ruleMeta
* @return {{ merger: function(import('postcss').Rule): void, clean: function(): void }}
*/
function selectorMerger(browsers, compatibilityCache) {
function selectorMerger(browsers, compatibilityCache, ruleCache, ruleMeta) {
/** @type {import('postcss').Rule | null} */
let cache = null;
return function (rule) {
// Prime the cache with the first rule, or alternately ensure that it is
// safe to merge both declarations before continuing
if (!cache || !canMerge(rule, cache, browsers, compatibilityCache)) {
cache = rule;
return;
}
// Ensure that we don't deduplicate the same rule; this is sometimes
// caused by a partial merge
if (cache === rule) {
cache = rule;
return;
}
// Parents merge: check if the rules have same parents, but not same parent nodes
mergeParents(cache, rule);
// Merge when declarations are exactly equal
// e.g. h1 { color: red } h2 { color: red }
if (sameDeclarationsAndOrder(getDecls(rule), getDecls(cache))) {
rule.selector = joinSelectors(cache, rule);
cache.remove();
cache = rule;
return;
}
// Merge when both selectors are exactly equal
// e.g. a { color: blue } a { font-weight: bold }
if (cache.selector === rule.selector) {
const cached = getDecls(cache);
rule.walk((node) => {
if (node.type === 'decl' && indexOfDeclaration(cached, node) !== -1) {
node.remove();
return;
return {
merger(rule) {
// Prime the cache with the first rule, or alternately ensure that it is
// safe to merge both declarations before continuing
if (
!cache ||
!canMerge(
rule,
cache,
browsers,
compatibilityCache,
ruleCache,
ruleMeta
)
) {
if (cache) {
flush(cache, ruleMeta);
}
/** @type {import('postcss').Rule} */ (cache).append(node);
});
rule.remove();
return;
}
// Partial merge: check if the rule contains a subset of the last; if
// so create a joined selector with the subset, if smaller.
cache = partialMerge(cache, rule);
cache = rule;
return;
}
// Ensure that we don't deduplicate the same rule; this is sometimes
// caused by a partial merge
if (cache === rule) {
cache = rule;
return;
}
// Parents merge: check if the rules have same parents, but not same parent nodes
mergeParents(cache, rule);
// Merge when declarations are exactly equal
// e.g. h1 { color: red } h2 { color: red }
if (
sameDeclarationsAndOrder(
getMeta(rule, ruleMeta).declarations,
getMeta(cache, ruleMeta).declarations
)
) {
const metaRule = getMeta(rule, ruleMeta);
const metaCache = getMeta(cache, ruleMeta);
metaRule.selectors = [...metaCache.selectors, ...metaRule.selectors];
metaRule.dirty = true;
cache.remove();
ruleMeta?.delete(cache);
cache = rule;
ruleCache?.add(rule);
return;
}
// Merge when both selectors are exactly equal
// e.g. a { color: blue } a { font-weight: bold }
if (
getMeta(cache, ruleMeta).selectors.join(',') ===
getMeta(rule, ruleMeta).selectors.join(',')
) {
const cachedDecls = getMeta(cache, ruleMeta).declarations;
rule.walk((node) => {
if (
node.type === 'decl' &&
indexOfDeclaration(cachedDecls, node) !== -1
) {
node.remove();
return;
}
/** @type {import('postcss').Rule} */ (cache).append(node);
});
getMeta(cache, ruleMeta).declarations = getDecls(cache);
rule.remove();
ruleMeta?.delete(rule);
return;
}
// Partial merge: check if the rule contains a subset of the last; if
// so create a joined selector with the subset, if smaller.
cache = partialMerge(
cache,
rule,
browsers,
compatibilityCache,
ruleCache,
ruleMeta
);
},
// Flushes any remaining rule in the cache to avoid memory leaks.
clean() {
if (cache) {
flush(cache, ruleMeta);
}
},
};
}
@@ -435,9 +604,21 @@ function pluginCreator(opts = {}) {
});
const compatibilityCache = new Map();
// Use WeakSet and WeakMap to avoid memory leaks because the keys are objects.
const ruleCache = new WeakSet();
const ruleMeta = new WeakMap();
return {
OnceExit(css) {
css.walkRules(selectorMerger(browsers, compatibilityCache));
const { merger, clean } = selectorMerger(
browsers,
compatibilityCache,
ruleCache,
ruleMeta
);
css.walkRules(merger);
clean();
},
};
},

View File

@@ -11,9 +11,27 @@ export = pluginCreator;
*/
declare function pluginCreator(opts?: Options): import("postcss").Plugin;
declare namespace pluginCreator {
export { postcss, AutoprefixerOptions, BrowserslistOptions, Options };
export { postcss, RuleMeta, AutoprefixerOptions, BrowserslistOptions, Options };
}
declare var postcss: true;
/**
* RuleMeta stores metadata about a `Rule` during the merging process.
* It tracks selectors and declarations without re-parsing the AST many times.
*/
type RuleMeta = {
/**
* - Array of selector strings for the rule
*/
selectors: string[];
/**
* - Array of declaration nodes for the rule
*/
declarations: import("postcss").Declaration[];
/**
* - Whether the selectors have been modified and need flushing
*/
dirty: boolean;
};
type AutoprefixerOptions = {
overrideBrowserslist?: string | string[];
};

View File

@@ -1 +1 @@
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":";AA0ZA;;;;GAIG;AAEH;;;;GAIG;AACH,sCAHW,OAAO,GACN,OAAO,SAAS,EAAE,MAAM,CAyBnC;;;;;2BAjCY;IAAE,oBAAoB,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAAE;2BAC5C,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;eACpD,mBAAmB,GAAG,mBAAmB"}
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":";AAmkBA;;;;GAIG;AAEH;;;;GAIG;AACH,sCAHW,OAAO,GACN,OAAO,SAAS,EAAE,MAAM,CAqCnC;;;;;;;;;;;;;eApjBa,MAAM,EAAE;;;;kBACR,OAAO,SAAS,EAAE,WAAW,EAAE;;;;WAC/B,OAAO;;2BAqgBR;IAAE,oBAAoB,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;CAAE;2BAC5C,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,CAAC;eACpD,mBAAmB,GAAG,mBAAmB"}