Skip to content

Commit

Permalink
[FEATURE] serveThemes: Support experimental CSS variables and skeleto…
Browse files Browse the repository at this point in the history
…n build (#278)

See SAP/ui5-builder#393
  • Loading branch information
matz3 authored Feb 19, 2020
1 parent b38956f commit 47d4b55
Show file tree
Hide file tree
Showing 6 changed files with 550 additions and 51 deletions.
8 changes: 4 additions & 4 deletions lib/middleware/serveResources.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const log = require("@ui5/logger").getLogger("server:middleware:serveResources");
const mime = require("mime-types");
const {getMimeInfo} = require("../mime-util");
const replaceStream = require("replacestream");
const etag = require("etag");
const fresh = require("fresh");
Expand Down Expand Up @@ -39,8 +39,6 @@ function createMiddleware({resources}) {
}

const resourcePath = resource.getPath();
const type = mime.lookup(resourcePath) || "application/octet-stream";
const charset = mime.charset(type);
if (rProperties.test(resourcePath)) {
// Special handling for *.properties files escape non ascii characters.
const nonAsciiEscaper = require("@ui5/builder").processors.nonAsciiEscaper;
Expand All @@ -54,8 +52,10 @@ function createMiddleware({resources}) {
}
});
}

const {contentType, charset} = getMimeInfo(resourcePath);
if (!res.getHeader("Content-Type")) {
res.setHeader("Content-Type", type + (charset ? "; charset=" + charset : ""));
res.setHeader("Content-Type", contentType);
}

// Enable ETag caching
Expand Down
104 changes: 57 additions & 47 deletions lib/middleware/serveThemes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const themeBuilder = require("@ui5/builder").processors.themeBuilder;
const fsInterface = require("@ui5/fs").fsInterface;
const {getMimeInfo} = require("../mime-util");
const {basename, dirname} = require("path").posix;
const etag = require("etag");
const fresh = require("fresh");
const parseurl = require("parseurl");
Expand All @@ -10,7 +12,21 @@ function isFresh(req, res) {
});
}

const themeRequest = /^(.*\/)library(?:(\.css)|(-RTL\.css)|(-parameters\.json))$/i;
// List of experimental css variables resources that should activate the "cssVariables" build
const cssVariablesThemeResources = [
"css-variables.source.less",
"css-variables.css",
"library-skeleton.css",
"library-skeleton-RTL.css"
];

// List of resources that should be handled by the middleware
const themeResources = [
"library.css",
"library-RTL.css",
"library-parameters.json",
...cssVariablesThemeResources
];

/**
* Creates and returns the middleware to build themes.
Expand All @@ -27,63 +43,57 @@ function createMiddleware({resources}) {
const builder = new themeBuilder.ThemeBuilder({
fs: fsInterface(resources.all)
});
const buildOptions = {};

return function theme(req, res, next) {
const pathname = parseurl(req).pathname;
/* pathname examples:
/resources/sap/ui/core/themes/sap_belize/library.css
*/
return async function theme(req, res, next) {
try {
const pathname = parseurl(req).pathname;
const filename = basename(pathname);
if (!themeResources.includes(filename)) {
next();
return;
}

/* groups (array index):
1 => theme directory (example: /resources/sap/ui/core/themes/sap_belize/)
2 => .css suffix
3 => -RTL.css suffix
4 => -parameters.json suffix
*/
const themeReq = themeRequest.exec(pathname);
if (!themeReq) {
next();
return;
}
if (cssVariablesThemeResources.includes(filename) && !buildOptions.cssVariables) {
// Activate CSS Variables build the first time a relevant resource is requested
buildOptions.cssVariables = true;
// Clear the cache to ensure that the build is executed again with "cssVariables: true"
builder.clearCache();
}

const sourceLessPath = themeReq[1] + "library.source.less";
resources.all.byPath(sourceLessPath).then((sourceLessResource) => {
const sourceLessPath = dirname(pathname) + "/library.source.less";
const sourceLessResource = await resources.all.byPath(sourceLessPath);
if (!sourceLessResource) { // Not found
next();
return;
}
return builder.build([sourceLessResource]).then(function([css, cssRtl, parameters]) {
let resource;
if (themeReq[2]) { // -> .css
res.setHeader("Content-Type", "text/css");
resource = css;
} else if (themeReq[3]) { // -> -RTL.css
res.setHeader("Content-Type", "text/css");
resource = cssRtl;
} else if (themeReq[4]) { // -parameters.json
res.setHeader("Content-Type", "application/json");
resource = parameters;
} else {
next("Couldn't decide on which theme file to return. This shouldn't happen");
return;
}

return resource.getBuffer().then((content) => {
res.setHeader("ETag", etag(content));
const createdResources = await builder.build([sourceLessResource], buildOptions);
// Pick requested file resource
const resource = createdResources.find((res) => res.getPath().endsWith(filename));
if (!resource) {
next(new Error(`Theme Build did not return requested file "${pathname}"`));
return;
}

const resourcePath = resource.getPath();
const {contentType} = getMimeInfo(resourcePath);
res.setHeader("Content-Type", contentType);

const content = await resource.getBuffer();
res.setHeader("ETag", etag(content));

if (isFresh(req, res)) {
// client has a fresh copy of the resource
res.statusCode = 304;
res.end();
return;
}
if (isFresh(req, res)) {
// client has a fresh copy of the resource
res.statusCode = 304;
res.end();
return;
}

res.end(content.toString());
});
});
}).catch(function(err) {
res.end(content.toString());
} catch (err) {
next(err);
});
}
};
}

