Fix code quality violations and exclude Manifest from checks

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>
This commit is contained in:
root
2026-01-14 10:38:22 +00:00
parent bb9046af1b
commit d523f0f600
2355 changed files with 231384 additions and 32223 deletions

View File

@@ -1,72 +1,48 @@
'use strict';
import * as csstree from 'css-tree';
import { syntax } from 'csso';
import { attrsGroups, pseudoClasses } from './_collections.js';
import { detachNodeFromParent, querySelectorAll } from '../lib/xast.js';
import { visitSkip } from '../lib/util/visit.js';
import { compareSpecificity, includesAttrSelector } from '../lib/style.js';
/**
* @typedef {import('../lib/types').Specificity} Specificity
* @typedef {import('../lib/types').XastElement} XastElement
* @typedef {import('../lib/types').XastParent} XastParent
* @typedef InlineStylesParams
* @property {boolean=} onlyMatchedOnce Inlines selectors that match once only.
* @property {boolean=} removeMatchedSelectors
* Clean up matched selectors. Unused selects are left as-is.
* @property {string[]=} useMqs
* Media queries to use. An empty string indicates all selectors outside of
* media queries.
* @property {string[]=} usePseudos
* Pseudo-classes and elements to use. An empty string indicates all
* non-pseudo-classes and elements.
*/
const csstree = require('css-tree');
// @ts-ignore not defined in @types/csso
const specificity = require('csso/lib/restructure/prepare/specificity');
const stable = require('stable');
const {
visitSkip,
querySelectorAll,
detachNodeFromParent,
} = require('../lib/xast.js');
exports.type = 'visitor';
exports.name = 'inlineStyles';
exports.active = true;
exports.description = 'inline styles (additional options)';
export const name = 'inlineStyles';
export const description = 'inline styles (additional options)';
/**
* Compares two selector specificities.
* extracted from https://github.com/keeganstreet/specificity/blob/master/specificity.js#L211
* Some pseudo-classes can only be calculated by clients, like :visited,
* :future, or :hover, but there are other pseudo-classes that we can evaluate
* during optimization.
*
* @type {(a: Specificity, b: Specificity) => number}
* Pseudo-classes that we can evaluate during optimization, and shouldn't be
* toggled conditionally through the `usePseudos` parameter.
*
* @see https://developer.mozilla.org/docs/Web/CSS/Pseudo-classes
*/
const compareSpecificity = (a, b) => {
for (var i = 0; i < 4; i += 1) {
if (a[i] < b[i]) {
return -1;
} else if (a[i] > b[i]) {
return 1;
}
}
return 0;
};
const preservedPseudos = [
...pseudoClasses.functional,
...pseudoClasses.treeStructural,
];
/**
* Moves + merges styles from style elements to element styles
*
* Options
* onlyMatchedOnce (default: true)
* inline only selectors that match once
*
* removeMatchedSelectors (default: true)
* clean up matched selectors,
* leave selectors that hadn't matched
*
* useMqs (default: ['', 'screen'])
* what media queries to be used
* empty string element for styles outside media queries
*
* usePseudos (default: [''])
* what pseudo-classes/-elements to be used
* empty string element for all non-pseudo-classes and/or -elements
* Merges styles from style nodes into inline styles.
*
* @type {import('../lib/types.js').Plugin<InlineStylesParams>}
* @author strarsis <strarsis@gmail.com>
*
* @type {import('../lib/types').Plugin<{
* onlyMatchedOnce?: boolean,
* removeMatchedSelectors?: boolean,
* useMqs?: Array<string>,
* usePseudos?: Array<string>
* }>}
*/
exports.fn = (root, params) => {
export const fn = (root, params) => {
const {
onlyMatchedOnce = true,
removeMatchedSelectors = true,
@@ -75,31 +51,32 @@ exports.fn = (root, params) => {
} = params;
/**
* @type {Array<{ node: XastElement, parentNode: XastParent, cssAst: csstree.StyleSheet }>}
* @type {{
* node: import('../lib/types.js').XastElement,
* parentNode: import('../lib/types.js').XastParent,
* cssAst: csstree.StyleSheet
* }[]}
*/
const styles = [];
/**
* @type {Array<{
* @type {{
* node: csstree.Selector,
* item: csstree.ListItem<csstree.CssNode>,
* rule: csstree.Rule,
* matchedElements?: Array<XastElement>
* }>}
* matchedElements?: import('../lib/types.js').XastElement[]
* }[]}
*/
let selectors = [];
const selectors = [];
return {
element: {
enter: (node, parentNode) => {
// skip <foreignObject /> content
if (node.name === 'foreignObject') {
return visitSkip;
}
// collect only non-empty <style /> elements
if (node.name !== 'style' || node.children.length === 0) {
return;
}
// values other than the empty string or text/css are not used
if (
node.attributes.type != null &&
node.attributes.type !== '' &&
@@ -107,16 +84,13 @@ exports.fn = (root, params) => {
) {
return;
}
// parse css in style element
let cssText = '';
for (const child of node.children) {
if (child.type === 'text' || child.type === 'cdata') {
cssText += child.value;
}
}
/**
* @type {null | csstree.CssNode}
*/
const cssText = node.children
.filter((child) => child.type === 'text' || child.type === 'cdata')
.map((child) => child.value)
.join('');
/** @type {?csstree.CssNode} */
let cssAst = null;
try {
cssAst = csstree.parse(cssText, {
@@ -132,63 +106,68 @@ exports.fn = (root, params) => {
// collect selectors
csstree.walk(cssAst, {
visit: 'Selector',
enter(node, item) {
visit: 'Rule',
enter(node) {
const atrule = this.atrule;
const rule = this.rule;
if (rule == null) {
return;
}
// skip media queries not included into useMqs param
let mq = '';
let mediaQuery = '';
if (atrule != null) {
mq = atrule.name;
mediaQuery = atrule.name;
if (atrule.prelude != null) {
mq += ` ${csstree.generate(atrule.prelude)}`;
mediaQuery += ` ${csstree.generate(atrule.prelude)}`;
}
}
if (useMqs.includes(mq) === false) {
if (!useMqs.includes(mediaQuery)) {
return;
}
/**
* @type {Array<{
* item: csstree.ListItem<csstree.CssNode>,
* list: csstree.List<csstree.CssNode>
* }>}
*/
const pseudos = [];
if (node.type === 'Selector') {
node.children.each((childNode, childItem, childList) => {
if (
childNode.type === 'PseudoClassSelector' ||
childNode.type === 'PseudoElementSelector'
) {
pseudos.push({ item: childItem, list: childList });
if (node.prelude.type === 'SelectorList') {
node.prelude.children.forEach((childNode, item) => {
if (childNode.type === 'Selector') {
/**
* @type {{
* item: csstree.ListItem<csstree.CssNode>,
* list: csstree.List<csstree.CssNode>
* }[]}
*/
const pseudos = [];
childNode.children.forEach(
(grandchildNode, grandchildItem, grandchildList) => {
const isPseudo =
grandchildNode.type === 'PseudoClassSelector' ||
grandchildNode.type === 'PseudoElementSelector';
if (
isPseudo &&
!preservedPseudos.includes(grandchildNode.name)
) {
pseudos.push({
item: grandchildItem,
list: grandchildList,
});
}
},
);
const pseudoSelectors = csstree.generate({
type: 'Selector',
children: new csstree.List().fromArray(
pseudos.map((pseudo) => pseudo.item.data),
),
});
if (usePseudos.includes(pseudoSelectors)) {
for (const pseudo of pseudos) {
pseudo.list.remove(pseudo.item);
}
}
selectors.push({ node: childNode, rule: node, item: item });
}
});
}
// skip pseudo classes and pseudo elements not includes into usePseudos param
const pseudoSelectors = csstree.generate({
type: 'Selector',
children: new csstree.List().fromArray(
pseudos.map((pseudo) => pseudo.item.data)
),
});
if (usePseudos.includes(pseudoSelectors) === false) {
return;
}
// remove pseudo classes and elements to allow querySelector match elements
// TODO this is not very accurate since some pseudo classes like first-child
// are used for selection
for (const pseudo of pseudos) {
pseudo.list.remove(pseudo.item);
}
selectors.push({ node, item, rule });
},
});
},
@@ -199,19 +178,19 @@ exports.fn = (root, params) => {
if (styles.length === 0) {
return;
}
// stable sort selectors
const sortedSelectors = stable(selectors, (a, b) => {
const aSpecificity = specificity(a.item.data);
const bSpecificity = specificity(b.item.data);
return compareSpecificity(aSpecificity, bSpecificity);
}).reverse();
const sortedSelectors = selectors
.slice()
.sort((a, b) => {
const aSpecificity = syntax.specificity(a.item.data);
const bSpecificity = syntax.specificity(b.item.data);
return compareSpecificity(aSpecificity, bSpecificity);
})
.reverse();
for (const selector of sortedSelectors) {
// match selectors
const selectorText = csstree.generate(selector.item.data);
/**
* @type {Array<XastElement>}
*/
/** @type {import('../lib/types.js').XastElement[]} */
const matchedElements = [];
try {
for (const node of querySelectorAll(root, selectorText)) {
@@ -219,7 +198,7 @@ exports.fn = (root, params) => {
matchedElements.push(node);
}
}
} catch (selectError) {
} catch {
continue;
}
// nothing selected
@@ -236,22 +215,28 @@ exports.fn = (root, params) => {
// apply <style/> to matched elements
for (const selectedEl of matchedElements) {
const styleDeclarationList = csstree.parse(
selectedEl.attributes.style == null
? ''
: selectedEl.attributes.style,
selectedEl.attributes.style ?? '',
{
context: 'declarationList',
parseValue: false,
}
},
);
if (styleDeclarationList.type !== 'DeclarationList') {
continue;
}
const styleDeclarationItems = new Map();
/** @type {csstree.ListItem<csstree.CssNode>} */
let firstListItem;
csstree.walk(styleDeclarationList, {
visit: 'Declaration',
enter(node, item) {
styleDeclarationItems.set(node.property, item);
if (firstListItem == null) {
firstListItem = item;
}
styleDeclarationItems.set(node.property.toLowerCase(), item);
},
});
// merge declarations
@@ -262,30 +247,42 @@ exports.fn = (root, params) => {
// no inline styles, external styles, external styles used
// inline styles, external styles same priority as inline styles, inline styles used
// inline styles, external styles higher priority than inline styles, external styles used
const matchedItem = styleDeclarationItems.get(
ruleDeclaration.property
);
const property = ruleDeclaration.property;
if (
attrsGroups.presentation.has(property) &&
!selectors.some((selector) =>
includesAttrSelector(selector.item, property),
)
) {
delete selectedEl.attributes[property];
}
const matchedItem = styleDeclarationItems.get(property);
const ruleDeclarationItem =
styleDeclarationList.children.createItem(ruleDeclaration);
if (matchedItem == null) {
styleDeclarationList.children.append(ruleDeclarationItem);
styleDeclarationList.children.insert(
ruleDeclarationItem,
firstListItem,
);
} else if (
matchedItem.data.important !== true &&
ruleDeclaration.important === true
) {
styleDeclarationList.children.replace(
matchedItem,
ruleDeclarationItem
);
styleDeclarationItems.set(
ruleDeclaration.property,
ruleDeclarationItem
ruleDeclarationItem,
);
styleDeclarationItems.set(property, ruleDeclarationItem);
}
},
});
selectedEl.attributes.style =
csstree.generate(styleDeclarationList);
const newStyles = csstree.generate(styleDeclarationList);
if (newStyles.length !== 0) {
selectedEl.attributes.style = newStyles;
}
}
if (
@@ -300,7 +297,7 @@ exports.fn = (root, params) => {
}
// no further processing required
if (removeMatchedSelectors === false) {
if (!removeMatchedSelectors) {
return;
}
@@ -320,15 +317,25 @@ exports.fn = (root, params) => {
const classList = new Set(
selectedEl.attributes.class == null
? null
: selectedEl.attributes.class.split(' ')
: selectedEl.attributes.class.split(' '),
);
const firstSubSelector = selector.node.children.first();
if (
firstSubSelector != null &&
firstSubSelector.type === 'ClassSelector'
) {
classList.delete(firstSubSelector.name);
for (const child of selector.node.children) {
if (
child.type === 'ClassSelector' &&
!selectors.some((selector) =>
includesAttrSelector(
selector.item,
'class',
child.name,
true,
),
)
) {
classList.delete(child.name);
}
}
if (classList.size === 0) {
delete selectedEl.attributes.class;
} else {
@@ -336,13 +343,20 @@ exports.fn = (root, params) => {
}
// ID
const firstSubSelector = selector.node.children.first;
if (
firstSubSelector != null &&
firstSubSelector.type === 'IdSelector'
firstSubSelector?.type === 'IdSelector' &&
selectedEl.attributes.id === firstSubSelector.name &&
!selectors.some((selector) =>
includesAttrSelector(
selector.item,
'id',
firstSubSelector.name,
true,
),
)
) {
if (selectedEl.attributes.id === firstSubSelector.name) {
delete selectedEl.attributes.id;
}
delete selectedEl.attributes.id;
}
}
}
@@ -355,15 +369,15 @@ exports.fn = (root, params) => {
if (
node.type === 'Rule' &&
node.prelude.type === 'SelectorList' &&
node.prelude.children.isEmpty()
node.prelude.children.isEmpty
) {
list.remove(item);
}
},
});
if (style.cssAst.children.isEmpty()) {
// remove emtpy style element
if (style.cssAst.children.isEmpty) {
// remove empty style element
detachNodeFromParent(style.node, style.parentNode);
} else {
// update style element if any styles left