Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix ben-eb/cssnano/448 don't remove fallbacks for custom css properties #486

Merged
merged 6 commits into from Jun 15, 2018
35 changes: 35 additions & 0 deletions packages/cssnano/src/__tests__/postcss-merge-longhand.js
Expand Up @@ -154,3 +154,38 @@ test(
'h1{margin:10px 20px 30px 40px}',
'h1{margin:10px 20px 30px 40px}',
);

test(
'should merge custom props and dont remove fallbacks',
processCss,
'h1{padding-top:10px;padding-right:15px;padding-bottom:20px;padding-left:25px;padding-top:var(--variable);padding-right:var(--variable);padding-bottom:var(--variable);padding-left:var(--variable)}',
'h1{padding:var(--variable);padding:10px 15px 20px 25px}'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be:

 h1{padding:10px 15px 20px 25px;padding:var(--variable)}

?

Copy link
Contributor Author

@karapetyan karapetyan Jun 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we look at many other tests that contain more than one unmegable rule, they have similar behavior.

Here is some examples from same test file (postcss-merge-longhand.js) :

test(
    'should not merge padding values with mixed !important',
    processCss,
    'h1{padding-top:10px!important;padding-right:20px;padding-bottom:30px!important;padding-left:40px}',
    'h1{padding-bottom:30px!important;padding-left:40px;padding-right:20px;padding-top:10px!important}',
);

test(
    'should not merge identical border values with mixed !important',
    processCss,
    'h1{border-top:1px solid #000;border-bottom:1px solid #000;border-left:1px solid #000!important;border-right:1px solid #000!important}',
    'h1{border-bottom:1px solid #000;border-left:1px solid #000!important;border-right:1px solid #000!important;border-top:1px solid #000}',
);

so I guess this is correct behavior

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree, the semantics have fundamentally changed. See my other comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andyjansson done, thanks for help!

);

test(
'should merge rules with custom props',
processCss,
'h1{padding-top:var(--variable);padding-right:var(--variable);padding-bottom:var(--variable);padding-left:var(--variable)}',
'h1{padding:var(--variable)}'
);

test(
'should not break unmergeable fallbacks with custom props (2)',
processCss,
'h1{padding:10em;padding:var(--variable)}',
'h1{padding:var(--variable);padding:10em}'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks wrong. Surely the output should be the same as the input..?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As described in issue #448
cssnano shouldn't remove fallbacks for custom CSS properties.

As far i understand that exactly what fix should do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, but the correct order should be:

h1{padding:10em;padding:var(--variable)}

var(--variable) wouldn't be understood by browsers not supporting it, thus falling back on 10em. In your version, you overwrite var(--variable) with 10em in all cases.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karapetyan yep, we should have h1{padding:10em;padding:var(--variable)} here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@andyjansson @evilebottnawi understood, thanks for review :) I’ll fix it soon.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@evilebottnawi friendly ping

);

test(
'should not break unmergeable rules with custom props',
processCss,
'h1{padding:var(--variable)}',
'h1{padding:var(--variable)}'
);

test(
'should not break unmergeable fallbacks with custom props',
processCss,
'h1{padding:var(--some);padding:10em}',
'h1{padding:var(--some);padding:10em}'
);
6 changes: 4 additions & 2 deletions packages/postcss-merge-longhand/src/lib/canMerge.js
@@ -1,11 +1,13 @@
import hasVariable from './hasVariable';

const important = node => node.important;
const unimportant = node => !node.important;
const hasInherit = node => ~node.value.indexOf('inherit');
const hasInitial = node => ~node.value.indexOf('initial');

