From b421e3390dd0511499ae1c406d0324d73dc5b897 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Mon, 3 Nov 2025 14:15:51 +0100 Subject: [PATCH 01/11] feat(project): Add component type RFC: https://github.com/UI5/cli/pull/1083 JIRA: CPOUI5FOUNDATION-1106 --- packages/project/lib/build/TaskRunner.js | 3 + .../lib/specifications/Specification.js | 3 + .../lib/specifications/types/Component.js | 303 ++++++ .../schema/specVersion/kind/project.json | 11 + .../specVersion/kind/project/component.json | 110 ++ .../test/fixtures/component.a/middleware.a.js | 1 + .../collection/library.a/package.json | 17 + .../library.a/src/library/a/.library | 17 + .../library/a/themes/base/library.source.less | 6 + .../library.a/test/library/a/Test.html | 0 .../collection/library.a/ui5.yaml | 5 + .../collection/library.b/package.json | 9 + .../library.b/src/library/b/.library | 17 + .../library.b/test/library/b/Test.html | 0 .../collection/library.b/ui5.yaml | 5 + .../collection/library.c/package.json | 9 + .../library.c/src/library/c/.library | 17 + .../library.c/test/LibraryC/Test.html | 0 .../collection/library.c/ui5.yaml | 5 + .../node_modules/library.d/package.json | 9 + .../library.d/src/library/d/.library | 11 + .../library.d/test/library/d/Test.html | 0 .../node_modules/library.d/ui5.yaml | 10 + .../node_modules/collection/package.json | 18 + .../node_modules/collection/ui5.yaml | 12 + .../library.d/main/src/library/d/.library | 11 + .../library.d/main/src/library/d/some.js | 4 + .../library.d/main/test/library/d/Test.html | 0 .../node_modules/library.d/package.json | 9 + .../node_modules/library.d/ui5.yaml | 10 + .../test/fixtures/component.a/package.json | 13 + .../fixtures/component.a/src/Component.js | 8 + .../test/fixtures/component.a/src/index.html | 9 + .../fixtures/component.a/src/manifest.json | 13 + .../test/fixtures/component.a/src/test.js | 5 + .../test/fixtures/component.a/task.a.js | 1 + .../component.a/ui5-test-configPath.yaml | 7 + .../component.a/ui5-test-corrupt.yaml | 1 + .../fixtures/component.a/ui5-test-empty.yaml | 0 .../fixtures/component.a/ui5-test-error.yaml | 7 + .../test/fixtures/component.a/ui5.yaml | 5 + .../project/test/fixtures/component.h/pom.xml | 41 + .../src-no-component/manifest.json | 13 + .../src-project.artifactId/Component.js | 8 + .../src-project.artifactId/manifest.json | 13 + .../src-properties.appId/Component.js | 8 + .../src-properties.appId/manifest.json | 13 + .../src-properties.componentName/Component.js | 8 + .../manifest.json | 13 + .../fixtures/component.h/webapp/Component.js | 8 + .../fixtures/component.h/webapp/manifest.json | 13 + .../component.h/webapp/sectionsA/section1.js | 3 + .../component.h/webapp/sectionsA/section2.js | 3 + .../component.h/webapp/sectionsA/section3.js | 3 + .../component.h/webapp/sectionsB/section1.js | 3 + .../component.h/webapp/sectionsB/section2.js | 3 + .../component.h/webapp/sectionsB/section3.js | 3 + .../lib/specifications/types/Component.js | 688 +++++++++++++ .../__helper__/builder-bundleOptions.js | 12 +- .../schema/__helper__/customConfiguration.js | 45 +- .../validation/schema/__helper__/framework.js | 8 +- .../validation/schema/__helper__/project.js | 54 +- .../schema/specVersion/kind/project.js | 22 + .../specVersion/kind/project/component.js | 963 ++++++++++++++++++ .../project/test/lib/validation/schema/ui5.js | 1 + 65 files changed, 2605 insertions(+), 45 deletions(-) create mode 100644 packages/project/lib/specifications/types/Component.js create mode 100644 packages/project/lib/validation/schema/specVersion/kind/project/component.json create mode 100644 packages/project/test/fixtures/component.a/middleware.a.js create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.a/package.json create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/.library create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.a/test/library/a/Test.html create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.a/ui5.yaml create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.b/package.json create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.b/src/library/b/.library create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.b/test/library/b/Test.html create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.b/ui5.yaml create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.c/package.json create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.c/src/library/c/.library create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.c/test/LibraryC/Test.html create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.c/ui5.yaml create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/package.json create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/src/library/d/.library create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/test/library/d/Test.html create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/ui5.yaml create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/package.json create mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/ui5.yaml create mode 100644 packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/.library create mode 100644 packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/some.js create mode 100644 packages/project/test/fixtures/component.a/node_modules/library.d/main/test/library/d/Test.html create mode 100644 packages/project/test/fixtures/component.a/node_modules/library.d/package.json create mode 100644 packages/project/test/fixtures/component.a/node_modules/library.d/ui5.yaml create mode 100644 packages/project/test/fixtures/component.a/package.json create mode 100644 packages/project/test/fixtures/component.a/src/Component.js create mode 100644 packages/project/test/fixtures/component.a/src/index.html create mode 100644 packages/project/test/fixtures/component.a/src/manifest.json create mode 100644 packages/project/test/fixtures/component.a/src/test.js create mode 100644 packages/project/test/fixtures/component.a/task.a.js create mode 100644 packages/project/test/fixtures/component.a/ui5-test-configPath.yaml create mode 100644 packages/project/test/fixtures/component.a/ui5-test-corrupt.yaml create mode 100644 packages/project/test/fixtures/component.a/ui5-test-empty.yaml create mode 100644 packages/project/test/fixtures/component.a/ui5-test-error.yaml create mode 100644 packages/project/test/fixtures/component.a/ui5.yaml create mode 100644 packages/project/test/fixtures/component.h/pom.xml create mode 100644 packages/project/test/fixtures/component.h/src-no-component/manifest.json create mode 100644 packages/project/test/fixtures/component.h/src-project.artifactId/Component.js create mode 100644 packages/project/test/fixtures/component.h/src-project.artifactId/manifest.json create mode 100644 packages/project/test/fixtures/component.h/src-properties.appId/Component.js create mode 100644 packages/project/test/fixtures/component.h/src-properties.appId/manifest.json create mode 100644 packages/project/test/fixtures/component.h/src-properties.componentName/Component.js create mode 100644 packages/project/test/fixtures/component.h/src-properties.componentName/manifest.json create mode 100644 packages/project/test/fixtures/component.h/webapp/Component.js create mode 100644 packages/project/test/fixtures/component.h/webapp/manifest.json create mode 100644 packages/project/test/fixtures/component.h/webapp/sectionsA/section1.js create mode 100644 packages/project/test/fixtures/component.h/webapp/sectionsA/section2.js create mode 100644 packages/project/test/fixtures/component.h/webapp/sectionsA/section3.js create mode 100644 packages/project/test/fixtures/component.h/webapp/sectionsB/section1.js create mode 100644 packages/project/test/fixtures/component.h/webapp/sectionsB/section2.js create mode 100644 packages/project/test/fixtures/component.h/webapp/sectionsB/section3.js create mode 100644 packages/project/test/lib/specifications/types/Component.js create mode 100644 packages/project/test/lib/validation/schema/specVersion/kind/project/component.js diff --git a/packages/project/lib/build/TaskRunner.js b/packages/project/lib/build/TaskRunner.js index 70e7bf32ae4..cd0441bbef2 100644 --- a/packages/project/lib/build/TaskRunner.js +++ b/packages/project/lib/build/TaskRunner.js @@ -53,6 +53,9 @@ class TaskRunner { case "library": buildDefinition = "./definitions/library.js"; break; + case "component": + buildDefinition = "./definitions/application.js"; + break; case "module": buildDefinition = "./definitions/module.js"; break; diff --git a/packages/project/lib/specifications/Specification.js b/packages/project/lib/specifications/Specification.js index 71553e73af7..02bdc58036e 100644 --- a/packages/project/lib/specifications/Specification.js +++ b/packages/project/lib/specifications/Specification.js @@ -39,6 +39,9 @@ class Specification { case "application": { return createAndInitializeSpec("types/Application.js", parameters); } + case "component": { + return createAndInitializeSpec("types/Component.js", parameters); + } case "library": { return createAndInitializeSpec("types/Library.js", parameters); } diff --git a/packages/project/lib/specifications/types/Component.js b/packages/project/lib/specifications/types/Component.js new file mode 100644 index 00000000000..fbf77f94ec8 --- /dev/null +++ b/packages/project/lib/specifications/types/Component.js @@ -0,0 +1,303 @@ +import fsPath from "node:path"; +import posixPath from "node:path/posix"; +import ComponentProject from "../ComponentProject.js"; +import {createReader} from "@ui5/fs/resourceFactory"; + +/** + * Component + * + * @public + * @class + * @alias @ui5/project/specifications/types/Component + * @extends @ui5/project/specifications/ComponentProject + * @hideconstructor + */ +class Component extends ComponentProject { + constructor(parameters) { + super(parameters); + + this._pManifests = Object.create(null); + + this._srcPath = "src"; + this._testPath = "test"; + this._testPathExists = false; + + this._propertiesFilesSourceEncoding = "UTF-8"; + } + + /* === Attributes === */ + + /** + * Get the cachebuster signature type configuration of the project + * + * @returns {string} time or hash + */ + getCachebusterSignatureType() { + return this._config.builder && this._config.builder.cachebuster && + this._config.builder.cachebuster.signatureType || "time"; + } + + /** + * Get the path of the project's source directory. This might not be POSIX-style on some platforms. + * + * @public + * @returns {string} Absolute path to the source directory of the project + */ + getSourcePath() { + return fsPath.join(this.getRootPath(), this._srcPath); + } + + getSourcePaths() { + const paths = [this.getSourcePath()]; + if (this._testPathExists) { + paths.push(fsPath.join(this.getRootPath(), this._testPath)); + } + return paths; + } + + getVirtualPath(sourceFilePath) { + const sourcePath = this.getSourcePath(); + if (sourceFilePath.startsWith(sourcePath)) { + const relSourceFilePath = fsPath.relative(sourcePath, sourceFilePath); + let virBasePath = "/resources/"; + if (!this._isSourceNamespaced) { + virBasePath += `${this._namespace}/`; + } + return posixPath.join(virBasePath, relSourceFilePath); + } + + const testPath = fsPath.join(this.getRootPath(), this._testPath); + if (sourceFilePath.startsWith(testPath)) { + const relSourceFilePath = fsPath.relative(testPath, sourceFilePath); + let virBasePath = "/test-resources/"; + if (!this._isSourceNamespaced) { + virBasePath += `${this._namespace}/`; + } + return posixPath.join(virBasePath, relSourceFilePath); + } + + throw new Error( + `Unable to convert source path ${sourceFilePath} to virtual path for project ${this.getName()}`); + } + + /* === Resource Access === */ + /** + * Get a resource reader for the sources of the project (excluding any test resources) + * + * @param {string[]} excludes List of glob patterns to exclude + * @returns {@ui5/fs/ReaderCollection} Reader collection + */ + _getSourceReader(excludes) { + return createReader({ + fsBasePath: this.getSourcePath(), + virBasePath: `/resources/${this._namespace}/`, + name: `Source reader for component project ${this.getName()}`, + project: this, + excludes + }); + } + + /** + * Get a resource reader for the test-resources of the project + * + * @param {string[]} excludes List of glob patterns to exclude + * @returns {@ui5/fs/ReaderCollection} Reader collection + */ + _getTestReader(excludes) { + if (!this._testPathExists) { + return null; + } + const testReader = createReader({ + fsBasePath: fsPath.join(this.getRootPath(), this._testPath), + virBasePath: `/test-resources/${this._namespace}/`, + name: `Runtime test-resources reader for component project ${this.getName()}`, + project: this, + excludes + }); + return testReader; + } + + /** + * Get a resource reader for the sources of the project (excluding any test resources) + * without a virtual base path + * + * @returns {@ui5/fs/ReaderCollection} Reader collection + */ + _getRawSourceReader() { + return createReader({ + fsBasePath: this.getSourcePath(), + virBasePath: "/", + name: `Raw source reader for component project ${this.getName()}`, + project: this + }); + } + + /* === Internals === */ + /** + * @private + * @param {object} config Configuration object + */ + async _configureAndValidatePaths(config) { + await super._configureAndValidatePaths(config); + + if (config.resources && config.resources.configuration && config.resources.configuration.paths) { + if (config.resources.configuration.paths.src) { + this._srcPath = config.resources.configuration.paths.src; + } + if (config.resources.configuration.paths.test) { + this._testPath = config.resources.configuration.paths.test; + } + } + if (!(await this._dirExists("/" + this._srcPath))) { + throw new Error( + `Unable to find source directory '${this._srcPath}' in component project ${this.getName()}`); + } + this._testPathExists = await this._dirExists("/" + this._testPath); + + this._log.verbose(`Path mapping for component project ${this.getName()}:`); + this._log.verbose(` Physical root path: ${this.getRootPath()}`); + this._log.verbose(` Mapped to:`); + this._log.verbose(` /resources/ => ${this._srcPath}`); + this._log.verbose( + ` /test-resources/ => ${this._testPath}${this._testPathExists ? "" : " [does not exist]"}`); + } + + /** + * @private + * @param {object} config Configuration object + * @param {object} buildDescription Cache metadata object + */ + async _parseConfiguration(config, buildDescription) { + await super._parseConfiguration(config, buildDescription); + + if (buildDescription) { + this._namespace = buildDescription.namespace; + return; + } + this._namespace = await this._getNamespace(); + await this._ensureComponent(); + } + + /** + * Determine component namespace either based on a project`s + * manifest.json or manifest.appdescr_variant (fallback if present) + * + * @returns {string} Namespace of the project + * @throws {Error} if namespace can not be determined + */ + async _getNamespace() { + try { + return await this._getNamespaceFromManifestJson(); + } catch (manifestJsonError) { + if (manifestJsonError.code !== "ENOENT") { + throw manifestJsonError; + } + // No manifest.json present + // => attempt fallback to manifest.appdescr_variant (typical for App Variants) + try { + return await this._getNamespaceFromManifestAppDescVariant(); + } catch (appDescVarError) { + if (appDescVarError.code === "ENOENT") { + // Fallback not possible: No manifest.appdescr_variant present + // => Throw error indicating missing manifest.json + // (do not mention manifest.appdescr_variant since it is only + // relevant for the rather "uncommon" App Variants) + throw new Error( + `Could not find required manifest.json for project ` + + `${this.getName()}: ${manifestJsonError.message}`); + } + throw appDescVarError; + } + } + } + + /** + * Determine application namespace by checking manifest.json. + * Any maven placeholders are resolved from the projects pom.xml + * + * @returns {string} Namespace of the project + * @throws {Error} if namespace can not be determined + */ + async _getNamespaceFromManifestJson() { + const manifest = await this._getManifest("/manifest.json"); + let appId; + // check for a proper sap.app/id in manifest.json to determine namespace + if (manifest["sap.app"] && manifest["sap.app"].id) { + appId = manifest["sap.app"].id; + } else { + throw new Error( + `No sap.app/id configuration found in manifest.json of project ${this.getName()}`); + } + + if (this._hasMavenPlaceholder(appId)) { + try { + appId = await this._resolveMavenPlaceholder(appId); + } catch (err) { + throw new Error( + `Failed to resolve namespace of project ${this.getName()}: ${err.message}`); + } + } + const namespace = appId.replace(/\./g, "/"); + this._log.verbose( + `Namespace of project ${this.getName()} is ${namespace} (from manifest.json)`); + return namespace; + } + + /** + * Determine application namespace by checking manifest.appdescr_variant. + * + * @returns {string} Namespace of the project + * @throws {Error} if namespace can not be determined + */ + async _getNamespaceFromManifestAppDescVariant() { + const manifest = await this._getManifest("/manifest.appdescr_variant"); + let appId; + // check for the id property in manifest.appdescr_variant to determine namespace + if (manifest && manifest.id) { + appId = manifest.id; + } else { + throw new Error( + `No "id" property found in manifest.appdescr_variant of project ${this.getName()}`); + } + + const namespace = appId.replace(/\./g, "/"); + this._log.verbose( + `Namespace of project ${this.getName()} is ${namespace} (from manifest.appdescr_variant)`); + return namespace; + } + + /** + * Reads and parses a JSON file with the provided name from the projects source directory + * + * @param {string} filePath Name of the JSON file to read. Typically "manifest.json" or "manifest.appdescr_variant" + * @returns {Promise} resolves with an object containing the content requested manifest file + */ + async _getManifest(filePath) { + if (this._pManifests[filePath]) { + return this._pManifests[filePath]; + } + return this._pManifests[filePath] = this._getRawSourceReader().byPath(filePath) + .then(async (resource) => { + if (!resource) { + throw new Error( + `Could not find resource ${filePath} in project ${this.getName()}`); + } + return JSON.parse(await resource.getString()); + }).catch((err) => { + throw new Error( + `Failed to read ${filePath} for project ` + + `${this.getName()}: ${err.message}`); + }); + } + + async _ensureComponent() { + // Ensure that a Component.js exists + const componentResource = await this._getRawSourceReader().byPath("/Component.js"); + if (!componentResource) { + throw new Error( + `Unable to find required file Component.js in component project ${this.getName()}`); + } + } +} + +export default Component; diff --git a/packages/project/lib/validation/schema/specVersion/kind/project.json b/packages/project/lib/validation/schema/specVersion/kind/project.json index 278dd340d6a..34b664d9984 100644 --- a/packages/project/lib/validation/schema/specVersion/kind/project.json +++ b/packages/project/lib/validation/schema/specVersion/kind/project.json @@ -14,6 +14,7 @@ "enum": [ "application", "library", + "component", "theme-library", "module" ] @@ -61,6 +62,16 @@ }, "then": { "$ref": "project/module.json" + }, + "else": { + "if": { + "properties": { + "type": {"const": "component"} + } + }, + "then": { + "$ref": "project/component.json" + } } } } diff --git a/packages/project/lib/validation/schema/specVersion/kind/project/component.json b/packages/project/lib/validation/schema/specVersion/kind/project/component.json new file mode 100644 index 00000000000..15e32a8f58e --- /dev/null +++ b/packages/project/lib/validation/schema/specVersion/kind/project/component.json @@ -0,0 +1,110 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/kind/project/component.json", + + "type": "object", + "required": ["specVersion", "type", "metadata"], + "if": { + "properties": { + "specVersion": { "enum": ["5.0"] } + } + }, + "then": { + "additionalProperties": false, + "properties": { + "specVersion": { "enum": ["5.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["component"] + }, + "metadata": { + "$ref": "../project.json#/definitions/metadata-3.0" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder-specVersion-4.0" + }, + "server": { + "$ref": "../project.json#/definitions/server" + }, + "customConfiguration": { + "type": "object", + "additionalProperties": true + } + } + }, + "else": { + }, + + "definitions": { + + "resources": { + "type": "object", + "additionalProperties": false, + "properties": { + "configuration": { + "type": "object", + "additionalProperties": false, + "properties": { + "propertiesFileSourceEncoding": { + "$ref": "../project.json#/definitions/resources-configuration-propertiesFileSourceEncoding" + }, + "paths": { + "type": "object", + "additionalProperties": false, + "properties": { + "src": { + "type": "string" + }, + "test": { + "type": "string" + } + } + } + } + } + } + }, + + "builder-specVersion-4.0": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "cachebuster": { + "type": "object", + "additionalProperties": false, + "properties": { + "signatureType": { + "enum": ["time", "hash"] + } + } + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles-4.0" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + }, + "minification": { + "$ref": "../project.json#/definitions/builder-minification" + }, + "settings": { + "$ref": "../project.json#/definitions/builder-settings" + } + } + } + } +} diff --git a/packages/project/test/fixtures/component.a/middleware.a.js b/packages/project/test/fixtures/component.a/middleware.a.js new file mode 100644 index 00000000000..ea41b01de46 --- /dev/null +++ b/packages/project/test/fixtures/component.a/middleware.a.js @@ -0,0 +1 @@ +module.exports = function () {}; diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/package.json new file mode 100644 index 00000000000..2179673d41d --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/package.json @@ -0,0 +1,17 @@ +{ + "name": "library.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "ui5": { + "name": "library.a", + "type": "library", + "settings": { + "src": "src", + "test": "test" + } + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/.library b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/.library new file mode 100644 index 00000000000..25c8603f31a --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/.library @@ -0,0 +1,17 @@ + + + + library.a + SAP SE + ${copyright} + ${version} + + Library A + + + + library.d + + + + diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less new file mode 100644 index 00000000000..ff0f1d5e3df --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less @@ -0,0 +1,6 @@ +@libraryAColor1: lightgoldenrodyellow; + +.library-a-foo { + color: @libraryAColor1; + padding: 1px 2px 3px 4px; +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/test/library/a/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/test/library/a/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/ui5.yaml new file mode 100644 index 00000000000..8d4784313c3 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.a diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/package.json new file mode 100644 index 00000000000..2a0243b1683 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.b", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/src/library/b/.library b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/src/library/b/.library new file mode 100644 index 00000000000..36052acebdc --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/src/library/b/.library @@ -0,0 +1,17 @@ + + + + library.b + SAP SE + ${copyright} + ${version} + + Library B + + + + library.d + + + + diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/test/library/b/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/test/library/b/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/ui5.yaml new file mode 100644 index 00000000000..b2fe5be59ee --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.b diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/package.json new file mode 100644 index 00000000000..64ac75d6ffe --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.c", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/src/library/c/.library b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/src/library/c/.library new file mode 100644 index 00000000000..4180ce2af2f --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/src/library/c/.library @@ -0,0 +1,17 @@ + + + + library.c + SAP SE + ${copyright} + ${version} + + Library C + + + + library.d + + + + diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/test/LibraryC/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/ui5.yaml new file mode 100644 index 00000000000..7c5e38a7fc1 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.c diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/src/library/d/.library new file mode 100644 index 00000000000..21251d1bbba --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + ${copyright} + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/package.json new file mode 100644 index 00000000000..81b948438bd --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/package.json @@ -0,0 +1,18 @@ +{ + "name": "collection", + "version": "1.0.0", + "description": "Simple Collection", + "dependencies": { + "library.d": "file:../library.d" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "collection": { + "modules": { + "library.a": "./library.a", + "library.b": "./library.b", + "library.c": "./library.c" + } + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/ui5.yaml new file mode 100644 index 00000000000..e47048de6a7 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/collection/ui5.yaml @@ -0,0 +1,12 @@ +specVersion: "2.1" +metadata: + name: application.a.collection.dependency.shim +kind: extension +type: project-shim +shims: + collections: + collection: + modules: + "library.a": "./library.a" + "library.b": "./library.b" + "library.c": "./library.c" \ No newline at end of file diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/.library new file mode 100644 index 00000000000..53c2d14c9d6 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/.library @@ -0,0 +1,11 @@ + + + + library.d + SAP SE + Some fancy copyright + ${version} + + Library D + + diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/some.js new file mode 100644 index 00000000000..81e73436075 --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/some.js @@ -0,0 +1,4 @@ +/*! + * ${copyright} + */ +console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/component.a/node_modules/library.d/main/test/library/d/Test.html new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/package.json b/packages/project/test/fixtures/component.a/node_modules/library.d/package.json new file mode 100644 index 00000000000..90c75040abe --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/library.d/package.json @@ -0,0 +1,9 @@ +{ + "name": "library.d", + "version": "1.0.0", + "description": "Simple SAPUI5 based library", + "dependencies": {}, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/library.d/ui5.yaml new file mode 100644 index 00000000000..a47c1f64c3d --- /dev/null +++ b/packages/project/test/fixtures/component.a/node_modules/library.d/ui5.yaml @@ -0,0 +1,10 @@ +--- +specVersion: "2.3" +type: library +metadata: + name: library.d +resources: + configuration: + paths: + src: main/src + test: main/test diff --git a/packages/project/test/fixtures/component.a/package.json b/packages/project/test/fixtures/component.a/package.json new file mode 100644 index 00000000000..cd7457d2ba8 --- /dev/null +++ b/packages/project/test/fixtures/component.a/package.json @@ -0,0 +1,13 @@ +{ + "name": "component.a", + "version": "1.0.0", + "description": "Simple SAPUI5 based component", + "main": "index.html", + "dependencies": { + "library.d": "file:../library.d", + "collection": "file:../collection" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + } +} diff --git a/packages/project/test/fixtures/component.a/src/Component.js b/packages/project/test/fixtures/component.a/src/Component.js new file mode 100644 index 00000000000..6fd2c3ac2ba --- /dev/null +++ b/packages/project/test/fixtures/component.a/src/Component.js @@ -0,0 +1,8 @@ +sap.ui.define(["sap/ui/core/UIComponent"], function(UIComponent){ + "use strict"; + return UIComponent.extend('application.a.Component', { + metadata: { + manifest: "json" + } + }); +}); diff --git a/packages/project/test/fixtures/component.a/src/index.html b/packages/project/test/fixtures/component.a/src/index.html new file mode 100644 index 00000000000..77b0207cc80 --- /dev/null +++ b/packages/project/test/fixtures/component.a/src/index.html @@ -0,0 +1,9 @@ + + + + Application A + + + + + \ No newline at end of file diff --git a/packages/project/test/fixtures/component.a/src/manifest.json b/packages/project/test/fixtures/component.a/src/manifest.json new file mode 100644 index 00000000000..781945df9dc --- /dev/null +++ b/packages/project/test/fixtures/component.a/src/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "id1", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} \ No newline at end of file diff --git a/packages/project/test/fixtures/component.a/src/test.js b/packages/project/test/fixtures/component.a/src/test.js new file mode 100644 index 00000000000..a3df410c341 --- /dev/null +++ b/packages/project/test/fixtures/component.a/src/test.js @@ -0,0 +1,5 @@ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test(); diff --git a/packages/project/test/fixtures/component.a/task.a.js b/packages/project/test/fixtures/component.a/task.a.js new file mode 100644 index 00000000000..ea41b01de46 --- /dev/null +++ b/packages/project/test/fixtures/component.a/task.a.js @@ -0,0 +1 @@ +module.exports = function () {}; diff --git a/packages/project/test/fixtures/component.a/ui5-test-configPath.yaml b/packages/project/test/fixtures/component.a/ui5-test-configPath.yaml new file mode 100644 index 00000000000..9dfaec758bf --- /dev/null +++ b/packages/project/test/fixtures/component.a/ui5-test-configPath.yaml @@ -0,0 +1,7 @@ +--- +specVersion: "3.1" +type: component +metadata: + name: component.a +customConfiguration: + configPathTest: true \ No newline at end of file diff --git a/packages/project/test/fixtures/component.a/ui5-test-corrupt.yaml b/packages/project/test/fixtures/component.a/ui5-test-corrupt.yaml new file mode 100644 index 00000000000..ecce9d7e78b --- /dev/null +++ b/packages/project/test/fixtures/component.a/ui5-test-corrupt.yaml @@ -0,0 +1 @@ +|-\nfoo\nbar diff --git a/packages/project/test/fixtures/component.a/ui5-test-empty.yaml b/packages/project/test/fixtures/component.a/ui5-test-empty.yaml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/project/test/fixtures/component.a/ui5-test-error.yaml b/packages/project/test/fixtures/component.a/ui5-test-error.yaml new file mode 100644 index 00000000000..639b4889a65 --- /dev/null +++ b/packages/project/test/fixtures/component.a/ui5-test-error.yaml @@ -0,0 +1,7 @@ +--- +specVersion: "3.1" +type: component +metadata: + name: component.a +xyz: + foo: true \ No newline at end of file diff --git a/packages/project/test/fixtures/component.a/ui5.yaml b/packages/project/test/fixtures/component.a/ui5.yaml new file mode 100644 index 00000000000..f149b0e8724 --- /dev/null +++ b/packages/project/test/fixtures/component.a/ui5.yaml @@ -0,0 +1,5 @@ +--- +specVersion: "3.1" +type: component +metadata: + name: component.a diff --git a/packages/project/test/fixtures/component.h/pom.xml b/packages/project/test/fixtures/component.h/pom.xml new file mode 100644 index 00000000000..7ee5daf7afc --- /dev/null +++ b/packages/project/test/fixtures/component.h/pom.xml @@ -0,0 +1,41 @@ + + + + + + + 4.0.0 + + + + + com.sap.test + component.h + 1.0.0 + war + + + + + component.h + Simple SAPUI5 based component + + + + + + + component.h + + + + + diff --git a/packages/project/test/fixtures/component.h/src-no-component/manifest.json b/packages/project/test/fixtures/component.h/src-no-component/manifest.json new file mode 100644 index 00000000000..7d63e359cdf --- /dev/null +++ b/packages/project/test/fixtures/component.h/src-no-component/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "${componentName}", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/packages/project/test/fixtures/component.h/src-project.artifactId/Component.js b/packages/project/test/fixtures/component.h/src-project.artifactId/Component.js new file mode 100644 index 00000000000..cb9bd406864 --- /dev/null +++ b/packages/project/test/fixtures/component.h/src-project.artifactId/Component.js @@ -0,0 +1,8 @@ +sap.ui.define(["sap/ui/core/UIComponent"], function(UIComponent){ + "use strict"; + return UIComponent.extend('application.h.Component', { + metadata: { + manifest: "json" + } + }); +}); diff --git a/packages/project/test/fixtures/component.h/src-project.artifactId/manifest.json b/packages/project/test/fixtures/component.h/src-project.artifactId/manifest.json new file mode 100644 index 00000000000..7de6072ce82 --- /dev/null +++ b/packages/project/test/fixtures/component.h/src-project.artifactId/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "${project.artifactId}", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/packages/project/test/fixtures/component.h/src-properties.appId/Component.js b/packages/project/test/fixtures/component.h/src-properties.appId/Component.js new file mode 100644 index 00000000000..cb9bd406864 --- /dev/null +++ b/packages/project/test/fixtures/component.h/src-properties.appId/Component.js @@ -0,0 +1,8 @@ +sap.ui.define(["sap/ui/core/UIComponent"], function(UIComponent){ + "use strict"; + return UIComponent.extend('application.h.Component', { + metadata: { + manifest: "json" + } + }); +}); diff --git a/packages/project/test/fixtures/component.h/src-properties.appId/manifest.json b/packages/project/test/fixtures/component.h/src-properties.appId/manifest.json new file mode 100644 index 00000000000..e1515df7025 --- /dev/null +++ b/packages/project/test/fixtures/component.h/src-properties.appId/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "${appId}", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/packages/project/test/fixtures/component.h/src-properties.componentName/Component.js b/packages/project/test/fixtures/component.h/src-properties.componentName/Component.js new file mode 100644 index 00000000000..cb9bd406864 --- /dev/null +++ b/packages/project/test/fixtures/component.h/src-properties.componentName/Component.js @@ -0,0 +1,8 @@ +sap.ui.define(["sap/ui/core/UIComponent"], function(UIComponent){ + "use strict"; + return UIComponent.extend('application.h.Component', { + metadata: { + manifest: "json" + } + }); +}); diff --git a/packages/project/test/fixtures/component.h/src-properties.componentName/manifest.json b/packages/project/test/fixtures/component.h/src-properties.componentName/manifest.json new file mode 100644 index 00000000000..7d63e359cdf --- /dev/null +++ b/packages/project/test/fixtures/component.h/src-properties.componentName/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "${componentName}", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/packages/project/test/fixtures/component.h/webapp/Component.js b/packages/project/test/fixtures/component.h/webapp/Component.js new file mode 100644 index 00000000000..cb9bd406864 --- /dev/null +++ b/packages/project/test/fixtures/component.h/webapp/Component.js @@ -0,0 +1,8 @@ +sap.ui.define(["sap/ui/core/UIComponent"], function(UIComponent){ + "use strict"; + return UIComponent.extend('application.h.Component', { + metadata: { + manifest: "json" + } + }); +}); diff --git a/packages/project/test/fixtures/component.h/webapp/manifest.json b/packages/project/test/fixtures/component.h/webapp/manifest.json new file mode 100644 index 00000000000..32b7e4a8458 --- /dev/null +++ b/packages/project/test/fixtures/component.h/webapp/manifest.json @@ -0,0 +1,13 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "application.h", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": ["embedded"], + "title": "{{title}}" + } +} diff --git a/packages/project/test/fixtures/component.h/webapp/sectionsA/section1.js b/packages/project/test/fixtures/component.h/webapp/sectionsA/section1.js new file mode 100644 index 00000000000..ac4a8129651 --- /dev/null +++ b/packages/project/test/fixtures/component.h/webapp/sectionsA/section1.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 1 included"); +}); diff --git a/packages/project/test/fixtures/component.h/webapp/sectionsA/section2.js b/packages/project/test/fixtures/component.h/webapp/sectionsA/section2.js new file mode 100644 index 00000000000..e009c828602 --- /dev/null +++ b/packages/project/test/fixtures/component.h/webapp/sectionsA/section2.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 2 included"); +}); diff --git a/packages/project/test/fixtures/component.h/webapp/sectionsA/section3.js b/packages/project/test/fixtures/component.h/webapp/sectionsA/section3.js new file mode 100644 index 00000000000..5fd9349d49b --- /dev/null +++ b/packages/project/test/fixtures/component.h/webapp/sectionsA/section3.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 3 included"); +}); diff --git a/packages/project/test/fixtures/component.h/webapp/sectionsB/section1.js b/packages/project/test/fixtures/component.h/webapp/sectionsB/section1.js new file mode 100644 index 00000000000..ac4a8129651 --- /dev/null +++ b/packages/project/test/fixtures/component.h/webapp/sectionsB/section1.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 1 included"); +}); diff --git a/packages/project/test/fixtures/component.h/webapp/sectionsB/section2.js b/packages/project/test/fixtures/component.h/webapp/sectionsB/section2.js new file mode 100644 index 00000000000..e009c828602 --- /dev/null +++ b/packages/project/test/fixtures/component.h/webapp/sectionsB/section2.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 2 included"); +}); diff --git a/packages/project/test/fixtures/component.h/webapp/sectionsB/section3.js b/packages/project/test/fixtures/component.h/webapp/sectionsB/section3.js new file mode 100644 index 00000000000..5fd9349d49b --- /dev/null +++ b/packages/project/test/fixtures/component.h/webapp/sectionsB/section3.js @@ -0,0 +1,3 @@ +sap.ui.define(["sap/m/Button"], function(Button) { + console.log("Section 3 included"); +}); diff --git a/packages/project/test/lib/specifications/types/Component.js b/packages/project/test/lib/specifications/types/Component.js new file mode 100644 index 00000000000..1f4da1e9ae1 --- /dev/null +++ b/packages/project/test/lib/specifications/types/Component.js @@ -0,0 +1,688 @@ +import test from "ava"; +import path from "node:path"; +import {fileURLToPath} from "node:url"; +import {createResource} from "@ui5/fs/resourceFactory"; +import sinonGlobal from "sinon"; +import Specification from "../../../../lib/specifications/Specification.js"; +import Component from "../../../../lib/specifications/types/Component.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const componentAPath = path.join(__dirname, "..", "..", "..", "fixtures", "component.a"); +const componentHPath = path.join(__dirname, "..", "..", "..", "fixtures", "component.h"); + +test.beforeEach((t) => { + t.context.sinon = sinonGlobal.createSandbox(); + t.context.projectInput = { + id: "component.a.id", + version: "1.0.0", + modulePath: componentAPath, + configuration: { + specVersion: "3.1", + kind: "project", + type: "component", + metadata: {name: "component.a"} + } + }; + + t.context.componentHInput = { + id: "component.h.id", + version: "1.0.0", + modulePath: componentHPath, + configuration: { + specVersion: "3.1", + kind: "project", + type: "component", + metadata: {name: "component.h"}, + resources: { + configuration: { + paths: { + src: "webapp" + } + } + } + } + }; +}); + +test.afterEach.always((t) => { + t.context.sinon.restore(); +}); + +test("Correct class", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.true(project instanceof Component, `Is an instance of the Component class`); +}); + +test("getNamespace", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.is(project.getNamespace(), "id1", + "Returned correct namespace"); +}); + +test("getSourcePath", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.is(project.getSourcePath(), path.join(componentAPath, "src"), + "Returned correct source path"); +}); + +test("getCachebusterSignatureType: Default", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + t.is(project.getCachebusterSignatureType(), "time", + "Returned correct default cachebuster signature type configuration"); +}); + +test("getCachebusterSignatureType: Configuration", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.builder = { + cachebuster: { + signatureType: "hash" + } + }; + const project = await Specification.create(projectInput); + t.is(project.getCachebusterSignatureType(), "hash", + "Returned correct default cachebuster signature type configuration"); +}); + +test("Access project resources via reader: buildtime style", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const reader = project.getReader(); + const resource = await reader.byPath("/resources/id1/manifest.json"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/id1/manifest.json", "Resource has correct path"); +}); + +test("Access project resources via reader: flat style", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const reader = project.getReader({style: "flat"}); + const resource = await reader.byPath("/manifest.json"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/manifest.json", "Resource has correct path"); +}); + +test("Access project resources via reader: runtime style", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const reader = project.getReader({style: "runtime"}); + const resource = await reader.byPath("/resources/id1/manifest.json"); + t.truthy(resource, "Found the requested resource"); + t.is(resource.getPath(), "/resources/id1/manifest.json", "Resource has correct path"); +}); + +test("Access project resources via reader w/ builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["**/manifest.json"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for flat style"); + + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found excluded resource for runtime style"); +}); + +test("Access project resources via workspace w/ builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["**/manifest.json"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getWorkspace().byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Access project resources w/ absolute builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["/resources/id1/manifest.json"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found excluded resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/manifest.json")).length, 0, + "Did not find excluded resource for default style"); +}); + +test("Access project resources w/ relative builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["manifest.json"] // Has no effect since component excludes must be absolute or use wildcards + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/manifest.json")).length, 1, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 1, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 1, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 1, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found excluded resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/manifest.json")).length, 1, + "Did not find excluded resource for default style"); +}); + +test("Access project resources w/ incorrect builder excludes", async (t) => { + const {projectInput} = t.context; + const baselineProject = await Specification.create(projectInput); + + projectInput.configuration.builder = { + resources: { + excludes: ["/manifest.json"] + } + }; + const excludesProject = await Specification.create(projectInput); + + // We now have two projects: One with excludes and one without + // Always compare the results of both to make sure a file is really excluded because of the + // configuration and not because of a typo or because of it's absence in the fixture + + t.is((await baselineProject.getReader({}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getReader({}).byGlob("**/manifest.json")).length, 1, + "Did not find excluded resource for default style"); + + t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for buildtime style"); + t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/manifest.json")).length, 1, + "Did not find excluded resource for buildtime style"); + + t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for dist style"); + t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/manifest.json")).length, 1, + "Did not find excluded resource for dist style"); + + t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for flat style"); + t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/manifest.json")).length, 1, + "Did not find excluded resource for flat style"); + + // Excludes are not applied for "runtime" style + t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for runtime style"); + t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/manifest.json")).length, 1, + "Found excluded resource for runtime style"); + + t.is((await baselineProject.getWorkspace().byGlob("**/manifest.json")).length, 1, + "Found resource in baseline project for default style"); + t.is((await excludesProject.getWorkspace().byGlob("**/manifest.json")).length, 1, + "Did not find excluded resource for default style"); +}); + +test("Modify project resources via workspace and access via flat and runtime readers", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + const workspace = project.getWorkspace(); + const workspaceResource = await workspace.byPath("/resources/id1/index.html"); + t.truthy(workspaceResource, "Found resource in workspace"); + + const newContent = (await workspaceResource.getString()).replace("Component A", "Some Name"); + workspaceResource.setString(newContent); + await workspace.write(workspaceResource); + + const flatReader = project.getReader({style: "flat"}); + const flatReaderResource = await flatReader.byPath("/index.html"); + t.truthy(flatReaderResource, "Found the requested resource byPath"); + t.is(flatReaderResource.getPath(), "/index.html", "Resource (byPath) has correct path"); + t.is(await flatReaderResource.getString(), newContent, "Found resource (byPath) has expected (changed) content"); + + const flatGlobResult = await flatReader.byGlob("**/index.html"); + t.is(flatGlobResult.length, 1, "Found the requested resource byGlob"); + t.is(flatGlobResult[0].getPath(), "/index.html", "Resource (byGlob) has correct path"); + t.is(await flatGlobResult[0].getString(), newContent, "Found resource (byGlob) has expected (changed) content"); + + const runtimeReader = project.getReader({style: "runtime"}); + const runtimeReaderResource = await runtimeReader.byPath("/resources/id1/index.html"); + t.truthy(runtimeReaderResource, "Found the requested resource byPath"); + t.is(runtimeReaderResource.getPath(), "/resources/id1/index.html", "Resource (byPath) has correct path"); + t.is(await runtimeReaderResource.getString(), newContent, "Found resource (byPath) has expected (changed) content"); + + const runtimeGlobResult = await runtimeReader.byGlob("**/index.html"); + t.is(runtimeGlobResult.length, 1, "Found the requested resource byGlob"); + t.is(runtimeGlobResult[0].getPath(), "/resources/id1/index.html", "Resource (byGlob) has correct path"); + 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 {projectInput} = t.context; + const project = await Specification.create(projectInput); + const workspace = project.getWorkspace(); + + await workspace.write(createResource({ + path: "/resources/my-custom-bundle.js" + })); + + const buildtimeReader = 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 = 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 = 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 {projectInput} = t.context; + const project = await Specification.create(projectInput); + + t.is(project._srcPath, "src", "Correct default path"); +}); + +test("_configureAndValidatePaths: Custom src directory", async (t) => { + const componentHPath = path.join(__dirname, "..", "..", "..", "fixtures", "component.h"); + const projectInput = { + id: "component.h.id", + version: "1.0.0", + modulePath: componentHPath, + configuration: { + specVersion: "3.1", + kind: "project", + type: "component", + metadata: {name: "component.h"}, + resources: { + configuration: { + paths: { + src: "src-properties.componentName" + } + } + } + } + }; + + const project = await Specification.create(projectInput); + + t.is(project._srcPath, "src-properties.componentName", "Correct path for src"); +}); + +test("_configureAndValidatePaths: src directory does not exist", async (t) => { + const {projectInput} = t.context; + projectInput.configuration.resources = { + configuration: { + paths: { + src: "does/not/exist" + } + } + }; + const err = await t.throwsAsync(Specification.create(projectInput)); + + t.is(err.message, "Unable to find source directory 'does/not/exist' in component project component.a"); +}); + +test("_getNamespaceFromManifestJson: No 'sap.app' configuration found", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + sinon.stub(project, "_getManifest").resolves({}); + + const error = await t.throwsAsync(project._getNamespaceFromManifestJson()); + t.is(error.message, "No sap.app/id configuration found in manifest.json of project component.a", + "Rejected with correct error message"); +}); + +test("_getNamespaceFromManifestJson: No component id in 'sap.app' configuration found", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + sinon.stub(project, "_getManifest").resolves({"sap.app": {}}); + + const error = await t.throwsAsync(project._getNamespaceFromManifestJson()); + t.is(error.message, "No sap.app/id configuration found in manifest.json of project component.a"); +}); + +test("_getNamespaceFromManifestJson: set namespace to id", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + sinon.stub(project, "_getManifest").resolves({"sap.app": {id: "my.id"}}); + + const namespace = await project._getNamespaceFromManifestJson(); + t.is(namespace, "my/id", "Returned correct namespace"); +}); + +test("_getNamespaceFromManifestAppDescVariant: No 'id' property found", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + sinon.stub(project, "_getManifest").resolves({}); + + const error = await t.throwsAsync(project._getNamespaceFromManifestAppDescVariant()); + t.is(error.message, `No "id" property found in manifest.appdescr_variant of project component.a`, + "Rejected with correct error message"); +}); + +test("_getNamespaceFromManifestAppDescVariant: set namespace to id", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + sinon.stub(project, "_getManifest").resolves({id: "my.id"}); + + const namespace = await project._getNamespaceFromManifestAppDescVariant(); + t.is(namespace, "my/id", "Returned correct namespace"); +}); + +test("_getNamespace: Correct fallback to manifest.appdescr_variant if manifest.json is missing", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects({code: "ENOENT"}) + .onSecondCall().resolves({id: "my.id"}); + + const namespace = await project._getNamespace(); + t.is(namespace, "my/id", "Returned correct namespace"); + t.is(_getManifestStub.callCount, 2, "_getManifest called exactly twice"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json first"); + t.is(_getManifestStub.getCall(1).args[0], "/manifest.appdescr_variant", + "_getManifest called for manifest.appdescr_variant in fallback"); +}); + +test("_getNamespace: Correct error message if fallback to manifest.appdescr_variant failed", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects({code: "ENOENT"}) + .onSecondCall().rejects(new Error("EPON: Pony Error")); + + const error = await t.throwsAsync(project._getNamespace()); + t.is(error.message, "EPON: Pony Error", + "Rejected with correct error message"); + t.is(_getManifestStub.callCount, 2, "_getManifest called exactly twice"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json first"); + t.is(_getManifestStub.getCall(1).args[0], "/manifest.appdescr_variant", + "_getManifest called for manifest.appdescr_variant in fallback"); +}); + +test("_getNamespace: Correct error message if fallback to manifest.appdescr_variant is not possible", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects({message: "No such stable or directory: manifest.json", code: "ENOENT"}) + .onSecondCall().rejects({code: "ENOENT"}); // both files are missing + + const error = await t.throwsAsync(project._getNamespace()); + t.deepEqual(error.message, + "Could not find required manifest.json for project component.a: " + + "No such stable or directory: manifest.json", + "Rejected with correct error message"); + + t.is(_getManifestStub.callCount, 2, "_getManifest called exactly twice"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json first"); + t.is(_getManifestStub.getCall(1).args[0], "/manifest.appdescr_variant", + "_getManifest called for manifest.appdescr_variant in fallback"); +}); + +test("_getNamespace: No fallback if manifest.json is present but failed to parse", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + const _getManifestStub = sinon.stub(project, "_getManifest") + .onFirstCall().rejects(new Error("EPON: Pony Error")); + + const error = await t.throwsAsync(project._getNamespace()); + t.is(error.message, "EPON: Pony Error", + "Rejected with correct error message"); + + t.is(_getManifestStub.callCount, 1, "_getManifest called exactly once"); + t.is(_getManifestStub.getCall(0).args[0], "/manifest.json", "_getManifest called for manifest.json only"); +}); + +test("_getManifest: reads correctly", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + + const content = await project._getManifest("/manifest.json"); + t.is(content._version, "1.1.0", "manifest.json content has been read"); +}); + +test("_getManifest: invalid JSON", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + + const byPathStub = sinon.stub().resolves({ + getString: async () => "no json" + }); + + project._getRawSourceReader = () => { + return { + byPath: byPathStub + }; + }; + + const error = await t.throwsAsync(project._getManifest("/some-manifest.json")); + t.regex(error.message, /^Failed to read \/some-manifest\.json for project component\.a: /, + "Rejected with correct error message"); + t.is(byPathStub.callCount, 1, "byPath got called once"); + t.is(byPathStub.getCall(0).args[0], "/some-manifest.json", "byPath got called with the correct argument"); +}); + +test.serial("_getManifest: File does not exist", async (t) => { + const {projectInput} = t.context; + const project = await Specification.create(projectInput); + + const error = await t.throwsAsync(project._getManifest("/does-not-exist.json")); + t.deepEqual(error.message, + "Failed to read /does-not-exist.json for project component.a: " + + "Could not find resource /does-not-exist.json in project component.a", + "Rejected with correct error message"); +}); + +test.serial("_getManifest: result is cached", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + + const byPathStub = sinon.stub().resolves({ + getString: async () => `{"pony": "no unicorn"}` + }); + + project._getRawSourceReader = () => { + return { + byPath: byPathStub + }; + }; + + const content = await project._getManifest("/some-manifest.json"); + t.deepEqual(content, {pony: "no unicorn"}, "Correct result on first call"); + + const content2 = await project._getManifest("/some-other-manifest.json"); + t.deepEqual(content2, {pony: "no unicorn"}, "Correct result on second call"); + + t.is(byPathStub.callCount, 2, "byPath got called exactly twice (and then cached)"); +}); + +test.serial("_getManifest: Caches successes and failures", async (t) => { + const {projectInput, sinon} = t.context; + const project = await Specification.create(projectInput); + + const getStringStub = sinon.stub() + .onFirstCall().rejects(new Error("EPON: Pony Error")) + .onSecondCall().resolves(`{"pony": "no unicorn"}`); + const byPathStub = sinon.stub().resolves({ + getString: getStringStub + }); + + project._getRawSourceReader = () => { + return { + byPath: byPathStub + }; + }; + + const error = await t.throwsAsync(project._getManifest("/some-manifest.json")); + t.deepEqual(error.message, + "Failed to read /some-manifest.json for project component.a: " + + "EPON: Pony Error", + "Rejected with correct error message"); + + const content = await project._getManifest("/some-other.manifest.json"); + t.deepEqual(content, {pony: "no unicorn"}, "Correct result on second call"); + + const error2 = await t.throwsAsync(project._getManifest("/some-manifest.json")); + t.deepEqual(error2.message, + "Failed to read /some-manifest.json for project component.a: " + + "EPON: Pony Error", + "From cache: Rejected with correct error message"); + + const content2 = await project._getManifest("/some-other.manifest.json"); + t.deepEqual(content2, {pony: "no unicorn"}, "From cache: Correct result on first call"); + + t.is(byPathStub.callCount, 2, + "byPath got called exactly twice (and then cached)"); +}); + +test("namespace: detect namespace from pom.xml via ${project.artifactId}", async (t) => { + const {componentHInput} = t.context; + componentHInput.configuration.resources.configuration.paths.src = "src-project.artifactId"; + const project = await Specification.create(componentHInput); + + t.is(project.getNamespace(), "component/h", + "namespace was successfully set since getJson provides the correct object structure"); +}); + +test("namespace: detect namespace from pom.xml via ${componentName} from properties", async (t) => { + const {componentHInput} = t.context; + componentHInput.configuration.resources.configuration.paths.src = "src-properties.componentName"; + const project = await Specification.create(componentHInput); + + t.is(project.getNamespace(), "component/h", + "namespace was successfully set since getJson provides the correct object structure"); +}); + +test("namespace: detect namespace from pom.xml via ${appId} from properties", async (t) => { + const {componentHInput} = t.context; + componentHInput.configuration.resources.configuration.paths.src = "src-properties.appId"; + + const error = await t.throwsAsync(Specification.create(componentHInput)); + t.deepEqual(error.message, "Failed to resolve namespace of project component.h: \"${appId}\"" + + " couldn't be resolved from maven property \"appId\" of pom.xml of project component.h"); +}); + +test("Throw for missing Component.js", async (t) => { + const {componentHInput} = t.context; + componentHInput.configuration.resources.configuration.paths.src = "src-no-component"; + + const error = await t.throwsAsync(Specification.create(componentHInput)); + t.is(error.message, + "Unable to find required file Component.js in component project component.h"); +}); diff --git a/packages/project/test/lib/validation/schema/__helper__/builder-bundleOptions.js b/packages/project/test/lib/validation/schema/__helper__/builder-bundleOptions.js index 144590c1740..967da600b4d 100644 --- a/packages/project/test/lib/validation/schema/__helper__/builder-bundleOptions.js +++ b/packages/project/test/lib/validation/schema/__helper__/builder-bundleOptions.js @@ -5,15 +5,20 @@ import SpecificationVersion from "../../../../../lib/specifications/Specificatio */ export default { /** - * Executes the tests for different kind of projects, e.g. "application", "library" + * Executes the tests for different kind of projects, e.g. "application", "library", "component" * * @param {Function} test ava test * @param {Function} assertValidation assertion function - * @param {string} type one of "application", "library" + * @param {string} type one of "application", "library", "component" */ defineTests: function(test, assertValidation, type) { // Version specific tests SpecificationVersion.getVersionsForRange(">=4.0").forEach(function(specVersion) { + if (type === "component" && SpecificationVersion.lt(specVersion, "5.0")) { + // Component type only became available with specVersion 5.0 + return; + } + test(`${type} (specVersion ${specVersion}): builder/bundles/bundleOptions`, async (t) => { await assertValidation(t, { "specVersion": specVersion, @@ -134,6 +139,9 @@ export default { }); }); + if (type === "component") { // Component type only became available with specVersion 5.0 + return; + } SpecificationVersion.getVersionsForRange("3.0 - 3.2").forEach(function(specVersion) { test(`${type} (specVersion ${specVersion}): builder/bundles/bundleOptions`, async (t) => { await assertValidation(t, { diff --git a/packages/project/test/lib/validation/schema/__helper__/customConfiguration.js b/packages/project/test/lib/validation/schema/__helper__/customConfiguration.js index 34db358c983..66ca7633f92 100644 --- a/packages/project/test/lib/validation/schema/__helper__/customConfiguration.js +++ b/packages/project/test/lib/validation/schema/__helper__/customConfiguration.js @@ -11,33 +11,40 @@ export default { * @param {Function} test ava test * @param {Function} assertValidation assertion function * @param {string} type one of "project-shim", "server-middleware" "task", - * "application", "library", "theme-library" and "module" + * "application", "library", "component", theme-library" and "module" * @param {object} additionalConfiguration additional configuration content */ defineTests: function(test, assertValidation, type, additionalConfiguration) { additionalConfiguration = additionalConfiguration || {}; // version specific tests for customConfiguration - test(`${type}: Invalid customConfiguration (specVersion 2.0)`, async (t) => { - await assertValidation(t, Object.assign({ - "specVersion": "2.0", - "type": type, - "metadata": { - "name": "my-" + type - }, - "customConfiguration": {} - }, additionalConfiguration), [ - { - dataPath: "", - keyword: "additionalProperties", - message: "should NOT have additional properties", - params: { - additionalProperty: "customConfiguration", + + if (type !== "component") { // Component type only became available with specVersion 5.0 + test(`${type}: Invalid customConfiguration (specVersion 2.0)`, async (t) => { + await assertValidation(t, Object.assign({ + "specVersion": "2.0", + "type": type, + "metadata": { + "name": "my-" + type + }, + "customConfiguration": {} + }, additionalConfiguration), [ + { + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "customConfiguration", + } } - } - ]); - }); + ]); + }); + } SpecificationVersion.getVersionsForRange(">=2.1").forEach((specVersion) => { + if (type === "component" && SpecificationVersion.lt(specVersion, "5.0")) { + // Component type only became available with specVersion 5.0 + return; + } test(`${type}: Valid customConfiguration (specVersion ${specVersion})`, async (t) => { await assertValidation(t, Object.assign( { "specVersion": specVersion, diff --git a/packages/project/test/lib/validation/schema/__helper__/framework.js b/packages/project/test/lib/validation/schema/__helper__/framework.js index 841ce8fc790..cc122546b5c 100644 --- a/packages/project/test/lib/validation/schema/__helper__/framework.js +++ b/packages/project/test/lib/validation/schema/__helper__/framework.js @@ -6,14 +6,18 @@ import SpecificationVersion from "../../../../../lib/specifications/Specificatio export default { /** * Executes the tests for different types of kind project, - * e.g. "application", library" and "theme-library" + * e.g. "application", library", "component" and "theme-library" * * @param {Function} test ava test * @param {Function} assertValidation assertion function - * @param {string} type one of "application", library" and "theme-library" + * @param {string} type one of "application", library", "component" and "theme-library" */ defineTests: function(test, assertValidation, type) { SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => { + if (type === "component" && SpecificationVersion.lt(specVersion, "5.0")) { + // Component type only became available with specVersion 5.0 + return; + } test(`${type} (specVersion ${specVersion}): framework configuration: OpenUI5`, async (t) => { const config = { "specVersion": specVersion, diff --git a/packages/project/test/lib/validation/schema/__helper__/project.js b/packages/project/test/lib/validation/schema/__helper__/project.js index cbac64534f7..c2a4cae1d5b 100644 --- a/packages/project/test/lib/validation/schema/__helper__/project.js +++ b/packages/project/test/lib/validation/schema/__helper__/project.js @@ -9,15 +9,15 @@ import bundleOptions from "./builder-bundleOptions.js"; export default { /** * Executes the tests for different types of kind project, - * e.g. "application", "library", "theme-library" and "module" + * e.g. "application", "library", "component", "theme-library" and "module" * * @param {Function} test ava test * @param {Function} assertValidation assertion function - * @param {string} type one of "application", "library", "theme-library" and "module" + * @param {string} type one of "application", "library", "component", "theme-library" and "module" */ defineTests: function(test, assertValidation, type) { // framework tests - if (["application", "library", "theme-library"].includes(type)) { + if (["application", "library", "component", "theme-library"].includes(type)) { framework.defineTests(test, assertValidation, type); } @@ -25,12 +25,16 @@ export default { customConfiguration.defineTests(test, assertValidation, type); // builder.bundleOptions tests - if (["application", "library"].includes(type)) { + if (["application", "library", "component"].includes(type)) { bundleOptions.defineTests(test, assertValidation, type); } // version specific tests SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => { + if (type === "component" && SpecificationVersion.lt(specVersion, "5.0")) { + // Component type only became available with specVersion 5.0 + return; + } // tests for all kinds and version 2.0 and above test(`${type} (specVersion ${specVersion}): No metadata`, async (t) => { await assertValidation(t, { @@ -261,28 +265,34 @@ export default { }); }); - ["2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"].forEach((specVersion) => { - test(`${type} (specVersion ${specVersion}): Invalid metadata.name`, async (t) => { - await assertValidation(t, { - "specVersion": specVersion, - "type": type, - "metadata": { - "name": {} - } - }, [ - { - dataPath: "/metadata/name", - keyword: "type", - message: "should be string", - params: { - type: "string" + if (type !== "component") { + ["2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"].forEach((specVersion) => { + test(`${type} (specVersion ${specVersion}): Invalid metadata.name`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": type, + "metadata": { + "name": {} } - } - ]); + }, [ + { + dataPath: "/metadata/name", + keyword: "type", + message: "should be string", + params: { + type: "string" + } + } + ]); + }); }); - }); + } SpecificationVersion.getVersionsForRange(">=3.0").forEach((specVersion) => { + if (type === "component" && SpecificationVersion.lt(specVersion, "5.0")) { + // Component type only became available with specVersion 5.0 + return; + } test(`${type} (specVersion ${specVersion}): Invalid metadata.name`, async (t) => { await assertValidation(t, { "specVersion": specVersion, diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project.js b/packages/project/test/lib/validation/schema/specVersion/kind/project.js index ba9d09ca579..5a17601f3a8 100644 --- a/packages/project/test/lib/validation/schema/specVersion/kind/project.js +++ b/packages/project/test/lib/validation/schema/specVersion/kind/project.js @@ -81,6 +81,27 @@ test("Type library (no kind)", async (t) => { }); }); +test("Type component", async (t) => { + await assertValidation(t, { + "specVersion": "3.1", + "kind": "project", + "type": "component", + "metadata": { + "name": "my-component" + } + }); +}); + +test("Type component (no kind)", async (t) => { + await assertValidation(t, { + "specVersion": "3.1", + "type": "component", + "metadata": { + "name": "my-component" + } + }); +}); + test("Type theme-library", async (t) => { await assertValidation(t, { "specVersion": "2.0", @@ -172,6 +193,7 @@ test("Invalid type", async (t) => { allowedValues: [ "application", "library", + "component", "theme-library", "module", ], diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project/component.js b/packages/project/test/lib/validation/schema/specVersion/kind/project/component.js new file mode 100644 index 00000000000..441e78d0d55 --- /dev/null +++ b/packages/project/test/lib/validation/schema/specVersion/kind/project/component.js @@ -0,0 +1,963 @@ +import test from "ava"; +import Ajv from "ajv"; +import ajvErrors from "ajv-errors"; +import SpecificationVersion from "../../../../../../../lib/specifications/SpecificationVersion.js"; +import AjvCoverage from "../../../../../../utils/AjvCoverage.js"; +import {_Validator as Validator} from "../../../../../../../lib/validation/validator.js"; +import ValidationError from "../../../../../../../lib/validation/ValidationError.js"; +import project from "../../../__helper__/project.js"; + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + validationError.errors.forEach((error) => { + delete error.schemaPath; + if (error.params && Array.isArray(error.params.errors)) { + error.params.errors.forEach(($) => { + delete $.schemaPath; + }); + } + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator({Ajv, ajvErrors, schemaName: "ui5"}); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/kind/project/component.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-component"}); + const thresholds = { + statements: 80, + branches: 75, + functions: 100, + lines: 80 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +SpecificationVersion.getVersionsForRange(">=5.0").forEach(function(specVersion) { + test(`Valid configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "okay" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8", + "paths": { + "src": "/my/path" + } + } + }, + "builder": { + "resources": { + "excludes": [ + "/resources/some/project/name/test_results/**", + "/test-resources/**", + "!/test-resources/some/project/name/demo-app/**" + ] + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": "my-raw-section", + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "resolveConditional": true, + "renderer": true, + "sort": true + }, + { + "mode": "provided", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": false, + "resolveConditional": false, + "renderer": false, + "sort": false, + "declareRawModules": true + } + ] + }, + "bundleOptions": { + "optimize": true, + "decorateBootstrapModule": true, + "addTryCatchRestartWrapper": true + } + }, + { + "bundleDefinition": { + "name": "app.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": "some-app-preload", + "mode": "preload", + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true, + "declareRawModules": false + }, + { + "mode": "require", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": true, + "numberOfParts": 3 + } + } + ], + "componentPreload": { + "paths": [ + "some/glob/**/pattern/Component.js", + "some/other/glob/**/pattern/Component.js" + ], + "namespaces": [ + "some/namespace", + "some/other/namespace" + ] + }, + "cachebuster": { + "signatureType": "hash" + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "configuration": { + "some-key": "some value" + } + }, + { + "name": "custom-task-2", + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + }, + { + "name": "custom-task-2", + "beforeTask": "not-valid", + "configuration": false + } + ] + }, + "server": { + "settings": { + "httpPort": 1337, + "httpsPort": 1443 + }, + "customMiddleware": [ + { + "name": "myCustomMiddleware", + "mountPath": "/myapp", + "afterMiddleware": "compression", + "configuration": { + "debug": true + } + }, + { + "name": "myCustomMiddleware-2", + "beforeMiddleware": "myCustomMiddleware", + "configuration": { + "debug": true + } + } + ] + } + }); + }); + + test(`Invalid resources configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "com.sap.ui5.test" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "FOO", + "paths": { + "app": "src", + "src": { + "path": "invalid" + } + }, + "notAllowed": true + }, + "notAllowed": true + } + }, [ + { + dataPath: "/resources", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }, + { + dataPath: "/resources/configuration", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }, + { + dataPath: "/resources/configuration/propertiesFileSourceEncoding", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "UTF-8", + "ISO-8859-1" + ], + } + }, + { + dataPath: "/resources/configuration/paths", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "app", + } + }, + { + dataPath: "/resources/configuration/paths/src", + keyword: "type", + message: "should be string", + params: { + type: "string" + } + } + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "com.sap.ui5.test" + }, + "resources": { + "configuration": { + "paths": "src" + } + } + }, [ + { + dataPath: "/resources/configuration/paths", + keyword: "type", + message: "should be object", + params: { + type: "object" + } + } + ]); + }); + + test(`Invalid builder configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + // jsdoc is not supported for type component + "jsdoc": { + "excludes": [ + "some/project/name/thirdparty/**" + ] + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "name": true, + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "sort": true, + "declareModules": true + } + ] + }, + "bundleOptions": { + "optimize": true + } + }, + { + "bundleDefinition": { + "defaultFileTypes": [ + ".js", true + ], + "sections": [ + { + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true, + "declareRawModules": [] + }, + { + "mode": "provide", + "filters": "*", + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": "true", + "numberOfParts": "3", + "notAllowed": true + } + } + ], + "componentPreload": { + "path": "some/invalid/path", + "paths": "some/invalid/glob/**/pattern/Component.js", + "namespaces": "some/invalid/namespace", + }, + "libraryPreload": {} // Only supported for type library + } + }, [ + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "jsdoc" + } + }, + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "libraryPreload" + } + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "declareModules", + } + }, + { + dataPath: "/builder/bundles/0/bundleDefinition/sections/0/name", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/defaultFileTypes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0", + keyword: "required", + message: "should have required property 'mode'", + params: { + missingProperty: "mode", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0/declareRawModules", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/mode", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "raw", + "preload", + "require", + "provided", + "bundleInfo", + "depCache", + ] + } + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/filters", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions/optimize", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + } + }, + { + dataPath: "/builder/bundles/1/bundleOptions/numberOfParts", + keyword: "type", + message: "should be number", + params: { + type: "number", + } + }, + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "path", + } + }, + { + dataPath: "/builder/componentPreload/paths", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + }, + { + dataPath: "/builder/componentPreload/namespaces", + keyword: "type", + message: "should be array", + params: { + type: "array", + } + } + ]); + }); + test(`component (specVersion ${specVersion}): builder/componentPreload/excludes`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "componentPreload": { + "excludes": [ + "some/excluded/files/**", + "some/other/excluded/files/**" + ] + } + } + }); + }); + test(`Invalid builder/componentPreload/excludes configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "componentPreload": { + "excludes": "some/excluded/files/**" + } + } + }, [ + { + dataPath: "/builder/componentPreload/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "componentPreload": { + "excludes": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/componentPreload/excludes/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/componentPreload/excludes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/componentPreload/excludes/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); + test(`component (specVersion ${specVersion}): builder/bundles/bundleDefinition/sections/mode: bundleInfo`, + async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "bundles": [{ + "bundleDefinition": { + "name": "my-bundle.js", + "sections": [{ + "name": "my-bundle-info", + "mode": "bundleInfo", + "filters": [] + }] + } + }] + } + }); + }); + test(`component (specVersion ${specVersion}): builder/settings/includeDependency*`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": [ + "sap.a", + "sap.b" + ], + "includeDependencyRegExp": [ + ".ui.[a-z]+", + "^sap.[mf]$" + ], + "includeDependencyTree": [ + "sap.c", + "sap.d" + ] + } + } + }); + }); + test(`Invalid builder/settings/includeDependency* configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": "a", + "includeDependencyRegExp": "b", + "includeDependencyTree": "c" + } + } + }, [ + { + dataPath: "/builder/settings/includeDependency", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "settings": { + "includeDependency": [ + true, + 1, + {} + ], + "includeDependencyRegExp": [ + true, + 1, + {} + ], + "includeDependencyTree": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/settings", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/settings/includeDependency/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependency/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependency/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyRegExp/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/settings/includeDependencyTree/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); + test(`component (specVersion ${specVersion}): builder/minification/excludes`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "kind": "project", + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "minification": { + "excludes": [ + "some/excluded/files/**", + "some/other/excluded/files/**" + ] + } + } + }); + }); + test(`Invalid builder/minification/excludes configuration (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "minification": { + "excludes": "some/excluded/files/**" + } + } + }, [ + { + dataPath: "/builder/minification/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + }, + ]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + "minification": { + "excludes": [ + true, + 1, + {} + ], + "notAllowed": true + } + } + }, [ + { + dataPath: "/builder/minification", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + }, + { + dataPath: "/builder/minification/excludes/0", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/minification/excludes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + { + dataPath: "/builder/minification/excludes/2", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + }, + ]); + }); + test(`Invalid project name (specVersion ${specVersion})`, async (t) => { + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "illegal/name" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "pattern", + message: `should match pattern "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$"`, + params: { + pattern: "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$", + }, + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "a" + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "minLength", + message: "should NOT be shorter than 3 characters", + params: { + limit: 3, + }, + }] + }, + }]); + await assertValidation(t, { + "specVersion": specVersion, + "type": "component", + "metadata": { + "name": "a".repeat(81) + } + }, [{ + dataPath: "/metadata/name", + keyword: "errorMessage", + message: `Not a valid project name. It must consist of lowercase alphanumeric characters, dash, underscore, and period only. Additionally, it may contain an npm-style package scope. For details, see: https://ui5.github.io/cli/stable/pages/Configuration/#name`, + params: { + errors: [{ + dataPath: "/metadata/name", + keyword: "maxLength", + message: "should NOT be longer than 80 characters", + params: { + limit: 80, + }, + }] + }, + }]); + }); +}); + +project.defineTests(test, assertValidation, "component"); diff --git a/packages/project/test/lib/validation/schema/ui5.js b/packages/project/test/lib/validation/schema/ui5.js index be965ca7daf..adf7cee71a9 100644 --- a/packages/project/test/lib/validation/schema/ui5.js +++ b/packages/project/test/lib/validation/schema/ui5.js @@ -150,6 +150,7 @@ test("Invalid type", async (t) => { allowedValues: [ "application", "library", + "component", "theme-library", "module" ] From cf45bf4aeaea23100089c6a844c01ef4325544fc Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 5 Nov 2025 16:57:50 +0100 Subject: [PATCH 02/11] refactor(project): Restrict component type to specVersion >= 5 --- .../schema/specVersion/kind/project.json | 20 +++++++++++++ .../component.a/ui5-test-configPath.yaml | 2 +- .../fixtures/component.a/ui5-test-error.yaml | 2 +- .../test/fixtures/component.a/ui5.yaml | 2 +- .../lib/specifications/types/Component.js | 6 ++-- .../schema/specVersion/kind/project.js | 30 +++++++++++++++++-- 6 files changed, 54 insertions(+), 8 deletions(-) diff --git a/packages/project/lib/validation/schema/specVersion/kind/project.json b/packages/project/lib/validation/schema/specVersion/kind/project.json index 34b664d9984..b13407bc001 100644 --- a/packages/project/lib/validation/schema/specVersion/kind/project.json +++ b/packages/project/lib/validation/schema/specVersion/kind/project.json @@ -4,6 +4,26 @@ "type": "object", "required": ["specVersion", "type"], + "allOf": [ + { + "if": { + "required": ["type"], + "properties": { + "type": { + "const": "component" + } + } + }, + "then": { + "properties": { + "specVersion": { + "const": "5.0", + "errorMessage": "The 'component' type is only supported with specVersion '5.0' and higher." + } + } + } + } + ], "properties": { "specVersion": { "enum": ["5.0", "4.0", "3.2", "3.1", "3.0", "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"] }, "kind": { diff --git a/packages/project/test/fixtures/component.a/ui5-test-configPath.yaml b/packages/project/test/fixtures/component.a/ui5-test-configPath.yaml index 9dfaec758bf..68fa0177e47 100644 --- a/packages/project/test/fixtures/component.a/ui5-test-configPath.yaml +++ b/packages/project/test/fixtures/component.a/ui5-test-configPath.yaml @@ -1,5 +1,5 @@ --- -specVersion: "3.1" +specVersion: "5.0" type: component metadata: name: component.a diff --git a/packages/project/test/fixtures/component.a/ui5-test-error.yaml b/packages/project/test/fixtures/component.a/ui5-test-error.yaml index 639b4889a65..2aa22544a42 100644 --- a/packages/project/test/fixtures/component.a/ui5-test-error.yaml +++ b/packages/project/test/fixtures/component.a/ui5-test-error.yaml @@ -1,5 +1,5 @@ --- -specVersion: "3.1" +specVersion: "5.0" type: component metadata: name: component.a diff --git a/packages/project/test/fixtures/component.a/ui5.yaml b/packages/project/test/fixtures/component.a/ui5.yaml index f149b0e8724..fc96c5c4dcd 100644 --- a/packages/project/test/fixtures/component.a/ui5.yaml +++ b/packages/project/test/fixtures/component.a/ui5.yaml @@ -1,5 +1,5 @@ --- -specVersion: "3.1" +specVersion: "5.0" type: component metadata: name: component.a diff --git a/packages/project/test/lib/specifications/types/Component.js b/packages/project/test/lib/specifications/types/Component.js index 1f4da1e9ae1..fec0ae0c744 100644 --- a/packages/project/test/lib/specifications/types/Component.js +++ b/packages/project/test/lib/specifications/types/Component.js @@ -17,7 +17,7 @@ test.beforeEach((t) => { version: "1.0.0", modulePath: componentAPath, configuration: { - specVersion: "3.1", + specVersion: "5.0", kind: "project", type: "component", metadata: {name: "component.a"} @@ -29,7 +29,7 @@ test.beforeEach((t) => { version: "1.0.0", modulePath: componentHPath, configuration: { - specVersion: "3.1", + specVersion: "5.0", kind: "project", type: "component", metadata: {name: "component.h"}, @@ -405,7 +405,7 @@ test("_configureAndValidatePaths: Custom src directory", async (t) => { version: "1.0.0", modulePath: componentHPath, configuration: { - specVersion: "3.1", + specVersion: "5.0", kind: "project", type: "component", metadata: {name: "component.h"}, diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project.js b/packages/project/test/lib/validation/schema/specVersion/kind/project.js index 5a17601f3a8..1c8240e8b17 100644 --- a/packages/project/test/lib/validation/schema/specVersion/kind/project.js +++ b/packages/project/test/lib/validation/schema/specVersion/kind/project.js @@ -83,7 +83,7 @@ test("Type library (no kind)", async (t) => { test("Type component", async (t) => { await assertValidation(t, { - "specVersion": "3.1", + "specVersion": "5.0", "kind": "project", "type": "component", "metadata": { @@ -94,7 +94,7 @@ test("Type component", async (t) => { test("Type component (no kind)", async (t) => { await assertValidation(t, { - "specVersion": "3.1", + "specVersion": "5.0", "type": "component", "metadata": { "name": "my-component" @@ -102,6 +102,32 @@ test("Type component (no kind)", async (t) => { }); }); +test("Type component, legacy specVersion", async (t) => { + await assertValidation(t, { + "specVersion": "4.0", + "kind": "project", + "type": "component", + "metadata": { + "name": "my-component" + } + }, [{ + dataPath: "/specVersion", + keyword: "errorMessage", + message: "The 'component' type is only supported with specVersion '5.0' and higher.", + params: { + errors: [{ + dataPath: "/specVersion", + keyword: "const", + message: "should be equal to constant", + params: { + allowedValue: "5.0", + }, + schemaPath: "#/allOf/0/then/properties/specVersion/const", + }], + } + }]); +}); + test("Type theme-library", async (t) => { await assertValidation(t, { "specVersion": "2.0", From c09779ec25545c85badd5473dc0e3e683e5578dc Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Tue, 11 Nov 2025 14:24:44 +0100 Subject: [PATCH 03/11] docs(documentation): Document component type --- packages/documentation/docs/pages/Project.md | 23 ++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/documentation/docs/pages/Project.md b/packages/documentation/docs/pages/Project.md index 1a5f4610402..e894b41b585 100644 --- a/packages/documentation/docs/pages/Project.md +++ b/packages/documentation/docs/pages/Project.md @@ -11,8 +11,23 @@ Types define how a project can be configured and how it is built. A type orchest Also see [UI5 Project: Configuration](./Configuration.md#general-configuration) +### component +*Available since [Specification Version 5.0](./Configuration.md#specification-version-50)* + +Projects of type `component` are your typical component-like UI5 applications. These will usually run in container-like root application, such as the Fiori Launchpad (FLP) Sandbox, along with other UI5 applications. + +In order to allow multiple component projects to coexist in the same environment, the projects are served under their namespace. For example: `/resources/my/bookstore/admin`. Opposing to `application`-type projects which act as root projects and are therefore served without a namespace, at `/`. + +By default, component projects use the same directory structure as library projects. This means there are `src` and `test` directories in the root. The integrated server will use both directories, however when building the project, the `test` directory will be ignored since it should not be deployed to production environments. Both directories may contain a flat-, _or_ a namespace structure. In case of a flat structure, the project namespace will be derived from the `"sap.app".id` property in the `manifest.json`. + +A component project must contain both, a `Component.js` and a `manifest.json` file. + +Unlike `application`-type projects, component projects usually do not have dedicated `index.html` files in their regular resources (`src/`). They can still be run standalone though. For instance via a dedicated HTML file located in their test-resources, or by declaring a development-dependency to an application-type project which is capable of serving the component (such as the FLP Sandbox). + +Component projects supports all [output-styles](./CLI.md#ui5-build) that are currently supported by library projects, allowing a deployment where the namespace may be omitted from the final directory structure (output-style `flat`). + ### application -Projects of type `application` are typically the main or root project. In a projects dependency tree, there should only be one project of type `application`. If multiple are found, those further away from the root are ignored. +Projects of type `application` are typically the main or root project. In a projects dependency tree, there should only be one project of type `application`. If additional ones are found, those further away from the root are ignored. The source directory of an application (typically named `webapp`) is mapped to the virtual root path `/`. @@ -26,7 +41,7 @@ A project of type `library` must have a source directory (typically named `src`) These directories should contain a directory structure representing the namespace of the library (e.g. `src/my/first/library`) to prevent name clashes between the resources of different libraries. ### theme-library -*Available since [Specification Version](./Configuration.md#specification-versions) 1.1* +*Available since [Specification Version 1.1](./Configuration.md#specification-version-11)* UI5 theme libraries provide theming resources for the controls of one or multiple libraries. @@ -50,6 +65,10 @@ In the table below you can find the available combinations of project type & out | `Default` | Root project is written `Flat`-style. ^1^ | | `Flat` | Same as `Default`. | | `Namespace` | Root project is written `Namespace`-style (resources are prefixed with the project's namespace). ^1^ | +| **component** | | +| `Default` | Root project is written `Namespace`-style. ^1^ | +| `Flat` | Root project is written `Flat`-style (without its namespace, logging warnings for resources outside of it). ^1^ | +| `Namespace` | Same as `Default`. | | **library** | | | `Default` | Root project is written `Namespace`-style. ^1^ | | `Flat` | Root project is written `Flat`-style (without its namespace, logging warnings for resources outside of it). ^1^ | From 3e76071e10e53f04b4aeb371b0e1fcea08cb0d71 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 12 Nov 2025 09:46:28 +0100 Subject: [PATCH 04/11] docs(documentation): Apply suggestions from UA review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günter Klatt <57760635+KlattG@users.noreply.github.com> --- packages/documentation/docs/pages/Project.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/documentation/docs/pages/Project.md b/packages/documentation/docs/pages/Project.md index e894b41b585..4384236302f 100644 --- a/packages/documentation/docs/pages/Project.md +++ b/packages/documentation/docs/pages/Project.md @@ -14,20 +14,20 @@ Also see [UI5 Project: Configuration](./Configuration.md#general-configuration) ### component *Available since [Specification Version 5.0](./Configuration.md#specification-version-50)* -Projects of type `component` are your typical component-like UI5 applications. These will usually run in container-like root application, such as the Fiori Launchpad (FLP) Sandbox, along with other UI5 applications. +Projects of the `component` type are typical component-like UI5 applications. They usually run in a container-like root application, such as the SAP Fiori launchpad (FLP) sandbox, alongside other UI5 applications. -In order to allow multiple component projects to coexist in the same environment, the projects are served under their namespace. For example: `/resources/my/bookstore/admin`. Opposing to `application`-type projects which act as root projects and are therefore served without a namespace, at `/`. +To allow multiple component projects to coexist in the same environment, each project is served under its own namespace, for example `/resources/my/bookstore/admin`. In contrast, `application`-type projects act as root projects and are served at `/`, without a namespace. -By default, component projects use the same directory structure as library projects. This means there are `src` and `test` directories in the root. The integrated server will use both directories, however when building the project, the `test` directory will be ignored since it should not be deployed to production environments. Both directories may contain a flat-, _or_ a namespace structure. In case of a flat structure, the project namespace will be derived from the `"sap.app".id` property in the `manifest.json`. +By default, component projects use the same directory structure as library projects: they include `src` and `test` directories in the root. The integrated server uses both directories. However, when you build the project, the `test` directory is ignored because it shouldn't be deployed to production environments. Both directories can have either a flat or a namespace structure. If you use a flat structure, the project namespace derives from the `"sap.app".id` property in the `manifest.json`. A component project must contain both, a `Component.js` and a `manifest.json` file. -Unlike `application`-type projects, component projects usually do not have dedicated `index.html` files in their regular resources (`src/`). They can still be run standalone though. For instance via a dedicated HTML file located in their test-resources, or by declaring a development-dependency to an application-type project which is capable of serving the component (such as the FLP Sandbox). +Unlike `application`-type projects, component projects typically don't have dedicated `index.html` files in their regular resources (`src/`). However, you can still run them standalone. You can do this by using a dedicated HTML file located in their test resources or by declaring a development dependency to an application-type project that can serve the component, such as the FLP sandbox. -Component projects supports all [output-styles](./CLI.md#ui5-build) that are currently supported by library projects, allowing a deployment where the namespace may be omitted from the final directory structure (output-style `flat`). +Component projects support all [output styles](./CLI.md#ui5-build) that library projects currently support. This allows a deployment where you can omit the namespace from the final directory structure using the output style: `flat`. ### application -Projects of type `application` are typically the main or root project. In a projects dependency tree, there should only be one project of type `application`. If additional ones are found, those further away from the root are ignored. +Projects of the type `application` typically serve as the main or root project. In a project's dependency tree, there should be only one project of this type. If the system detects additional application projects, it ignores those that are further away from the root. The source directory of an application (typically named `webapp`) is mapped to the virtual root path `/`. From f0fb1d1e567ff6877a5827d9a94b144946d7b01a Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 12 Nov 2025 14:35:43 +0100 Subject: [PATCH 05/11] docs(documentation): Clarify difference between UI5 CLI project type and manifest.json type --- packages/documentation/docs/pages/Project.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/documentation/docs/pages/Project.md b/packages/documentation/docs/pages/Project.md index 4384236302f..b8a3a9fcaae 100644 --- a/packages/documentation/docs/pages/Project.md +++ b/packages/documentation/docs/pages/Project.md @@ -14,6 +14,8 @@ Also see [UI5 Project: Configuration](./Configuration.md#general-configuration) ### component *Available since [Specification Version 5.0](./Configuration.md#specification-version-50)* +> **Note:** The term `component` as used in the UI5 CLI project type differs from the `type` property in the `manifest.json` file at runtime. In most cases, a CLI project of type `component` is still a runtime "application". For details on the `type` property in `manifest.json`, refer to the [manifest documentation](https://ui5.sap.com/#/topic/be0cf40f61184b358b5faedaec98b2da.html#loiobe0cf40f61184b358b5faedaec98b2da/section_sap_app). + Projects of the `component` type are typical component-like UI5 applications. They usually run in a container-like root application, such as the SAP Fiori launchpad (FLP) sandbox, alongside other UI5 applications. To allow multiple component projects to coexist in the same environment, each project is served under its own namespace, for example `/resources/my/bookstore/admin`. In contrast, `application`-type projects act as root projects and are served at `/`, without a namespace. From db2bb54f173172b6764ac48941be1b8cd56ad94f Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 12 Nov 2025 15:05:14 +0100 Subject: [PATCH 06/11] docs(documentation): Wording --- packages/documentation/docs/pages/Configuration.md | 2 +- packages/documentation/docs/pages/Project.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/documentation/docs/pages/Configuration.md b/packages/documentation/docs/pages/Configuration.md index 8db3f6db502..8b7d25bf0d6 100644 --- a/packages/documentation/docs/pages/Configuration.md +++ b/packages/documentation/docs/pages/Configuration.md @@ -3,7 +3,7 @@ A projects UI5 CLI configuration is typically located in a [YAML](https://yaml.org/) file named `ui5.yaml`, located in the root directory. ::: info Info -This document describes the configuration of UI5 CLI-based projects and extensions. It represents **[Specification Version 3.0](#specification-versions)**. +This document describes the configuration of UI5 CLI-based projects and extensions. It represents **[Specification Version 5.0](#specification-versions)**. ::: diff --git a/packages/documentation/docs/pages/Project.md b/packages/documentation/docs/pages/Project.md index b8a3a9fcaae..769c7cfb5e8 100644 --- a/packages/documentation/docs/pages/Project.md +++ b/packages/documentation/docs/pages/Project.md @@ -14,7 +14,7 @@ Also see [UI5 Project: Configuration](./Configuration.md#general-configuration) ### component *Available since [Specification Version 5.0](./Configuration.md#specification-version-50)* -> **Note:** The term `component` as used in the UI5 CLI project type differs from the `type` property in the `manifest.json` file at runtime. In most cases, a CLI project of type `component` is still a runtime "application". For details on the `type` property in `manifest.json`, refer to the [manifest documentation](https://ui5.sap.com/#/topic/be0cf40f61184b358b5faedaec98b2da.html#loiobe0cf40f61184b358b5faedaec98b2da/section_sap_app). +> **Note:** The term `component` as used in the UI5 CLI project type differs from the `sap.app/type` property in the `manifest.json` file at runtime. In most cases, a CLI project of type `component` is still a runtime "application". For details on the `sap.app/type` property in `manifest.json`, refer to the [manifest documentation](https://ui5.sap.com/#/topic/be0cf40f61184b358b5faedaec98b2da.html#loiobe0cf40f61184b358b5faedaec98b2da/section_sap_app). Projects of the `component` type are typical component-like UI5 applications. They usually run in a container-like root application, such as the SAP Fiori launchpad (FLP) sandbox, alongside other UI5 applications. @@ -29,7 +29,7 @@ Unlike `application`-type projects, component projects typically don't have dedi Component projects support all [output styles](./CLI.md#ui5-build) that library projects currently support. This allows a deployment where you can omit the namespace from the final directory structure using the output style: `flat`. ### application -Projects of the type `application` typically serve as the main or root project. In a project's dependency tree, there should be only one project of this type. If the system detects additional application projects, it ignores those that are further away from the root. +Projects of the `application` type typically serve as the main or root project. In a project's dependency tree, there should be only one project of this type. If the system detects additional application projects, it ignores those that are further away from the root. The source directory of an application (typically named `webapp`) is mapped to the virtual root path `/`. @@ -38,7 +38,7 @@ An applications source directory may or may not contain a `Component.js` file. I ### library UI5 libraries are often referred to as reuse-, custom- or [control libraries](https://github.com/SAP/openui5/blob/-/docs/controllibraries.md). They are a key component in sharing code across multiple projects in UI5. -A project of type `library` must have a source directory (typically named `src`). It may also feature a "test" directory. These directories are mapped to the virtual directories `/resources` for the sources and `/test-resources` for the test resources. +A project of the `library` type must have a source directory (typically named `src`). It may also feature a "test" directory. These directories are mapped to the virtual directories `/resources` for the sources and `/test-resources` for the test resources. These directories should contain a directory structure representing the namespace of the library (e.g. `src/my/first/library`) to prevent name clashes between the resources of different libraries. From d42d9a5e5c419744b9e3844ab2d5073139dd3484 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 12 Nov 2025 16:03:33 +0100 Subject: [PATCH 07/11] docs(documentation): Update project documentation based on feedback --- packages/documentation/docs/pages/Project.md | 63 ++++++++++---------- 1 file changed, 33 insertions(+), 30 deletions(-) diff --git a/packages/documentation/docs/pages/Project.md b/packages/documentation/docs/pages/Project.md index 769c7cfb5e8..85a610b4b08 100644 --- a/packages/documentation/docs/pages/Project.md +++ b/packages/documentation/docs/pages/Project.md @@ -16,20 +16,28 @@ Also see [UI5 Project: Configuration](./Configuration.md#general-configuration) > **Note:** The term `component` as used in the UI5 CLI project type differs from the `sap.app/type` property in the `manifest.json` file at runtime. In most cases, a CLI project of type `component` is still a runtime "application". For details on the `sap.app/type` property in `manifest.json`, refer to the [manifest documentation](https://ui5.sap.com/#/topic/be0cf40f61184b358b5faedaec98b2da.html#loiobe0cf40f61184b358b5faedaec98b2da/section_sap_app). -Projects of the `component` type are typical component-like UI5 applications. They usually run in a container-like root application, such as the SAP Fiori launchpad (FLP) sandbox, alongside other UI5 applications. +Projects of the `component` type cover a range of use cases beyond typical standalone UI5 applications: + +- **Application components:** These are typical UI5 applications, designed to run in container-like application such as the SAP Fiori launchpad (FLP). These generally inherit from `sap.ui.core.UIComponent` (or a subclass) and define the `manifest.json` property `sap.app/type: application`. +- **Reusable UI components:** Provide UI elements or features that can be embedded in different contexts. These typically inherit from `sap.ui.core.UIComponent` and define the `manifest.json` property `sap.app/type: component`. +- **Faceless components:** Provide functionality without a user interface. These are defined with `manifest.json` property `sap.app/type: component` and inherit from `sap.ui.core.Component` (not `UIComponent`). + +Please also refer to the [components documentation](https://ui5.sap.com/#/topic/958ead51e2e94ab8bcdc90fb7e9d53d0) for more details. To allow multiple component projects to coexist in the same environment, each project is served under its own namespace, for example `/resources/my/bookstore/admin`. In contrast, `application`-type projects act as root projects and are served at `/`, without a namespace. -By default, component projects use the same directory structure as library projects: they include `src` and `test` directories in the root. The integrated server uses both directories. However, when you build the project, the `test` directory is ignored because it shouldn't be deployed to production environments. Both directories can have either a flat or a namespace structure. If you use a flat structure, the project namespace derives from the `"sap.app".id` property in the `manifest.json`. +By default, component projects use the same directory structure as library projects: they include `src` and `test` directories in the root. Both directories can have either a flat or a namespace structure. If you use a flat structure, the project namespace derives from the `"sap.app".id` property in the `manifest.json`. A component project must contain both, a `Component.js` and a `manifest.json` file. Unlike `application`-type projects, component projects typically don't have dedicated `index.html` files in their regular resources (`src/`). However, you can still run them standalone. You can do this by using a dedicated HTML file located in their test resources or by declaring a development dependency to an application-type project that can serve the component, such as the FLP sandbox. -Component projects support all [output styles](./CLI.md#ui5-build) that library projects currently support. This allows a deployment where you can omit the namespace from the final directory structure using the output style: `flat`. +Component projects support all [output styles](#build-output-style) that library projects currently support. This allows a deployment where you can omit the namespace from the final directory structure using the output style: `flat`. + +For more details, see also [RFC 0018 Component Type](https://github.com/UI5/cli/blob/rfc-component-type/rfcs/0018-component-type.md#rfc-0018-component-type). ### application -Projects of the `application` type typically serve as the main or root project. In a project's dependency tree, there should be only one project of this type. If the system detects additional application projects, it ignores those that are further away from the root. +Projects of the `application` type typically serve as the main or root project. In a project's dependency tree, there shouldn't be more than one project of this type. If the system detects additional application projects, it ignores those that are further away from the root. The source directory of an application (typically named `webapp`) is mapped to the virtual root path `/`. @@ -61,32 +69,27 @@ The _Output Style_ offers you control over your project's build output folder. N In the table below you can find the available combinations of project type & output style. -| Project Type / Requested Output Style | Resulting Style | -|---|---| -| **application** | | -| `Default` | Root project is written `Flat`-style. ^1^ | -| `Flat` | Same as `Default`. | -| `Namespace` | Root project is written `Namespace`-style (resources are prefixed with the project's namespace). ^1^ | -| **component** | | -| `Default` | Root project is written `Namespace`-style. ^1^ | -| `Flat` | Root project is written `Flat`-style (without its namespace, logging warnings for resources outside of it). ^1^ | -| `Namespace` | Same as `Default`. | -| **library** | | -| `Default` | Root project is written `Namespace`-style. ^1^ | -| `Flat` | Root project is written `Flat`-style (without its namespace, logging warnings for resources outside of it). ^1^ | -| `Namespace` | Same as `Default`. | -| **theme-library** | | -| `Default` | Root project is written in the style of the sources (multiple namespaces). ^1^ | -| `Flat` | **Unsupported** ^2^ | -| `Namespace` | **Unsupported** ^2^ | -| **module** | | -| `Default` | Root project is written with the [configured paths](https://ui5.github.io/cli/v5/pages/Configuration/#available-path-mappings). ^1^ | -| `Flat` | **Unsupported** ^3^ | -| `Namespace` | **Unsupported** ^3^ | - -^1^ The Output Style is only applied to the root project's output folder structure. Any dependencies included in the build would retain their `Default` output style. -^2^ Theme libraries in most cases have more than one namespace. -^3^ Modules have explicit path mappings configured and no namespace concept. +| Project Type | Requested Output Style | Resulting Style | +| :--- | :--- | :--- | +| **component** | `Default` | Root project is written `Namespace`-style.¹ | +| | `Flat` | Root project is written `Flat`-style (without its namespace, logging warnings for resources outside of it).¹ | +| | `Namespace` | Same as `Default`. | +| **application** | `Default` | Root project is written `Flat`-style.¹ | +| | `Flat` | Same as `Default`. | +| | `Namespace` | Root project is written `Namespace`-style (resources are prefixed with the project's namespace).¹ | +| **library** | `Default` | Root project is written `Namespace`-style.¹ | +| | `Flat` | Root project is written `Flat`-style (without its namespace, logging warnings for resources outside of it).¹ | +| | `Namespace` | Same as `Default`. | +| **theme-library** | `Default` | Root project is written in the style of the sources (multiple namespaces).¹ | +| | `Flat` | **Unsupported** ² | +| | `Namespace` | **Unsupported** ² | +| **module** | `Default` | Root project is written with the [configured paths](https://ui5.github.io/cli/v5/pages/Configuration/#available-path-mappings).¹ | +| | `Flat` | **Unsupported** ³ | +| | `Namespace` | **Unsupported** ³ | + +¹ The Output Style is only applied to the root project's output folder structure. Any dependencies included in the build would retain their `Default` output style. +² Theme libraries in most cases have more than one namespace. +³ Modules have explicit path mappings configured and no namespace concept.
From 7349a628a6fb3a3b538167e694461ca257720c1b Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 12 Nov 2025 16:04:15 +0100 Subject: [PATCH 08/11] test(project): Remove unused test fixtures --- .../collection/library.a/package.json | 17 ----------------- .../library.a/src/library/a/.library | 17 ----------------- .../library/a/themes/base/library.source.less | 6 ------ .../library.a/test/library/a/Test.html | 0 .../node_modules/collection/library.a/ui5.yaml | 5 ----- .../collection/library.b/package.json | 9 --------- .../library.b/src/library/b/.library | 17 ----------------- .../library.b/test/library/b/Test.html | 0 .../node_modules/collection/library.b/ui5.yaml | 5 ----- .../collection/library.c/package.json | 9 --------- .../library.c/src/library/c/.library | 17 ----------------- .../library.c/test/LibraryC/Test.html | 0 .../node_modules/collection/library.c/ui5.yaml | 5 ----- .../node_modules/library.d/package.json | 9 --------- .../library.d/src/library/d/.library | 11 ----------- .../library.d/test/library/d/Test.html | 0 .../collection/node_modules/library.d/ui5.yaml | 10 ---------- .../node_modules/collection/package.json | 18 ------------------ .../node_modules/collection/ui5.yaml | 12 ------------ .../library.d/main/src/library/d/.library | 11 ----------- .../library.d/main/src/library/d/some.js | 4 ---- .../library.d/main/test/library/d/Test.html | 0 .../node_modules/library.d/package.json | 9 --------- .../node_modules/library.d/ui5.yaml | 10 ---------- 24 files changed, 201 deletions(-) delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.a/package.json delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/.library delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.a/test/library/a/Test.html delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.a/ui5.yaml delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.b/package.json delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.b/src/library/b/.library delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.b/test/library/b/Test.html delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.b/ui5.yaml delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.c/package.json delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.c/src/library/c/.library delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.c/test/LibraryC/Test.html delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/library.c/ui5.yaml delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/package.json delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/src/library/d/.library delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/test/library/d/Test.html delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/ui5.yaml delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/package.json delete mode 100644 packages/project/test/fixtures/component.a/node_modules/collection/ui5.yaml delete mode 100644 packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/.library delete mode 100644 packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/some.js delete mode 100644 packages/project/test/fixtures/component.a/node_modules/library.d/main/test/library/d/Test.html delete mode 100644 packages/project/test/fixtures/component.a/node_modules/library.d/package.json delete mode 100644 packages/project/test/fixtures/component.a/node_modules/library.d/ui5.yaml diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/package.json deleted file mode 100644 index 2179673d41d..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "library.a", - "version": "1.0.0", - "description": "Simple SAPUI5 based library", - "dependencies": {}, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "ui5": { - "name": "library.a", - "type": "library", - "settings": { - "src": "src", - "test": "test" - } - } -} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/.library b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/.library deleted file mode 100644 index 25c8603f31a..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/.library +++ /dev/null @@ -1,17 +0,0 @@ - - - - library.a - SAP SE - ${copyright} - ${version} - - Library A - - - - library.d - - - - diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less deleted file mode 100644 index ff0f1d5e3df..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less +++ /dev/null @@ -1,6 +0,0 @@ -@libraryAColor1: lightgoldenrodyellow; - -.library-a-foo { - color: @libraryAColor1; - padding: 1px 2px 3px 4px; -} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/test/library/a/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/test/library/a/Test.html deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/library.a/ui5.yaml deleted file mode 100644 index 8d4784313c3..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/library.a/ui5.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -specVersion: "2.3" -type: library -metadata: - name: library.a diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/package.json deleted file mode 100644 index 2a0243b1683..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "library.b", - "version": "1.0.0", - "description": "Simple SAPUI5 based library", - "dependencies": {}, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - } -} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/src/library/b/.library b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/src/library/b/.library deleted file mode 100644 index 36052acebdc..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/src/library/b/.library +++ /dev/null @@ -1,17 +0,0 @@ - - - - library.b - SAP SE - ${copyright} - ${version} - - Library B - - - - library.d - - - - diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/test/library/b/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/test/library/b/Test.html deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/library.b/ui5.yaml deleted file mode 100644 index b2fe5be59ee..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/library.b/ui5.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -specVersion: "2.3" -type: library -metadata: - name: library.b diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/package.json deleted file mode 100644 index 64ac75d6ffe..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "library.c", - "version": "1.0.0", - "description": "Simple SAPUI5 based library", - "dependencies": {}, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - } -} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/src/library/c/.library b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/src/library/c/.library deleted file mode 100644 index 4180ce2af2f..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/src/library/c/.library +++ /dev/null @@ -1,17 +0,0 @@ - - - - library.c - SAP SE - ${copyright} - ${version} - - Library C - - - - library.d - - - - diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/test/LibraryC/Test.html deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/library.c/ui5.yaml deleted file mode 100644 index 7c5e38a7fc1..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/library.c/ui5.yaml +++ /dev/null @@ -1,5 +0,0 @@ ---- -specVersion: "2.3" -type: library -metadata: - name: library.c diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/package.json deleted file mode 100644 index 90c75040abe..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "library.d", - "version": "1.0.0", - "description": "Simple SAPUI5 based library", - "dependencies": {}, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - } -} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/src/library/d/.library deleted file mode 100644 index 21251d1bbba..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/src/library/d/.library +++ /dev/null @@ -1,11 +0,0 @@ - - - - library.d - SAP SE - ${copyright} - ${version} - - Library D - - diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/test/library/d/Test.html deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/ui5.yaml deleted file mode 100644 index a47c1f64c3d..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/node_modules/library.d/ui5.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -specVersion: "2.3" -type: library -metadata: - name: library.d -resources: - configuration: - paths: - src: main/src - test: main/test diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/package.json b/packages/project/test/fixtures/component.a/node_modules/collection/package.json deleted file mode 100644 index 81b948438bd..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "collection", - "version": "1.0.0", - "description": "Simple Collection", - "dependencies": { - "library.d": "file:../library.d" - }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "collection": { - "modules": { - "library.a": "./library.a", - "library.b": "./library.b", - "library.c": "./library.c" - } - } -} diff --git a/packages/project/test/fixtures/component.a/node_modules/collection/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/collection/ui5.yaml deleted file mode 100644 index e47048de6a7..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/collection/ui5.yaml +++ /dev/null @@ -1,12 +0,0 @@ -specVersion: "2.1" -metadata: - name: application.a.collection.dependency.shim -kind: extension -type: project-shim -shims: - collections: - collection: - modules: - "library.a": "./library.a" - "library.b": "./library.b" - "library.c": "./library.c" \ No newline at end of file diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/.library deleted file mode 100644 index 53c2d14c9d6..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/.library +++ /dev/null @@ -1,11 +0,0 @@ - - - - library.d - SAP SE - Some fancy copyright - ${version} - - Library D - - diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/some.js deleted file mode 100644 index 81e73436075..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/library.d/main/src/library/d/some.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! - * ${copyright} - */ -console.log('HelloWorld'); \ No newline at end of file diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/component.a/node_modules/library.d/main/test/library/d/Test.html deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/package.json b/packages/project/test/fixtures/component.a/node_modules/library.d/package.json deleted file mode 100644 index 90c75040abe..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/library.d/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "library.d", - "version": "1.0.0", - "description": "Simple SAPUI5 based library", - "dependencies": {}, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - } -} diff --git a/packages/project/test/fixtures/component.a/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/component.a/node_modules/library.d/ui5.yaml deleted file mode 100644 index a47c1f64c3d..00000000000 --- a/packages/project/test/fixtures/component.a/node_modules/library.d/ui5.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -specVersion: "2.3" -type: library -metadata: - name: library.d -resources: - configuration: - paths: - src: main/src - test: main/test From 1ba5eac4742c9f4f3820ec0f2ef007a3dd6052c4 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 12 Nov 2025 16:17:03 +0100 Subject: [PATCH 09/11] refactor(project): Use dedicated build definition for component type --- packages/project/lib/build/TaskRunner.js | 2 +- .../lib/build/definitions/component.js | 128 ++++++ .../test/lib/build/definitions/component.js | 400 ++++++++++++++++++ 3 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 packages/project/lib/build/definitions/component.js create mode 100644 packages/project/test/lib/build/definitions/component.js diff --git a/packages/project/lib/build/TaskRunner.js b/packages/project/lib/build/TaskRunner.js index cd0441bbef2..0f5677170a3 100644 --- a/packages/project/lib/build/TaskRunner.js +++ b/packages/project/lib/build/TaskRunner.js @@ -54,7 +54,7 @@ class TaskRunner { buildDefinition = "./definitions/library.js"; break; case "component": - buildDefinition = "./definitions/application.js"; + buildDefinition = "./definitions/component.js"; break; case "module": buildDefinition = "./definitions/module.js"; diff --git a/packages/project/lib/build/definitions/component.js b/packages/project/lib/build/definitions/component.js new file mode 100644 index 00000000000..48684b6df03 --- /dev/null +++ b/packages/project/lib/build/definitions/component.js @@ -0,0 +1,128 @@ +import {enhancePatternWithExcludes} from "./_utils.js"; +import {enhanceBundlesWithDefaults} from "../../validation/validator.js"; + +/** + * Get tasks and their configuration for a given component project + * + * @private + * @param {object} parameters + * @param {object} parameters.project + * @param {object} parameters.taskUtil + * @param {Function} parameters.getTask + */ +export default function({project, taskUtil, getTask}) { + const tasks = new Map(); + tasks.set("escapeNonAsciiCharacters", { + options: { + encoding: project.getPropertiesFileSourceEncoding(), + pattern: "/**/*.properties" + } + }); + + tasks.set("replaceCopyright", { + options: { + copyright: project.getCopyright(), + pattern: "/**/*.{js,json}" + } + }); + + tasks.set("replaceVersion", { + options: { + version: project.getVersion(), + pattern: "/**/*.{js,json}" + } + }); + + // Support rules should not be minified to have readable code in the Support Assistant + const minificationPattern = ["/**/*.js", "!**/*.support.js"]; + const minificationExcludes = project.getMinificationExcludes(); + if (minificationExcludes.length) { + enhancePatternWithExcludes(minificationPattern, minificationExcludes, "/resources/"); + } + + tasks.set("minify", { + options: { + pattern: minificationPattern + } + }); + + tasks.set("enhanceManifest", {}); + + tasks.set("generateFlexChangesBundle", {}); + + const bundles = project.getBundles(); + const existingBundleDefinitionNames = + bundles.map(({bundleDefinition}) => bundleDefinition.name).filter(Boolean); + + const componentPreloadPaths = project.getComponentPreloadPaths(); + const componentPreloadNamespaces = project.getComponentPreloadNamespaces(); + const componentPreloadExcludes = project.getComponentPreloadExcludes(); + if (componentPreloadPaths.length || componentPreloadNamespaces.length) { + tasks.set("generateComponentPreload", { + options: { + paths: componentPreloadPaths, + namespaces: componentPreloadNamespaces, + excludes: componentPreloadExcludes, + skipBundles: existingBundleDefinitionNames + } + }); + } else { + // Default component preload + tasks.set("generateComponentPreload", { + options: { + namespaces: [project.getNamespace()], + excludes: componentPreloadExcludes, + skipBundles: existingBundleDefinitionNames + } + }); + } + + if (bundles.length) { + tasks.set("generateBundle", { + requiresDependencies: true, + taskFunction: async ({workspace, dependencies, taskUtil, options}) => { + const generateBundleTask = await getTask("generateBundle"); + // Async resolve default values for bundle definitions and options + const bundlesDefaults = await enhanceBundlesWithDefaults(bundles, taskUtil.getProject()); + + return bundlesDefaults.reduce(async function(sequence, bundle) { + return sequence.then(function() { + return generateBundleTask.task({ + workspace, + dependencies, + taskUtil, + options: { + projectName: options.projectName, + bundleDefinition: bundle.bundleDefinition, + bundleOptions: bundle.bundleOptions + } + }); + }); + }, Promise.resolve()); + } + }); + } else { + // No bundles defined. Just set task so that it can be referenced by custom tasks + tasks.set("generateBundle", { + taskFunction: null + }); + } + + tasks.set("generateVersionInfo", { + requiresDependencies: true, + options: { + rootProject: project, + pattern: "/resources/**/.library" + } + }); + + tasks.set("generateCachebusterInfo", { + options: { + signatureType: project.getCachebusterSignatureType(), + } + }); + + tasks.set("generateResourcesJson", {requiresDependencies: true}); + + return tasks; +} diff --git a/packages/project/test/lib/build/definitions/component.js b/packages/project/test/lib/build/definitions/component.js new file mode 100644 index 00000000000..abb281a86ed --- /dev/null +++ b/packages/project/test/lib/build/definitions/component.js @@ -0,0 +1,400 @@ +import test from "ava"; +import sinon from "sinon"; +import component from "../../../../lib/build/definitions/component.js"; + +function emptyarray() { + return []; +} + +function getMockProject() { + return { + getName: () => "project.b", + getNamespace: () => "project/b", + getType: () => "component", + getPropertiesFileSourceEncoding: () => "UTF-412", + getCopyright: () => "copyright", + getVersion: () => "version", + getSpecVersion: () => { + return { + toString: () => "5.0" + }; + }, + getMinificationExcludes: emptyarray, + getComponentPreloadPaths: emptyarray, + getComponentPreloadNamespaces: emptyarray, + getComponentPreloadExcludes: emptyarray, + getBundles: emptyarray, + getCachebusterSignatureType: () => "PONY", + getCustomTasks: emptyarray, + }; +} + +test.beforeEach((t) => { + t.context.project = getMockProject(); + t.context.taskUtil = { + getProject: sinon.stub().returns(t.context.project), + isRootProject: sinon.stub().returns(true), + getBuildOption: sinon.stub(), + getInterface: sinon.stub() + }; + + t.context.getTask = sinon.stub(); +}); + +test("Standard build", (t) => { + const {project, taskUtil, getTask} = t.context; + const tasks = component({ + project, taskUtil, getTask + }); + + t.deepEqual(Object.fromEntries(tasks), { + escapeNonAsciiCharacters: { + options: { + encoding: "UTF-412", pattern: "/**/*.properties" + } + }, + replaceCopyright: { + options: { + copyright: "copyright", pattern: "/**/*.{js,json}" + } + }, + replaceVersion: { + options: { + version: "version", pattern: "/**/*.{js,json}" + } + }, + minify: { + options: { + pattern: [ + "/**/*.js", + "!**/*.support.js", + ] + } + }, + enhanceManifest: {}, + generateFlexChangesBundle: {}, + generateComponentPreload: { + options: { + namespaces: ["project/b"], + excludes: [], + skipBundles: [] + } + }, + generateBundle: { + taskFunction: null + }, + generateVersionInfo: { + requiresDependencies: true, + options: { + rootProject: project, + pattern: "/resources/**/.library" + } + }, + generateCachebusterInfo: { + options: { + signatureType: "PONY" + } + }, + generateResourcesJson: { + requiresDependencies: true + } + }, "Correct task definitions"); + + t.is(taskUtil.getBuildOption.callCount, 0, "taskUtil#getBuildOption has not been called"); +}); + +test("Custom bundles", async (t) => { + const {project, taskUtil, getTask} = t.context; + project.getBundles = () => [{ + bundleDefinition: { + name: "project/b/sectionsA/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsA/", + "!project/b/sectionsA/section2**", + ] + }] + }, + bundleOptions: { + optimize: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + }, { + bundleDefinition: { + name: "project/b/sectionsB/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsB/", + "!project/b/sectionsB/section2**", + ] + }] + }, + bundleOptions: { + optimize: false, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + }]; + + const generateBundleTaskStub = sinon.stub(); + getTask.returns({ + task: generateBundleTaskStub + }); + + const tasks = component({ + project, taskUtil, getTask + }); + const generateBundleTaskDefinition = tasks.get("generateBundle"); + + t.deepEqual(Object.fromEntries(tasks), { + escapeNonAsciiCharacters: { + options: { + encoding: "UTF-412", pattern: "/**/*.properties" + } + }, + replaceCopyright: { + options: { + copyright: "copyright", pattern: "/**/*.{js,json}" + } + }, + replaceVersion: { + options: { + version: "version", pattern: "/**/*.{js,json}" + } + }, + minify: { + options: { + pattern: [ + "/**/*.js", + "!**/*.support.js", + ] + } + }, + enhanceManifest: {}, + generateFlexChangesBundle: {}, + generateComponentPreload: { + options: { + namespaces: ["project/b"], + excludes: [], + skipBundles: [ + "project/b/sectionsA/customBundle.js", + "project/b/sectionsB/customBundle.js" + ] + } + }, + generateBundle: { + requiresDependencies: true, + taskFunction: generateBundleTaskDefinition.taskFunction + }, + generateVersionInfo: { + requiresDependencies: true, + options: { + rootProject: project, + pattern: "/resources/**/.library" + } + }, + generateCachebusterInfo: { + options: { + signatureType: "PONY" + } + }, + generateResourcesJson: { + requiresDependencies: true + } + }, "Correct task definitions"); + + + await generateBundleTaskDefinition.taskFunction({ + workspace: "workspace", + dependencies: "dependencies", + taskUtil, + options: { + projectName: "projectName" + } + }); + + t.is(generateBundleTaskStub.callCount, 2, "generateBundle task got called twice"); + t.deepEqual(generateBundleTaskStub.getCall(0).args[0], { + workspace: "workspace", + dependencies: "dependencies", + taskUtil, + options: { + projectName: "projectName", + bundleDefinition: { + name: "project/b/sectionsA/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsA/", + "!project/b/sectionsA/section2**", + ], + declareRawModules: false, + renderer: false, + resolve: false, + resolveConditional: false, + sort: true, + }] + }, + bundleOptions: { + optimize: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + sourceMap: true, + } + } + }, "generateBundle task got called with correct arguments"); + t.deepEqual(generateBundleTaskStub.getCall(1).args[0], { + workspace: "workspace", + dependencies: "dependencies", + taskUtil, + options: { + projectName: "projectName", + bundleDefinition: { + name: "project/b/sectionsB/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsB/", + "!project/b/sectionsB/section2**", + ], + declareRawModules: false, + renderer: false, + resolve: false, + resolveConditional: false, + sort: true, + }] + }, + bundleOptions: { + optimize: false, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + sourceMap: true, + } + } + }, "generateBundle task got called with correct arguments"); +}); + +test("Minification excludes", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getMinificationExcludes = () => ["**.html"]; + + const tasks = component({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("minify"); + t.deepEqual(taskDefinition, { + options: { + pattern: [ + "/**/*.js", + "!**/*.support.js", + "!/resources/**.html", + ] + } + }, "Correct minify task definition"); +}); + +test("generateComponentPreload with custom paths, excludes and custom bundle", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getBundles = () => [{ + bundleDefinition: { + name: "project/b/sectionsA/customBundle.js", + defaultFileTypes: [".js"], + sections: [{ + mode: "preload", + filters: [ + "project/b/sectionsA/", + "!project/b/sectionsA/section2**", + ] + }] + }, + bundleOptions: { + optimize: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, + } + }]; + + project.getComponentPreloadPaths = () => [ + "project/b/**/Component.js", + "project/b/**/SubComponent.js" + ]; + project.getComponentPreloadExcludes = () => ["project/b/dir/**"]; + + const tasks = component({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("generateComponentPreload"); + t.deepEqual(taskDefinition, { + options: { + paths: [ + "project/b/**/Component.js", + "project/b/**/SubComponent.js" + ], + namespaces: [], + excludes: ["project/b/dir/**"], + skipBundles: [ + "project/b/sectionsA/customBundle.js" + ] + } + }, "Correct generateComponentPreload task definition"); +}); + +test("generateComponentPreload with custom namespaces and excludes", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getComponentPreloadNamespaces = () => [ + "project/b/componentA", + "project/b/componentB" + ]; + project.getComponentPreloadExcludes = () => ["project/b/componentA/dir/**"]; + + const tasks = component({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("generateComponentPreload"); + t.deepEqual(taskDefinition, { + options: { + paths: [], + namespaces: [ + "project/b/componentA", + "project/b/componentB" + ], + excludes: ["project/b/componentA/dir/**"], + skipBundles: [] + } + }, "Correct generateComponentPreload task definition"); +}); + +test("generateComponentPreload with excludes", (t) => { + const {project, taskUtil, getTask} = t.context; + project.getComponentPreloadExcludes = () => ["project/b/componentA/dir/**"]; + + const tasks = component({ + project, taskUtil, getTask + }); + + const taskDefinition = tasks.get("generateComponentPreload"); + t.deepEqual(taskDefinition, { + options: { + namespaces: [ + "project/b", + ], + excludes: ["project/b/componentA/dir/**"], + skipBundles: [] + } + }, "Correct generateComponentPreload task definition"); +}); From 73659ca6399e6a6388c2bfd7d16de452209c9163 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Thu, 13 Nov 2025 13:44:59 +0100 Subject: [PATCH 10/11] refactor(project): Use enum in component schema --- .../lib/validation/schema/specVersion/kind/project.json | 2 +- .../lib/validation/schema/specVersion/kind/project.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/project/lib/validation/schema/specVersion/kind/project.json b/packages/project/lib/validation/schema/specVersion/kind/project.json index b13407bc001..55761a15600 100644 --- a/packages/project/lib/validation/schema/specVersion/kind/project.json +++ b/packages/project/lib/validation/schema/specVersion/kind/project.json @@ -17,7 +17,7 @@ "then": { "properties": { "specVersion": { - "const": "5.0", + "enum": ["5.0"], "errorMessage": "The 'component' type is only supported with specVersion '5.0' and higher." } } diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project.js b/packages/project/test/lib/validation/schema/specVersion/kind/project.js index 1c8240e8b17..40319e20666 100644 --- a/packages/project/test/lib/validation/schema/specVersion/kind/project.js +++ b/packages/project/test/lib/validation/schema/specVersion/kind/project.js @@ -117,12 +117,12 @@ test("Type component, legacy specVersion", async (t) => { params: { errors: [{ dataPath: "/specVersion", - keyword: "const", - message: "should be equal to constant", + keyword: "enum", + message: "should be equal to one of the allowed values", params: { - allowedValue: "5.0", + allowedValues: ["5.0"], }, - schemaPath: "#/allOf/0/then/properties/specVersion/const", + schemaPath: "#/allOf/0/then/properties/specVersion/enum", }], } }]); From 830b97543b6fd7505434605a097e84080197bd14 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Fri, 14 Nov 2025 13:12:19 +0100 Subject: [PATCH 11/11] docs(documentation): Apply suggestions from UA review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Günter Klatt <57760635+KlattG@users.noreply.github.com> --- packages/documentation/docs/pages/Project.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/documentation/docs/pages/Project.md b/packages/documentation/docs/pages/Project.md index 85a610b4b08..a3f5108842c 100644 --- a/packages/documentation/docs/pages/Project.md +++ b/packages/documentation/docs/pages/Project.md @@ -14,19 +14,19 @@ Also see [UI5 Project: Configuration](./Configuration.md#general-configuration) ### component *Available since [Specification Version 5.0](./Configuration.md#specification-version-50)* -> **Note:** The term `component` as used in the UI5 CLI project type differs from the `sap.app/type` property in the `manifest.json` file at runtime. In most cases, a CLI project of type `component` is still a runtime "application". For details on the `sap.app/type` property in `manifest.json`, refer to the [manifest documentation](https://ui5.sap.com/#/topic/be0cf40f61184b358b5faedaec98b2da.html#loiobe0cf40f61184b358b5faedaec98b2da/section_sap_app). +> **Note:** The UI5 CLI project type `component` differs from the `sap.app/type` runtime property with the same name defined in the `manifest.json` file. In most cases, a CLI project of type `component` is still a runtime `application`. For more information, see the [manifest documentation](https://ui5.sap.com/#/topic/be0cf40f61184b358b5faedaec98b2da.html#loiobe0cf40f61184b358b5faedaec98b2da/section_sap_app). Projects of the `component` type cover a range of use cases beyond typical standalone UI5 applications: -- **Application components:** These are typical UI5 applications, designed to run in container-like application such as the SAP Fiori launchpad (FLP). These generally inherit from `sap.ui.core.UIComponent` (or a subclass) and define the `manifest.json` property `sap.app/type: application`. -- **Reusable UI components:** Provide UI elements or features that can be embedded in different contexts. These typically inherit from `sap.ui.core.UIComponent` and define the `manifest.json` property `sap.app/type: component`. -- **Faceless components:** Provide functionality without a user interface. These are defined with `manifest.json` property `sap.app/type: component` and inherit from `sap.ui.core.Component` (not `UIComponent`). +- **Application components:** These are typical UI5 applications designed to run in container-like applications such as the SAP Fiori launchpad (FLP). They generally inherit from `sap.ui.core.UIComponent` (or a subclass) and define the `manifest.json` property `sap.app/type: application`. +- **Reusable UI components:** These provide UI elements or features that you can embed in different contexts. They typically inherit from `sap.ui.core.UIComponent` and define the `manifest.json` property `sap.app/type: component`. +- **Faceless components:** These provide functionality without a user interface. They are defined with `manifest.json` property `sap.app/type: component` and inherit from `sap.ui.core.Component` (not `UIComponent`). -Please also refer to the [components documentation](https://ui5.sap.com/#/topic/958ead51e2e94ab8bcdc90fb7e9d53d0) for more details. +For more information, see [Components](https://ui5.sap.com/#/topic/958ead51e2e94ab8bcdc90fb7e9d53d0). To allow multiple component projects to coexist in the same environment, each project is served under its own namespace, for example `/resources/my/bookstore/admin`. In contrast, `application`-type projects act as root projects and are served at `/`, without a namespace. -By default, component projects use the same directory structure as library projects: they include `src` and `test` directories in the root. Both directories can have either a flat or a namespace structure. If you use a flat structure, the project namespace derives from the `"sap.app".id` property in the `manifest.json`. +By default, component projects use the same directory structure as library projects: they include `src` and `test` directories in the root. Both directories can have either a flat or a namespace structure. If you use a flat structure, the project namespace derives from the `sap.app/id` property in the `manifest.json`. A component project must contain both, a `Component.js` and a `manifest.json` file. @@ -87,7 +87,7 @@ In the table below you can find the available combinations of project type & out | | `Flat` | **Unsupported** ³ | | | `Namespace` | **Unsupported** ³ | -¹ The Output Style is only applied to the root project's output folder structure. Any dependencies included in the build would retain their `Default` output style. +¹ The output style is only applied to the root project's output folder structure. Any dependencies included in the build would retain their `Default` output style. ² Theme libraries in most cases have more than one namespace. ³ Modules have explicit path mappings configured and no namespace concept.