Skip to content

Commit

Permalink
[INTERNAL] serveThemes: Support new files / MIME refactoring
Browse files Browse the repository at this point in the history
  • Loading branch information
matz3 committed Feb 11, 2020
1 parent 94670cc commit 00ce503
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 94 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
51 changes: 24 additions & 27 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,16 @@ function isFresh(req, res) {
});
}

const themeRequest = /^(.*\/)library(?:(\.css)|(-RTL\.css)|(-parameters\.json))$/i;
// List of resources that should be handled by the middleware
const themeResources = [
"library.css",
"library-RTL.css",
"library-parameters.json",
"css-variables.source.less",
"css-variables.css",
"library-skeleton.css",
"library-skeleton-RTL.css"
];

/**
* Creates and returns the middleware to build themes.
Expand All @@ -30,44 +41,30 @@ function createMiddleware({resources}) {

return function theme(req, res, next) {
const pathname = parseurl(req).pathname;
/* pathname examples:
/resources/sap/ui/core/themes/sap_belize/library.css
*/

/* 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) {
const filename = basename(pathname);
if (!themeResources.includes(filename)) {
next();
return;
}

const sourceLessPath = themeReq[1] + "library.source.less";
const sourceLessPath = dirname(pathname) + "/library.source.less";
resources.all.byPath(sourceLessPath).then((sourceLessResource) => {
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 builder.build([sourceLessResource]).then(function(createdResources) {
// Pick requested file resource
const resource = createdResources.find((res) => res.getPath().endsWith(filename));
if (!resource) {
next(new Error(`Theme Build did not return request file "${pathname}"`));
return;
}

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

return resource.getBuffer().then((content) => {
res.setHeader("ETag", etag(content));

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 : "")
};
};
148 changes: 85 additions & 63 deletions test/lib/server/middleware/serveThemes.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,24 +22,34 @@ const createResources = function() {

// Default result
"library.css": {
getBuffer: sinon.stub().resolves("/* library.css */")
getBuffer: sinon.stub().resolves("/* library.css */"),
getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/library.css")
},
"library-RTL.css": {
getBuffer: sinon.stub().resolves("/* library-RTL.css */")
getBuffer: sinon.stub().resolves("/* library-RTL.css */"),
getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/library-RTL.css")
},
"library-parameters.json": {
getBuffer: sinon.stub().resolves(`{ "parameters":"json" }`)
getBuffer: sinon.stub().resolves("/* library-parameters.json */"),
getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/library-parameters.json")
},

// CSS Variables result
"css-variables.source.less": {
getBuffer: sinon.stub().resolves(`/* css-variables.source.less */`),
getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/css-variables.source.less")
},
"css-variables.css": {
getBuffer: sinon.stub().resolves(`/* css-variables.css */`),
getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/css-variables.css")
},
"library-skeleton.css": {
getBuffer: sinon.stub().resolves(`/* library-skeleton.css */`)
getBuffer: sinon.stub().resolves(`/* library-skeleton.css */`),
getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/library-skeleton.css")
},
"library-skeleton-RTL.css": {
getBuffer: sinon.stub().resolves(`/* library-skeleton-RTL.css */`)
},
"css-variables.css": {
getBuffer: sinon.stub().resolves(`/* css-variables.css */`)
getBuffer: sinon.stub().resolves(`/* library-skeleton-RTL.css */`),
getPath: sinon.stub().returns("/resources/sap/ui/test/themes/base/library-skeleton-RTL.css")
}
};
};
Expand All @@ -50,7 +60,11 @@ const stubThemeBuild = function(resources) {
build.withArgs([resources["library.source.less"]]).resolves([
resources["library.css"],
resources["library-RTL.css"],
resources["library-parameters.json"]
resources["library-parameters.json"],
resources["css-variables.source.less"],
resources["css-variables.css"],
resources["library-skeleton.css"],
resources["library-skeleton-RTL.css"]
]);
};

Expand All @@ -68,13 +82,7 @@ const createMiddleware = function() {
};
};

test.afterEach.always((t) => {
sinon.restore();
mock.stopAll();
mock.reRequire("../../../../lib/middleware/serveThemes");
});