export default (...props) => {
if (props.some(hasInherit) || props.some(hasInitial)) {
return props.every(hasInherit) || props.every(hasInitial);
if (props.some(hasInherit) || props.some(hasInitial) || props.some(hasVariable)) {
return props.every(hasInherit) || props.every(hasInitial) || props.every(hasVariable);
}
return props.every(unimportant) || props.every(important);
};
59 changes: 32 additions & 27 deletions packages/postcss-merge-longhand/src/lib/decl/borders.js
Expand Up @@ -11,6 +11,7 @@ import minifyTrbl from '../minifyTrbl';
import canMerge from '../canMerge';
import remove from '../remove';
import trbl from '../trbl';
import hasVariable from '../hasVariable';

const wsc = ['width', 'style', 'color'];
const defaults = ['medium', 'none', 'currentColor'];
Expand Down Expand Up @@ -282,33 +283,35 @@ function merge (rule) {
wsc.forEach((d, i) => {
const names = directions.filter(name => name !== lastNode.prop).map(name => `${name}-${d}`);
const props = rule.nodes.filter(node => node.prop && ~names.indexOf(node.prop) && node.important === lastNode.important);
const rules = getRules(props, names);
if (hasAllProps(rules, ...names) && !rules.some(detect)) {
const values = rules.map(node => node ? node.value : null);
const filteredValues = values.filter(Boolean);
const lastNodeValue = list.space(lastNode.value)[i];
values[directions.indexOf(lastNode.prop)] = lastNodeValue;
let value = minifyTrbl(values.join(' '));
if (
filteredValues[0] === filteredValues[1] &&
filteredValues[1] === filteredValues[2]
) {
value = filteredValues[0];
}
let refNode = props[props.length - 1];
if (value === lastNodeValue) {
refNode = lastNode;
let valueArray = list.space(lastNode.value);
valueArray.splice(i, 1);
lastNode.value = valueArray.join(' ');
const rulesSet = getRules(props, names);
rulesSet.forEach(rules => {
if (hasAllProps(rules, ...names) && !rules.some(detect)) {
const values = rules.map(node => node ? node.value : null);
const filteredValues = values.filter(Boolean);
const lastNodeValue = list.space(lastNode.value)[i];
values[directions.indexOf(lastNode.prop)] = lastNodeValue;
let value = minifyTrbl(values.join(' '));
if (
filteredValues[0] === filteredValues[1] &&
filteredValues[1] === filteredValues[2]
) {
value = filteredValues[0];
}
let refNode = props[props.length - 1];
if (value === lastNodeValue) {
refNode = lastNode;
let valueArray = list.space(lastNode.value);
valueArray.splice(i, 1);
lastNode.value = valueArray.join(' ');
}
insertCloned(refNode.parent, refNode, {
prop: borderProperty(d),
value,
});
decls = decls.filter(node => !~rules.indexOf(node));
rules.forEach(remove);
}
insertCloned(refNode.parent, refNode, {
prop: borderProperty(d),
value,
});
decls = decls.filter(node => !~rules.indexOf(node));
rules.forEach(remove);
}
});
});
decls = decls.filter(node => node !== lastNode);
}
Expand Down Expand Up @@ -408,7 +411,9 @@ function merge (rule) {
!detect(node) &&
node !== lastNode &&
node.important === lastNode.important &&
node.prop === lastNode.prop);
node.prop === lastNode.prop &&
hasVariable(node) === hasVariable(lastNode)
);

if (duplicates.length) {
if (/hsla|rgba/.test(getColorValue(lastNode))) {
Expand Down
5 changes: 4 additions & 1 deletion packages/postcss-merge-longhand/src/lib/decl/boxBase.js
Expand Up @@ -8,6 +8,7 @@ import mergeRules from '../mergeRules';
import mergeValues from '../mergeValues';
import remove from '../remove';
import trbl from '../trbl';
import hasVariable from '../hasVariable';

export default prop => {
const properties = trbl.map(direction => `${prop}-${direction}`);
Expand All @@ -34,7 +35,9 @@ export default prop => {
!detect(node) &&
node !== lastNode &&
node.important === lastNode.important &&
node.prop === lastNode.prop);
node.prop === lastNode.prop &&
hasVariable(node) === hasVariable(lastNode)
);

duplicates.forEach(remove);
decls = decls.filter(node => node !== lastNode && !~duplicates.indexOf(node));
Expand Down
5 changes: 4 additions & 1 deletion packages/postcss-merge-longhand/src/lib/decl/columns.js
Expand Up @@ -7,6 +7,7 @@ import getValue from '../getValue';
import mergeRules from '../mergeRules';
import insertCloned from '../insertCloned';
import remove from '../remove';
import hasVariable from '../hasVariable';

const properties = ['column-width', 'column-count'];
const auto = 'auto';
Expand Down Expand Up @@ -84,7 +85,9 @@ function cleanup (rule) {
!detect(node) &&
node !== lastNode &&
node.important === lastNode.important &&
node.prop === lastNode.prop);
node.prop === lastNode.prop &&
hasVariable(node) === hasVariable(lastNode)
);

duplicates.forEach(remove);
decls = decls.filter(node => node !== lastNode && !~duplicates.indexOf(node));
Expand Down
33 changes: 30 additions & 3 deletions packages/postcss-merge-longhand/src/lib/getLastNode.js
@@ -1,3 +1,30 @@
export default (rule, prop) => {
return rule.filter(n => n.prop && ~n.prop.indexOf(prop)).pop();
};
function getRulesWithCustomProps (props, properties) {
return properties.map(property =>
props.filter(n => n.prop && ~n.prop.indexOf(property) && n.value && ~n.value.search(/var/i)).pop()
).filter(Boolean);
}

function getRulesWithoutCustomProps (props, properties) {
return properties.map(property =>
props.filter(n => n.prop && ~n.prop.indexOf(property) && n.value && !~n.value.search(/var/i)).pop()
).filter(Boolean);
}

export function ruleContainsCustomPropertiesAndFallbacks (props, properties) {
return getRulesWithCustomProps(props, properties).length === getRulesWithoutCustomProps(props, properties).length;
}

export function getAllRules (props, properties) {
return new Array(
properties.map(property =>
props.filter(n => n.prop && ~n.prop.indexOf(property)).pop()
).filter(Boolean)
);
}

export function getRulesWithAndWithoutCustomProps (props, properties) {
return new Array(
getRulesWithoutCustomProps(props, properties),
getRulesWithCustomProps(props, properties)
);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks we should rename this file to utils.js

8 changes: 4 additions & 4 deletions packages/postcss-merge-longhand/src/lib/getRules.js
@@ -1,7 +1,7 @@
import getLastNode from './getLastNode';
import {ruleContainsCustomPropertiesAndFallbacks, getRulesWithAndWithoutCustomProps, getAllRules} from './getLastNode';

export default function getRules (props, properties) {
return properties.map(property => {
return getLastNode(props, property);
}).filter(Boolean);
return ruleContainsCustomPropertiesAndFallbacks(props, properties) ?
getRulesWithAndWithoutCustomProps(props, properties) :
getAllRules(props, properties);
}
2 changes: 2 additions & 0 deletions packages/postcss-merge-longhand/src/lib/hasVariable.js
@@ -0,0 +1,2 @@
export default node =>
~node.value.search(/var/i);
20 changes: 12 additions & 8 deletions packages/postcss-merge-longhand/src/lib/mergeRules.js
Expand Up @@ -5,15 +5,19 @@ import getRules from './getRules';
export default function mergeRules (rule, properties, callback) {
let decls = getDecls(rule, properties);
while (decls.length) {
const last = decls[decls.length - 1];
let last = decls[decls.length - 1];
const props = decls.filter(node => node.important === last.important);
const rules = getRules(props, properties);

if (hasAllProps(rules, ...properties)) {
if (callback(rules, last, props)) {
decls = decls.filter(node => !~rules.indexOf(node));
}
}
const rulesSet = getRules(props, properties);
rulesSet.forEach((rules, index) => {
if (hasAllProps(rules, ...properties)) {
if (callback(rules, last, props)) {
decls = decls.filter(node => !~rules.indexOf(node));
if (rulesSet[index + 1]) {
last = rulesSet[index + 1][rulesSet[index + 1].length - 1];
}
}
}
});
decls = decls.filter(node => node !== last);
}
}