diff --git a/lib/specifications/Specification.js b/lib/specifications/Specification.js
index 5c2af4142..3e60eed07 100644
--- a/lib/specifications/Specification.js
+++ b/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/lib/specifications/SpecificationVersion.js b/lib/specifications/SpecificationVersion.js
index ae48edee3..7bbac1eef 100644
--- a/lib/specifications/SpecificationVersion.js
+++ b/lib/specifications/SpecificationVersion.js
@@ -4,7 +4,7 @@ const SPEC_VERSION_PATTERN = /^\d+\.\d+$/;
const SUPPORTED_VERSIONS = [
"0.1", "1.0", "1.1",
"2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6",
- "3.0"
+ "3.0", "3.1"
];
/**
@@ -63,8 +63,8 @@ class SpecificationVersion {
* Test whether the instance's Specification Version falls into the provided range
*
* @public
-@param {string} range [Semver]{@link https://www.npmjs.com/package/semver}-style version range,
-for example 2.2 - 2.4
+ * @param {string} range [Semver]{@link https://www.npmjs.com/package/semver}-style version range,
+ * for example 2.2 - 2.4
or =3.0
* @returns {boolean} True if the instance's Specification Version falls into the provided range
*/
satisfies(range) {
@@ -263,6 +263,22 @@ for example 2.2 - 2.4
const comparator = new SpecificationVersion(specVersion);
return comparator.neq(testVersion);
}
+
+ /**
+ * Create an array of Specification Versions that match with the provided range. This is mainly used
+ * for testing purposes. I.e. to execute identical tests for a range of specification versions.
+ *
+ * @public
+ * @param {string} range [Semver]{@link https://www.npmjs.com/package/semver}-style version range,
+ * for example 2.2 - 2.4
or =3.0
+ * @returns {string[]} Array of versions that match the specified range
+ */
+ static getVersionsForRange(range) {
+ return SUPPORTED_VERSIONS.filter((specVersion) => {
+ const comparator = new SpecificationVersion(specVersion);
+ return comparator.satisfies(range);
+ });
+ }
}
function getUnsupportedSpecVersionMessage(specVersion) {
diff --git a/lib/specifications/types/Application.js b/lib/specifications/types/Application.js
index 05303eb92..9506c74b9 100644
--- a/lib/specifications/types/Application.js
+++ b/lib/specifications/types/Application.js
@@ -66,11 +66,17 @@ class Application extends ComponentProject {
return null; // Applications do not have a dedicated test directory
}
+ /**
+ * 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: `Source reader for application project ${this.getName()}`,
+ name: `Raw source reader for application project ${this.getName()}`,
project: this
});
}
diff --git a/lib/specifications/types/Component.js b/lib/specifications/types/Component.js
new file mode 100644
index 000000000..227dc629c
--- /dev/null
+++ b/lib/specifications/types/Component.js
@@ -0,0 +1,259 @@
+import fsPath from "node:path";
+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);
+ }
+
+ /* === 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();
+ }
+
+ /**
+ * 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