test.serial.cb("Serving library.css", (t) => {
const verifyThemeRequest = function(t, filename) {
const resources = createResources();

stubThemeBuild(resources);
Expand All @@ -84,75 +92,63 @@ test.serial.cb("Serving library.css", (t) => {
.resolves(resources["library.source.less"]);

const req = {
url: "/resources/sap/ui/test/themes/base/library.css",
url: "/resources/sap/ui/test/themes/base/" + filename,
headers: {}
};

const res = {
setHeader: sinon.stub(),
getHeader: sinon.stub(),
end: function(responseText) {
t.is(responseText, "/* library.css */");
t.true(res.setHeader.calledWith("Content-Type", "text/css"));
t.is(responseText, `/* ${filename} */`);
if (filename.endsWith(".css")) {
t.deepEqual(res.setHeader.getCall(0).args, ["Content-Type", "text/css; charset=UTF-8"]);
} else if (filename.endsWith(".less")) {
t.deepEqual(res.setHeader.getCall(0).args, ["Content-Type", "text/less; charset=UTF-8"]);
} else if (filename.endsWith(".json")) {
t.deepEqual(res.setHeader.getCall(0).args, ["Content-Type", "application/json; charset=UTF-8"]);
} else {
t.fail("Invalid file extension provided to 'verifyThemeRequest'");
}
t.end();
}
};

middleware(req, res, failOnNext(t));
});

test.serial.cb("Serving library-RTL.css", (t) => {
const resources = createResources();

stubThemeBuild(resources);

const {middleware, byPath} = createMiddleware();
byPath.withArgs("/resources/sap/ui/test/themes/base/library.source.less")
.resolves(resources["library.source.less"]);
};

const req = {
url: "/resources/sap/ui/test/themes/base/library-RTL.css",
headers: {}
};
test.afterEach.always((t) => {
sinon.restore();
mock.stopAll();
mock.reRequire("../../../../lib/middleware/serveThemes");
});

const res = {
setHeader: sinon.stub(),
getHeader: sinon.stub(),
end: function(responseText) {
t.is(responseText, "/* library-RTL.css */");
t.true(res.setHeader.calledWith("Content-Type", "text/css"));
t.end();
}
};
test.serial.cb("Serving library.css", (t) => {
verifyThemeRequest(t, "library.css");
});

middleware(req, res, failOnNext(t));
test.serial.cb("Serving library-RTL.css", (t) => {
verifyThemeRequest(t, "library-RTL.css");
});

test.serial.cb("Serving library-parameters.json", (t) => {
const resources = createResources();

stubThemeBuild(resources);
verifyThemeRequest(t, "library-parameters.json");
});

const {middleware, byPath} = createMiddleware();
byPath.withArgs("/resources/sap/ui/test/themes/base/library.source.less")
.resolves(resources["library.source.less"]);
test.serial.cb("Serving css-variables.source.less", (t) => {
verifyThemeRequest(t, "css-variables.source.less");
});

const req = {
url: "/resources/sap/ui/test/themes/base/library-parameters.json",
headers: {}
};
test.serial.cb("Serving css-variables.css", (t) => {
verifyThemeRequest(t, "css-variables.css");
});

const res = {
setHeader: sinon.stub(),
getHeader: sinon.stub(),
end: function(responseText) {
t.is(responseText, `{ "parameters":"json" }`);
t.true(res.setHeader.calledWith("Content-Type", "application/json"));
t.end();
}
};
test.serial.cb("Serving library-skeleton.css", (t) => {
verifyThemeRequest(t, "library-skeleton.css");
});

middleware(req, res, failOnNext(t));
test.serial.cb("Serving library-skeleton-RTL.css", (t) => {
verifyThemeRequest(t, "library-skeleton-RTL.css");
});

test.serial.cb("Do not handle non-theme requests", (t) => {
Expand Down Expand Up @@ -236,3 +232,29 @@ test.serial.cb("Only send 304 response in case the client has cached the respons

middleware(req, res, failOnNext(t));
});

// This could only happen when the theme build processor does not return an expected resource
test.serial.cb("Error handling: Request resource that ThemeBuild doesn't return", (t) => {
const resources = createResources();

// Adopt path of library.css so that it can't be found from the theme build results
resources["library.css"].getPath.returns("/foo.js");

stubThemeBuild(resources);

const {middleware, byPath} = createMiddleware();
byPath.withArgs("/resources/sap/ui/test/themes/base/library.source.less")
.resolves(resources["library.source.less"]);

const req = {
url: "/resources/sap/ui/test/themes/base/library.css",
headers: {}
};

const res = {};

middleware(req, res, function(err) {
t.is(err.message, `Theme Build did not return request file "/resources/sap/ui/test/themes/base/library.css"`);
t.end();
});
});

0 comments on commit 00ce503

Please sign in to comment.