Expand Down
11 changes: 11 additions & 0 deletions lib/mime-util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
const mime = require("mime-types");

module.exports.getMimeInfo = function(resourcePath) {
const type = mime.lookup(resourcePath) || "application/octet-stream";
const charset = mime.charset(type);
return {
type,
charset,
contentType: type + (charset ? "; charset=" + charset : "")
};
};
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

63 changes: 63 additions & 0 deletions test/lib/server/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,69 @@ test("Get library-parameters.json from theme middleware (/resources/library/a/th
});
});

test("Get css-variables.source.less from theme middleware (/resources/library/a/themes/base/css-variables.source.less)", (t) => {
return request.get("/resources/library/a/themes/base/css-variables.source.less").then((res) => {
if (res.error) {
t.fail(res.error.text);
}
t.deepEqual(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /less/, "Correct content type");
t.deepEqual(res.text, `@libraryAColor1: #fafad2;
:root {
--libraryAColor1: @libraryAColor1;
}
`, "Correct response");
});
});

test("Get css-variables.css from theme middleware (/resources/library/a/themes/base/css-variables.css)", (t) => {
return request.get("/resources/library/a/themes/base/css-variables.css").then((res) => {
if (res.error) {
t.fail(res.error.text);
}
t.deepEqual(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /css/, "Correct content type");
t.deepEqual(res.text, `:root {
--libraryAColor1: #fafad2;
}
/* Inline theming parameters */
#sap-ui-theme-library\\.a{background-image:url('data:text/plain;utf-8,%7B%22libraryAColor1%22%3A%22%23fafad2%22%7D')}
`, "Correct response");
});
});

test("Get library-skeleton.css from theme middleware (/resources/library/a/themes/base/library-skeleton.css)", (t) => {
return request.get("/resources/library/a/themes/base/library-skeleton.css").then((res) => {
if (res.error) {
t.fail(res.error.text);
}
t.deepEqual(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /css/, "Correct content type");
t.deepEqual(res.text, `.library-a-foo {
color: var(--libraryAColor1);
padding: 1px 2px 3px 4px;
}
`, "Correct response");
});
});

test("Get library-skeleton-RTL.css from theme middleware (/resources/library/a/themes/base/library-skeleton-RTL.css)", (t) => {
return request.get("/resources/library/a/themes/base/library-skeleton-RTL.css").then((res) => {
if (res.error) {
t.fail(res.error.text);
}
t.deepEqual(res.statusCode, 200, "Correct HTTP status code");
t.regex(res.headers["content-type"], /css/, "Correct content type");
t.deepEqual(res.text, `.library-a-foo {
color: var(--libraryAColor1);
padding: 1px 4px 3px 2px;
}
`, "Correct response");
});
});

test("Stop server", (t) => {
const port = 3350;
const request = supertest(`http://localhost:${port}`);
Expand Down
Loading

0 comments on commit 47d4b55

Please sign in to comment.