Skip to content

[FIX] CSS Variables: Fix variable handling / relative urls #172

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

Merged
merged 13 commits into from
Jun 28, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ Builder.prototype.build = function(options) {
// inject the library name as prefix (e.g. "sap.m" as "_sap_m")
if (options.library && typeof options.library.name === "string") {
const libName = config.libName = options.library.name;
config.libPath = libName.replace(/\./g, "/");
config.prefix = "_" + libName.replace(/\./g, "_") + "_";
}

Expand Down Expand Up @@ -287,10 +288,13 @@ Builder.prototype.build = function(options) {
// generate the skeleton-css and the less-variables
const CSSVariablesCollectorPlugin = require("./plugin/css-variables-collector");
const oCSSVariablesCollector = new CSSVariablesCollectorPlugin(config);
const CSSVariablesUrlResolverPlugin = require("./plugin/css-variables-urlresolver");
const oCSSVariablesUrlResolverPlugin = new CSSVariablesUrlResolverPlugin(config);
result.cssSkeleton = tree.toCSS(Object.assign({}, options.compiler, {
plugins: [oCSSVariablesCollector]
plugins: [oCSSVariablesCollector, oCSSVariablesUrlResolverPlugin]
}));
result.cssVariablesSource = oCSSVariablesCollector.toLessVariables();
const varsOverride = oCSSVariablesUrlResolverPlugin.getVariables();
result.cssVariablesSource = oCSSVariablesCollector.toLessVariables(varsOverride);
if (oRTL) {
const oCSSVariablesCollectorRTL = new CSSVariablesCollectorPlugin(config);
result.cssSkeletonRtl = tree.toCSS(Object.assign({}, options.compiler, {
Expand Down
85 changes: 50 additions & 35 deletions lib/plugin/css-variables-collector.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ const CSSVariablesCollectorPlugin = module.exports = function(config) {
this.ruleStack = [];
this.mixinStack = [];
this.parenStack = [];
this.importStack = [];
};

CSSVariablesCollectorPlugin.prototype = {

// needed to keep the less variable references intact to use this info for the CSS variables references
isPreEvalVisitor: true,

isReplacing: true,

_isInMixinOrParen() {
Expand All @@ -27,27 +28,41 @@ CSSVariablesCollectorPlugin.prototype = {
return this.ruleStack.length > 0 && !this.ruleStack[this.ruleStack.length - 1].variable;
},

_isInGlobalOrBaseImport() {
return this.config.libName !== "sap.ui.core" && this.importStack.filter((importNode) => {
return /\/(?:global|base)\.less$/.test(importNode.importedFilename);
}).length > 0;
_isVarInLibrary({varName, filename}) {
// for libraries we check that the file is within the libraries theme path
// in all other cases with no filename (indicates calculated variables)
// or in case of variables in standalone less files we just include them!
const include = !filename ||
(this.config.libPath ? filename.startsWith(`/resources/${this.config.libPath}/themes/`) : true);
Copy link
Member

Choose a reason for hiding this comment

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

We can't rely on the fact that the path starts with /resources/. In the grunt/connect use-case they just start with /sap/....
The current solution in #168 is to lookup the library namespace within the path. Not a bullet-proof solution in case an absolute real FS path is used, but it will at least support the both main use cases (UI5 Tooling + grunt/connect).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, so more my initial regex approach again!

return include;
},

_isRelevant() {
return !this._isInMixinOrParen() && this._isVarInRule();
},

toLessVariables() {
toLessVariables(varsOverride) {
const vars = {};
Object.keys(this.vars).forEach((key) => {
const override = this.vars[key].updateAfterEval && varsOverride[key] !== undefined;
if (override && process && process.env && process.env.LESS_OPENUI5_CSSVARS_REPORT_OVERRIDE) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

console log only with ENV variable set (to be on the safe side for eventual usage in browser checking process.env to be available first)

Copy link
Member

Choose a reason for hiding this comment

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

For that I think you would need to to a typeof process check, as just using process will result in a ReferenceError.

console.log(`Override variable "${key}" from "${this.vars[key].css}" to "${varsOverride[key].css}"`);
}
vars[key] = {
css: override ? varsOverride[key].css : this.vars[key].css,
export: this.vars[key].export
};
});
let lessVariables = "";
Object.keys(this.vars).forEach((value, index) => {
lessVariables += "@" + value + ": " + this.vars[value].css + ";\n";
Object.keys(vars).forEach((value, index) => {
lessVariables += "@" + value + ": " + vars[value].css + ";\n";
});
Object.keys(this.calcVars).forEach((value, index) => {
lessVariables += "@" + value + ": " + this.calcVars[value].css + ";\n";
});
lessVariables += "\n:root {\n";
Object.keys(this.vars).forEach((value, index) => {
if (this.vars[value].export) {
Object.keys(vars).forEach((value, index) => {
if (vars[value].export) {
lessVariables += "--" + value + ": @" + value + ";\n";
}
});
Expand Down Expand Up @@ -95,7 +110,7 @@ CSSVariablesCollectorPlugin.prototype = {
},

visitCall(node, visitArgs) {
// if variables are used inside rules, generate a new dynamic variable for it!
// if variables are used inside rules, generate a new calculated variable for it!
const isRelevantFunction = typeof less.tree.functions[node.name] === "function" && ["rgba"].indexOf(node.name) === -1;
if (this._isRelevant() && isRelevantFunction) {
const css = this._getCSS(node);
Expand All @@ -109,7 +124,9 @@ CSSVariablesCollectorPlugin.prototype = {
}
this.calcVars[newName] = {
css: css,
export: !this._isInGlobalOrBaseImport()
export: this._isVarInLibrary({
varName: newName
})
};
return new less.tree.Call("var", [new less.tree.Anonymous("--" + newName, node.index, node.currentFileInfo, node.mapLines)]);
}
Expand All @@ -133,6 +150,20 @@ CSSVariablesCollectorPlugin.prototype = {
},

visitRule(node, visitArgs) {
// check rule for being a variable declaration
const isVarDeclaration = typeof node.name === "string" && node.name.startsWith("@");
if (!this._isInMixinOrParen() && isVarDeclaration) {
// add the variable declaration to the list of vars
const varName = node.name.substr(1);
const isVarInLib = this._isVarInLibrary({
varName,
filename: node.currentFileInfo.filename
});
this.vars[varName] = {
css: this._getCSS(node.value),
export: isVarInLib
};
}
// store the rule context for the call variable extraction
this.ruleStack.push(node);
return node;
Expand Down Expand Up @@ -168,29 +199,13 @@ CSSVariablesCollectorPlugin.prototype = {
return node;
},

visitImport(node, visitArgs) {
// store the import context
this.importStack.push(node);
return node;
},

visitImportOut(node) {
// remove import context
this.importStack.pop();
return node;
},

visitRuleset(node, visitArgs) {
node.rules.forEach((value) => {
const isVarDeclaration = value instanceof less.tree.Rule && typeof value.name === "string" && value.name.startsWith("@");
if (!this._isInMixinOrParen() && isVarDeclaration) {
// add the variable declaration to the list of vars
this.vars[value.name.substr(1)] = {
css: this._getCSS(value.value),
export: !this._isInGlobalOrBaseImport()
};
}
});
visitUrl(node, visitArgs) {
// we mark the less variables which should be updated after eval
Copy link
Member

Choose a reason for hiding this comment

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

My expectation was actually the other way round.
To use the "override" values (from the new preVisitor) in general, and only use the current ones for the variable references (which was the main reason for enabling the preEvalVisitor mode).
Otherwise I'd expect that also some other values might differ unexpectedly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There are issues within the isPreEval phase where less variables with the value none also have the URL content - so doing it this way is the best to determine properly the variables and apply only the needed later! All other options require much more code!

Copy link
Member

Choose a reason for hiding this comment

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

Got it. Would be great to have those specialities documented somewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It is described in my comments

// => strangewise less variables with "none" values are also urls
// after the less variables have been evaluated
if (this.ruleStack.length > 0 && this.ruleStack[0].variable) {
this.vars[this.ruleStack[0].name.substr(1)].updateAfterEval = true;
}
return node;
}

Expand Down
48 changes: 48 additions & 0 deletions lib/plugin/css-variables-urlresolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use strict";

const less = require("../thirdparty/less");

const CSSVariablesUrlResolverPlugin = module.exports = function(config) {
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need another plugin? Can't we just re-use what the variable-collector plugin provides?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because of running it at a later point of time! isPreEvalVisitor vs. isPreVisitor - otherwise we have to write ugly if conditions in the coding

Copy link
Member

Choose a reason for hiding this comment

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

Can you give me some more details here? VariableCollector also uses isPreVisitor: true, so I would expect it to work almost the same as this plugin.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The logic for the detection of the URLs is pretty simple and it need to run in the PreVisitor phase. The CSS Var Collector is running in the PreEvalVisitor phase to not resolve the vars. I don't want to run the full enchilada of the code of the CSS Var Collector again in the PreVisitor phase as this is unnecessary and IMO the code of the URL Collector is pretty trivial.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ahh - got your point - I am checking the url-collector right now

Copy link
Member

Choose a reason for hiding this comment

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

There's no need to run anything again.
The variables are already collected and available:

result.variables = oVariableCollector.getVariables(Object.keys(mFileMappings[filename] || {}));

It's a different tree.toCSS run, but both are based on the exact same input, so I wouldn't expect any differences in the values. It's the list we already use for the inline theming parameters, so their values are exactly what we expect. That's why I don't expect the need for another plugin.

Copy link
Member

Choose a reason for hiding this comment

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

url-collector is not what I meant, and I think it doesn't really fit for this purpose.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

variable-collector seems to be sufficient!

this.config = config;
// eslint-disable-next-line new-cap
this.native = new less.tree.visitor(this);
this.vars = {};
this.ruleStack = [];
};

CSSVariablesUrlResolverPlugin.prototype = {

isPreVisitor: true,

run(root) {
return this.native.visit(root);
},

getVariables() {
return this.vars;
},

visitRule(node, visitArgs) {
// store the rule context for the call variable extraction
this.ruleStack.push(node);
return node;
},

visitRuleOut(node) {
// remove rule context
this.ruleStack.pop();
return node;
},

visitUrl(node, visitArgs) {
// for top-level variables we need the resolved urls
if (this.ruleStack.length > 0 && this.ruleStack[0].variable) {
const varName = this.ruleStack[0].name.substr(1);
this.vars[varName] = {
css: node.toCSS()
};
}
return node;
}

};
2 changes: 1 addition & 1 deletion test/expected/complex/test-cssvars-variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@
--link-color-hover: #3071a9;
--other-var: 0 0 0.25rem 0 rgba(0, 0, 0, 0.15), inset 0 -0.0625rem 0 0 #428bca;
--link-color-active: var(--link-color);
--a: 100%;
--var: var(--a);
--a: 9%;
}