From 6bae28267c448666c0302be8d7fcafe0a6e78fa2 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 1 Jun 2022 17:44:21 +0200 Subject: [PATCH] [INTERNAL] ComponentProject: Allow writing resources outside of namespace --- lib/specifications/ComponentProject.js | 129 ++++++++++-------- lib/specifications/types/Application.js | 4 +- lib/specifications/types/Library.js | 68 +++++---- lib/specifications/types/ThemeLibrary.js | 1 + test/fixtures/library.h/src/.library | 11 ++ test/fixtures/library.h/src/manifest.json | 26 ++++ test/fixtures/library.h/src/some.js | 4 + test/fixtures/library.h/ui5.yaml | 5 + .../theme/library/e/themes/my_theme/.theme | 9 ++ .../theme/library/e/themes/my_theme/.theming | 27 ++++ .../e/themes/my_theme/library.source.less | 9 ++ .../test/theme/library/e/Test.html | 0 test/fixtures/theme.library.e/ui5.yaml | 9 ++ test/lib/specifications/types/Application.js | 46 ++++++- test/lib/specifications/types/Library.js | 106 +++++++++++--- test/lib/specifications/types/ThemeLibrary.js | 122 +++++++++++++++++ 16 files changed, 465 insertions(+), 111 deletions(-) create mode 100644 test/fixtures/library.h/src/.library create mode 100644 test/fixtures/library.h/src/manifest.json create mode 100644 test/fixtures/library.h/src/some.js create mode 100644 test/fixtures/library.h/ui5.yaml create mode 100644 test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme create mode 100644 test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming create mode 100644 test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less create mode 100644 test/fixtures/theme.library.e/test/theme/library/e/Test.html create mode 100644 test/fixtures/theme.library.e/ui5.yaml create mode 100644 test/lib/specifications/types/ThemeLibrary.js diff --git a/lib/specifications/ComponentProject.js b/lib/specifications/ComponentProject.js index ec9de3971..2fad4065d 100644 --- a/lib/specifications/ComponentProject.js +++ b/lib/specifications/ComponentProject.js @@ -101,32 +101,27 @@ class ComponentProject extends Project { * @returns {module:@ui5/fs.ReaderCollection} A reader collection instance */ getReader({style = "buildtime"} = {}) { - // TODO: Additional parameter 'includeWorkspace' to include reader to relevant Memory Adapter? // TODO: Additional style 'ABAP' using "sap.platform.abap".uri from manifest.json? + + if (style === "runtime" && this._isRuntimeNamespaced) { + // If the project's runtime requires namespaces, paths are identical to "buildtime" style + style = "buildtime"; + } let reader; - let testReader; switch (style) { case "buildtime": - reader = this._getFlatSourceReader(`/resources/${this._namespace}/`); - testReader = this._getFlatTestReader(`/test-resources/${this._namespace}/`); - if (testReader) { - reader = resourceFactory.createReaderCollection({ - name: `Reader collection for project ${this.getName()}`, - readers: [reader, testReader] - }); - } + reader = this._getReader(); break; case "runtime": - if (this._isRuntimeNamespaced) { - // Same as buildtime - return this.getReader(); - } - // TODO 3.0: Refactor this - return this.getReader().link({ + // No test-resources for runtime resource access, + // unless runtime is namespaced + reader = this.getReader().link({ linkPath: `/`, targetPath: `/resources/${this._namespace}/` }); + break; case "flat": + // No test-resources for flat resource access reader = this._getFlatSourceReader("/"); break; default: @@ -138,16 +133,7 @@ class ComponentProject extends Project { } /** - * Get a resource reader for the sources of the project (not including any test resources) - * - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - _getFullSourceReader() { - throw new Error(`_getFullSourceReader must be implemented by subclass ${this.constructor.name}`); - } - - /** - * TODO + * Get a resource reader for the resources of the project * * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ @@ -156,15 +142,7 @@ class ComponentProject extends Project { } /** - * Get a resource reader for the test-sources of the project - * - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - _getFullTestReader() { - throw new Error(`_getFullTestReader must be implemented by subclass ${this.constructor.name}`); - } - /** - * TODO + * Get a resource reader for the test resources of the project * * @returns {module:@ui5/fs.ReaderCollection} Reader collection */ @@ -180,63 +158,98 @@ class ComponentProject extends Project { */ getWorkspace() { // Workspace is always of style "buildtime" - const reader = this.getReader({ - style: "buildtime" - }); - - const writer = this._getWriter(); return resourceFactory.createWorkspace({ - reader, - writer + name: `Workspace for project ${this.getName()}`, + reader: this._getReader(), + writer: this._getWriter().collection }); } _getWriter() { - if (!this._writer) { + if (!this._writers) { // writer is always of style "buildtime" - this._writer = resourceFactory.createAdapter({ + const namespaceWriter = resourceFactory.createAdapter({ + virBasePath: "/", + project: this + }); + + const generalWriter = resourceFactory.createAdapter({ virBasePath: "/", project: this }); + + const collection = resourceFactory.createWriterCollection({ + name: `Writers for project ${this.getName()}`, + writerMapping: { + [`/resources/${this._namespace}/`]: namespaceWriter, + [`/test-resources/${this._namespace}/`]: namespaceWriter, + [`/`]: generalWriter + } + }); + + this._writers = { + namespaceWriter, + generalWriter, + collection + }; + } + return this._writers; + } + + _getReader() { + let reader = this._getFlatSourceReader(`/resources/${this._namespace}/`); + const testReader = this._getFlatTestReader(`/test-resources/${this._namespace}/`); + if (testReader) { + reader = resourceFactory.createReaderCollection({ + name: `Reader collection for project ${this.getName()}`, + readers: [reader, testReader] + }); } - return this._writer; + return reader; } - _addWriter(reader, style = "buildtime") { - let writer = this._getWriter(); + _addWriter(reader, style) { + const {namespaceWriter, generalWriter} = this._getWriter(); + + if (style === "runtime" && this._isRuntimeNamespaced) { + // If the project's runtime requires namespaces, "runtime" paths are identical to "buildtime" paths + style = "buildtime"; + } + const readers = []; switch (style) { case "buildtime": { // Writer already uses buildtime style + readers.push(namespaceWriter); + readers.push(generalWriter); break; } case "runtime": { - if (this._isRuntimeNamespaced) { - // Same as buildtime - return this._addWriter(reader); - } - - // Rewrite paths from "runtime" to "buildtime" - writer = writer.link({ + // Runtime is not namespaced: link namespace to / + readers.push(namespaceWriter.link({ linkPath: `/`, targetPath: `/resources/${this._namespace}/` - }); + })); + // Add general writer as is + readers.push(generalWriter); break; } case "flat": { // Rewrite paths from "flat" to "buildtime" - writer = writer.link({ + readers.push(namespaceWriter.link({ linkPath: `/`, targetPath: `/resources/${this._namespace}/` - }); + })); + // General writer resources can't be flattened, so they are not available break; } default: throw new Error(`Unknown path mapping style ${style}`); } + readers.push(reader); return resourceFactory.createReaderCollectionPrioritized({ name: `Reader/Writer collection for project ${this.getName()}`, - readers: [writer, reader] + readers }); } diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js index 38123c0e8..d11509704 100644 --- a/lib/specifications/types/Application.js +++ b/lib/specifications/types/Application.js @@ -46,7 +46,7 @@ class Application extends ComponentProject { return null; // Applications do not have a dedicated test directory } - _getSourceReader() { + _getRawSourceReader() { return resourceFactory.createReader({ fsBasePath: fsPath.join(this.getPath(), this._webappPath), virBasePath: "/", @@ -190,7 +190,7 @@ class Application extends ComponentProject { if (this._pManifests[filePath]) { return this._pManifests[filePath]; } - return this._pManifests[filePath] = this._getSourceReader().byPath(filePath) + return this._pManifests[filePath] = this._getRawSourceReader().byPath(filePath) .then(async (resource) => { if (!resource) { throw new Error( diff --git a/lib/specifications/types/Library.js b/lib/specifications/types/Library.js index dba64ad75..53c72d013 100644 --- a/lib/specifications/types/Library.js +++ b/lib/specifications/types/Library.js @@ -15,6 +15,8 @@ class Library extends ComponentProject { this._srcPath = "src"; this._testPath = "test"; this._testPathExists = false; + this._isSourceNamespaced = true; + this._propertiesFilesSourceEncoding = "UTF-8"; } @@ -29,27 +31,14 @@ class Library extends ComponentProject { } /* === Resource Access === */ - - /** - * - * Get a resource reader for the sources of the project (excluding any test resources) - * In the future the path structure can be flat or namespaced depending on the project - * - * @returns {module:@ui5/fs.ReaderCollection} Reader collection - */ - _getSourceReader() { - return resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._srcPath), - virBasePath: "/", - name: `Source reader for library project ${this.getName()}`, - project: this - }); - } - _getFlatSourceReader(virBasePath = "/") { // TODO: Throw for libraries with additional namespaces like sap.ui.core? + let fsBasePath = fsPath.join(this.getPath(), this._srcPath); + if (this._isSourceNamespaced) { + fsBasePath = fsPath.join(fsBasePath, ...this._namespace.split("/")); + } return resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._srcPath, ...this._namespace.split("/")), + fsBasePath, virBasePath, name: `Source reader for library project ${this.getName()}`, project: this @@ -60,8 +49,12 @@ class Library extends ComponentProject { if (!this._testPathExists) { return null; } + let fsBasePath = fsPath.join(this.getPath(), this._testPath); + if (this._isSourceNamespaced) { + fsBasePath = fsPath.join(fsBasePath, ...this._namespace.split("/")); + } const testReader = resourceFactory.createReader({ - fsBasePath: fsPath.join(this.getPath(), this._testPath, ...this._namespace.split("/")), + fsBasePath, virBasePath, name: `Runtime test-resources reader for library project ${this.getName()}`, project: this @@ -69,6 +62,22 @@ class Library extends ComponentProject { return testReader; } + /** + * + * Get a resource reader for the sources of the project (excluding any test resources) + * In the future the path structure can be flat or namespaced depending on the project + * + * @returns {module:@ui5/fs.ReaderCollection} Reader collection + */ + _getRawSourceReader() { + return resourceFactory.createReader({ + fsBasePath: fsPath.join(this.getPath(), this._srcPath), + virBasePath: "/", + name: `Source reader for library project ${this.getName()}`, + project: this + }); + } + /* === Internals === */ /** * @private @@ -246,11 +255,16 @@ class Library extends ComponentProject { } namespace = libraryNs.replace(/\./g, "/"); - namespacePath = namespacePath.replace("/", ""); // remove leading slash - if (namespacePath !== namespace) { - throw new Error( - `Detected namespace "${namespace}" does not match detected directory ` + - `structure "${namespacePath}" for project ${this.getName()}`); + if (namespacePath === "/") { + this._log.verbose(`Detected flat library source structure for project ${this.getName()}`); + this._isSourceNamespaced = false; + } else { + namespacePath = namespacePath.replace("/", ""); // remove leading slash + if (namespacePath !== namespace) { + throw new Error( + `Detected namespace "${namespace}" does not match detected directory ` + + `structure "${namespacePath}" for project ${this.getName()}`); + } } } else { try { @@ -384,7 +398,7 @@ class Library extends ComponentProject { if (this._pManifest) { return this._pManifest; } - return this._pManifest = this._getSourceReader().byGlob("**/manifest.json") + return this._pManifest = this._getRawSourceReader().byGlob("**/manifest.json") .then(async (manifestResources) => { if (!manifestResources.length) { throw new Error(`Could not find manifest.json file for project ${this.getName()}`); @@ -416,7 +430,7 @@ class Library extends ComponentProject { if (this._pDotLibrary) { return this._pDotLibrary; } - return this._pDotLibrary = this._getSourceReader().byGlob("**/.library") + return this._pDotLibrary = this._getRawSourceReader().byGlob("**/.library") .then(async (dotLibraryResources) => { if (!dotLibraryResources.length) { throw new Error(`Could not find .library file for project ${this.getName()}`); @@ -456,7 +470,7 @@ class Library extends ComponentProject { if (this._pLibraryJs) { return this._pLibraryJs; } - return this._pLibraryJs = this._getSourceReader().byGlob("**/library.js") + return this._pLibraryJs = this._getRawSourceReader().byGlob("**/library.js") .then(async (libraryJsResources) => { if (!libraryJsResources.length) { throw new Error(`Could not find library.js file for project ${this.getName()}`); diff --git a/lib/specifications/types/ThemeLibrary.js b/lib/specifications/types/ThemeLibrary.js index b0f2b635d..54154ec1d 100644 --- a/lib/specifications/types/ThemeLibrary.js +++ b/lib/specifications/types/ThemeLibrary.js @@ -8,6 +8,7 @@ class ThemeLibrary extends Project { this._srcPath = "src"; this._testPath = "test"; + this._testPathExists = false; this._writer = null; } diff --git a/test/fixtures/library.h/src/.library b/test/fixtures/library.h/src/.library new file mode 100644 index 000000000..8de6bd2eb --- /dev/null +++ b/test/fixtures/library.h/src/.library @@ -0,0 +1,11 @@ + + + + library.h + SAP SE + ${copyright} + ${version} + + Library G + + diff --git a/test/fixtures/library.h/src/manifest.json b/test/fixtures/library.h/src/manifest.json new file mode 100644 index 000000000..2279cb6ce --- /dev/null +++ b/test/fixtures/library.h/src/manifest.json @@ -0,0 +1,26 @@ +{ + "_version": "1.21.0", + "sap.app": { + "id": "library.h", + "type": "library", + "embeds": [], + "applicationVersion": { + "version": "1.0.0" + }, + "title": "Library H", + "description": "Library H" + }, + "sap.ui": { + "technology": "UI5", + "supportedThemes": [] + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.0", + "libs": {} + }, + "library": { + "i18n": false + } + } +} diff --git a/test/fixtures/library.h/src/some.js b/test/fixtures/library.h/src/some.js new file mode 100644 index 000000000..81e734360 --- /dev/null +++ b/test/fixtures/library.h/src/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/test/fixtures/library.h/ui5.yaml b/test/fixtures/library.h/ui5.yaml new file mode 100644 index 000000000..cbea83db5 --- /dev/null +++ b/test/fixtures/library.h/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.6" +type: library +metadata: + name: library.h diff --git a/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme new file mode 100644 index 000000000..4c62f2611 --- /dev/null +++ b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme @@ -0,0 +1,9 @@ + + + + my_theme + me + ${copyright} + ${version} + + \ No newline at end of file diff --git a/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming new file mode 100644 index 000000000..83b6c785a --- /dev/null +++ b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming @@ -0,0 +1,27 @@ +{ + "sEntity": "Theme", + "sId": "sap_belize", + "oExtends": "base", + "sVendor": "SAP", + "aBundled": ["sap_belize_plus"], + "mCssScopes": { + "library": { + "sBaseFile": "library", + "sEmbeddingMethod": "APPEND", + "aScopes": [ + { + "sLabel": "Contrast", + "sSelector": "sapContrast", + "sEmbeddedFile": "sap_belize_plus.library", + "sEmbeddedCompareFile": "library", + "sThemeIdSuffix": "Contrast", + "sThemability": "PUBLIC", + "aThemabilityFilter": [ + "Color" + ], + "rExcludeSelector": "\\.sapContrastPlus\\W" + } + ] + } + } +} diff --git a/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less new file mode 100644 index 000000000..d3286002b --- /dev/null +++ b/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less @@ -0,0 +1,9 @@ +/*! + * ${copyright} + */ + +@mycolor: blue; + +.sapUiBody { + background-color: @mycolor; +} diff --git a/test/fixtures/theme.library.e/test/theme/library/e/Test.html b/test/fixtures/theme.library.e/test/theme/library/e/Test.html new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixtures/theme.library.e/ui5.yaml b/test/fixtures/theme.library.e/ui5.yaml new file mode 100644 index 000000000..cf89c2432 --- /dev/null +++ b/test/fixtures/theme.library.e/ui5.yaml @@ -0,0 +1,9 @@ +--- +specVersion: "1.1" +type: theme-library +metadata: + name: theme.library.e + copyright: |- + UI development toolkit for HTML5 (OpenUI5) + * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company. + * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. diff --git a/test/lib/specifications/types/Application.js b/test/lib/specifications/types/Application.js index 05b4d9c68..769e9d495 100644 --- a/test/lib/specifications/types/Application.js +++ b/test/lib/specifications/types/Application.js @@ -1,6 +1,7 @@ const test = require("ava"); const path = require("path"); const sinon = require("sinon"); +const {createResource} = require("@ui5/fs").resourceFactory; const Specification = require("../../../../lib/specifications/Specification"); const Application = require("../../../../lib/specifications/types/Application"); @@ -105,6 +106,45 @@ test("Modify project resources via workspace and access via flat and runtime rea t.is(await runtimeGlobResult[0].getString(), newContent, "Found resource (byGlob) has expected (changed) content"); }); + +test("Read and write resources outside of app namespace", async (t) => { + const project = await Specification.create(basicProjectInput); + const workspace = await project.getWorkspace(); + + await workspace.write(createResource({ + path: "/resources/my-custom-bundle.js" + })); + + const buildtimeReader = await project.getReader({style: "buildtime"}); + const buildtimeReaderResource = await buildtimeReader.byPath("/resources/my-custom-bundle.js"); + t.truthy(buildtimeReaderResource, "Found the requested resource byPath (buildtime)"); + t.is(buildtimeReaderResource.getPath(), "/resources/my-custom-bundle.js", + "Resource (byPath) has correct path (buildtime)"); + + const buildtimeGlobResult = await buildtimeReader.byGlob("**/my-custom-bundle.js"); + t.is(buildtimeGlobResult.length, 1, "Found the requested resource byGlob (buildtime)"); + t.is(buildtimeGlobResult[0].getPath(), "/resources/my-custom-bundle.js", + "Resource (byGlob) has correct path (buildtime)"); + + const flatReader = await project.getReader({style: "flat"}); + const flatReaderResource = await flatReader.byPath("/resources/my-custom-bundle.js"); + t.falsy(flatReaderResource, "Resource outside of app namespace can't be read using flat reader"); + + const flatGlobResult = await flatReader.byGlob("**/my-custom-bundle.js"); + t.is(flatGlobResult.length, 0, "Resource outside of app namespace can't be found using flat reader"); + + const runtimeReader = await project.getReader({style: "runtime"}); + const runtimeReaderResource = await runtimeReader.byPath("/resources/my-custom-bundle.js"); + t.truthy(runtimeReaderResource, "Found the requested resource byPath (runtime)"); + t.is(runtimeReaderResource.getPath(), "/resources/my-custom-bundle.js", + "Resource (byPath) has correct path (runtime)"); + + const runtimeGlobResult = await runtimeReader.byGlob("**/my-custom-bundle.js"); + t.is(runtimeGlobResult.length, 1, "Found the requested resource byGlob (runtime)"); + t.is(runtimeGlobResult[0].getPath(), "/resources/my-custom-bundle.js", + "Resource (byGlob) has correct path (runtime)"); +}); + test("_configureAndValidatePaths: Default paths", async (t) => { const project = await Specification.create(basicProjectInput); @@ -267,7 +307,7 @@ test("_getManifest: invalid JSON", async (t) => { getString: async () => "no json" }); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byPath: byPathStub }; @@ -299,7 +339,7 @@ test.serial("_getManifest: result is cached", async (t) => { getString: async () => `{"pony": "no unicorn"}` }); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byPath: byPathStub }; @@ -324,7 +364,7 @@ test.serial("_getManifest: Caches successes and failures", async (t) => { getString: getStringStub }); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byPath: byPathStub }; diff --git a/test/lib/specifications/types/Library.js b/test/lib/specifications/types/Library.js index 259cc9d3b..2fb78dfa5 100644 --- a/test/lib/specifications/types/Library.js +++ b/test/lib/specifications/types/Library.js @@ -31,6 +31,21 @@ const basicProjectInput = { } }; +const libraryHPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.h"); +const flatProjectInput = { + id: "library.d.id", + version: "1.0.0", + modulePath: libraryHPath, + configuration: { + specVersion: "2.6", + kind: "project", + type: "library", + metadata: { + name: "library.h", + } + } +}; + test.afterEach.always((t) => { sinon.restore(); mock.stopAll(); @@ -133,6 +148,14 @@ test("Modify project resources via workspace and access via flat and runtime rea "Found resource (byGlob) has expected (changed) content (runtime)"); }); +test("Access flat project resources via reader: buildtime style", async (t) => { + const project = await Specification.create(flatProjectInput); + const reader = await project.getReader({style: "buildtime"}); + const resource = await reader.byPath("/resources/library/h/some.js"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/library/h/some.js", "Resource has correct path"); +}); + test("_configureAndValidatePaths: Default paths", async (t) => { const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e"); const projectInput = { @@ -166,7 +189,6 @@ test("_configureAndValidatePaths: Test directory does not exist", async (t) => { t.false(project._testPathExists, "Test path detected as non-existent"); }); - test("_configureAndValidatePaths: Source directory does not exist", async (t) => { const projectInput = clone(basicProjectInput); projectInput.configuration.resources.configuration.paths.src = "does/not/exist"; @@ -340,7 +362,7 @@ test("_getManifest: Reads correctly", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -357,7 +379,7 @@ test("_getManifest: No manifest.json", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().resolves([]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -376,7 +398,7 @@ test("_getManifest: Invalid JSON", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -395,7 +417,7 @@ test("_getManifest: Propagates exception", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().rejects(new Error("because shark")); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -417,7 +439,7 @@ test("_getManifest: Multiple manifest.json files", async (t) => { getPath: () => "some other path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -435,7 +457,7 @@ test("_getManifest: Result is cached", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -461,7 +483,7 @@ test("_getDotLibrary: Reads correctly", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -478,7 +500,7 @@ test("_getDotLibrary: No .library file", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().resolves([]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -497,7 +519,7 @@ test("_getDotLibrary: Invalid XML", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -516,7 +538,7 @@ test("_getDotLibrary: Propagates exception", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().rejects(new Error("because shark")); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -538,7 +560,7 @@ test("_getDotLibrary: Multiple .library files", async (t) => { getPath: () => "some other path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -556,7 +578,7 @@ test("_getDotLibrary: Result is cached", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -581,7 +603,7 @@ test("_getLibraryJsPath: Reads correctly", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -597,7 +619,7 @@ test("_getLibraryJsPath: No library.js file", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().resolves([]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -613,7 +635,7 @@ test("_getLibraryJsPath: Propagates exception", async (t) => { const project = await Specification.create(basicProjectInput); const byGlobStub = sinon.stub().rejects(new Error("because shark")); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -633,7 +655,7 @@ test("_getLibraryJsPath: Multiple library.js files", async (t) => { getPath: () => "some other path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -650,7 +672,7 @@ test("_getLibraryJsPath: Result is cached", async (t) => { getPath: () => "some path" }]); - project._getSourceReader = () => { + project._getRawSourceReader = () => { return { byGlob: byGlobStub }; @@ -714,12 +736,48 @@ test("_getNamespace: from manifest.json with .library on same level", async (t) }); sinon.stub(project, "_getDotLibrary").resolves({ content: { - library: {name: "dot-pony"} + library: {name: {_: "dot-pony"}} }, filePath: "/mani-pony/.library" }); const res = await project._getNamespace(); - t.deepEqual(res, "mani-pony", "Returned correct namespace"); + t.is(res, "mani-pony", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); +}); + +test("_getNamespace: from manifest.json for flat project", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").resolves({ + content: { + "sap.app": { + id: "mani-pony" + } + }, + filePath: "/manifest.json" + }); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/.library" + }); + const res = await project._getNamespace(); + t.is(res, "mani-pony", "Returned correct namespace"); + t.false(project._isSourceNamespaced, "Project flagged as flat source structure"); +}); + +test("_getNamespace: from .library for flat project", async (t) => { + const project = await Specification.create(basicProjectInput); + sinon.stub(project, "_getManifest").rejects("No manifest aint' here"); + sinon.stub(project, "_getDotLibrary").resolves({ + content: { + library: {name: {_: "dot-pony"}} + }, + filePath: "/.library" + }); + const res = await project._getNamespace(); + t.is(res, "dot-pony", "Returned correct namespace"); + t.false(project._isSourceNamespaced, "Project flagged as flat source structure"); }); test("_getNamespace: from manifest.json with .library on same level but different directory", async (t) => { @@ -762,7 +820,7 @@ test("_getNamespace: from manifest.json with not matching file path", async (t) }); sinon.stub(project, "_getDotLibrary").resolves({ content: { - library: {name: "dot-pony"} + library: {name: {_: "dot-pony"}} }, filePath: "/different/namespace/.library" }); @@ -806,6 +864,7 @@ test.serial("_getNamespace: from manifest.json without sap.app id", async (t) => `Namespace resolution from manifest.json failed for project library.d: ` + `No sap.app/id configuration found in manifest.json of project library.d at ${manifestPath}`, "correct verbose message"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test("_getNamespace: from .library", async (t) => { @@ -819,6 +878,7 @@ test("_getNamespace: from .library", async (t) => { }); const res = await project._getNamespace(); t.deepEqual(res, "dot-pony", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test("_getNamespace: from .library with ignored manifest.json on lower level", async (t) => { @@ -839,6 +899,7 @@ test("_getNamespace: from .library with ignored manifest.json on lower level", a }); const res = await project._getNamespace(); t.deepEqual(res, "dot-pony", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test("_getNamespace: manifest.json on higher level than .library", async (t) => { @@ -889,6 +950,7 @@ test("_getNamespace: from .library with maven placeholder", async (t) => { t.deepEqual(resolveMavenPlaceholderStub.getCall(0).args[0], "${mvn-pony}", "resolveMavenPlaceholder called with correct argument"); t.deepEqual(res, "mvn-unicorn", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test("_getNamespace: from .library with not matching file path", async (t) => { @@ -905,6 +967,7 @@ test("_getNamespace: from .library with not matching file path", async (t) => { t.deepEqual(err.message, `Detected namespace "mvn-pony" does not match detected directory structure ` + `"different/namespace" for project library.d`, "Rejected with correct error message"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test("_getNamespace: from library.js", async (t) => { @@ -914,6 +977,7 @@ test("_getNamespace: from library.js", async (t) => { sinon.stub(project, "_getLibraryJsPath").resolves("/my/namespace/library.js"); const res = await project._getNamespace(); t.deepEqual(res, "my/namespace", "Returned correct namespace"); + t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure"); }); test.serial("_getNamespace: from project root level library.js", async (t) => { diff --git a/test/lib/specifications/types/ThemeLibrary.js b/test/lib/specifications/types/ThemeLibrary.js new file mode 100644 index 000000000..e2f053f02 --- /dev/null +++ b/test/lib/specifications/types/ThemeLibrary.js @@ -0,0 +1,122 @@ +const test = require("ava"); +const path = require("path"); +const sinon = require("sinon"); +const mock = require("mock-require"); +const Specification = require("../../../../lib/specifications/Specification"); + +function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +const themeLibraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "theme.library.e"); +const basicProjectInput = { + id: "theme.library.e.id", + version: "1.0.0", + modulePath: themeLibraryEPath, + configuration: { + specVersion: "2.6", + kind: "project", + type: "theme-library", + metadata: { + name: "theme.library.e", + copyright: "Some fancy copyright" + } + } +}; + +test.afterEach.always((t) => { + sinon.restore(); + mock.stopAll(); +}); + +test("Correct class", async (t) => { + const ThemeLibrary = mock.reRequire("../../../../lib/specifications/types/ThemeLibrary"); + const project = await Specification.create(basicProjectInput); + t.true(project instanceof ThemeLibrary, `Is an instance of the ThemeLibrary class`); +}); + +test("getCopyright", async (t) => { + const project = await Specification.create(basicProjectInput); + + t.deepEqual(project.getCopyright(), "Some fancy copyright", "Copyright was read correctly"); +}); + +test("Access project resources via reader", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader(); + const resource = await reader.byPath("/resources/theme/library/e/themes/my_theme/.theme"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/theme/library/e/themes/my_theme/.theme", "Resource has correct path"); +}); + +test("Access project test-resources via reader", async (t) => { + const project = await Specification.create(basicProjectInput); + const reader = await project.getReader(); + const resource = await reader.byPath("/test-resources/theme/library/e/Test.html"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/test-resources/theme/library/e/Test.html", "Resource has correct path"); +}); + +test("Modify project resources via workspace and access via flat and runtime reader", async (t) => { + const project = await Specification.create(basicProjectInput); + const workspace = await project.getWorkspace(); + const workspaceResource = await workspace.byPath("/resources/theme/library/e/themes/my_theme/library.source.less"); + t.truthy(workspaceResource, "Found resource in workspace"); + + const newContent = (await workspaceResource.getString()).replace("fancy", "fancy dancy"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const reader = await project.getReader(); + const readerResource = await reader.byPath("/resources/theme/library/e/themes/my_theme/library.source.less"); + t.truthy(readerResource, "Found the requested resource byPath"); + t.is(readerResource.getPath(), "/resources/theme/library/e/themes/my_theme/library.source.less", + "Resource (byPath) has correct path"); + t.is(await readerResource.getString(), newContent, + "Found resource (byPath) has expected (changed) content"); + + const globResult = await reader.byGlob("**/library.source.less"); + t.is(globResult.length, 1, "Found the requested resource byGlob"); + t.is(globResult[0].getPath(), "/resources/theme/library/e/themes/my_theme/library.source.less", + "Resource (byGlob) has correct path"); + t.is(await globResult[0].getString(), newContent, + "Found resource (byGlob) has expected (changed) content"); +}); + +test("_configureAndValidatePaths: Default paths", async (t) => { + const project = await Specification.create(basicProjectInput); + + t.is(project._srcPath, "src", "Correct default path for src"); + t.is(project._testPath, "test", "Correct default path for test"); + t.true(project._testPathExists, "Test path detected as existing"); +}); + +test("_configureAndValidatePaths: Test directory does not exist", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.resources = { + configuration: { + paths: { + test: "does/not/exist" + } + } + }; + const project = await Specification.create(projectInput); + + t.is(project._srcPath, "src", "Correct path for src"); + t.is(project._testPath, "does/not/exist", "Correct path for test"); + t.false(project._testPathExists, "Test path detected as non-existent"); +}); + +test("_configureAndValidatePaths: Source directory does not exist", async (t) => { + const projectInput = clone(basicProjectInput); + projectInput.configuration.resources = { + configuration: { + paths: { + src: "does/not/exist" + } + } + }; + const err = await t.throwsAsync(Specification.create(projectInput)); + + t.is(err.message, "Unable to find directory 'does/not/exist' in theme-library project theme.library.e"); +});