From afc2fde2a438575296dbdcb92b09d4a52bcc48fb Mon Sep 17 00:00:00 2001 From: Florian Vogt Date: Mon, 20 Nov 2023 16:31:38 +0100 Subject: [PATCH] [FEATURE] manifest.json: Auto-fill supportedLocales JIRA: CPOUI5FOUNDATION-296 --- lib/middleware/serveResources.js | 15 +- test/lib/server/middleware/serveResources.js | 581 ++++++++++++++++++- 2 files changed, 590 insertions(+), 6 deletions(-) diff --git a/lib/middleware/serveResources.js b/lib/middleware/serveResources.js index 6da92b02..6709587b 100644 --- a/lib/middleware/serveResources.js +++ b/lib/middleware/serveResources.js @@ -3,10 +3,11 @@ const log = getLogger("server:middleware:serveResources"); import replaceStream from "replacestream"; import etag from "etag"; import fresh from "fresh"; +import fsInterface from "@ui5/fs/fsInterface"; const rProperties = /\.properties$/i; const rReplaceVersion = /\.(library|js|json)$/i; -const rManifest = /\/manifest.json$/i; +const rManifest = /\/manifest\.json$/i; const rResourcesPrefix = /^\/resources\//i; function isFresh(req, res) { @@ -15,7 +16,6 @@ function isFresh(req, res) { }); } - /** * Creates and returns the middleware to serve application resources. * @@ -49,6 +49,17 @@ function createMiddleware({resources, middlewareUtil}) { next(); return; } + } else if (rManifest.test(resource.getPath()) && resource.getProject()?.getNamespace() ) { + // Special handling for manifest.json file by adding additional content to the served manifest.json + // NOTE: This should only be done for manifest.json files that exist in the sources. + // Files created by generateLibraryManifest (see above) should not be handled in here. + const {default: manifestEnhancer} = await import("@ui5/builder/processors/manifestEnhancer"); + await manifestEnhancer({ + resources: [resource], + // Ensure that only files within the manifest's project are accessible + // Using the "runtime" style to match the style used by the UI5 server + fs: fsInterface(resource.getProject().getReader({style: "runtime"})) + }); } const resourcePath = resource.getPath(); diff --git a/test/lib/server/middleware/serveResources.js b/test/lib/server/middleware/serveResources.js index c3fa822d..9d34269a 100644 --- a/test/lib/server/middleware/serveResources.js +++ b/test/lib/server/middleware/serveResources.js @@ -24,10 +24,13 @@ const writeResource = function(writer, path, size, stringContent, stringEncoding project }); // stub resource functionality in order to be able to get the Resource's content. Otherwise it would be drained. - sinon.stub(resource, "getStream").returns({ - pipe: function() { - } - }); + const getStreamStub = { + pipe() { + return this; + }, + setEncoding() {} + }; + sinon.stub(resource, "getStream").returns(getStreamStub); writer.byPath = sinon.stub(); writer.byPath.withArgs(path).resolves(resource); @@ -762,3 +765,573 @@ test.serial("Missing manifest.json is not generated for non-library projects", a }); }); }); + +test.serial("manifestEnhancer: request manifest.json with auto generated supported locales", async (t) => { + t.plan(4); + + const readerWriter = resourceFactory.createAdapter({virBasePath: "/"}); + + const input = `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "fallbackLocale": "de" + } + } + } + } +}`; + const expected = `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "supportedLocales": [ + "", + "de", + "en" + ] + } + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "fallbackLocale": "de", + "supportedLocales": [ + "", + "de", + "en" + ] + } + } + } + } +}`; + + const project = { + getNamespace: () => "sap.ui.demo.app", + getVersion: () => "1.0.0", + getReader: () => readerWriter + }; + + const resource = await writeResource(readerWriter, "/manifest.json", 1024 * 1024, + input, "utf8", project + ); + const setStringSpy = sinon.spy(resource, "setString"); + + const serveResourcesMiddlewareWithMock = t.context.serveResourcesMiddlewareWithMock = + await esmock.p("../../../../lib/middleware/serveResources", { + "@ui5/fs/fsInterface": sinon.stub().returns({ + readdir(fsPath, callback) { + callback(null, ["i18n_de.properties", "i18n_en.properties", "i18n.properties"]); + } + }) + }); + const middleware = serveResourcesMiddlewareWithMock({ + middlewareUtil: new MiddlewareUtil({graph: "graph", project: "project"}), + resources: { + all: readerWriter + } + }); + + const response = fakeResponse; + + const setHeaderSpy = sinon.spy(response, "setHeader"); + const req = { + url: "/manifest.json", + headers: {} + }; + const next = function(err) { + throw new Error(`Next callback called with error: ${err.stack}`); + }; + + await middleware(req, response, next); + const content = await resource.getString(); + + t.is(content, expected); + t.is(setHeaderSpy.callCount, 2); + t.is(setStringSpy.callCount, 1); + t.is(setHeaderSpy.getCall(0).lastArg, "application/json; charset=UTF-8"); +}); + +test.serial("manifestEnhancer: request manifest.json with auto generated supported locales " + + "(non-root level manifest.json)", +async (t) => { + t.plan(4); + + const readerWriter = resourceFactory.createAdapter({virBasePath: "/"}); + + const input = `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "fallbackLocale": "de" + } + } + } + } +}`; + const expected = `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "supportedLocales": [ + "", + "de", + "en" + ] + } + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "fallbackLocale": "de", + "supportedLocales": [ + "", + "de", + "en" + ] + } + } + } + } +}`; + + const project = { + getNamespace: () => "sap.ui.demo.app", + getVersion: () => "1.0.0", + getReader: () => readerWriter + }; + + const resource = await writeResource(readerWriter, "/customfolder/manifest.json", 1024 * 1024, + input, "utf8", project + ); + const setStringSpy = sinon.spy(resource, "setString"); + + const serveResourcesMiddlewareWithMock = t.context.serveResourcesMiddlewareWithMock = + await esmock.p("../../../../lib/middleware/serveResources", { + "@ui5/fs/fsInterface": sinon.stub().returns({ + readdir(fsPath, callback) { + callback(null, ["i18n_de.properties", "i18n_en.properties", "i18n.properties"]); + } + }) + }); + const middleware = serveResourcesMiddlewareWithMock({ + middlewareUtil: new MiddlewareUtil({graph: "graph", project: "project"}), + resources: { + all: readerWriter + } + }); + + const response = fakeResponse; + + const setHeaderSpy = sinon.spy(response, "setHeader"); + const req = { + url: "/customfolder/manifest.json", + headers: {} + }; + const next = function(err) { + throw new Error(`Next callback called with error: ${err.stack}`); + }; + + await middleware(req, response, next); + const content = await resource.getString(); + + t.is(content, expected); + t.is(setHeaderSpy.callCount, 2); + t.is(setStringSpy.callCount, 1); + t.is(setHeaderSpy.getCall(0).lastArg, "application/json; charset=UTF-8"); +}); + +test.serial("manifestEnhancer: manifest.json with manual defined supported locales", async (t) => { + t.plan(4); + + const readerWriter = resourceFactory.createAdapter({virBasePath: "/"}); + + const input = `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "supportedLocales": ["fr", "de"], + "fallbackLocale": "de" + } + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "supportedLocales": ["fr", "de"], + "fallbackLocale": "de" + } + } + } + } +}`; + const expected = `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application", + "i18n": { + "bundleUrl": "i18n/i18n.properties", + "supportedLocales": ["fr", "de"], + "fallbackLocale": "de" + } + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "supportedLocales": ["fr", "de"], + "fallbackLocale": "de" + } + } + } + } +}`; + + const project = { + getNamespace: () => "sap.ui.demo.app", + getVersion: () => "1.0.0", + getReader: () => readerWriter + }; + + const resource = await writeResource(readerWriter, "/manifest.json", 1024 * 1024, + input, "utf8", project + ); + const setStringSpy = sinon.spy(resource, "setString"); + + const serveResourcesMiddlewareWithMock = t.context.serveResourcesMiddlewareWithMock = + await esmock.p("../../../../lib/middleware/serveResources", { + "@ui5/fs/fsInterface": sinon.stub().returns({ + readdir(fsPath, callback) { + t.fail("fs.readdir should never be called"); + } + }) + }); + const middleware = serveResourcesMiddlewareWithMock({ + middlewareUtil: new MiddlewareUtil({graph: "graph", project: "project"}), + resources: { + all: readerWriter + } + }); + + const response = fakeResponse; + + const setHeaderSpy = sinon.spy(response, "setHeader"); + const req = { + url: "/manifest.json", + headers: {} + }; + const next = function(err) { + throw new Error(`Next callback called with error: ${err.stack}`); + }; + + await middleware(req, response, next); + const content = await resource.getString(); + + t.is(content, expected); + t.is(setHeaderSpy.callCount, 2); + t.is(setStringSpy.callCount, 0); + t.is(setHeaderSpy.getCall(0).lastArg, "application/json; charset=UTF-8"); +}); + +test.serial("manifestEnhancer: no generation of supported locales " + + "if manifest.json version is below 1.21.0", +async (t) => { + t.plan(4); + + const readerWriter = resourceFactory.createAdapter({virBasePath: "/"}); + + const input = `{ + "_version": "1.20.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "fallbackLocale": "de" + } + } + } + } +}`; + const expected = `{ + "_version": "1.20.0", + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "fallbackLocale": "de" + } + } + } + } +}`; + + const project = { + getNamespace: () => "sap.ui.demo.app", + getVersion: () => "1.0.0", + getReader: () => readerWriter + }; + + const resource = await writeResource(readerWriter, "/manifest.json", 1024 * 1024, + input, "utf8", project + ); + const setStringSpy = sinon.spy(resource, "setString"); + + const serveResourcesMiddlewareWithMock = t.context.serveResourcesMiddlewareWithMock = + await esmock.p("../../../../lib/middleware/serveResources", { + "@ui5/fs/fsInterface": sinon.stub().returns({ + readdir(fsPath, callback) { + t.fail("fs.readdir should never be called"); + } + }) + }); + const middleware = serveResourcesMiddlewareWithMock({ + middlewareUtil: new MiddlewareUtil({graph: "graph", project: "project"}), + resources: { + all: readerWriter + } + }); + + const response = fakeResponse; + + const setHeaderSpy = sinon.spy(response, "setHeader"); + const req = { + url: "/manifest.json", + headers: {} + }; + const next = function(err) { + throw new Error(`Next callback called with error: ${err.stack}`); + }; + + await middleware(req, response, next); + const content = await resource.getString(); + + t.is(content, expected); + t.is(setHeaderSpy.callCount, 2); + t.is(setStringSpy.callCount, 0); + t.is(setHeaderSpy.getCall(0).lastArg, "application/json; charset=UTF-8"); +}); + +test.serial("manifestEnhancer: no generation of supported locales " + + "if manifest.json version is not defined", +async (t) => { + t.plan(4); + + const readerWriter = resourceFactory.createAdapter({virBasePath: "/"}); + + const input = `{ + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "fallbackLocale": "de" + } + } + } + } +}`; + const expected = `{ + "sap.app": { + "id": "sap.ui.demo.app", + "type": "application" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "fallbackLocale": "de" + } + } + } + } +}`; + + const project = { + getNamespace: () => "sap.ui.demo.app", + getVersion: () => "1.0.0", + getReader: () => readerWriter + }; + + const resource = await writeResource(readerWriter, "/manifest.json", 1024 * 1024, + input, "utf8", project + ); + const setStringSpy = sinon.spy(resource, "setString"); + + const serveResourcesMiddlewareWithMock = t.context.serveResourcesMiddlewareWithMock = + await esmock.p("../../../../lib/middleware/serveResources", { + "@ui5/fs/fsInterface": sinon.stub().returns({ + readdir(fsPath, callback) { + t.fail("fs.readdir should never be called"); + } + }) + }); + const middleware = serveResourcesMiddlewareWithMock({ + middlewareUtil: new MiddlewareUtil({graph: "graph", project: "project"}), + resources: { + all: readerWriter + } + }); + + const response = fakeResponse; + + const setHeaderSpy = sinon.spy(response, "setHeader"); + const req = { + url: "/manifest.json", + headers: {} + }; + const next = function(err) { + throw new Error(`Next callback called with error: ${err.stack}`); + }; + + await middleware(req, response, next); + const content = await resource.getString(); + + t.is(content, expected); + t.is(setHeaderSpy.callCount, 2); + t.is(setStringSpy.callCount, 0); + t.is(setHeaderSpy.getCall(0).lastArg, "application/json; charset=UTF-8"); +}); + +test.serial("manifestEnhancer: no generation of supported locales for theme libraries", async (t) => { + t.plan(4); + + const readerWriter = resourceFactory.createAdapter({virBasePath: "/"}); + + const input = `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.themelibrary", + "type": "library" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "fallbackLocale": "de" + } + } + } + } +}`; + const expected = `{ + "_version": "1.58.0", + "sap.app": { + "id": "sap.ui.demo.themelibrary", + "type": "library" + }, + "sap.ui5": { + "models": { + "i18n": { + "type": "sap.ui.model.resource.ResourceModel", + "settings": { + "bundleName": "sap.ui.demo.app.i18n.i18n", + "fallbackLocale": "de" + } + } + } + } +}`; + + const project = { + getNamespace: () => null, + getVersion: () => "1.0.0", + getReader: () => readerWriter + }; + + const resource = await writeResource(readerWriter, "/manifest.json", 1024 * 1024, + input, "utf8", project + ); + const setStringSpy = sinon.spy(resource, "setString"); + + const serveResourcesMiddlewareWithMock = t.context.serveResourcesMiddlewareWithMock = + await esmock.p("../../../../lib/middleware/serveResources", { + "@ui5/fs/fsInterface": sinon.stub().returns({ + readdir(fsPath, callback) { + t.fail("fs.readdir should never be called"); + } + }) + }); + const middleware = serveResourcesMiddlewareWithMock({ + middlewareUtil: new MiddlewareUtil({ graph: "graph", project: "project" }), + resources: { + all: readerWriter + } + }); + + const response = fakeResponse; + + const setHeaderSpy = sinon.spy(response, "setHeader"); + const req = { + url: "/manifest.json", + headers: {} + }; + const next = function (err) { + throw new Error(`Next callback called with error: ${err.stack}`); + }; + + await middleware(req, response, next); + const content = await resource.getString(); + + t.is(content, expected); + t.is(setHeaderSpy.callCount, 2); + t.is(setStringSpy.callCount, 0); + t.is(setHeaderSpy.getCall(0).lastArg, "application/json; charset=UTF-8"); +});