Skip to content

Commit

Permalink
[FEATURE] serveResources: Dynamically generate missing library manife…
Browse files Browse the repository at this point in the history
…st.json

UI5 libraries (especially framework libraries) often lack a
manifest.json in their source. However some UI5 runtime API requires a
manifest.json to function properly. For example to locate i18n resources
at a non-default location.

With this change, the serveResources middleware will attempt to generate
missing manifest.json files on-the-fly. This mimics the behavior of the
ui5-builder's generateLibraryManifest task and the result is almost
identical.

JIRA: CPOUI5FOUNDATION-688
  • Loading branch information
RandomByte committed Apr 24, 2024
1 parent 1c0e71d commit d31f2c5
Show file tree
Hide file tree
Showing 2 changed files with 203 additions and 6 deletions.
36 changes: 30 additions & 6 deletions lib/middleware/serveResources.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import fresh from "fresh";

const rProperties = /\.properties$/i;
const rReplaceVersion = /\.(library|js|json)$/i;
const rManifest = /\/manifest.json$/i;
const rResourcesPrefix = /^\/resources/i;

function isFresh(req, res) {
return fresh(req.headers, {
Expand All @@ -20,18 +22,33 @@ function isFresh(req, res) {
* @module @ui5/server/middleware/serveResources
* @param {object} parameters Parameters
* @param {@ui5/server/internal/MiddlewareManager.middlewareResources} parameters.resources Parameters
* @param {object} parameters.middlewareUtil Specification version dependent interface to a
* [MiddlewareUtil]{@link @ui5/server/middleware/MiddlewareUtil} instance
* @param {object} parameters.middlewareUtil [MiddlewareUtil]{@link @ui5/server/middleware/MiddlewareUtil} instance
* @returns {Function} Returns a server middleware closure.
*/
function createMiddleware({resources, middlewareUtil}) {
return async function serveResources(req, res, next) {
try {
const pathname = middlewareUtil.getPathname(req);
const resource = await resources.all.byPath(pathname);
let resource = await resources.all.byPath(pathname);
if (!resource) { // Not found
next();
return;
if (!rManifest.test(pathname) || !rResourcesPrefix.test(pathname)) {
next();
return;
}
log.verbose(`Could not find manifest.json for ${pathname}. ` +
`Checking for .library file to generate manifest.json from.`);
const {default: generateLibraryManifest} = await import("./helper/generateLibraryManifest.js");
// Attempt to find a .library file, which is required for generating a manifest.json
const dotLibraryPath = pathname.replace(rManifest, "/.library");
const dotLibraryResource = await resources.all.byPath(dotLibraryPath);
if (!dotLibraryResource) {
log.verbose(
`Could not find a .library to generate manifest.json from at ${dotLibraryPath}. ` +
`This might indicate that the project is not a library project.`);
next();
return;
}
resource = await generateLibraryManifest(middlewareUtil, dotLibraryResource);
}

const resourcePath = resource.getPath();
Expand Down Expand Up @@ -64,7 +81,13 @@ function createMiddleware({resources, middlewareUtil}) {
}

// Enable ETag caching
res.setHeader("ETag", etag(resource.getStatInfo()));
const statInfo = resource.getStatInfo();
if (statInfo?.size !== undefined) {
res.setHeader("ETag", etag(statInfo));
} else {
// Fallback to buffer if stats are not available or insufficient
res.setHeader("ETag", etag(await resource.getBuffer()));
}

if (isFresh(req, res)) {
// client has a fresh copy of the resource
Expand All @@ -90,6 +113,7 @@ function createMiddleware({resources, middlewareUtil}) {

stream.pipe(res);
} catch (err) {
console.log(err.stack);
next(err);
}
};
Expand Down
173 changes: 173 additions & 0 deletions test/lib/server/middleware/serveResources.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,3 +525,176 @@ test.serial("Check if utf8 characters are correctly processed in version replace
});
});
});

test.serial("Missing manifest.json is generated", async (t) => {
// For projects not extending type "ComponentProject" the method "getPropertiesFileSourceEncoding" is not available
const project = {
getName: () => "library",
getNamespace: () => "library",
getVersion: () => "1.0.0",
getSpecVersion: () => {
return {
toString: () => "3.0",
lte: () => false,
};
}
};

const readerWriter = resourceFactory.createAdapter({virBasePath: "/", project});

project.getReader = () => readerWriter;

const dotLibraryMock = await writeResource(readerWriter, "/resources/foo/.library", 1024 * 1024,
`dot library content`, project);

const manifestMock = resourceFactory.createResource({
path: "/resources/foo/manifest.json",
string: "mocked manifest.json ${version}",
project,
});

const generateLibraryManifestHelperStub = sinon.stub().resolves(manifestMock);
const serveResourcesMiddlewareWithMock = t.context.serveResourcesMiddlewareWithMock =
await esmock.p("../../../../lib/middleware/serveResources", {
"../../../../lib/middleware/helper/generateLibraryManifest.js": generateLibraryManifestHelperStub
});

const middleware = serveResourcesMiddlewareWithMock({
middlewareUtil: new MiddlewareUtil({
graph: {
getProject: () => project
},
project: "project"
}),
resources: {
all: readerWriter
}
});

const req = {
url: "/resources/foo/manifest.json",
headers: {}
};
const res = new Writable();
const buffers = [];
res.setHeader = sinon.stub();
res.getHeader = sinon.stub();
res._write = function(chunk, encoding, callback) {
buffers.push(chunk);
callback();
};
const next = function(err) {
throw new Error(`Next callback called with error: ${err.message}`);
};

const pipeEnd = new Promise((resolve) => res.end = resolve);
await middleware(req, res, next);
await pipeEnd;

t.is(Buffer.concat(buffers).toString(), "mocked manifest.json 1.0.0");
t.is(res.setHeader.callCount, 2);
t.is(res.setHeader.getCall(0).lastArg, "application/json; charset=UTF-8");
t.is(generateLibraryManifestHelperStub.callCount, 1, "generateLibraryManifest helper got called once");
t.is(generateLibraryManifestHelperStub.getCall(0).args[1], dotLibraryMock,
"generateLibraryManifest helper got called with expected argument");
});

test.serial("Missing manifest.json is not generated with missing .library", async (t) => {
// For projects not extending type "ComponentProject" the method "getPropertiesFileSourceEncoding" is not available
const project = {
getName: () => "library",
getNamespace: () => "library",
getVersion: () => "1.0.0",
getSpecVersion: () => {
return {
toString: () => "3.0",
lte: () => false,
};
}
};

const readerWriter = resourceFactory.createAdapter({virBasePath: "/", project});

const generateLibraryManifestHelperStub = sinon.stub().resolves();
const serveResourcesMiddlewareWithMock = t.context.serveResourcesMiddlewareWithMock =
await esmock.p("../../../../lib/middleware/serveResources", {
"../../../../lib/middleware/helper/generateLibraryManifest.js": generateLibraryManifestHelperStub
});

const middleware = serveResourcesMiddlewareWithMock({
middlewareUtil: new MiddlewareUtil({
graph: {
getProject: () => project
},
project: "project"
}),
resources: {
all: readerWriter
}
});

const req = {
url: "/resources/foo/manifest.json",
headers: {}
};

return new Promise((resolve, reject) => {
middleware(req, undefined, function(err) {
if (err) {
throw new Error(`Next callback called with error: ${err.message}`);
}
t.is(generateLibraryManifestHelperStub.callCount, 0, "generateLibraryManifest helper never got called");
resolve();
});
});
});

test.serial("Missing manifest.json is not generated for request outside /resources", async (t) => {
// For projects not extending type "ComponentProject" the method "getPropertiesFileSourceEncoding" is not available
const project = {
getName: () => "library",
getNamespace: () => "library",
getVersion: () => "1.0.0",
getSpecVersion: () => {
return {
toString: () => "3.0",
lte: () => false,
};
}
};

const readerWriter = resourceFactory.createAdapter({virBasePath: "/"});

const generateLibraryManifestHelperStub = sinon.stub().resolves();
const serveResourcesMiddlewareWithMock = t.context.serveResourcesMiddlewareWithMock =
await esmock.p("../../../../lib/middleware/serveResources", {
"../../../../lib/middleware/helper/generateLibraryManifest.js": generateLibraryManifestHelperStub
});

const middleware = serveResourcesMiddlewareWithMock({
middlewareUtil: new MiddlewareUtil({
graph: {
getProject: () => project
},
project: "project"
}),
resources: {
all: readerWriter
}
});

const req = {
url: "/manifest.json",
headers: {}
};

return new Promise((resolve, reject) => {
middleware(req, undefined, function(err) {
if (err) {
throw new Error(`Next callback called with error: ${err.message}`);
}
t.is(generateLibraryManifestHelperStub.callCount, 0, "generateLibraryManifest helper never got called");
resolve();
});
});
});

0 comments on commit d31f2c5

Please sign in to comment.