diff --git a/lib/middleware/serveResources.js b/lib/middleware/serveResources.js index 3c308e83..847a483f 100644 --- a/lib/middleware/serveResources.js +++ b/lib/middleware/serveResources.js @@ -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, { @@ -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(); @@ -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 @@ -90,6 +113,7 @@ function createMiddleware({resources, middlewareUtil}) { stream.pipe(res); } catch (err) { + console.log(err.stack); next(err); } }; diff --git a/test/lib/server/middleware/serveResources.js b/test/lib/server/middleware/serveResources.js index 75ea1f11..a33717f8 100644 --- a/test/lib/server/middleware/serveResources.js +++ b/test/lib/server/middleware/serveResources.js @@ -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(); + }); + }); +});