Document application modes (development/debug/production) Add global file drop handler, order column normalization, SPA hash fix Serve CDN assets via /_vendor/ URLs instead of merging into bundles Add production minification with license preservation Improve JSON formatting for debugging and production optimization Add CDN asset caching with CSS URL inlining for production builds Add three-mode system (development, debug, production) Update Manifest CLAUDE.md to reflect helper class architecture Refactor Manifest.php into helper classes for better organization Pre-manifest-refactor checkpoint: Add app_mode documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
274 lines
6.1 KiB
JavaScript
Executable File
274 lines
6.1 KiB
JavaScript
Executable File
'use strict';
|
|
const parser = require('postcss-selector-parser');
|
|
const canUnquote = require('./lib/canUnquote.js');
|
|
|
|
const pseudoElements = new Set([
|
|
'::before',
|
|
'::after',
|
|
'::first-letter',
|
|
'::first-line',
|
|
]);
|
|
|
|
/**
|
|
* @param {parser.Attribute} selector
|
|
* @return {void}
|
|
*/
|
|
function attribute(selector) {
|
|
if (selector.value) {
|
|
if (selector.raws.value) {
|
|
// Join selectors that are split over new lines
|
|
selector.raws.value = selector.raws.value.replace(/\\\n/g, '').trim();
|
|
}
|
|
if (canUnquote(selector.value)) {
|
|
selector.quoteMark = null;
|
|
}
|
|
|
|
if (selector.operator) {
|
|
selector.operator = /** @type {parser.AttributeOperator} */ (
|
|
selector.operator.trim()
|
|
);
|
|
}
|
|
}
|
|
|
|
selector.rawSpaceBefore = '';
|
|
selector.rawSpaceAfter = '';
|
|
selector.spaces.attribute = { before: '', after: '' };
|
|
selector.spaces.operator = { before: '', after: '' };
|
|
selector.spaces.value = {
|
|
before: '',
|
|
after: selector.insensitive ? ' ' : '',
|
|
};
|
|
|
|
if (selector.raws.spaces) {
|
|
selector.raws.spaces.attribute = {
|
|
before: '',
|
|
after: '',
|
|
};
|
|
|
|
selector.raws.spaces.operator = {
|
|
before: '',
|
|
after: '',
|
|
};
|
|
|
|
selector.raws.spaces.value = {
|
|
before: '',
|
|
after: selector.insensitive ? ' ' : '',
|
|
};
|
|
|
|
if (selector.insensitive) {
|
|
selector.raws.spaces.insensitive = {
|
|
before: '',
|
|
after: '',
|
|
};
|
|
}
|
|
}
|
|
|
|
selector.attribute = selector.attribute.trim();
|
|
}
|
|
|
|
/**
|
|
* @param {parser.Combinator} selector
|
|
* @return {void}
|
|
*/
|
|
function combinator(selector) {
|
|
const value = selector.value.trim();
|
|
selector.spaces.before = '';
|
|
selector.spaces.after = '';
|
|
selector.rawSpaceBefore = '';
|
|
selector.rawSpaceAfter = '';
|
|
selector.value = value.length ? value : ' ';
|
|
}
|
|
|
|
const pseudoReplacements = new Map([
|
|
[':nth-child', ':first-child'],
|
|
[':nth-of-type', ':first-of-type'],
|
|
[':nth-last-child', ':last-child'],
|
|
[':nth-last-of-type', ':last-of-type'],
|
|
]);
|
|
|
|
/**
|
|
* @param {parser.Pseudo} selector
|
|
* @return {void}
|
|
*/
|
|
function pseudo(selector) {
|
|
const value = selector.value.toLowerCase();
|
|
|
|
if (selector.nodes.length === 1 && pseudoReplacements.has(value)) {
|
|
const first = selector.at(0);
|
|
const one = first.at(0);
|
|
|
|
if (first.length === 1) {
|
|
if (one.value === '1') {
|
|
selector.replaceWith(
|
|
parser.pseudo({
|
|
value: /** @type {string} */ (pseudoReplacements.get(value)),
|
|
})
|
|
);
|
|
}
|
|
|
|
if (one.value && one.value.toLowerCase() === 'even') {
|
|
one.value = '2n';
|
|
}
|
|
}
|
|
|
|
if (first.length === 3) {
|
|
const two = first.at(1);
|
|
const three = first.at(2);
|
|
|
|
if (
|
|
one.value &&
|
|
one.value.toLowerCase() === '2n' &&
|
|
two.value === '+' &&
|
|
three.value === '1'
|
|
) {
|
|
one.value = 'odd';
|
|
|
|
two.remove();
|
|
three.remove();
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
selector.walk((child) => {
|
|
if (child.type === 'selector' && child.parent) {
|
|
const uniques = new Set();
|
|
child.parent.each((sibling) => {
|
|
const siblingStr = String(sibling);
|
|
|
|
if (!uniques.has(siblingStr)) {
|
|
uniques.add(siblingStr);
|
|
} else {
|
|
sibling.remove();
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
if (pseudoElements.has(value)) {
|
|
selector.value = selector.value.slice(1);
|
|
}
|
|
}
|
|
|
|
const tagReplacements = new Map([
|
|
['from', '0%'],
|
|
['100%', 'to'],
|
|
]);
|
|
|
|
/**
|
|
* @param {parser.Tag} selector
|
|
* @return {void}
|
|
*/
|
|
function tag(selector) {
|
|
const value = selector.value.toLowerCase();
|
|
|
|
const isSimple = selector.parent && selector.parent.nodes.length === 1;
|
|
// Avoid simplifying complex selectors (`entry 100% {...}`)
|
|
if (!isSimple) {
|
|
return;
|
|
}
|
|
|
|
// Simplify simple selectors that have replacements (`100% {...}`)
|
|
if (tagReplacements.has(value)) {
|
|
selector.value = /** @type {string} */ (tagReplacements.get(value));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {parser.Universal} selector
|
|
* @return {void}
|
|
*/
|
|
function universal(selector) {
|
|
const next = selector.next();
|
|
|
|
if (next && next.type !== 'combinator') {
|
|
selector.remove();
|
|
}
|
|
}
|
|
|
|
const reducers = new Map(
|
|
/** @type {[string, ((selector: parser.Node) => void)][]}*/ ([
|
|
['attribute', attribute],
|
|
['combinator', combinator],
|
|
['pseudo', pseudo],
|
|
['tag', tag],
|
|
['universal', universal],
|
|
])
|
|
);
|
|
|
|
/** @typedef {object} Options
|
|
* @property {boolean} [sort=true]
|
|
*/
|
|
/**
|
|
* @type {import('postcss').PluginCreator<void>}
|
|
* @param {Options} opts
|
|
* @return {import('postcss').Plugin}
|
|
*/
|
|
function pluginCreator(opts = { sort: true }) {
|
|
return {
|
|
postcssPlugin: 'postcss-minify-selectors',
|
|
|
|
OnceExit(css) {
|
|
const cache = new Map();
|
|
const processor = parser((selectors) => {
|
|
const uniqueSelectors = new Set();
|
|
|
|
selectors.walk((sel) => {
|
|
// Trim whitespace around the value
|
|
sel.spaces.before = sel.spaces.after = '';
|
|
const reducer = reducers.get(sel.type);
|
|
if (reducer !== undefined) {
|
|
reducer(sel);
|
|
return;
|
|
}
|
|
|
|
const toString = String(sel);
|
|
|
|
if (
|
|
sel.type === 'selector' &&
|
|
sel.parent &&
|
|
sel.parent.type !== 'pseudo'
|
|
) {
|
|
if (!uniqueSelectors.has(toString)) {
|
|
uniqueSelectors.add(toString);
|
|
} else {
|
|
sel.remove();
|
|
}
|
|
}
|
|
});
|
|
if (opts.sort) {
|
|
selectors.nodes.sort();
|
|
}
|
|
});
|
|
|
|
css.walkRules((rule) => {
|
|
const selector =
|
|
rule.raws.selector && rule.raws.selector.value === rule.selector
|
|
? rule.raws.selector.raw
|
|
: rule.selector;
|
|
|
|
// If the selector ends with a ':' it is likely a part of a custom mixin,
|
|
// so just pass through.
|
|
if (selector[selector.length - 1] === ':') {
|
|
return;
|
|
}
|
|
|
|
if (cache.has(selector)) {
|
|
rule.selector = cache.get(selector);
|
|
|
|
return;
|
|
}
|
|
|
|
const optimizedSelector = processor.processSync(selector);
|
|
|
|
rule.selector = optimizedSelector;
|
|
cache.set(selector, optimizedSelector);
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
pluginCreator.postcss = true;
|
|
module.exports = pluginCreator;
|