From df8c39b3f5a69086662b6f92c32e1364c1a93903 Mon Sep 17 00:00:00 2001 From: Peter Muessig Date: Mon, 10 Feb 2020 18:24:43 +0100 Subject: [PATCH] [FEATURE] Add experimental CSS variables and skeleton build (#393) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This enables the usage via the processor/task APIs. By default the CSS variables files are not built. Co-authored-by: Matthias Oßwald <1410947+matz3@users.noreply.github.com> --- lib/processors/themeBuilder.js | 31 +++++++++-- lib/tasks/buildThemes.js | 4 +- test/lib/processors/themeBuilder.js | 81 +++++++++++++++++++++++++---- test/lib/tasks/buildThemes.js | 72 ++++++++++++++++++++++++- 4 files changed, 170 insertions(+), 18 deletions(-) diff --git a/lib/processors/themeBuilder.js b/lib/processors/themeBuilder.js index 5df68ab40..d8391338f 100644 --- a/lib/processors/themeBuilder.js +++ b/lib/processors/themeBuilder.js @@ -30,9 +30,10 @@ class ThemeBuilder { * @param {module:@ui5/fs.Resource[]} resources Library files * @param {Object} [options] Build options * @param {boolean} [options.compress=false] Compress build output (CSS / JSON) + * @param {boolean} [options.cssVariables=false] Generates the CSS variables (css-variables.css, css-variables.source.less) and the skeleton for a theme (library-skeleton.css, [library-skeleton-RTL.css]) * @returns {Promise} Resolving with array of created files */ - build(resources, {compress = false} = {}) { + build(resources, {compress = false, cssVariables = false} = {}) { const files = []; const compile = (resource) => { @@ -51,7 +52,8 @@ class ThemeBuilder { }, compiler: { compress - } + }, + cssVariables }).then((result) => { const themeDir = path.dirname(resource.getPath()); @@ -71,6 +73,27 @@ class ThemeBuilder { }); files.push(libCss, libCssRtl, libParams); + + if (cssVariables) { + const libCssVarsSource = new Resource({ + path: themeDir + "/css-variables.source.less", + string: result.cssVariablesSource + }); + const libCssVars = new Resource({ + path: themeDir + "/css-variables.css", + string: result.cssVariables + }); + const libCssSkel = new Resource({ + path: themeDir + "/library-skeleton.css", + string: result.cssSkeleton + }); + const libCssSkelRtl = new Resource({ + path: themeDir + "/library-skeleton-RTL.css", + string: result.cssSkeletonRtl + }); + + files.push(libCssVarsSource, libCssVars, libCssSkel, libCssSkelRtl); + } }); }; @@ -100,12 +123,12 @@ class ThemeBuilder { * @param {fs|module:@ui5/fs.fsInterface} parameters.fs Node fs or custom [fs interface]{@link module:resources/module:@ui5/fs.fsInterface} * @param {Object} [parameters.options] Options * @param {Object} [parameters.options.compress=false] Compress build output (CSS / JSON) + * @param {boolean} [parameters.options.cssVariables=false] Generates the CSS variables (css-variables.css, css-variables.source.less) and the skeleton for a theme (library-skeleton.css, [library-skeleton-RTL.css]) * @returns {Promise} Promise resolving with theme resources */ module.exports = ({resources, fs, options}) => { const themeBuilder = new ThemeBuilder({fs}); - const compress = options && options.compress; - return themeBuilder.build(resources, {compress}).then((files) => { + return themeBuilder.build(resources, options).then((files) => { themeBuilder.clearCache(); return files; }); diff --git a/lib/tasks/buildThemes.js b/lib/tasks/buildThemes.js index a8a80b987..9e30b7f31 100644 --- a/lib/tasks/buildThemes.js +++ b/lib/tasks/buildThemes.js @@ -15,6 +15,7 @@ const fsInterface = require("@ui5/fs").fsInterface; * @param {string} parameters.options.inputPattern Search pattern for *.less files to be built * @param {string} [parameters.options.librariesPattern] Search pattern for .library files * @param {boolean} [parameters.options.compress=true] + * @param {boolean} [parameters.options.cssVariables=false] * @returns {Promise} Promise resolving with undefined once data has been written */ module.exports = function({workspace, dependencies, options}) { @@ -60,7 +61,8 @@ module.exports = function({workspace, dependencies, options}) { resources, fs: fsInterface(combo), options: { - compress + compress, + cssVariables: !!options.cssVariables } }); }).then((processedResources) => { diff --git a/test/lib/processors/themeBuilder.js b/test/lib/processors/themeBuilder.js index c20ef1a34..eb7f59c07 100644 --- a/test/lib/processors/themeBuilder.js +++ b/test/lib/processors/themeBuilder.js @@ -38,50 +38,80 @@ function prepareResources({library} = {}) { }; } -function getExpectedResults({compress, library}) { - let css; let cssRtl; let json; +function getExpectedResults({compress, library, cssVariables}) { + const result = {}; if (compress) { - css = + result.css = `.someClass{color:#000;padding:1px 2px 3px 4px}`; - cssRtl = + result.cssRtl = `.someClass{color:#000;padding:1px 4px 3px 2px}`; - json = `{"someColor":"#000"}`; + result.json = `{"someColor":"#000"}`; } else { - css = + result.css = `.someClass { color: #000000; padding: 1px 2px 3px 4px; } `; - cssRtl = + result.cssRtl = `.someClass { color: #000000; padding: 1px 4px 3px 2px; } `; - json = + result.json = `{ "someColor": "#000000" }`; } if (library !== false) { - css += + result.css += ` /* Inline theming parameters */ #sap-ui-theme-sap\\.ui\\.foo{background-image:url('data:text/plain;utf-8,%7B%22someColor%22%3A%22%23${compress ? "000" : "000000"}%22%7D')} `; - cssRtl += + result.cssRtl += ` /* Inline theming parameters */ #sap-ui-theme-sap\\.ui\\.foo{background-image:url('data:text/plain;utf-8,%7B%22someColor%22%3A%22%23${compress ? "000" : "000000"}%22%7D')} `; } - return {css, cssRtl, json}; + if (cssVariables) { + result.cssVariablesSource = +`@someColor: #000000; + +:root { +--someColor: @someColor; +} +`; + result.cssVariables = +`:root { + --someColor: #000000; +} + +/* Inline theming parameters */ +#sap-ui-theme-sap\\.ui\\.foo{background-image:url('data:text/plain;utf-8,%7B%22someColor%22%3A%22%23000000%22%7D')} +`; + result.cssSkeleton = +`.someClass { + color: var(--someColor); + padding: 1px 2px 3px 4px; +} +`; + result.cssSkeletonRtl = +`.someClass { + color: var(--someColor); + padding: 1px 4px 3px 2px; +} +`; + } + + return result; } test("Processor: Builds a less file (default options)", async (t) => { @@ -206,3 +236,32 @@ test("ThemeBuilder: Builds a less file (no library)", async (t) => { t.is(await cssRtlResource.getString(), expected.cssRtl, "Right-to-left CSS should be correct"); t.is(await jsonResource.getString(), expected.json, "JSON should be correct"); }); + +test("Processor: Builds a less file (cssVariables = true)", async (t) => { + const {resource, memoryAdapter} = prepareResources(); + + const [ + cssResource, + cssRtlResource, + jsonResource, + cssVariablesSourceResource, + cssVariablesResource, + cssSkeletonResource, + cssSkeletonRtlResource + ] = await themeBuilderProcessor({ + resources: [resource], + fs: fsInterface(memoryAdapter), + options: { + cssVariables: true + } + }); + + const expected = getExpectedResults({cssVariables: true}); + t.is(await cssResource.getString(), expected.css, "CSS should be correct"); + t.is(await cssRtlResource.getString(), expected.cssRtl, "Right-to-left CSS should be correct"); + t.is(await jsonResource.getString(), expected.json, "JSON should be correct"); + t.is(await cssVariablesSourceResource.getString(), expected.cssVariablesSource, "CSS Variables source should be correct"); + t.is(await cssVariablesResource.getString(), expected.cssVariables, "CSS Variables should be correct"); + t.is(await cssSkeletonResource.getString(), expected.cssSkeleton, "Skeleton CSS should be correct"); + t.is(await cssSkeletonRtlResource.getString(), expected.cssSkeletonRtl, "Right-to-left skeleton CSS should be correct"); +}); diff --git a/test/lib/tasks/buildThemes.js b/test/lib/tasks/buildThemes.js index 166fc20e3..8035e7c41 100644 --- a/test/lib/tasks/buildThemes.js +++ b/test/lib/tasks/buildThemes.js @@ -62,7 +62,8 @@ test.serial("buildThemes", async (t) => { resources: [lessResource], fs: {}, options: { - compress: true // default + compress: true, // default + cssVariables: false // default } }, "Processor should be called with expected arguments"); @@ -116,7 +117,8 @@ test.serial("buildThemes (compress = false)", async (t) => { resources: [lessResource], fs: {}, options: { - compress: false + compress: false, + cssVariables: false } }, "Processor should be called with expected arguments"); @@ -126,3 +128,69 @@ test.serial("buildThemes (compress = false)", async (t) => { t.true(workspace.write.calledWithExactly(cssRtlResource)); t.true(workspace.write.calledWithExactly(jsonParametersResource)); }); + +test.serial("buildThemes (cssVariables = true)", async (t) => { + t.plan(10); + + const lessResource = {}; + + const workspace = { + byGlob: async (globPattern) => { + if (globPattern === "/resources/test/library.source.less") { + return [lessResource]; + } else { + return []; + } + }, + write: sinon.stub() + }; + + const cssResource = {}; + const cssRtlResource = {}; + const jsonParametersResource = {}; + const cssVariablesSourceResource = {}; + const cssVariablesResource = {}; + const cssSkeletonResource = {}; + const cssSkeletonRtlResource = {}; + + t.context.themeBuilderStub.returns([ + cssResource, + cssRtlResource, + jsonParametersResource, + cssVariablesSourceResource, + cssVariablesResource, + cssSkeletonResource, + cssSkeletonRtlResource + ]); + + await buildThemes({ + workspace, + options: { + projectName: "sap.ui.demo.app", + inputPattern: "/resources/test/library.source.less", + cssVariables: true + } + }); + + t.deepEqual(t.context.themeBuilderStub.callCount, 1, + "Processor should be called once"); + + t.deepEqual(t.context.themeBuilderStub.getCall(0).args[0], { + resources: [lessResource], + fs: {}, + options: { + compress: true, + cssVariables: true + } + }, "Processor should be called with expected arguments"); + + t.deepEqual(workspace.write.callCount, 7, + "workspace.write should be called 7 times"); + t.true(workspace.write.calledWithExactly(cssResource)); + t.true(workspace.write.calledWithExactly(cssRtlResource)); + t.true(workspace.write.calledWithExactly(jsonParametersResource)); + t.true(workspace.write.calledWithExactly(cssVariablesSourceResource)); + t.true(workspace.write.calledWithExactly(cssVariablesResource)); + t.true(workspace.write.calledWithExactly(cssSkeletonResource)); + t.true(workspace.write.calledWithExactly(cssSkeletonRtlResource)); +});