} Resolves with a JSON representation of the content
+ */
+ async _getPom() {
+ if (this._pPom) {
+ return this._pPom;
+ }
+
+ return this._pPom = this.getRootReader().byPath("/pom.xml")
+ .then(async (resource) => {
+ if (!resource) {
+ throw new Error(
+ `Could not find pom.xml in project ${this.getName()}`);
+ }
+ const content = await resource.getString();
+ const {
+ default: xml2js
+ } = await import("xml2js");
+ const parser = new xml2js.Parser({
+ explicitArray: false,
+ ignoreAttrs: true
+ });
+ const readXML = promisify(parser.parseString);
+ return readXML(content);
+ }).catch((err) => {
+ throw new Error(
+ `Failed to read pom.xml for project ${this.getName()}: ${err.message}`);
+ });
+ }
+}
+
+export default ComponentProject;
diff --git a/packages/project/lib/specifications/Extension.js b/packages/project/lib/specifications/Extension.js
new file mode 100644
index 00000000000..a30b11c35b6
--- /dev/null
+++ b/packages/project/lib/specifications/Extension.js
@@ -0,0 +1,45 @@
+import Specification from "./Specification.js";
+
+/**
+ * Extension
+ *
+ * @public
+ * @abstract
+ * @class
+ * @alias @ui5/project/specifications/Extension
+ * @extends @ui5/project/specifications/Specification
+ * @hideconstructor
+ */
+class Extension extends Specification {
+ constructor(parameters) {
+ super(parameters);
+ if (new.target === Extension) {
+ throw new TypeError("Class 'Extension' is abstract. Please use one of the 'types' subclasses");
+ }
+ }
+
+ /**
+ * @param {object} parameters Specification parameters
+ * @param {string} parameters.id Unique ID
+ * @param {string} parameters.version Version
+ * @param {string} parameters.modulePath File System path to access resources
+ * @param {object} parameters.configuration Configuration object
+ */
+ async init(parameters) {
+ await super.init(parameters);
+
+ try {
+ await this._validateConfig();
+ } catch (err) {
+ throw new Error(
+ `Failed to validate configuration of ${this.getType()} extension ${this.getName()}: ` +
+ err.message);
+ }
+
+ return this;
+ }
+
+ async _validateConfig() {}
+}
+
+export default Extension;
diff --git a/packages/project/lib/specifications/Project.js b/packages/project/lib/specifications/Project.js
new file mode 100644
index 00000000000..17177b08536
--- /dev/null
+++ b/packages/project/lib/specifications/Project.js
@@ -0,0 +1,292 @@
+import Specification from "./Specification.js";
+import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection";
+
+/**
+ * Project
+ *
+ * @public
+ * @abstract
+ * @class
+ * @alias @ui5/project/specifications/Project
+ * @extends @ui5/project/specifications/Specification
+ * @hideconstructor
+ */
+class Project extends Specification {
+ constructor(parameters) {
+ super(parameters);
+ if (new.target === Project) {
+ throw new TypeError("Class 'Project' is abstract. Please use one of the 'types' subclasses");
+ }
+
+ this._resourceTagCollection = null;
+ }
+
+ /**
+ * @param {object} parameters Specification parameters
+ * @param {string} parameters.id Unique ID
+ * @param {string} parameters.version Version
+ * @param {string} parameters.modulePath File System path to access resources
+ * @param {object} parameters.configuration Configuration object
+ * @param {object} [parameters.buildManifest] Build metadata object
+ */
+ async init(parameters) {
+ await super.init(parameters);
+
+ this._buildManifest = parameters.buildManifest;
+
+ await this._configureAndValidatePaths(this._config);
+ await this._parseConfiguration(this._config, this._buildManifest);
+
+ return this;
+ }
+
+ /* === Attributes === */
+ /**
+ * Get the project namespace. Returns `null` for projects that have none or multiple namespaces,
+ * for example Modules or Theme Libraries.
+ *
+ * @public
+ * @returns {string|null} Project namespace in slash notation (e.g. my/project/name) or null
+ */
+ getNamespace() {
+ // Default namespace for general Projects:
+ // Their resources should be structured with globally unique paths, hence their namespace is undefined
+ return null;
+ }
+
+ /**
+ * Check whether the project is a UI5-Framework project
+ *
+ * @public
+ * @returns {boolean} True if the project is a framework project
+ */
+ isFrameworkProject() {
+ const id = this.getId();
+ return id.startsWith("@openui5/") || id.startsWith("@sapui5/");
+ }
+
+ /**
+ * Get the project's customConfiguration
+ *
+ * @public
+ * @returns {object} Custom Configuration
+ */
+ getCustomConfiguration() {
+ return this._config.customConfiguration;
+ }
+
+ /**
+ * Get the path of the project's source directory. This might not be POSIX-style on some platforms.
+ * Projects with multiple source paths will throw an error. For example Modules.
+ *
+ * @public
+ * @returns {string} Absolute path to the source directory of the project
+ * @throws {Error} In case a project has multiple source directories
+ */
+ getSourcePath() {
+ throw new Error(`getSourcePath must be implemented by subclass ${this.constructor.name}`);
+ }
+
+ /**
+ * Get the project's framework name configuration
+ *
+ * @public
+ * @returns {string} Framework name configuration, either OpenUI5 or SAPUI5
+ */
+ getFrameworkName() {
+ return this._config.framework?.name;
+ }
+
+ /**
+ * Get the project's framework version configuration
+ *
+ * @public
+ * @returns {string} Framework version configuration, e.g 1.110.0
+ */
+ getFrameworkVersion() {
+ return this._config.framework?.version;
+ }
+
+
+ /**
+ * Framework dependency entry of the project configuration.
+ * Also see [Framework Configuration: Dependencies]{@link https://ui5.github.io/cli/stable/pages/Configuration/#dependencies}
+ *
+ * @public
+ * @typedef {object} @ui5/project/specifications/Project~FrameworkDependency
+ * @property {string} name Name of the framework library. For example sap.ui.core
+ * @property {boolean} development Whether the dependency is meant for development purposes only
+ * @property {boolean} optional Whether the dependency should be treated as optional
+ */
+
+ /**
+ * Get the project's framework dependencies configuration
+ *
+ * @public
+ * @returns {@ui5/project/specifications/Project~FrameworkDependency[]} Framework dependencies configuration
+ */
+ getFrameworkDependencies() {
+ return this._config.framework?.libraries || [];
+ }
+
+ /**
+ * Get the project's deprecated configuration
+ *
+ * @private
+ * @returns {boolean} True if the project is flagged as deprecated
+ */
+ isDeprecated() {
+ return !!this._config.metadata.deprecated;
+ }
+
+ /**
+ * Get the project's sapInternal configuration
+ *
+ * @private
+ * @returns {boolean} True if the project is flagged as SAP-internal
+ */
+ isSapInternal() {
+ return !!this._config.metadata.sapInternal;
+ }
+
+ /**
+ * Get the project's allowSapInternal configuration
+ *
+ * @private
+ * @returns {boolean} True if the project allows for using SAP-internal projects
+ */
+ getAllowSapInternal() {
+ return !!this._config.metadata.allowSapInternal;
+ }
+
+ /**
+ * Get the project's builderResourcesExcludes configuration
+ *
+ * @private
+ * @returns {string[]} BuilderResourcesExcludes configuration
+ */
+ getBuilderResourcesExcludes() {
+ return this._config.builder?.resources?.excludes || [];
+ }
+
+ /**
+ * Get the project's customTasks configuration
+ *
+ * @private
+ * @returns {object[]} CustomTasks configuration
+ */
+ getCustomTasks() {
+ return this._config.builder?.customTasks || [];
+ }
+
+ /**
+ * Get the project's customMiddleware configuration
+ *
+ * @private
+ * @returns {object[]} CustomMiddleware configuration
+ */
+ getCustomMiddleware() {
+ return this._config.server?.customMiddleware || [];
+ }
+
+ /**
+ * Get the project's serverSettings configuration
+ *
+ * @private
+ * @returns {object} ServerSettings configuration
+ */
+ getServerSettings() {
+ return this._config.server?.settings;
+ }
+
+ /**
+ * Get the project's builderSettings configuration
+ *
+ * @private
+ * @returns {object} BuilderSettings configuration
+ */
+ getBuilderSettings() {
+ return this._config.builder?.settings;
+ }
+
+ /**
+ * Get the project's buildManifest configuration
+ *
+ * @private
+ * @returns {object|null} BuildManifest configuration or null if none is available
+ */
+ getBuildManifest() {
+ return this._buildManifest || null;
+ }
+
+ /* === Resource Access === */
+ /**
+ * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the
+ * project in the specified "style":
+ *
+ *
+ * buildtime: Resource paths are always prefixed with /resources/
+ * or /test-resources/ followed by the project's namespace.
+ * Any configured build-excludes are applied
+ * dist: Resource paths always match with what the UI5 runtime expects.
+ * This means that paths generally depend on the project type. Applications for example use a "flat"-like
+ * structure, while libraries use a "buildtime"-like structure.
+ * Any configured build-excludes are applied
+ * runtime: Resource paths always match with what the UI5 runtime expects.
+ * This means that paths generally depend on the project type. Applications for example use a "flat"-like
+ * structure, while libraries use a "buildtime"-like structure.
+ * This style is typically used for serving resources directly. Therefore, build-excludes are not applied
+ * flat: Resource paths are never prefixed and namespaces are omitted if possible. Note that
+ * project types like "theme-library", which can have multiple namespaces, can't omit them.
+ * Any configured build-excludes are applied
+ *
+ *
+ * Resource readers always use POSIX-style paths.
+ *
+ * @public
+ * @param {object} [options]
+ * @param {string} [options.style=buildtime] Path style to access resources.
+ * Can be "buildtime", "dist", "runtime" or "flat"
+ * @returns {@ui5/fs/ReaderCollection} Reader collection allowing access to all resources of the project
+ */
+ getReader(options) {
+ throw new Error(`getReader must be implemented by subclass ${this.constructor.name}`);
+ }
+
+ getResourceTagCollection() {
+ if (!this._resourceTagCollection) {
+ this._resourceTagCollection = new ResourceTagCollection({
+ allowedTags: ["ui5:IsDebugVariant", "ui5:HasDebugVariant"],
+ allowedNamespaces: ["project"],
+ tags: this.getBuildManifest()?.tags
+ });
+ }
+ return this._resourceTagCollection;
+ }
+
+ /**
+ * Get a [DuplexCollection]{@link @ui5/fs/DuplexCollection} for accessing and modifying a
+ * project's resources. This is always of style buildtime.
+ *
+ * @public
+ * @returns {@ui5/fs/DuplexCollection} DuplexCollection
+ */
+ getWorkspace() {
+ throw new Error(`getWorkspace must be implemented by subclass ${this.constructor.name}`);
+ }
+
+ /* === Internals === */
+ /**
+ * @private
+ * @param {object} config Configuration object
+ */
+ async _configureAndValidatePaths(config) {}
+
+ /**
+ * @private
+ * @param {object} config Configuration object
+ */
+ async _parseConfiguration(config) {}
+}
+
+export default Project;
diff --git a/packages/project/lib/specifications/Specification.js b/packages/project/lib/specifications/Specification.js
new file mode 100644
index 00000000000..71553e73af7
--- /dev/null
+++ b/packages/project/lib/specifications/Specification.js
@@ -0,0 +1,296 @@
+import path from "node:path";
+import {getLogger} from "@ui5/logger";
+import {createReader} from "@ui5/fs/resourceFactory";
+import SpecificationVersion from "./SpecificationVersion.js";
+
+/**
+ * Abstract superclass for all projects and extensions
+ *
+ * @public
+ * @abstract
+ * @class
+ * @alias @ui5/project/specifications/Specification
+ * @hideconstructor
+ */
+class Specification {
+ /**
+ * Create a Specification instance for the given parameters
+ *
+ * @param {object} parameters
+ * @param {string} parameters.id Unique ID
+ * @param {string} parameters.version Version
+ * @param {string} parameters.modulePath Absolute File System path to access resources
+ * @param {object} parameters.configuration
+ * Type-dependent configuration object. Typically defined in a ui5.yaml
+ * @static
+ * @public
+ */
+ static async create(parameters) {
+ if (!parameters.configuration) {
+ throw new Error(
+ `Unable to create Specification instance: Missing configuration parameter`);
+ }
+ const {kind, type} = parameters.configuration;
+ if (!["project", "extension"].includes(kind)) {
+ throw new Error(`Unable to create Specification instance: Unknown kind '${kind}'`);
+ }
+
+ switch (type) {
+ case "application": {
+ return createAndInitializeSpec("types/Application.js", parameters);
+ }
+ case "library": {
+ return createAndInitializeSpec("types/Library.js", parameters);
+ }
+ case "theme-library": {
+ return createAndInitializeSpec("types/ThemeLibrary.js", parameters);
+ }
+ case "module": {
+ return createAndInitializeSpec("types/Module.js", parameters);
+ }
+ case "task": {
+ return createAndInitializeSpec("extensions/Task.js", parameters);
+ }
+ case "server-middleware": {
+ return createAndInitializeSpec("extensions/ServerMiddleware.js", parameters);
+ }
+ case "project-shim": {
+ return createAndInitializeSpec("extensions/ProjectShim.js", parameters);
+ }
+ default:
+ throw new Error(
+ `Unable to create Specification instance: Unknown specification type '${type}'`);
+ }
+ }
+
+ constructor() {
+ if (new.target === Specification) {
+ throw new TypeError("Class 'Specification' is abstract. Please use one of the 'types' subclasses");
+ }
+ this._log = getLogger(`specifications:types:${this.constructor.name}`);
+ }
+
+ /**
+ * @param {object} parameters Specification parameters
+ * @param {string} parameters.id Unique ID
+ * @param {string} parameters.version Version
+ * @param {string} parameters.modulePath Absolute File System path to access resources
+ * @param {object} parameters.configuration Configuration object
+ */
+ async init({id, version, modulePath, configuration}) {
+ if (!id) {
+ throw new Error(`Could not create Specification: Missing or empty parameter 'id'`);
+ }
+ if (!version) {
+ throw new Error(`Could not create Specification: Missing or empty parameter 'version'`);
+ }
+ if (!modulePath) {
+ throw new Error(`Could not create Specification: Missing or empty parameter 'modulePath'`);
+ }
+ if (!path.isAbsolute(modulePath)) {
+ throw new Error(`Could not create Specification: Parameter 'modulePath' must contain an absolute path`);
+ }
+ if (!configuration) {
+ throw new Error(`Could not create Specification: Missing or empty parameter 'configuration'`);
+ }
+
+ this._version = version;
+ this._modulePath = modulePath;
+
+ // The ID property is filled from the provider (e.g. package.json "name") and might differ between providers.
+ // It is mainly used to detect framework libraries marked by @openui5 / @sapui5 scopes of npm package.
+ // (see Project#isFrameworkProject)
+ // In general, the configured name (metadata.name) should be used instead as the unique identifier of a project.
+ this.__id = id;
+
+ // Deep clone config to prevent changes by reference
+ const config = JSON.parse(JSON.stringify(configuration));
+ const {validate} = await import("../validation/validator.js");
+
+ if (SpecificationVersion.major(config.specVersion) <= 1) {
+ const originalSpecVersion = config.specVersion;
+ this._log.verbose(`Detected legacy Specification Version ${config.specVersion}, defined for ` +
+ `${config.kind} ${config.metadata.name}. ` +
+ `Attempting to migrate the project to a supported specification version...`);
+ this._migrateLegacyProject(config);
+ try {
+ await validate({
+ config,
+ project: {
+ id
+ }
+ });
+ } catch (err) {
+ this._log.verbose(
+ `Validation error after migration of ${config.kind} ${config.metadata.name}:`);
+ this._log.verbose(err.message);
+ throw new Error(
+ `${config.kind} ${config.metadata.name} defines unsupported Specification Version ` +
+ `${originalSpecVersion}. Please manually upgrade to 3.0 or higher. ` +
+ `For details see https://ui5.github.io/cli/pages/Configuration/#specification-versions - ` +
+ `An attempted migration to a supported specification version failed, ` +
+ `likely due to unrecognized configuration. Check verbose log for details.`);
+ }
+ } else {
+ await validate({
+ config,
+ project: {
+ id
+ }
+ });
+ }
+
+ // Check whether the given configuration matches the class by guessing the type name from the class name
+ if (config.type.replace("-", "") !== this.constructor.name.toLowerCase()) {
+ throw new Error(
+ `Configuration mismatch: Supplied configuration of type '${config.type}' does not match with ` +
+ `specification class ${this.constructor.name}`);
+ }
+
+ this._name = config.metadata.name;
+ this._kind = config.kind;
+ this._type = config.type;
+ this._specVersionString = config.specVersion;
+ this._specVersion = new SpecificationVersion(this._specVersionString);
+ this._config = config;
+
+ return this;
+ }
+
+ /* === Attributes === */
+ /**
+ * Gets the ID of this specification.
+ *
+ * Note: Only to be used for special occasions as it is specific to the provider that was used and does
+ * not necessarily represent something defined by the project.
+ *
+ * For general purposes of a unique identifier use
+ * {@link @ui5/project/specifications/Specification#getName getName} instead.
+ *
+ * @public
+ * @returns {string} Specification ID
+ */
+ getId() {
+ return this.__id;
+ }
+
+ /**
+ * Gets the name of this specification. Represents a unique identifier.
+ *
+ * @public
+ * @returns {string} Specification name
+ */
+ getName() {
+ return this._name;
+ }
+
+ /**
+ * Gets the kind of this specification, for example project or extension
+ *
+ * @public
+ * @returns {string} Specification kind
+ */
+ getKind() {
+ return this._kind;
+ }
+
+ /**
+ * Gets the type of this specification,
+ * for example application or library in case of projects,
+ * and task or server-middleware in case of extensions
+ *
+ * @public
+ * @returns {string} Specification type
+ */
+ getType() {
+ return this._type;
+ }
+
+ /**
+ * Returns an instance of a helper class representing a Specification Version
+ *
+ * @public
+ * @returns {@ui5/project/specifications/SpecificationVersion}
+ */
+ getSpecVersion() {
+ return this._specVersion;
+ }
+
+ /**
+ * Gets the specification's generic version, as typically defined in a package.json
+ *
+ * @public
+ * @returns {string} Project version
+ */
+ getVersion() {
+ return this._version;
+ }
+
+ /**
+ * Gets the specification's file system path. This might not be POSIX-style on some platforms
+ *
+ * @public
+ * @returns {string} Project root path
+ */
+ getRootPath() {
+ return this._modulePath;
+ }
+
+ /* === Resource Access === */
+ /**
+ * Gets a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for the root directory of the specification.
+ * Resource readers always use POSIX-style
+ *
+ * @public
+ * @param {object} [parameters] Parameters
+ * @param {object} [parameters.useGitignore=true]
+ * Whether to apply any excludes defined in an optional .gitignore in the root directory
+ * @returns {@ui5/fs/ReaderCollection} Reader collection
+ */
+ getRootReader({useGitignore=true} = {}) {
+ return createReader({
+ fsBasePath: this.getRootPath(),
+ virBasePath: "/",
+ name: `Root reader for ${this.getType()} ${this.getKind()} ${this.getName()}`,
+ useGitignore
+ });
+ }
+
+ /* === Internals === */
+ /* === Helper === */
+ /**
+ * @private
+ * @param {string} dirPath Directory path, relative to the specification root
+ */
+ async _dirExists(dirPath) {
+ const resource = await this.getRootReader().byPath(dirPath, {nodir: false});
+ if (resource && resource.getStatInfo().isDirectory()) {
+ return true;
+ }
+ return false;
+ }
+
+ _migrateLegacyProject(config) {
+ // Stick to 2.6 since 3.0 adds further restrictions (i.e. for the name) and enables
+ // functionality for extensions that shouldn't be enabled if the specVersion is not
+ // explicitly set to 3.x
+ config.specVersion = "2.6";
+
+ // propertiesFileSourceEncoding (relevant for applications and libraries) default
+ // has been changed to UTF-8 with specVersion 2.0
+ // Adding back the old default if no configuration is provided.
+ if (config.kind === "project" && ["application", "library"].includes(config.type) &&
+ !config.resources?.configuration?.propertiesFileSourceEncoding) {
+ config.resources = config.resources || {};
+ config.resources.configuration = config.resources.configuration || {};
+ config.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1";
+ }
+ }
+}
+
+async function createAndInitializeSpec(moduleName, params) {
+ const {default: Spec} = await import(`./${moduleName}`);
+ return new Spec().init(params);
+}
+
+export default Specification;
diff --git a/packages/project/lib/specifications/SpecificationVersion.js b/packages/project/lib/specifications/SpecificationVersion.js
new file mode 100644
index 00000000000..6448f1638ad
--- /dev/null
+++ b/packages/project/lib/specifications/SpecificationVersion.js
@@ -0,0 +1,310 @@
+import semver from "semver";
+
+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.1", "3.2",
+ "4.0"
+];
+
+/**
+ * Helper class representing a Specification Version. Featuring helper functions for easy comparison
+ * of versions.
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/specifications/utils/SpecificationVersion
+ */
+class SpecificationVersion {
+ #specVersion;
+ #semverVersion;
+
+ /**
+ * @public
+ * @param {string} specVersion Specification Version to use for all comparison operations
+ * @throws {Error} Throws if provided Specification Version is not supported by this version of @ui5/project
+ */
+ constructor(specVersion) {
+ this.#specVersion = specVersion;
+ this.#semverVersion = getSemverCompatibleVersion(specVersion); // Throws for unsupported versions
+ }
+
+ /**
+ * Returns the Specification Version
+ *
+ * @public
+ * @returns {string} Specification Version
+ */
+ toString() {
+ return this.#specVersion;
+ }
+
+ /**
+ * Returns the major-version of the instance's Specification Version
+ *
+ * @public
+ * @returns {integer} Major version
+ */
+ major() {
+ return semver.major(this.#semverVersion);
+ }
+
+ /**
+ * Returns the minor-version of the instance's Specification Version
+ *
+ * @public
+ * @returns {integer} Minor version
+ */
+ minor() {
+ return semver.minor(this.#semverVersion);
+ }
+
+ /**
+ * 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 or =3.0
+ * @returns {boolean} True if the instance's Specification Version falls into the provided range
+ */
+ satisfies(range) {
+ return semver.satisfies(this.#semverVersion, range);
+ }
+
+ /**
+ * Test whether the instance's Specification Version is greater than the provided test version
+ *
+ * @public
+ * @param {string} testVersion A Specification Version to compare the instance's Specification Version to
+ * @returns {boolean} True if the instance's Specification Version is greater than the provided version
+ */
+ gt(testVersion) {
+ return handleSemverComparator(semver.gt, this.#semverVersion, testVersion);
+ }
+
+ /**
+ * Test whether the instance's Specification Version is greater than or equal to the provided test version
+ *
+ * @public
+ * @param {string} testVersion A Specification Version to compare the instance's Specification Version to
+ * @returns {boolean} True if the instance's Specification Version is greater than or equal to the provided version
+ */
+ gte(testVersion) {
+ return handleSemverComparator(semver.gte, this.#semverVersion, testVersion);
+ }
+
+ /**
+ * Test whether the instance's Specification Version is smaller than the provided test version
+ *
+ * @public
+ * @param {string} testVersion A Specification Version to compare the instance's Specification Version to
+ * @returns {boolean} True if the instance's Specification Version is smaller than the provided version
+ */
+ lt(testVersion) {
+ return handleSemverComparator(semver.lt, this.#semverVersion, testVersion);
+ }
+
+ /**
+ * Test whether the instance's Specification Version is smaller than or equal to the provided test version
+ *
+ * @public
+ * @param {string} testVersion A Specification Version to compare the instance's Specification Version to
+ * @returns {boolean} True if the instance's Specification Version is smaller than or equal to the provided version
+ */
+ lte(testVersion) {
+ return handleSemverComparator(semver.lte, this.#semverVersion, testVersion);
+ }
+
+ /**
+ * Test whether the instance's Specification Version is equal to the provided test version
+ *
+ * @public
+ * @param {string} testVersion A Specification Version to compare the instance's Specification Version to
+ * @returns {boolean} True if the instance's Specification Version is equal to the provided version
+ */
+ eq(testVersion) {
+ return handleSemverComparator(semver.eq, this.#semverVersion, testVersion);
+ }
+
+ /**
+ * Test whether the instance's Specification Version is not equal to the provided test version
+ *
+ * @public
+ * @param {string} testVersion A Specification Version to compare the instance's Specification Version to
+ * @returns {boolean} True if the instance's Specification Version is not equal to the provided version
+ */
+ neq(testVersion) {
+ return handleSemverComparator(semver.neq, this.#semverVersion, testVersion);
+ }
+
+ /**
+ * Test whether the provided Specification Version is supported by this version of @ui5/project
+ *
+ * @public
+ * @param {string} testVersion A Specification Version to compare the instance's Specification Version to
+ * @returns {boolean} True if the provided Specification Version is supported
+ */
+ static isSupportedSpecVersion(testVersion) {
+ return SUPPORTED_VERSIONS.includes(testVersion);
+ }
+
+ /**
+ * Returns the major-version of the provided Specification Version
+ *
+ * @public
+ * @param {string} specVersion Specification Version
+ * @returns {integer} Major version
+ */
+ static major(specVersion) {
+ const comparator = new SpecificationVersion(specVersion);
+ return comparator.major();
+ }
+
+ /**
+ * Returns the minor-version of the provided Specification Version
+ *
+ * @public
+ * @param {string} specVersion Specification Version
+ * @returns {integer} Minor version
+ */
+ static minor(specVersion) {
+ const comparator = new SpecificationVersion(specVersion);
+ return comparator.minor();
+ }
+
+ /**
+ * Test whether the provided Specification Version falls into the provided range
+ *
+ * @public
+ * @param {string} specVersion Specification Version
+ * @param {string} range [Semver]{@link https://www.npmjs.com/package/semver}-style version range,
+ * for example 2.2 - 2.4
+ * @returns {boolean} True if the provided Specification Version falls into the provided range
+ */
+ static satisfies(specVersion, range) {
+ const comparator = new SpecificationVersion(specVersion);
+ return comparator.satisfies(range);
+ }
+
+ /**
+ * Test whether the provided Specification Version is greater than the provided test version
+ *
+ * @public
+ * @param {string} specVersion Specification Version
+ * @param {string} testVersion A Specification Version to compare the provided Specification Version to
+ * @returns {boolean} True if the provided Specification Version is greater than the provided version
+ */
+ static gt(specVersion, testVersion) {
+ const comparator = new SpecificationVersion(specVersion);
+ return comparator.gt(testVersion);
+ }
+
+ /**
+ * Test whether the provided Specification Version is greater than or equal to the provided test version
+ *
+ * @public
+ * @param {string} specVersion Specification Version
+ * @param {string} testVersion A Specification Version to compare the provided Specification Version to
+ * @returns {boolean} True if the provided Specification Version is greater than or equal to the provided version
+ */
+ static gte(specVersion, testVersion) {
+ const comparator = new SpecificationVersion(specVersion);
+ return comparator.gte(testVersion);
+ }
+
+ /**
+ * Test whether the provided Specification Version is smaller than the provided test version
+ *
+ * @public
+ * @param {string} specVersion Specification Version
+ * @param {string} testVersion A Specification Version to compare the provided Specification Version to
+ * @returns {boolean} True if the provided Specification Version is smaller than the provided version
+ */
+ static lt(specVersion, testVersion) {
+ const comparator = new SpecificationVersion(specVersion);
+ return comparator.lt(testVersion);
+ }
+
+ /**
+ * Test whether the provided Specification Version is smaller than or equal to the provided test version
+ *
+ * @public
+ * @param {string} specVersion Specification Version
+ * @param {string} testVersion A Specification Version to compare the provided Specification Version to
+ * @returns {boolean} True if the provided Specification Version is smaller than or equal to the provided version
+ */
+ static lte(specVersion, testVersion) {
+ const comparator = new SpecificationVersion(specVersion);
+ return comparator.lte(testVersion);
+ }
+
+ /**
+ * Test whether the provided Specification Version is equal to the provided test version
+ *
+ * @public
+ * @param {string} specVersion Specification Version
+ * @param {string} testVersion A Specification Version to compare the provided Specification Version to
+ * @returns {boolean} True if the provided Specification Version is equal to the provided version
+ */
+ static eq(specVersion, testVersion) {
+ const comparator = new SpecificationVersion(specVersion);
+ return comparator.eq(testVersion);
+ }
+
+ /**
+ * Test whether the provided Specification Version is not equal to the provided test version
+ *
+ * @public
+ * @param {string} specVersion Specification Version
+ * @param {string} testVersion A Specification Version to compare the provided Specification Version to
+ * @returns {boolean} True if the provided Specification Version is not equal to the provided version
+ */
+ static neq(specVersion, testVersion) {
+ const comparator = new SpecificationVersion(specVersion);
+ return comparator.neq(testVersion);
+ }
+
+ /**
+ * Creates 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) {
+ return `Unsupported Specification Version ${specVersion} defined. Your UI5 CLI installation might be outdated. ` +
+ `For details, see https://ui5.github.io/cli/pages/Configuration/#specification-versions`;
+}
+
+function getSemverCompatibleVersion(specVersion) {
+ if (SpecificationVersion.isSupportedSpecVersion(specVersion)) {
+ return specVersion + ".0";
+ }
+ throw new Error(getUnsupportedSpecVersionMessage(specVersion));
+}
+
+function handleSemverComparator(comparator, baseVersion, testVersion) {
+ if (SPEC_VERSION_PATTERN.test(testVersion)) {
+ const a = baseVersion;
+ const b = testVersion + ".0";
+ return comparator(a, b);
+ }
+ throw new Error("Invalid spec version expectation given in comparator: " + testVersion);
+}
+
+export default SpecificationVersion;
+
+// Export local function for testing only
+export const __localFunctions__ = (process.env.NODE_ENV === "test") ?
+ {getSemverCompatibleVersion, handleSemverComparator} : /* istanbul ignore next */ undefined;
diff --git a/packages/project/lib/specifications/extensions/ProjectShim.js b/packages/project/lib/specifications/extensions/ProjectShim.js
new file mode 100644
index 00000000000..6bf00140fca
--- /dev/null
+++ b/packages/project/lib/specifications/extensions/ProjectShim.js
@@ -0,0 +1,60 @@
+import Extension from "../Extension.js";
+
+/**
+ * ProjectShim
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/specifications/extensions/ProjectShim
+ * @extends @ui5/project/specifications/Extension
+ * @hideconstructor
+ */
+class ProjectShim extends Extension {
+ constructor(parameters) {
+ super(parameters);
+ }
+
+
+ /* === Attributes === */
+ /**
+ * @public
+ */
+ getDependencyShims() {
+ return this._config.shims.dependencies || {};
+ }
+
+ /**
+ * @public
+ */
+ getConfigurationShims() {
+ return this._config.shims.configurations || {};
+ }
+
+ /**
+ * @public
+ */
+ getCollectionShims() {
+ return this._config.shims.collections || {};
+ }
+
+ /* === Internals === */
+ /**
+ * @private
+ */
+ async _validateConfig() {
+ if (this._config.shims.collections) {
+ const {
+ default: path
+ } = await import("path");
+ for (const dependencyDefinition of Object.values(this._config.shims.collections)) {
+ Object.values(dependencyDefinition.modules).forEach((depPath) => {
+ if (path.isAbsolute(depPath)) {
+ throw new Error("All module paths of collections defined in a project-shim must be relative");
+ }
+ });
+ }
+ }
+ }
+}
+
+export default ProjectShim;
diff --git a/packages/project/lib/specifications/extensions/ServerMiddleware.js b/packages/project/lib/specifications/extensions/ServerMiddleware.js
new file mode 100644
index 00000000000..d18e237ffa5
--- /dev/null
+++ b/packages/project/lib/specifications/extensions/ServerMiddleware.js
@@ -0,0 +1,41 @@
+import path from "node:path";
+import Extension from "../Extension.js";
+import {pathToFileURL} from "node:url";
+
+/**
+ * ServerMiddleware
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/specifications/extensions/ServerMiddleware
+ * @extends @ui5/project/specifications/Extension
+ * @hideconstructor
+ */
+class ServerMiddleware extends Extension {
+ constructor(parameters) {
+ super(parameters);
+ }
+
+ /* === Attributes === */
+ /**
+ * @public
+ */
+ async getMiddleware() {
+ const middlewarePath = path.join(this.getRootPath(), this._config.middleware.path);
+ const {default: middleware} = await import(pathToFileURL(middlewarePath));
+ return middleware;
+ }
+ /* === Internals === */
+ /**
+ * @private
+ */
+ async _validateConfig() {
+ // TODO: Move to validator
+ if (/--\d+$/.test(this.getName())) {
+ throw new Error(`Server middleware name must not end with '--'`);
+ }
+ // TODO: Check that paths exist
+ }
+}
+
+export default ServerMiddleware;
diff --git a/packages/project/lib/specifications/extensions/Task.js b/packages/project/lib/specifications/extensions/Task.js
new file mode 100644
index 00000000000..c5aee60b7a0
--- /dev/null
+++ b/packages/project/lib/specifications/extensions/Task.js
@@ -0,0 +1,58 @@
+import path from "node:path";
+import Extension from "../Extension.js";
+import {pathToFileURL} from "node:url";
+
+/**
+ * Task
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/specifications/extensions/Task
+ * @extends @ui5/project/specifications/Extension
+ * @hideconstructor
+ */
+class Task extends Extension {
+ constructor(parameters) {
+ super(parameters);
+ }
+
+ /* === Attributes === */
+ /**
+ * @public
+ */
+ async getTask() {
+ return (await this._getImplementation()).task;
+ }
+
+ /**
+ * @public
+ */
+ async getRequiredDependenciesCallback() {
+ return (await this._getImplementation()).determineRequiredDependencies;
+ }
+
+ /* === Internals === */
+ /**
+ * @private
+ */
+ async _getImplementation() {
+ const taskPath = path.join(this.getRootPath(), this._config.task.path);
+ const {default: task, determineRequiredDependencies} = await import(pathToFileURL(taskPath));
+ return {
+ task, determineRequiredDependencies
+ };
+ }
+
+ /**
+ * @private
+ */
+ async _validateConfig() {
+ // TODO: Move to validator
+ if (/--\d+$/.test(this.getName())) {
+ throw new Error(`Task name must not end with '--'`);
+ }
+ // TODO: Check that paths exist
+ }
+}
+
+export default Task;
diff --git a/packages/project/lib/specifications/types/Application.js b/packages/project/lib/specifications/types/Application.js
new file mode 100644
index 00000000000..1dc17b4bc1c
--- /dev/null
+++ b/packages/project/lib/specifications/types/Application.js
@@ -0,0 +1,244 @@
+import fsPath from "node:path";
+import ComponentProject from "../ComponentProject.js";
+import {createReader} from "@ui5/fs/resourceFactory";
+
+/**
+ * Application
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/specifications/types/Application
+ * @extends @ui5/project/specifications/ComponentProject
+ * @hideconstructor
+ */
+class Application extends ComponentProject {
+ constructor(parameters) {
+ super(parameters);
+
+ this._pManifests = Object.create(null);
+
+ this._webappPath = "webapp";
+
+ this._isRuntimeNamespaced = false;
+ }
+
+
+ /* === 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._webappPath);
+ }
+
+ /* === 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 application project ${this.getName()}`,
+ project: this,
+ excludes
+ });
+ }
+
+ _getTestReader() {
+ 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: `Raw source reader for application 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 && config.resources.configuration.paths.webapp) {
+ this._webappPath = config.resources.configuration.paths.webapp;
+ }
+
+ this._log.verbose(`Path mapping for application project ${this.getName()}:`);
+ this._log.verbose(` Physical root path: ${this.getRootPath()}`);
+ this._log.verbose(` Mapped to: ${this._webappPath}`);
+
+ if (!(await this._dirExists("/" + this._webappPath))) {
+ throw new Error(
+ `Unable to find source directory '${this._webappPath}' in application project ${this.getName()}`);
+ }
+ }
+
+ /**
+ * @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 application 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}\n\n` +
+ `If you are about to start a new project, please refer to:\n` +
+ `https://ui5.github.io/cli/v4/pages/GettingStarted/#starting-a-new-project`, {
+ cause: manifestJsonError
+ });
+ }
+ 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) {
+ const error = new Error(
+ `Could not find resource ${filePath} in project ${this.getName()}`);
+ error.code = "ENOENT"; // "File or directory does not exist"
+ throw error;
+ }
+ return JSON.parse(await resource.getString());
+ }).catch((err) => {
+ if (err.code === "ENOENT") {
+ throw err;
+ }
+ throw new Error(
+ `Failed to read ${filePath} for project ` +
+ `${this.getName()}: ${err.message}`);
+ });
+ }
+}
+
+export default Application;
diff --git a/packages/project/lib/specifications/types/Library.js b/packages/project/lib/specifications/types/Library.js
new file mode 100644
index 00000000000..d3d2059a055
--- /dev/null
+++ b/packages/project/lib/specifications/types/Library.js
@@ -0,0 +1,541 @@
+import fsPath from "node:path";
+import posixPath from "node:path/posix";
+import {promisify} from "node:util";
+import ComponentProject from "../ComponentProject.js";
+import * as resourceFactory from "@ui5/fs/resourceFactory";
+
+/**
+ * Library
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/specifications/types/Library
+ * @extends @ui5/project/specifications/ComponentProject
+ * @hideconstructor
+ */
+class Library extends ComponentProject {
+ constructor(parameters) {
+ super(parameters);
+
+ this._pManifest = null;
+ this._pDotLibrary = null;
+ this._pLibraryJs = null;
+
+ this._srcPath = "src";
+ this._testPath = "test";
+ this._testPathExists = false;
+ this._isSourceNamespaced = true;
+
+ this._propertiesFilesSourceEncoding = "UTF-8";
+ }
+
+ /* === Attributes === */
+ /**
+ *
+ * @private
+ */
+ getLibraryPreloadExcludes() {
+ return this._config.builder && this._config.builder.libraryPreload &&
+ this._config.builder.libraryPreload.excludes || [];
+ }
+
+ /**
+ * @private
+ */
+ getJsdocExcludes() {
+ return this._config.builder && this._config.builder.jsdoc && this._config.builder.jsdoc.excludes || [];
+ }
+
+ /**
+ * 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) {
+ // TODO: Throw for libraries with additional namespaces like sap.ui.core?
+ let virBasePath = "/resources/";
+ if (!this._isSourceNamespaced) {
+ // In case the namespace is not represented in the source directory
+ // structure, add it to the virtual base path
+ virBasePath += `${this._namespace}/`;
+ }
+ return resourceFactory.createReader({
+ fsBasePath: this.getSourcePath(),
+ virBasePath,
+ name: `Source reader for library 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;
+ }
+ let virBasePath = "/test-resources/";
+ if (!this._isSourceNamespaced) {
+ // In case the namespace is not represented in the source directory
+ // structure, add it to the virtual base path
+ virBasePath += `${this._namespace}/`;
+ }
+ const testReader = resourceFactory.createReader({
+ fsBasePath: fsPath.join(this.getRootPath(), this._testPath),
+ virBasePath,
+ name: `Runtime test-resources reader for library 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.
+ * In the future the path structure can be flat or namespaced depending on the project
+ * setup
+ *
+ * @returns {@ui5/fs/ReaderCollection} Reader collection
+ */
+ _getRawSourceReader() {
+ return resourceFactory.createReader({
+ fsBasePath: this.getSourcePath(),
+ virBasePath: "/",
+ name: `Raw source reader for library 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 library project ${this.getName()}`);
+ }
+ this._testPathExists = await this._dirExists("/" + this._testPath);
+
+ this._log.verbose(`Path mapping for library 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();
+
+ if (!config.metadata.copyright) {
+ const copyright = await this._getCopyrightFromDotLibrary();
+ if (copyright) {
+ config.metadata.copyright = copyright;
+ }
+ }
+
+ if (this.isFrameworkProject()) {
+ // Only framework projects are allowed to provide preload-excludes in their .library file,
+ // and only if it is not already defined in the ui5.yaml
+ if (config.builder?.libraryPreload?.excludes) {
+ this._log.verbose(
+ `Using preload excludes for framework library ${this.getName()} from project configuration`);
+ } else {
+ this._log.verbose(
+ `No preload excludes defined in project configuration of framework library ` +
+ `${this.getName()}. Falling back to .library...`);
+ const excludes = await this._getPreloadExcludesFromDotLibrary();
+ if (excludes) {
+ if (!config.builder) {
+ config.builder = {};
+ }
+ if (!config.builder.libraryPreload) {
+ config.builder.libraryPreload = {};
+ }
+ config.builder.libraryPreload.excludes = excludes;
+ }
+ }
+ }
+ }
+
+ /**
+ * Determine library namespace by checking manifest.json with fallback to .library.
+ * 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 _getNamespace() {
+ // Trigger both reads asynchronously
+ const [{
+ namespace: manifestNs,
+ filePath: manifestPath
+ }, {
+ namespace: dotLibraryNs,
+ filePath: dotLibraryPath
+ }] = await Promise.all([
+ this._getNamespaceFromManifest(),
+ this._getNamespaceFromDotLibrary()
+ ]);
+
+ let libraryNs;
+ let namespacePath;
+ if (manifestNs && dotLibraryNs) {
+ // Both files present
+ // => check whether they are on the same level
+ const manifestDepth = manifestPath.split("/").length;
+ const dotLibraryDepth = dotLibraryPath.split("/").length;
+
+ if (manifestDepth < dotLibraryDepth) {
+ // We see the .library file as the "leading" file of a library
+ // Therefore, a manifest.json on a higher level is something we do not except
+ throw new Error(`Failed to detect namespace for project ${this.getName()}: ` +
+ `Found a manifest.json on a higher directory level than the .library file. ` +
+ `It should be on the same or a lower level. ` +
+ `Note that a manifest.json on a lower level will be ignored.\n` +
+ ` manifest.json path: ${manifestPath}\n` +
+ ` is higher than\n` +
+ ` .library path: ${dotLibraryPath}`);
+ }
+ if (manifestDepth === dotLibraryDepth) {
+ if (posixPath.dirname(manifestPath) !== posixPath.dirname(dotLibraryPath)) {
+ // This just should not happen in your project
+ throw new Error(`Failed to detect namespace for project ${this.getName()}: ` +
+ `Found a manifest.json on the same directory level but in a different directory ` +
+ `than the .library file. They should be in the same directory.\n` +
+ ` manifest.json path: ${manifestPath}\n` +
+ ` is different to\n` +
+ ` .library path: ${dotLibraryPath}`);
+ }
+ // Typical scenario if both files are present
+ this._log.verbose(
+ `Found a manifest.json and a .library file on the same level for ` +
+ `project ${this.getName()}.`);
+ this._log.verbose(
+ `Resolving namespace of project ${this.getName()} from manifest.json...`);
+ libraryNs = manifestNs;
+ namespacePath = posixPath.dirname(manifestPath);
+ } else {
+ // Typical scenario: Some nested component has a manifest.json but the library itself only
+ // features a .library. => Ignore the manifest.json
+ this._log.verbose(
+ `Ignoring manifest.json found on a lower level than the .library file of ` +
+ `project ${this.getName()}.`);
+ this._log.verbose(
+ `Resolving namespace of project ${this.getName()} from .library...`);
+ libraryNs = dotLibraryNs;
+ namespacePath = posixPath.dirname(dotLibraryPath);
+ }
+ } else if (manifestNs) {
+ // Only manifest available
+ this._log.verbose(
+ `Resolving namespace of project ${this.getName()} from manifest.json...`);
+ libraryNs = manifestNs;
+ namespacePath = posixPath.dirname(manifestPath);
+ } else if (dotLibraryNs) {
+ // Only .library available
+ this._log.verbose(
+ `Resolving namespace of project ${this.getName()} from .library...`);
+ libraryNs = dotLibraryNs;
+ namespacePath = posixPath.dirname(dotLibraryPath);
+ } else {
+ this._log.verbose(
+ `Failed to resolve namespace of project ${this.getName()} from manifest.json ` +
+ `or .library file. Falling back to library.js file path...`);
+ }
+
+ let namespace;
+ if (libraryNs) {
+ // Maven placeholders can only exist in manifest.json or .library configuration
+ if (this._hasMavenPlaceholder(libraryNs)) {
+ try {
+ libraryNs = await this._resolveMavenPlaceholder(libraryNs);
+ } catch (err) {
+ throw new Error(
+ `Failed to resolve namespace maven placeholder of project ` +
+ `${this.getName()}: ${err.message}`);
+ }
+ }
+
+ namespace = libraryNs.replace(/\./g, "/");
+ if (namespacePath === "/") {
+ this._log.verbose(`Detected flat library source structure for project ${this.getName()}`);
+ this._isSourceNamespaced = false;
+ } else {
+ namespacePath = namespacePath.replace("/", ""); // remove leading slash
+ if (namespacePath !== namespace) {
+ throw new Error(
+ `Detected namespace "${namespace}" does not match detected directory ` +
+ `structure "${namespacePath}" for project ${this.getName()}`);
+ }
+ }
+ } else {
+ try {
+ const libraryJsPath = await this._getLibraryJsPath();
+ namespacePath = posixPath.dirname(libraryJsPath);
+ namespace = namespacePath.replace("/", ""); // remove leading slash
+ if (namespace === "") {
+ throw new Error(`Found library.js file in root directory. ` +
+ `Expected it to be in namespace directory.`);
+ }
+ this._log.verbose(
+ `Deriving namespace for project ${this.getName()} from ` +
+ `path of library.js file`);
+ } catch (err) {
+ this._log.verbose(
+ `Namespace resolution from library.js file path failed for project ` +
+ `${this.getName()}: ${err.message}`);
+ }
+ }
+
+ if (!namespace) {
+ throw new Error(`Failed to detect namespace or namespace is empty for ` +
+ `project ${this.getName()}. Check verbose log for details.`);
+ }
+
+ this._log.verbose(
+ `Namespace of project ${this.getName()} is ${namespace}`);
+ return namespace;
+ }
+
+ async _getNamespaceFromManifest() {
+ try {
+ const {content: manifest, filePath} = await this._getManifest();
+ // check for a proper sap.app/id in manifest.json to determine namespace
+ if (manifest["sap.app"] && manifest["sap.app"].id) {
+ const namespace = manifest["sap.app"].id;
+ this._log.verbose(
+ `Found namespace ${namespace} in manifest.json of project ${this.getName()} ` +
+ `at ${filePath}`);
+ return {
+ namespace,
+ filePath
+ };
+ } else {
+ throw new Error(
+ `No sap.app/id configuration found in manifest.json of project ${this.getName()} ` +
+ `at ${filePath}`);
+ }
+ } catch (err) {
+ this._log.verbose(
+ `Namespace resolution from manifest.json failed for project ` +
+ `${this.getName()}: ${err.message}`);
+ }
+ return {};
+ }
+
+ async _getNamespaceFromDotLibrary() {
+ try {
+ const {content: dotLibrary, filePath} = await this._getDotLibrary();
+ const namespace = dotLibrary?.library?.name?._;
+ if (namespace) {
+ this._log.verbose(
+ `Found namespace ${namespace} in .library file of project ${this.getName()} ` +
+ `at ${filePath}`);
+ return {
+ namespace,
+ filePath
+ };
+ } else {
+ throw new Error(
+ `No library name found in .library of project ${this.getName()} ` +
+ `at ${filePath}`);
+ }
+ } catch (err) {
+ this._log.verbose(
+ `Namespace resolution from .library failed for project ` +
+ `${this.getName()}: ${err.message}`);
+ }
+ return {};
+ }
+
+ /**
+ * Determines library copyright from given project configuration with fallback to .library.
+ *
+ * @returns {string|null} Copyright of the project
+ */
+ async _getCopyrightFromDotLibrary() {
+ try {
+ // If no copyright replacement was provided by ui5.yaml,
+ // check if the .library file has a valid copyright replacement
+ const {content: dotLibrary, filePath} = await this._getDotLibrary();
+ if (dotLibrary?.library?.copyright?._) {
+ this._log.verbose(
+ `Using copyright from ${filePath} for project ${this.getName()}...`);
+ return dotLibrary.library.copyright._;
+ } else {
+ this._log.verbose(
+ `No copyright configuration found in ${filePath} ` +
+ `of project ${this.getName()}`);
+ return null;
+ }
+ } catch (err) {
+ this._log.verbose(
+ `Copyright determination from .library failed for project ` +
+ `${this.getName()}: ${err.message}`);
+ return null;
+ }
+ }
+
+ async _getPreloadExcludesFromDotLibrary() {
+ const {content: dotLibrary, filePath} = await this._getDotLibrary();
+ let excludes = dotLibrary?.library?.appData?.packaging?.["all-in-one"]?.exclude;
+ if (excludes) {
+ if (!Array.isArray(excludes)) {
+ excludes = [excludes];
+ }
+ this._log.verbose(
+ `Found ${excludes.length} preload excludes in .library file of ` +
+ `project ${this.getName()} at ${filePath}`);
+ return excludes.map((exclude) => {
+ return exclude.$.name;
+ });
+ } else {
+ this._log.verbose(
+ `No preload excludes found in .library of project ${this.getName()} ` +
+ `at ${filePath}`);
+ return null;
+ }
+ }
+
+ /**
+ * Reads the projects manifest.json
+ *
+ * @returns {Promise} resolves with an object containing the content (as JSON) and
+ * filePath (as string) of the manifest.json file
+ */
+ async _getManifest() {
+ if (this._pManifest) {
+ return this._pManifest;
+ }
+ return this._pManifest = this._getRawSourceReader().byGlob("**/manifest.json")
+ .then(async (manifestResources) => {
+ if (!manifestResources.length) {
+ throw new Error(`Could not find manifest.json file for project ${this.getName()}`);
+ }
+ if (manifestResources.length > 1) {
+ throw new Error(`Found multiple (${manifestResources.length}) manifest.json files ` +
+ `for project ${this.getName()}`);
+ }
+ const resource = manifestResources[0];
+ try {
+ return {
+ content: JSON.parse(await resource.getString()),
+ filePath: resource.getPath()
+ };
+ } catch (err) {
+ throw new Error(
+ `Failed to read ${resource.getPath()} for project ${this.getName()}: ${err.message}`);
+ }
+ });
+ }
+
+ /**
+ * Reads the .library file
+ *
+ * @returns {Promise} resolves with an object containing the content (as JSON) and
+ * filePath (as string) of the .library file
+ */
+ async _getDotLibrary() {
+ if (this._pDotLibrary) {
+ return this._pDotLibrary;
+ }
+ return this._pDotLibrary = this._getRawSourceReader().byGlob("**/.library")
+ .then(async (dotLibraryResources) => {
+ if (!dotLibraryResources.length) {
+ throw new Error(`Could not find .library file for project ${this.getName()}`);
+ }
+ if (dotLibraryResources.length > 1) {
+ throw new Error(`Found multiple (${dotLibraryResources.length}) .library files ` +
+ `for project ${this.getName()}`);
+ }
+ const resource = dotLibraryResources[0];
+ const content = await resource.getString();
+
+ try {
+ const {
+ default: xml2js
+ } = await import("xml2js");
+ const parser = new xml2js.Parser({
+ explicitArray: false,
+ explicitCharkey: true
+ });
+ const readXML = promisify(parser.parseString);
+ return {
+ content: await readXML(content),
+ filePath: resource.getPath()
+ };
+ } catch (err) {
+ throw new Error(
+ `Failed to read ${resource.getPath()} for project ${this.getName()}: ${err.message}`);
+ }
+ });
+ }
+
+ /**
+ * Determines the path of the library.js file
+ *
+ * @returns {Promise} resolves with an a string containing the file system path
+ * of the library.js file
+ */
+ async _getLibraryJsPath() {
+ if (this._pLibraryJs) {
+ return this._pLibraryJs;
+ }
+ return this._pLibraryJs = this._getRawSourceReader().byGlob("**/library.js")
+ .then(async (libraryJsResources) => {
+ if (!libraryJsResources.length) {
+ throw new Error(`Could not find library.js file for project ${this.getName()}`);
+ }
+ if (libraryJsResources.length > 1) {
+ throw new Error(`Found multiple (${libraryJsResources.length}) library.js files ` +
+ `for project ${this.getName()}`);
+ }
+ // Content is not yet relevant, so don't read it
+ return libraryJsResources[0].getPath();
+ });
+ }
+}
+
+export default Library;
diff --git a/packages/project/lib/specifications/types/Module.js b/packages/project/lib/specifications/types/Module.js
new file mode 100644
index 00000000000..14e6a116442
--- /dev/null
+++ b/packages/project/lib/specifications/types/Module.js
@@ -0,0 +1,166 @@
+import fsPath from "node:path";
+import Project from "../Project.js";
+import * as resourceFactory from "@ui5/fs/resourceFactory";
+
+/**
+ * Module
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/specifications/types/Module
+ * @extends @ui5/project/specifications/Project
+ * @hideconstructor
+ */
+class Module extends Project {
+ constructor(parameters) {
+ super(parameters);
+
+ this._paths = null;
+ this._writer = null;
+ }
+
+ /* === Attributes === */
+
+ /**
+ * Since Modules have multiple source paths, this function always throws with an exception
+ *
+ * @public
+ * @throws {Error} Projects of type module have more than one source path
+ */
+ getSourcePath() {
+ throw new Error(`Projects of type module have more than one source path`);
+ }
+
+ /* === Resource Access === */
+
+ /**
+ * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the
+ * project in the specified "style":
+ *
+ *
+ * buildtime: Resource paths are always prefixed with /resources/
+ * or /test-resources/ followed by the project's namespace.
+ * Any configured build-excludes are applied
+ * dist: Resource paths always match with what the UI5 runtime expects.
+ * This means that paths generally depend on the project type. Applications for example use a "flat"-like
+ * structure, while libraries use a "buildtime"-like structure.
+ * Any configured build-excludes are applied
+ * runtime: Resource paths always match with what the UI5 runtime expects.
+ * This means that paths generally depend on the project type. Applications for example use a "flat"-like
+ * structure, while libraries use a "buildtime"-like structure.
+ * This style is typically used for serving resources directly. Therefore, build-excludes are not applied
+ * flat: Resource paths are never prefixed and namespaces are omitted if possible. Note that
+ * project types like "theme-library", which can have multiple namespaces, can't omit them.
+ * Any configured build-excludes are applied
+ *
+ *
+ * If project resources have been changed through the means of a workspace, those changes
+ * are reflected in the provided reader too.
+ *
+ * Resource readers always use POSIX-style paths.
+ *
+ * @public
+ * @param {object} [options]
+ * @param {string} [options.style=buildtime] Path style to access resources.
+ * Can be "buildtime", "dist", "runtime" or "flat"
+ * @returns {@ui5/fs/ReaderCollection} A reader collection instance
+ */
+ getReader({style = "buildtime"} = {}) {
+ // Apply builder excludes to all styles but "runtime"
+ const excludes = style === "runtime" ? [] : this.getBuilderResourcesExcludes();
+
+ const readers = this._paths.map(({name, virBasePath, fsBasePath}) => {
+ return resourceFactory.createReader({
+ name,
+ virBasePath,
+ fsBasePath,
+ project: this,
+ excludes
+ });
+ });
+ if (readers.length === 1) {
+ return readers[0];
+ }
+ const readerCollection = resourceFactory.createReaderCollection({
+ name: `Reader collection for module project ${this.getName()}`,
+ readers
+ });
+ return resourceFactory.createReaderCollectionPrioritized({
+ name: `Reader/Writer collection for project ${this.getName()}`,
+ readers: [this._getWriter(), readerCollection]
+ });
+ }
+
+ /**
+ * Get a resource reader/writer for accessing and modifying a project's resources
+ *
+ * @public
+ * @returns {@ui5/fs/ReaderCollection} A reader collection instance
+ */
+ getWorkspace() {
+ const reader = this.getReader();
+
+ const writer = this._getWriter();
+ return resourceFactory.createWorkspace({
+ reader,
+ writer
+ });
+ }
+
+ _getWriter() {
+ if (!this._writer) {
+ this._writer = resourceFactory.createAdapter({
+ virBasePath: "/"
+ });
+ }
+
+ return this._writer;
+ }
+
+ /* === Internals === */
+ /**
+ * @private
+ * @param {object} config Configuration object
+ */
+ async _configureAndValidatePaths(config) {
+ await super._configureAndValidatePaths(config);
+
+ this._log.verbose(`Path mapping for module project ${this.getName()}:`);
+ this._log.verbose(` Physical root path: ${this.getRootPath()}`);
+ this._log.verbose(` Mapped to:`);
+
+ if (config.resources?.configuration?.paths) {
+ const pathMappings = Object.entries(config.resources.configuration.paths);
+ if (this._log.isLevelEnabled("verbose")) {
+ // Log synchronously before async dir-exists checks
+ pathMappings.forEach(([virBasePath, relFsPath]) => {
+ this._log.verbose(` ${virBasePath} => ${relFsPath}`);
+ });
+ }
+ this._paths = await Promise.all(pathMappings.map(async ([virBasePath, relFsPath]) => {
+ if (!(await this._dirExists("/" + relFsPath))) {
+ throw new Error(
+ `Unable to find source directory '${relFsPath}' in module project ${this.getName()}`);
+ }
+ return {
+ name: `'${relFsPath}'' reader for module project ${this.getName()}`,
+ virBasePath,
+ fsBasePath: fsPath.join(this.getRootPath(), relFsPath)
+ };
+ }));
+ } else {
+ this._log.verbose(` / => `);
+ if (!(await this._dirExists("/"))) {
+ throw new Error(
+ `Unable to find root directory of module project ${this.getName()}`);
+ }
+ this._paths = [{
+ name: `Root reader for module project ${this.getName()}`,
+ virBasePath: "/",
+ fsBasePath: this.getRootPath()
+ }];
+ }
+ }
+}
+
+export default Module;
diff --git a/packages/project/lib/specifications/types/ThemeLibrary.js b/packages/project/lib/specifications/types/ThemeLibrary.js
new file mode 100644
index 00000000000..e0bcd90785d
--- /dev/null
+++ b/packages/project/lib/specifications/types/ThemeLibrary.js
@@ -0,0 +1,170 @@
+import Project from "../Project.js";
+import fsPath from "node:path";
+import * as resourceFactory from "@ui5/fs/resourceFactory";
+
+/**
+ * ThemeLibrary
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/specifications/types/ThemeLibrary
+ * @extends @ui5/project/specifications/Project
+ * @hideconstructor
+ */
+class ThemeLibrary extends Project {
+ constructor(parameters) {
+ super(parameters);
+
+ this._srcPath = "src";
+ this._testPath = "test";
+ this._testPathExists = false;
+ this._writer = null;
+ }
+
+ /* === Attributes === */
+ /**
+ * @private
+ */
+ getCopyright() {
+ return this._config.metadata.copyright;
+ }
+
+ /**
+ * Get the path of the project's source directory
+ *
+ * @public
+ * @returns {string} Absolute path to the source directory of the project
+ */
+ getSourcePath() {
+ return fsPath.join(this.getRootPath(), this._srcPath);
+ }
+
+ /* === Resource Access === */
+ /**
+ * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the
+ * project in the specified "style":
+ *
+ *
+ * buildtime: Resource paths are always prefixed with /resources/
+ * or /test-resources/ followed by the project's namespace.
+ * Any configured build-excludes are applied
+ * dist: Resource paths always match with what the UI5 runtime expects.
+ * This means that paths generally depend on the project type. Applications for example use a "flat"-like
+ * structure, while libraries use a "buildtime"-like structure.
+ * Any configured build-excludes are applied
+ * runtime: Resource paths always match with what the UI5 runtime expects.
+ * This means that paths generally depend on the project type. Applications for example use a "flat"-like
+ * structure, while libraries use a "buildtime"-like structure.
+ * This style is typically used for serving resources directly. Therefore, build-excludes are not applied
+ * flat: Resource paths are never prefixed and namespaces are omitted if possible. Note that
+ * project types like "theme-library", which can have multiple namespaces, can't omit them.
+ * Any configured build-excludes are applied
+ *
+ *
+ * If project resources have been changed through the means of a workspace, those changes
+ * are reflected in the provided reader too.
+ *
+ * Resource readers always use POSIX-style paths.
+ *
+ * @public
+ * @param {object} [options]
+ * @param {string} [options.style=buildtime] Path style to access resources.
+ * Can be "buildtime", "dist", "runtime" or "flat"
+ * @returns {@ui5/fs/ReaderCollection} A reader collection instance
+ */
+ getReader({style = "buildtime"} = {}) {
+ // Apply builder excludes to all styles but "runtime"
+ const excludes = style === "runtime" ? [] : this.getBuilderResourcesExcludes();
+
+ let reader = resourceFactory.createReader({
+ fsBasePath: this.getSourcePath(),
+ virBasePath: "/resources/",
+ name: `Runtime resources reader for theme-library project ${this.getName()}`,
+ project: this,
+ excludes
+ });
+ if (this._testPathExists) {
+ const testReader = resourceFactory.createReader({
+ fsBasePath: fsPath.join(this.getRootPath(), this._testPath),
+ virBasePath: "/test-resources/",
+ name: `Runtime test-resources reader for theme-library project ${this.getName()}`,
+ project: this,
+ excludes
+ });
+ reader = resourceFactory.createReaderCollection({
+ name: `Reader collection for theme-library project ${this.getName()}`,
+ readers: [reader, testReader]
+ });
+ }
+ const writer = this._getWriter();
+
+ return resourceFactory.createReaderCollectionPrioritized({
+ name: `Reader/Writer collection for project ${this.getName()}`,
+ readers: [writer, reader]
+ });
+ }
+
+ /**
+ * Get a [DuplexCollection]{@link @ui5/fs/DuplexCollection} for accessing and modifying a
+ * project's resources.
+ *
+ * This is always of style buildtime, wich for theme libraries is identical to style
+ * runtime.
+ *
+ * @public
+ * @returns {@ui5/fs/DuplexCollection} DuplexCollection
+ */
+ getWorkspace() {
+ const reader = this.getReader();
+
+ const writer = this._getWriter();
+ return resourceFactory.createWorkspace({
+ reader,
+ writer
+ });
+ }
+
+ _getWriter() {
+ if (!this._writer) {
+ this._writer = resourceFactory.createAdapter({
+ virBasePath: "/",
+ project: this
+ });
+ }
+
+ return this._writer;
+ }
+
+ /* === 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 theme-library project ${this.getName()}`);
+ }
+ this._testPathExists = await this._dirExists("/" + this._testPath);
+
+ this._log.verbose(`Path mapping for theme-library 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]"}`);
+ }
+}
+
+export default ThemeLibrary;
diff --git a/packages/project/lib/ui5Framework/AbstractInstaller.js b/packages/project/lib/ui5Framework/AbstractInstaller.js
new file mode 100644
index 00000000000..e13dea7f6e0
--- /dev/null
+++ b/packages/project/lib/ui5Framework/AbstractInstaller.js
@@ -0,0 +1,63 @@
+import path from "node:path";
+import {mkdirp} from "../utils/fs.js";
+import {promisify} from "node:util";
+import {getLogger} from "@ui5/logger";
+const log = getLogger("ui5Framework:Installer");
+
+// File name must not start with one or multiple dots and should not contain characters other than:
+// * alphanumeric
+// * Slash (typically present in package names, hence is accepted and then replaced with a dash)
+// * Dot, dash, underscore, at-sign
+const illegalFileNameRegExp = /[^0-9a-zA-Z\-._@/]/;
+
+class AbstractInstaller {
+ /**
+ * @param {string} ui5DataDir UI5 home directory location. This will be used to store packages,
+ * metadata and configuration used by the resolvers.
+ */
+ constructor(ui5DataDir) {
+ if (new.target === AbstractInstaller) {
+ throw new TypeError("Class 'AbstractInstaller' is abstract");
+ }
+ if (!ui5DataDir) {
+ throw new Error(`Installer: Missing parameter "ui5DataDir"`);
+ }
+ this._lockDir = path.join(ui5DataDir, "framework", "locks");
+ }
+
+ async _synchronize(lockName, callback) {
+ const {
+ default: lockfile
+ } = await import("lockfile");
+ const lock = promisify(lockfile.lock);
+ const unlock = promisify(lockfile.unlock);
+ const lockPath = this._getLockPath(lockName);
+ await mkdirp(this._lockDir);
+ log.verbose("Locking " + lockPath);
+ await lock(lockPath, {
+ wait: 10000,
+ stale: 60000,
+ retries: 10
+ });
+ try {
+ const res = await callback();
+ return res;
+ } finally {
+ log.verbose("Unlocking " + lockPath);
+ await unlock(lockPath);
+ }
+ }
+
+ _sanitizeFileName(fileName) {
+ if (fileName.startsWith(".") || illegalFileNameRegExp.test(fileName)) {
+ throw new Error(`Illegal file name: ${fileName}`);
+ }
+ return fileName.replace(/\//g, "-");
+ }
+
+ _getLockPath(lockName) {
+ return path.join(this._lockDir, `${this._sanitizeFileName(lockName)}.lock`);
+ }
+}
+
+export default AbstractInstaller;
diff --git a/packages/project/lib/ui5Framework/AbstractResolver.js b/packages/project/lib/ui5Framework/AbstractResolver.js
new file mode 100644
index 00000000000..6cb532d98a6
--- /dev/null
+++ b/packages/project/lib/ui5Framework/AbstractResolver.js
@@ -0,0 +1,315 @@
+import path from "node:path";
+import os from "node:os";
+import {getLogger} from "@ui5/logger";
+const log = getLogger("ui5Framework:AbstractResolver");
+import semver from "semver";
+
+// Reduced Semantic Versioning pattern
+// Matches MAJOR or MAJOR.MINOR as a simple version range to be resolved to the latest minor/patch
+const VERSION_RANGE_REGEXP = /^(0|[1-9]\d*)(?:\.(0|[1-9]\d*))?(?:-SNAPSHOT)?$/;
+
+/**
+ * Abstract Resolver
+ *
+ * @abstract
+ * @public
+ * @class
+ * @alias @ui5/project/ui5Framework/AbstractResolver
+ * @hideconstructor
+ */
+class AbstractResolver {
+ /* eslint-disable max-len */
+ /**
+ * @param {*} options options
+ * @param {string} [options.version] Framework version to use. When omitted, all libraries need to be available
+ * via providedLibraryMetadata parameter. Otherwise an error is thrown.
+ * @param {boolean} [options.sources=false] Whether to install framework libraries as sources or
+ * pre-built (with build manifest)
+ * @param {string} [options.cwd=process.cwd()] Current working directory
+ * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages,
+ * metadata and configuration used by the resolvers. Relative to `process.cwd()`
+ * @param {object.} [options.providedLibraryMetadata]
+ * Resolver skips installing listed libraries and uses the dependency information to resolve their dependencies.
+ * version can be omitted in case all libraries can be resolved via the providedLibraryMetadata.
+ * Otherwise an error is thrown.
+ */
+ /* eslint-enable max-len */
+ constructor({cwd, version, sources, ui5DataDir, providedLibraryMetadata}) {
+ if (new.target === AbstractResolver) {
+ throw new TypeError("Class 'AbstractResolver' is abstract");
+ }
+
+ // In some CI environments, the homedir might be set explicitly to a relative
+ // path (e.g. "./"), but tooling requires an absolute path
+ this._ui5DataDir = path.resolve(
+ ui5DataDir || path.join(os.homedir(), ".ui5")
+ );
+ this._cwd = cwd ? path.resolve(cwd) : process.cwd();
+ this._version = version;
+
+ // Environment variable should always enforce usage of sources
+ if (process.env.UI5_PROJECT_USE_FRAMEWORK_SOURCES) {
+ sources = true;
+ }
+ this._sources = !!sources;
+
+ this._providedLibraryMetadata = providedLibraryMetadata;
+ }
+
+ async _processLibrary(libraryName, libraryMetadata, errors) {
+ // Check if library is already processed
+ if (libraryMetadata[libraryName]) {
+ return;
+ }
+ // Mark library as handled
+ libraryMetadata[libraryName] = Object.create(null);
+
+ log.verbose("Processing " + libraryName);
+
+ let promises;
+ const providedLibraryMetadata = this._providedLibraryMetadata?.[libraryName];
+ if (providedLibraryMetadata) {
+ log.verbose(`Skipping install for ${libraryName} (provided)`);
+ promises = {
+ // Use existing metadata if library is provided from outside (e.g. workspace)
+ metadata: Promise.resolve(providedLibraryMetadata),
+ // Provided libraries are already "installed"
+ install: Promise.resolve({
+ pkgPath: providedLibraryMetadata.path
+ })
+ };
+ } else if (!this._version) {
+ throw new Error(`Unable to install library ${libraryName}. No framework version provided.`);
+ } else {
+ promises = await this.handleLibrary(libraryName);
+ }
+
+ const [metadata, {pkgPath}] = await Promise.all([
+ promises.metadata.then((metadata) =>
+ this._processDependencies(libraryName, metadata, libraryMetadata, errors)),
+ promises.install
+ ]);
+
+ // Add path to installed package to metadata
+ metadata.path = pkgPath;
+
+ // Add metadata entry
+ libraryMetadata[libraryName] = metadata;
+ }
+
+ async _processDependencies(libraryName, metadata, libraryMetadata, errors) {
+ if (metadata.dependencies.length > 0) {
+ log.verbose("Processing dependencies of " + libraryName);
+ await this._processLibraries(metadata.dependencies, libraryMetadata, errors);
+ log.verbose("Done processing dependencies of " + libraryName);
+ }
+ return metadata;
+ }
+
+ async _processLibraries(libraryNames, libraryMetadata, errors) {
+ const sourceErrors = new Set();
+ const results = await Promise.all(libraryNames.map(async (libraryName) => {
+ try {
+ await this._processLibrary(libraryName, libraryMetadata, errors);
+ } catch (err) {
+ if (sourceErrors.has(err.message)) {
+ return `Failed to resolve library ${libraryName}: Error already logged`;
+ }
+ sourceErrors.add(err.message);
+ log.verbose(`Failed to process library ${libraryName}`);
+ log.verbose(`Error: ${err.message}`);
+ log.verbose(`Call stack: ${err.stack}`);
+ return `Failed to resolve library ${libraryName}: ${err.message}`;
+ }
+ }));
+ // Don't add empty results (success)
+ errors.push(...results.filter(($) => $));
+ }
+
+ /**
+ * Library metadata entry
+ *
+ * @example
+ * const libraryMetadataEntry = {
+ * "id": "@openui5/sap.ui.core",
+ * "version": "1.75.0",
+ * "path": "~/.ui5/framework/packages/@openui5/sap.ui.core/1.75.0",
+ * "dependencies": [],
+ * "optionalDependencies": []
+ * };
+ *
+ * @public
+ * @typedef {object} @ui5/project/ui5Framework/AbstractResolver~LibraryMetadataEntry
+ * @property {string} id Identifier
+ * @property {string} version Version
+ * @property {string} path Path
+ * @property {string[]} dependencies List of dependency ids
+ * @property {string[]} optionalDependencies List of optional dependency ids
+ */
+
+ /**
+ * Install result
+ *
+ * @example
+ * const resolverInstallResult = {
+ * "libraryMetadata": {
+ * "sap.ui.core": {
+ * // ...
+ * },
+ * "sap.m": {
+ * // ...
+ * }
+ * }
+ * };
+ *
+ * @public
+ * @typedef {object} @ui5/project/ui5Framework/AbstractResolver~ResolverInstallResult
+ * @property {object.} libraryMetadata
+ * Object containing all installed libraries with library name as key
+ */
+
+ /**
+ * Installs the provided libraries and their dependencies
+ *
+ * @example
+ * const resolver = new Sapui5Resolver({version: "1.76.0"});
+ * // Or for OpenUI5:
+ * // const resolver = new Openui5Resolver({version: "1.76.0"});
+ *
+ * resolver.install(["sap.ui.core", "sap.m"]).then(({libraryMetadata}) => {
+ * // Installation done
+ * }).catch((err) => {
+ * // Handle installation errors
+ * });
+ *
+ * @public
+ * @param {string[]} libraryNames List of library names to be installed
+ * @returns {@ui5/project/ui5Framework/AbstractResolver~ResolverInstallResult}
+ * Resolves with an object containing the libraryMetadata
+ */
+ async install(libraryNames) {
+ const libraryMetadata = Object.create(null);
+ const errors = [];
+
+ await this._processLibraries(libraryNames, libraryMetadata, errors);
+
+ if (errors.length === 1) {
+ throw new Error(errors[0]);
+ } if (errors.length > 1) {
+ const msg = errors.map((err, idx) => ` ${idx + 1}. ${err}`).join("\n");
+ throw new Error(`Resolution of framework libraries failed with errors:\n${msg}`);
+ }
+
+ return {
+ libraryMetadata
+ };
+ }
+
+ static async resolveVersion(version, {ui5DataDir, cwd} = {}) {
+ // Don't allow nullish values
+ // An empty string is a valid semver range that converts to "*", which should not be supported
+ if (!version) {
+ throw new Error(`Framework version specifier "${version}" is incorrect or not supported`);
+ }
+
+ const spec = await this._getVersionSpec(version, {ui5DataDir, cwd});
+
+ // For all invalid cases which are not explicitly handled in _getVersionSpec
+ if (!spec) {
+ throw new Error(`Framework version specifier "${version}" is incorrect or not supported`);
+ }
+
+ const versions = await this.fetchAllVersions({ui5DataDir, cwd});
+ const resolvedVersion = semver.maxSatisfying(versions, spec, {
+ // Allow ranges that end with -SNAPSHOT to match any -SNAPSHOT version
+ // like a normal version in order to support ranges like 1.x.x-SNAPSHOT.
+ includePrerelease: this._isSnapshotVersionOrRange(version)
+ });
+
+ if (!resolvedVersion) {
+ if (semver.valid(spec)) {
+ if (this.name === "Sapui5Resolver" && semver.lt(spec, "1.76.0")) {
+ throw new Error(`Could not resolve framework version ${version}. ` +
+ `Note that SAPUI5 framework libraries can only be consumed by the UI5 CLI ` +
+ `starting with SAPUI5 v1.76.0`);
+ } else if (this.name === "Openui5Resolver" && semver.lt(spec, "1.52.5")) {
+ throw new Error(`Could not resolve framework version ${version}. ` +
+ `Note that OpenUI5 framework libraries can only be consumed by the UI5 CLI ` +
+ `starting with OpenUI5 v1.52.5`);
+ }
+ }
+ throw new Error(
+ `Could not resolve framework version ${version}. ` +
+ `Make sure the version is valid and available in the configured registry.`);
+ }
+
+ return resolvedVersion;
+ }
+
+ static async _getVersionSpec(version, {ui5DataDir, cwd}) {
+ if (this._isSnapshotVersionOrRange(version)) {
+ const versionMatch = version.match(VERSION_RANGE_REGEXP);
+ if (versionMatch) {
+ // For snapshot version ranges we need to insert a stand-in "x" for the patch level
+ // and - in case none is provided - another "x" for the major version in order to
+ // convert it to a valid semver range:
+ // "1-SNAPSHOT" becomes "1.x.x-SNAPSHOT" and "1.112-SNAPSHOT" becomes "1.112.x-SNAPSHOT"
+ return `${versionMatch[1]}.${versionMatch[2] || "x"}.x-SNAPSHOT`;
+ }
+ }
+
+ // Covers versions and ranges, as versions are also valid ranges
+ if (semver.validRange(version)) {
+ return version;
+ }
+
+ // Check for invalid tag name (same check as npm does)
+ if (encodeURIComponent(version) !== version) {
+ return null;
+ }
+
+ const allTags = await this.fetchAllTags({ui5DataDir, cwd});
+
+ if (!allTags) {
+ // Resolver doesn't support tags (e.g. Sapui5MavenSnapshotResolver)
+ // Only latest and latest-snapshot are supported which both resolve
+ // to the latest available version.
+ // See "isSnapshotVersionOrRange" for -snapshot handling
+ if ((version === "latest" || version === "latest-snapshot")) {
+ return "*";
+ } else {
+ return null;
+ }
+ }
+
+ if (!allTags[version]) {
+ throw new Error(
+ `Could not resolve framework version via tag '${version}'. ` +
+ `Make sure the tag is available in the configured registry.`
+ );
+ }
+
+ // Use version from tag
+ return allTags[version];
+ }
+
+ static _isSnapshotVersionOrRange(version) {
+ return version.toLowerCase().endsWith("-snapshot");
+ }
+
+ // To be implemented by resolver
+ async getLibraryMetadata(libraryName) {
+ throw new Error("AbstractResolver: getLibraryMetadata must be implemented!");
+ }
+ async handleLibrary(libraryName) {
+ throw new Error("AbstractResolver: handleLibrary must be implemented!");
+ }
+ static fetchAllVersions(options) {
+ throw new Error("AbstractResolver: static fetchAllVersions must be implemented!");
+ }
+ static fetchAllTags(options) {
+ return null;
+ }
+}
+
+export default AbstractResolver;
diff --git a/packages/project/lib/ui5Framework/Openui5Resolver.js b/packages/project/lib/ui5Framework/Openui5Resolver.js
new file mode 100644
index 00000000000..a6a9c4fc02a
--- /dev/null
+++ b/packages/project/lib/ui5Framework/Openui5Resolver.js
@@ -0,0 +1,107 @@
+import path from "node:path";
+import os from "node:os";
+import AbstractResolver from "./AbstractResolver.js";
+import Installer from "./npm/Installer.js";
+
+const OPENUI5_CORE_PACKAGE = "@openui5/sap.ui.core";
+
+/**
+ * Resolver for the OpenUI5 framework
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/ui5Framework/Openui5Resolver
+ * @extends @ui5/project/ui5Framework/AbstractResolver
+ */
+class Openui5Resolver extends AbstractResolver {
+ /**
+ * @param {*} options options
+ * @param {string} options.version OpenUI5 version to use
+ * @param {string} [options.cwd=process.cwd()] Working directory to resolve configurations like .npmrc
+ * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages,
+ * metadata and configuration used by the resolvers. Relative to `process.cwd()`
+ * @param {string} [options.cacheDir] Where to store temp/cached packages.
+ * @param {string} [options.packagesDir] Where to install packages
+ * @param {string} [options.stagingDir] The staging directory for the packages
+ */
+ constructor(options) {
+ super(options);
+
+ const {cacheDir, packagesDir, stagingDir} = options;
+
+ this._installer = new Installer({
+ cwd: this._cwd,
+ ui5DataDir: this._ui5DataDir,
+ cacheDir, packagesDir, stagingDir
+ });
+ this._loadLibraryMetadata = Object.create(null);
+ }
+ static _getNpmPackageName(libraryName) {
+ return "@openui5/" + libraryName;
+ }
+ static _getLibaryName(pkgName) {
+ return pkgName.replace(/^@openui5\//, "");
+ }
+ getLibraryMetadata(libraryName) {
+ if (!this._loadLibraryMetadata[libraryName]) {
+ this._loadLibraryMetadata[libraryName] = Promise.resolve().then(async () => {
+ // Trigger manifest request to gather transitive dependencies
+ const pkgName = Openui5Resolver._getNpmPackageName(libraryName);
+ const libraryManifest = await this._installer.fetchPackageManifest({pkgName, version: this._version});
+ let dependencies = [];
+ if (libraryManifest.dependencies) {
+ const depNames = Object.keys(libraryManifest.dependencies);
+ dependencies = depNames.map(Openui5Resolver._getLibaryName);
+ }
+
+ // npm devDependencies are handled as "optionalDependencies"
+ // in terms of the UI5 framework metadata structure
+ let optionalDependencies = [];
+ if (libraryManifest.devDependencies) {
+ const devDepNames = Object.keys(libraryManifest.devDependencies);
+ optionalDependencies = devDepNames.map(Openui5Resolver._getLibaryName);
+ }
+
+ return {
+ id: pkgName,
+ version: this._version,
+ dependencies,
+ optionalDependencies
+ };
+ });
+ }
+ return this._loadLibraryMetadata[libraryName];
+ }
+ async handleLibrary(libraryName) {
+ const pkgName = Openui5Resolver._getNpmPackageName(libraryName);
+ return {
+ // Trigger metadata request
+ metadata: this.getLibraryMetadata(libraryName),
+ // Also trigger installation of package
+ install: this._installer.installPackage({
+ pkgName,
+ version: this._version
+ })
+ };
+ }
+ static async fetchAllVersions(options) {
+ const installer = this._getInstaller(options);
+ return await installer.fetchPackageVersions({pkgName: OPENUI5_CORE_PACKAGE});
+ }
+
+ static async fetchAllTags(options) {
+ const installer = this._getInstaller(options);
+ return installer.fetchPackageDistTags({pkgName: OPENUI5_CORE_PACKAGE});
+ }
+
+ static _getInstaller({ui5DataDir, cwd} = {}) {
+ return new Installer({
+ cwd: cwd ? path.resolve(cwd) : process.cwd(),
+ ui5DataDir:
+ ui5DataDir ? path.resolve(ui5DataDir) :
+ path.join(os.homedir(), ".ui5")
+ });
+ }
+}
+
+export default Openui5Resolver;
diff --git a/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js b/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js
new file mode 100644
index 00000000000..7002bddbd27
--- /dev/null
+++ b/packages/project/lib/ui5Framework/Sapui5MavenSnapshotResolver.js
@@ -0,0 +1,275 @@
+import path from "node:path";
+import os from "node:os";
+import semver from "semver";
+import AbstractResolver from "./AbstractResolver.js";
+import Installer from "./maven/Installer.js";
+import {getLogger} from "@ui5/logger";
+const log = getLogger("ui5Framework:Sapui5MavenSnapshotResolver");
+
+const DIST_PKG_NAME = "@sapui5/distribution-metadata";
+const DIST_GROUP_ID = "com.sap.ui5.dist";
+const DIST_ARTIFACT_ID = "sapui5-sdk-dist";
+
+/**
+ * Resolver for the SAPUI5 framework
+ *
+ * This Resolver downloads and installs SNAPSHOTS of UI5 libraries from
+ * a Maven repository. It's meant for internal usage only as no use cases
+ * outside of SAP are known.
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/ui5Framework/Sapui5MavenSnapshotResolver
+ * @extends @ui5/project/ui5Framework/AbstractResolver
+ */
+class Sapui5MavenSnapshotResolver extends AbstractResolver {
+ /**
+ * @param {*} options options
+ * @param {string} [options.snapshotEndpointUrl] Maven Repository Snapshot URL. Can by overruled
+ * by setting the UI5_MAVEN_SNAPSHOT_ENDPOINT_URL environment variable. If neither is provided,
+ * falling back to the standard Maven settings.xml file (if existing).
+ * @param {string} options.version SAPUI5 version to use
+ * @param {boolean} [options.sources=false] Whether to install framework libraries as sources or
+ * pre-built (with build manifest)
+ * @param {string} [options.cwd=process.cwd()] Current working directory
+ * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages,
+ * metadata and configuration used by the resolvers. Relative to `process.cwd()`
+ * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [options.cacheMode=Default]
+ * Cache mode to use
+ */
+ constructor(options) {
+ super(options);
+
+ const {
+ cacheMode,
+ } = options;
+
+ this._installer = new Installer({
+ ui5DataDir: this._ui5DataDir,
+ snapshotEndpointUrlCb:
+ Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback(options.snapshotEndpointUrl),
+ cacheMode,
+ });
+ this._loadDistMetadata = null;
+
+ // TODO 5.0: Remove support for legacy snapshot versions
+ this._isLegacySnapshotVersion = semver.lt(this._version, "1.116.0-SNAPSHOT", {
+ includePrerelease: true
+ });
+ }
+ loadDistMetadata() {
+ if (!this._loadDistMetadata) {
+ this._loadDistMetadata = Promise.resolve().then(async () => {
+ const version = this._version;
+ log.verbose(
+ `Installing ${DIST_ARTIFACT_ID} in version ${version}...`
+ );
+
+ const {pkgPath: distPkgPath} = await this._installer.installPackage({
+ pkgName: DIST_PKG_NAME,
+ groupId: DIST_GROUP_ID,
+ artifactId: DIST_ARTIFACT_ID,
+ version,
+ classifier: "npm-sources",
+ extension: "zip",
+ });
+
+ return await this._installer.readJson(
+ path.join(distPkgPath, "metadata.json")
+ );
+ });
+ }
+ return this._loadDistMetadata;
+ }
+ async getLibraryMetadata(libraryName) {
+ const distMetadata = await this.loadDistMetadata();
+ const metadata = distMetadata.libraries[libraryName];
+
+ if (!metadata) {
+ throw new Error(`Could not find library "${libraryName}"`);
+ }
+
+ return metadata;
+ }
+ async handleLibrary(libraryName) {
+ const metadata = await this.getLibraryMetadata(libraryName);
+ if (!metadata.gav) {
+ throw new Error(
+ "Metadata is missing GAV (group, artifact and version) " +
+ "information. This might indicate an unsupported SNAPSHOT version."
+ );
+ }
+ const gav = metadata.gav.split(":");
+ let pkgName = metadata.npmPackageName;
+
+ // Use "npm-dist" artifact by default
+ let classifier;
+ let extension;
+ if (this._sources) {
+ // Use npm-sources artifact if sources are requested
+ classifier = "npm-sources";
+ extension = "zip";
+ } else {
+ // Add "prebuilt" suffix to package name
+ pkgName += "-prebuilt";
+
+ if (this._isLegacySnapshotVersion) {
+ // For legacy versions < 1.116.0-SNAPSHOT where npm-dist artifact is not
+ // yet available, use "default" JAR
+ classifier = null;
+ extension = "jar";
+ } else {
+ // Use "npm-dist" artifact by default
+ classifier = "npm-dist";
+ extension = "zip";
+ }
+ }
+
+ return {
+ metadata: Promise.resolve({
+ id: pkgName,
+ version: metadata.version,
+ dependencies: metadata.dependencies,
+ optionalDependencies: metadata.optionalDependencies,
+ }),
+ // Trigger installation of package
+ install: this._installer.installPackage({
+ pkgName,
+ groupId: gav[0],
+ artifactId: gav[1],
+ version: metadata.version,
+ classifier,
+ extension,
+ }),
+ };
+ }
+
+ static async fetchAllVersions({ui5DataDir, cwd, snapshotEndpointUrl} = {}) {
+ const installer = new Installer({
+ cwd: cwd ? path.resolve(cwd) : process.cwd(),
+ ui5DataDir: path.resolve(
+ ui5DataDir || path.join(os.homedir(), ".ui5")
+ ),
+ snapshotEndpointUrlCb: Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback(snapshotEndpointUrl),
+ });
+ return await installer.fetchPackageVersions({
+ groupId: DIST_GROUP_ID,
+ artifactId: DIST_ARTIFACT_ID,
+ });
+ }
+
+ static _createSnapshotEndpointUrlCallback(snapshotEndpointUrl) {
+ snapshotEndpointUrl = process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL || snapshotEndpointUrl;
+
+ if (!snapshotEndpointUrl) {
+ // Here we return a function which returns a promise that resolves with the URL.
+ // If we would already start resolving the settings.xml at this point, we'd need to always ask the
+ // end user for confirmation whether the resolved URL should be used. In some cases where the resources
+ // are already cached, this is actually not necessary and could be skipped
+ return Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrl;
+ } else {
+ return () => Promise.resolve(snapshotEndpointUrl);
+ }
+ }
+
+ /**
+ * Read the Maven repository snapshot endpoint URL from the central
+ * UI5 CLI configuration, with a fallback to central Maven configuration (is existing)
+ *
+ * @returns {Promise} The resolved snapshotEndpointUrl
+ */
+ static async _resolveSnapshotEndpointUrl() {
+ const {default: Configuration} = await import("../config/Configuration.js");
+ const config = await Configuration.fromFile();
+ let url = config.getMavenSnapshotEndpointUrl();
+ if (url) {
+ log.verbose(`Using UI5 CLI configuration for mavenSnapshotEndpointUrl: ${url}`);
+ } else {
+ log.verbose(`No mavenSnapshotEndpointUrl configuration found`);
+ url = await Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven();
+ if (url) {
+ log.verbose(`Updating UI5 CLI configuration with new mavenSnapshotEndpointUrl: ${url}`);
+ const configJson = config.toJson();
+ configJson.mavenSnapshotEndpointUrl = url;
+ await Configuration.toFile(new Configuration(configJson));
+ }
+ }
+ return url;
+ }
+
+ /**
+ * Tries to detect whether ~/.m2/settings.xml exist, and if so, whether
+ * the snapshot.build URL is extracted from there
+ *
+ * @param {string} [settingsXML=~/.m2/settings.xml] Path to the settings.xml.
+ * If not provided, the default location is used
+ * @returns {Promise} The resolved snapshot.build URL from ~/.m2/settings.xml
+ */
+ static async _resolveSnapshotEndpointUrlFromMaven(settingsXML) {
+ if (!process.stdout.isTTY) {
+ // We can't prompt the user if stdout is non-interactive (i.e. in CI environments)
+ // Therefore skip resolution from Maven settings.xml altogether
+ return null;
+ }
+
+ settingsXML =
+ settingsXML || path.resolve(path.join(os.homedir(), ".m2", "settings.xml"));
+
+ const {default: fs} = await import("graceful-fs");
+ const {promisify} = await import("node:util");
+ const readFile = promisify(fs.readFile);
+ const xml2js = await import("xml2js");
+ const parser = new xml2js.Parser({
+ preserveChildrenOrder: true,
+ xmlns: true,
+ });
+ let url;
+
+ log.verbose(`Attempting to resolve snapshot endpoint URL from Maven configuration file at ${settingsXML}...`);
+ try {
+ const fileContent = await readFile(settingsXML);
+ const xmlContents = await parser.parseStringPromise(fileContent);
+
+ const snapshotBuildChunk = xmlContents?.settings?.profiles[0]?.profile.filter(
+ (prof) => prof.id[0]._ === "snapshot.build"
+ )[0];
+
+ url =
+ snapshotBuildChunk?.repositories?.[0]?.repository?.[0]?.url?.[0]?._ ||
+ snapshotBuildChunk?.pluginRepositories?.[0]?.pluginRepository?.[0]?.url?.[0]?._;
+
+ if (!url) {
+ log.verbose(`"snapshot.build" attribute could not be found in ${settingsXML}`);
+ return null;
+ }
+ } catch (err) {
+ if (err.code === "ENOENT") {
+ // "File or directory does not exist"
+ log.verbose(`File does not exist: ${settingsXML}`);
+ } else {
+ log.warning(`Failed to read Maven configuration file from ${settingsXML}: ${err.message}`);
+ }
+ return null;
+ }
+
+ const {default: yesno} = await import("yesno");
+ const ok = await yesno({
+ question:
+ "\nA Maven repository endpoint URL is required for consuming snapshot versions of UI5 libraries.\n" +
+ "You can configure one using the command: 'ui5 config set mavenSnapshotEndpointUrl '\n\n" +
+ `The following URL has been found in a Maven configuration file at ${settingsXML}:\n${url}\n\n` +
+ `Continue with this endpoint URL and remember it for the future? (yes)`,
+ defaultValue: true,
+ });
+
+ if (ok) {
+ log.verbose(`Using Maven snapshot endpoint URL resolved from Maven configuration file: ${url}`);
+ return url;
+ } else {
+ log.verbose(`User rejected usage of the resolved URL`);
+ return null;
+ }
+ }
+}
+
+export default Sapui5MavenSnapshotResolver;
diff --git a/packages/project/lib/ui5Framework/Sapui5Resolver.js b/packages/project/lib/ui5Framework/Sapui5Resolver.js
new file mode 100644
index 00000000000..300020dce25
--- /dev/null
+++ b/packages/project/lib/ui5Framework/Sapui5Resolver.js
@@ -0,0 +1,128 @@
+import path from "node:path";
+import os from "node:os";
+import semver from "semver";
+import AbstractResolver from "./AbstractResolver.js";
+import Installer from "./npm/Installer.js";
+import {getLogger} from "@ui5/logger";
+const log = getLogger("ui5Framework:Sapui5Resolver");
+
+const DIST_PKG_NAME = "@sapui5/distribution-metadata";
+
+/**
+ * Resolver for the SAPUI5 framework
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/ui5Framework/Sapui5Resolver
+ * @extends @ui5/project/ui5Framework/AbstractResolver
+ */
+class Sapui5Resolver extends AbstractResolver {
+ /**
+ * @param {*} options options
+ * @param {string} options.version SAPUI5 version to use
+ * @param {string} [options.cwd=process.cwd()] Working directory to resolve configurations like .npmrc
+ * @param {string} [options.ui5DataDir="~/.ui5"] UI5 home directory location. This will be used to store packages,
+ * metadata and configuration used by the resolvers. Relative to `process.cwd()`
+ * @param {string} [options.cacheDir] Where to store temp/cached packages.
+ * @param {string} [options.packagesDir] Where to install packages
+ * @param {string} [options.stagingDir] The staging directory for packages
+ */
+ constructor(options) {
+ super(options);
+
+ const {cacheDir, packagesDir, stagingDir} = options;
+
+ this._installer = new Installer({
+ cwd: this._cwd,
+ ui5DataDir: this._ui5DataDir,
+ cacheDir, packagesDir, stagingDir
+ });
+ this._loadDistMetadata = null;
+ }
+ loadDistMetadata() {
+ if (!this._loadDistMetadata) {
+ this._loadDistMetadata = Promise.resolve().then(async () => {
+ const version = this._version;
+ log.verbose(`Installing ${DIST_PKG_NAME} in version ${version}...`);
+ const pkgName = DIST_PKG_NAME;
+ const {pkgPath} = await this._installer.installPackage({
+ pkgName,
+ version
+ });
+
+ const metadata = await this._installer.readJson(path.join(pkgPath, "metadata.json"));
+ return metadata;
+ });
+ }
+ return this._loadDistMetadata;
+ }
+ async getLibraryMetadata(libraryName) {
+ const distMetadata = await this.loadDistMetadata();
+ const metadata = distMetadata.libraries[libraryName];
+
+ if (!metadata) {
+ throw new Error(`Could not find library "${libraryName}"`);
+ }
+
+ if (metadata.npmPackageName.startsWith("@openui5/") &&
+ semver.satisfies(this._version, "1.77.x")) {
+ // TODO: Remove this workaround once SAPUI5 1.77.x isn't used anymore.
+ // As of Dec 2022 there are still ~80 downloads per week (npmjs.com stats).
+ // 1.77.x (at least 1.77.0-1.77.2) distribution metadata.json is missing
+ // dependency information for all OpenUI5 libraries.
+ // Therefore we need to request those from the registry like it is done
+ // for OpenUI5 projects.
+ const {default: Openui5Resolver} = await import("./Openui5Resolver.js");
+ const openui5Resolver = new Openui5Resolver({
+ cwd: this._cwd,
+ version: metadata.version
+ });
+ const openui5Metadata = await openui5Resolver.getLibraryMetadata(libraryName);
+ return {
+ npmPackageName: openui5Metadata.id,
+ version: openui5Metadata.version,
+ dependencies: openui5Metadata.dependencies,
+ optionalDependencies: openui5Metadata.optionalDependencies
+ };
+ }
+
+ return metadata;
+ }
+ async handleLibrary(libraryName) {
+ const metadata = await this.getLibraryMetadata(libraryName);
+
+ return {
+ metadata: Promise.resolve({
+ id: metadata.npmPackageName,
+ version: metadata.version,
+ dependencies: metadata.dependencies,
+ optionalDependencies: metadata.optionalDependencies
+ }),
+ // Trigger installation of package
+ install: this._installer.installPackage({
+ pkgName: metadata.npmPackageName,
+ version: metadata.version
+ })
+ };
+ }
+ static async fetchAllVersions(options) {
+ const installer = this._getInstaller(options);
+ return await installer.fetchPackageVersions({pkgName: DIST_PKG_NAME});
+ }
+
+ static async fetchAllTags(options) {
+ const installer = this._getInstaller(options);
+ return installer.fetchPackageDistTags({pkgName: DIST_PKG_NAME});
+ }
+
+ static _getInstaller({ui5DataDir, cwd} = {}) {
+ return new Installer({
+ cwd: cwd ? path.resolve(cwd) : process.cwd(),
+ ui5DataDir:
+ ui5DataDir ? path.resolve(ui5DataDir) :
+ path.join(os.homedir(), ".ui5")
+ });
+ }
+}
+
+export default Sapui5Resolver;
diff --git a/packages/project/lib/ui5Framework/maven/CacheMode.js b/packages/project/lib/ui5Framework/maven/CacheMode.js
new file mode 100644
index 00000000000..d1b5af0d422
--- /dev/null
+++ b/packages/project/lib/ui5Framework/maven/CacheMode.js
@@ -0,0 +1,18 @@
+
+
+/**
+ * Cache modes for maven consumption
+ *
+ * @public
+ * @readonly
+ * @enum {string}
+ * @property {string} Default Cache everything, invalidate after 9 hours
+ * @property {string} Force Use cache only. Do not send any requests to the repository
+ * @property {string} Off Invalidate the cache and update from the repository
+ * @module @ui5/project/ui5Framework/maven/CacheMode
+ */
+export default {
+ Default: "Default",
+ Force: "Force",
+ Off: "Off"
+};
diff --git a/packages/project/lib/ui5Framework/maven/Installer.js b/packages/project/lib/ui5Framework/maven/Installer.js
new file mode 100644
index 00000000000..4dd6d1bc8cd
--- /dev/null
+++ b/packages/project/lib/ui5Framework/maven/Installer.js
@@ -0,0 +1,525 @@
+import path from "node:path";
+import {mkdirp} from "../../utils/fs.js";
+import fs from "graceful-fs";
+import _StreamZip from "node-stream-zip";
+const StreamZip = _StreamZip.async;
+import {promisify} from "node:util";
+import Registry from "./Registry.js";
+import AbstractInstaller from "../AbstractInstaller.js";
+import CacheMode from "./CacheMode.js";
+import {rmrf} from "../../utils/fs.js";
+const stat = promisify(fs.stat);
+const readFile = promisify(fs.readFile);
+const writeFile = promisify(fs.writeFile);
+const rename = promisify(fs.rename);
+const rm = promisify(fs.rm);
+import {getLogger} from "@ui5/logger";
+const log = getLogger("ui5Framework:maven:Installer");
+const mvnTimestampRegex = /^(\d{4})(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/;
+
+const CACHE_TIME = 32400000; // 9 hours
+
+class Installer extends AbstractInstaller {
+ /**
+ * @param {object} parameters Parameters
+ * @param {string} parameters.ui5DataDir UI5 home directory location. This will be used to store packages,
+ * metadata and configuration used by the resolvers.
+ * @param {Function} parameters.snapshotEndpointUrlCb Callback that returns a Promise ,
+ * resolving to the Maven repository URL.
+ * Example: https://registry.corp/vendor/build-snapshots/
+ * @param {module:@ui5/project/ui5Framework/maven/CacheMode} [parameters.cacheMode=Default] Cache mode to use
+ */
+ constructor({ui5DataDir, snapshotEndpointUrlCb, cacheMode = CacheMode.Default}) {
+ super(ui5DataDir);
+
+ this._artifactsDir = path.join(ui5DataDir, "framework", "artifacts");
+ this._packagesDir = path.join(ui5DataDir, "framework", "packages");
+ this._metadataDir = path.join(ui5DataDir, "framework", "metadata");
+ this._stagingDir = path.join(ui5DataDir, "framework", "staging");
+
+ this._cacheMode = cacheMode;
+ this._snapshotEndpointUrlCb = snapshotEndpointUrlCb;
+
+ if (!this._snapshotEndpointUrlCb) {
+ throw new Error(`Installer: Missing Snapshot-Endpoint URL callback parameter`);
+ }
+ if (!Object.values(CacheMode).includes(cacheMode)) {
+ throw new Error(`Installer: Invalid value '${cacheMode}' for cacheMode parameter. ` +
+ `Must be one of ${Object.values(CacheMode).join(", ")}`);
+ }
+
+ log.verbose(`Installing Maven artifacts to: ${this._artifactsDir}`);
+ log.verbose(`Installing Packages to: ${this._packagesDir}`);
+ log.verbose(`Caching mode: ${this._cacheMode}`);
+ }
+
+ async getRegistry() {
+ if (this._cachedRegistry) {
+ return this._cachedRegistry;
+ }
+ return (this._cachedRegistry = Promise.resolve().then(async () => {
+ const snapshotEndpointUrl = await this._snapshotEndpointUrlCb();
+ if (!snapshotEndpointUrl) {
+ throw new Error(
+ `Installer: Missing or empty Maven repository URL for snapshot consumption. ` +
+ `This URL is required for consuming snapshot versions of UI5 libraries. ` +
+ `Please configure the correct URL using the following command: ` +
+ `'ui5 config set mavenSnapshotEndpointUrl '`);
+ } else {
+ return new Registry({endpointUrl: snapshotEndpointUrl});
+ }
+ }));
+ }
+
+ async readJson(jsonPath) {
+ return JSON.parse(await readFile(jsonPath, {encoding: "utf8"}));
+ }
+
+ async _writeJson(jsonPath, jsonObject) {
+ return writeFile(jsonPath, JSON.stringify(jsonObject));
+ }
+
+ async fetchPackageVersions({groupId, artifactId}) {
+ const reg = await this.getRegistry();
+ const metadata = await reg.requestMavenMetadata({groupId, artifactId});
+
+ if (!metadata?.versioning?.versions?.version) {
+ throw new Error(`Missing Maven metadata for artifact ${groupId}:${artifactId}`);
+ }
+ return metadata.versioning.versions.version.filter((version) => {
+ // This resolver can only handle SNAPSHOT versions
+ return version.endsWith("-SNAPSHOT");
+ });
+ }
+
+
+ /**
+ * Metadata for an artifact as identified by it's Maven coordinates
+ *
+ * @typedef {object} @ui5/project/ui5Framework/maven/Installer~LocalMetadata
+ * @property {integer} lastCheck Timestamp of the last time these metadata have been compared with the repository
+ * @property {integer} lastUpdate Timestamp of the last time the artifact has been updated in the repository
+ * (typically older than last check)
+ * @property {string} revision Current revision of the artifact
+ * @property {string[]} staleRevisions Previously installed revisions of the artifact
+ */
+
+ /**
+ * Fills and maintains locally cached metadata for the given artifact coordinates
+ *
+ * @param {object} coordinates
+ * @param {string} coordinates.groupId GroupId of the requested artifact
+ * @param {string} coordinates.artifactId ArtifactId of the requested artifact
+ * @param {string} coordinates.version Version of the requested artifact
+ * @param {string|null} coordinates.classifier Classifier of the requested artifact
+ * @param {string} coordinates.extension Extension of the requested artifact
+ * @param {string} [coordinates.pkgName] npm package name the artifact corresponds to (if any)
+ * @returns {@ui5/project/ui5Framework/maven/Installer~LocalMetadata}
+ */
+ async _fetchArtifactMetadata(coordinates) {
+ const fsId = this._generateFsIdFromCoordinates(coordinates);
+ const logId = this._generateLogIdFromCoordinates(coordinates);
+ return this._synchronize("metadata-" + fsId, async () => {
+ const localMetadata = await this._getLocalArtifactMetadata(fsId);
+
+ if (this._cacheMode === CacheMode.Force && !localMetadata.revision) {
+ throw new Error(`Could not find artifact ` +
+ `${logId} in local cache`);
+ }
+
+ const now = new Date().getTime();
+ const timeSinceLastCheck = now - localMetadata.lastCheck;
+
+ if (this._cacheMode !== CacheMode.Force &&
+ (timeSinceLastCheck > CACHE_TIME || this._cacheMode === CacheMode.Off)) {
+ // No cached metadata (-> timeSinceLastCheck equals time since 1970) or
+ // too old metadata or disabled cache
+ // => Retrieve metadata from repository
+ if (localMetadata.lastCheck === 0) {
+ log.verbose(
+ `Could not find metadata for artifact ${logId} in local cache. Fetching from repository...`);
+ } else {
+ log.verbose(
+ `Refreshing metadata cache for artifact ${logId} ` +
+ // TODO better formatting of elapsed time
+ `(last checked ${timeSinceLastCheck/1000} seconds ago)`);
+ }
+
+ log.info(
+ `Fetching latest metadata for artifact ${coordinates.artifactId} version ${coordinates.version} ` +
+ `from Maven registry...`);
+ const {lastUpdate, revision} = await this._getRemoteArtifactMetadata(coordinates);
+
+ // TODO better formatting of elapsed time
+ log.verbose(`Retrieved metadata for artifact ${logId} is ` +
+ `${(lastUpdate - localMetadata.lastUpdate) / 1000} seconds younger than local metadata`);
+ log.verbose(`Retrieved deployment version is ${revision}`);
+
+ this._rotateRevision(localMetadata, revision);
+
+ await this._removeStaleRevisions(logId, localMetadata, coordinates);
+
+ localMetadata.lastCheck = now;
+ localMetadata.lastUpdate = lastUpdate;
+ await this._writeLocalArtifactMetadata(fsId, localMetadata);
+ } else {
+ log.verbose(`Using metadata for artifact ${logId} from local cache`);
+ }
+ return localMetadata;
+ });
+ }
+
+ /**
+ * Fills and maintains locally cached metadata for the given artifact coordinates
+ *
+ * @param {object} coordinates
+ * @param {string} coordinates.groupId GroupId of the requested artifact
+ * @param {string} coordinates.artifactId ArtifactId of the requested artifact
+ * @param {string} coordinates.version Version of the requested artifact
+ * @param {string|null} coordinates.classifier Classifier of the requested artifact
+ * @param {string} coordinates.extension Extension of the requested artifact
+ * @returns {@ui5/project/ui5Framework/maven/Installer~LocalMetadata}
+ */
+ async _getRemoteArtifactMetadata({groupId, artifactId, version, classifier, extension}) {
+ const reg = await this.getRegistry();
+ const metadata = await reg.requestMavenMetadata({groupId, artifactId, version});
+
+ if (!metadata?.versioning?.snapshotVersions?.snapshotVersion) {
+ throw new Error(`Missing Maven snapshot metadata for artifact ${groupId}:${artifactId}:${version}`);
+ }
+
+ const snapshotVersion = metadata.versioning.snapshotVersions.snapshotVersion;
+ const deploymentMetadata = snapshotVersion.find(({
+ classifier: candidateClassifier, // Classifier can be null, e.g. for the default "jar" artifact
+ extension: candidateExtension
+ }) => (!classifier || candidateClassifier === classifier) && candidateExtension === extension);
+
+ if (!deploymentMetadata) {
+ const optionalClassifier = classifier ? `${classifier}.` : "";
+ throw new Error(
+ `Could not find ${optionalClassifier}${extension} deployment for artifact ` +
+ `${groupId}:${artifactId}:${version} in snapshot metadata:\n` +
+ `${JSON.stringify(snapshotVersion)}`);
+ }
+ // Convert Maven timestamp (yyyyMMddHHmmss UTC) to ISO string (YYYY-MM-DDTHH:mm:ss.sssZ)
+ // E.g. 20220828080910 becomes 2022-08-28T08:09:10.000Z
+ const isoTimestamp = deploymentMetadata.updated.replace(mvnTimestampRegex, "$1-$2-$3T$4:$5:$6.000Z");
+ const ts = new Date(isoTimestamp);
+
+ const logId = this._generateLogIdFromCoordinates({groupId, artifactId, version, classifier, extension});
+ log.verbose(`Retrieved metadata for ${logId}:` +
+ `\n Last update was at: ${ts.toISOString()}` +
+ `\n Current deployment version is: ${deploymentMetadata.value}`);
+ return {
+ lastUpdate: ts.getTime(),
+ revision: deploymentMetadata.value
+ };
+ }
+
+ /**
+ * Reads locally cached metadata for the given artifact coordinates
+ *
+ * @param {string} id File System identifier for the artifact. Typically derived from the coordinates
+ * @returns {@ui5/project/ui5Framework/maven/Installer~LocalMetadata}
+ */
+ async _getLocalArtifactMetadata(id) {
+ try {
+ return await this.readJson(path.join(this._metadataDir, `${id}.json`));
+ } catch (err) {
+ if (err.code === "ENOENT") { // "File or directory does not exist"
+ // If not found, initialize metadata
+ return {
+ lastCheck: 0,
+ lastUpdate: 0,
+ revision: null,
+ staleRevisions: []
+ };
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ async _writeLocalArtifactMetadata(id, content) {
+ await mkdirp(this._metadataDir);
+ return await this._writeJson(path.join(this._metadataDir, `${id}.json`), content);
+ }
+
+ _rotateRevision(metadata, newRevision) {
+ if (metadata.revision) {
+ metadata.staleRevisions.push(metadata.revision);
+ }
+ metadata.revision = newRevision;
+ }
+
+ async _removeStaleRevisions(logId, metadata, {pkgName, groupId, artifactId, classifier, extension}) {
+ if (metadata.staleRevisions.length <= 1) {
+ // Keep at least one revision. Nothing to do
+ return;
+ }
+ log.verbose(`Removing ${metadata.staleRevisions.length - 1} stale revision for ${logId}`);
+ while (metadata.staleRevisions.length > 3) {
+ const revision = metadata.staleRevisions.shift();
+ const artifactPath = this._getTargetPathForArtifact({
+ groupId,
+ artifactId,
+ revision,
+ classifier,
+ extension
+ });
+ log.verbose(`Removing ${artifactPath}...`);
+ await rm(artifactPath, {
+ force: true
+ });
+
+ if (pkgName) {
+ const packageDir = this._getTargetDirForPackage(pkgName, revision);
+ log.verbose(`Removing directory ${packageDir}...`);
+ await rmrf(packageDir);
+ }
+ }
+ }
+
+ /**
+ * @typedef {object} @ui5/project/ui5Framework/maven/Installer~InstalledPackage
+ * @property {string} pkgPath
+ */
+
+ /**
+ * Downloads the respective artifact and extracts the zip archive into a structure similar to
+ * the npm installer
+ *
+ * @param {object} parameters
+ * @param {string} parameters.pkgName Name of the npm package
+ * @param {string} parameters.groupId GroupId of the requested artifact
+ * @param {string} parameters.artifactId ArtifactId of the requested artifact
+ * @param {string} parameters.version Version of the requested artifact
+ * @param {string|null} parameters.classifier Classifier of the requested artifact
+ * @param {string} parameters.extension Extension of the requested artifact
+ * @returns {@ui5/project/ui5Framework/maven/Installer~InstalledPackage}
+ */
+ async installPackage({pkgName, groupId, artifactId, version, classifier, extension}) {
+ const {revision} = await this._fetchArtifactMetadata({
+ pkgName, groupId, artifactId, version, classifier, extension
+ });
+
+ const coordinates = {
+ groupId, artifactId,
+ version, revision,
+ classifier, extension
+ };
+
+ const targetDir = this._getTargetDirForPackage(pkgName, revision);
+ const installed = await this._projectExists(targetDir);
+
+ if (!installed) {
+ await this._synchronize(`package-${pkgName}@${revision}`, async () => {
+ const installed = await this._projectExists(targetDir);
+
+ if (installed) {
+ log.verbose(`Already installed: ${pkgName} in SNAPSHOT version ${revision}`);
+ return;
+ }
+
+ const stagingDir = this._getStagingDirForPackage(pkgName, revision);
+
+ // Check whether staging dir already exists and remove it
+ if (await this._pathExists(stagingDir)) {
+ log.verbose(`Removing stale staging directory at ${stagingDir}...`);
+ await rmrf(stagingDir);
+ }
+
+ await mkdirp(stagingDir);
+
+ const {artifactPath, removeArtifact} = await this.installArtifact(coordinates);
+
+ log.verbose(`Extracting archive at ${artifactPath} to ${stagingDir}...`);
+ const zip = new StreamZip({file: artifactPath});
+ let rootDir = null;
+ if (extension === "jar") {
+ rootDir = "META-INF";
+ }
+ await zip.extract(rootDir, stagingDir);
+ await zip.close();
+
+ // Check whether target dir already exists and remove it
+ if (await this._pathExists(targetDir)) {
+ log.verbose(`Removing existing target directory at ${targetDir}...`);
+ await rmrf(targetDir);
+ }
+
+ // Do not create target dir itself to prevent EPERM error in following rename operation
+ // (https://github.com/UI5/cli/issues/487)
+ await mkdirp(path.dirname(targetDir));
+ log.verbose(`Promoting staging directory from ${stagingDir} to ${targetDir}...`);
+ await rename(stagingDir, targetDir);
+
+ await removeArtifact();
+ });
+ } else {
+ log.verbose(`Already installed: ${pkgName} in SNAPSHOT version ${revision}`);
+ }
+ return {
+ pkgPath: targetDir
+ };
+ }
+
+ /**
+ * @typedef {object} @ui5/project/ui5Framework/maven/Installer~InstalledArtifact
+ * @property {string} artifactPath
+ * @property {Function} removeArtifact Callback to trigger removal of the artifact file in case it
+ * is no longer required.
+ */
+
+ /**
+ * @param {object} parameters
+ * @param {string} parameters.groupId GroupId of the requested artifact
+ * @param {string} parameters.artifactId ArtifactId of the requested artifact
+ * @param {string} parameters.version Version of the requested artifact
+ * @param {string|null} parameters.classifier Classifier of the requested artifact
+ * @param {string} parameters.extension Extension of the requested artifact
+ * @param {string} [parameters.revision] Optional revision of the artifact to request.
+ * If not provided, the latest revision will be determined from the registry metadata.
+ * @returns {@ui5/project/ui5Framework/maven/Installer~InstalledArtifact}
+ */
+ async installArtifact({groupId, artifactId, version, classifier, extension, revision}) {
+ if (!revision) {
+ const metadata = await this._fetchArtifactMetadata({
+ groupId, artifactId, version, classifier, extension
+ });
+ revision = metadata.revision;
+ }
+ const coordinates = {
+ groupId, artifactId,
+ version, revision,
+ classifier, extension
+ };
+
+ const targetPath = this._getTargetPathForArtifact(coordinates);
+ const installed = await this._pathExists(targetPath);
+ const logId = this._generateLogIdFromCoordinates(coordinates);
+ const fsId = this._generateFsIdFromCoordinates(coordinates);
+ if (!installed) {
+ await this._synchronize(`artifact-${fsId}`, async () => {
+ // check again whether the artifact is now installed
+ const installed = await this._pathExists(targetPath);
+ if (installed) {
+ log.verbose(`Already installed: ${artifactId} in version ${revision}`);
+ return;
+ }
+
+ const stagingPath = this._getStagingPathForArtifact(coordinates);
+ log.info(`Installing missing artifact ${logId}...`);
+
+ // Check whether staging dir already exists and remove it
+ if (await this._pathExists(stagingPath)) {
+ log.verbose(`Removing existing file in staging dir at ${stagingPath}...`);
+ await rm(stagingPath);
+ }
+ await mkdirp(path.dirname(stagingPath));
+
+ log.verbose(`Installing ${artifactId} in version ${version} to ${stagingPath}...`);
+
+ // TODO: Stream response body to installPackage and unzip directly via
+ // https://github.com/isaacs/minizlib (already in dependencies through pacote)
+ // This way we do not store the archive unnecessarily
+ const reg = await this.getRegistry();
+ await reg.requestArtifact(coordinates, stagingPath);
+
+ await mkdirp(path.dirname(targetPath));
+ log.verbose(
+ `Promoting artifact from staging path ${stagingPath} to target path at ${targetPath}...`);
+ await rename(stagingPath, targetPath);
+ });
+ } else {
+ log.verbose(`Already installed: ${artifactId} in version ${revision}`);
+ }
+ return {
+ artifactPath: targetPath,
+ removeArtifact: () => {
+ return rm(targetPath);
+ }
+ };
+ }
+
+ async _projectExists(targetDir) {
+ return this._pathExists(path.join(targetDir, "package.json"));
+ }
+
+ async _pathExists(targetPath) {
+ try {
+ await stat(targetPath);
+ return true;
+ } catch (err) {
+ if (err.code === "ENOENT") { // "File or directory does not exist"
+ return false;
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ _getStagingPathForArtifact(coordinates) {
+ // Staging dir should only contain single files, no directory hierarchy.
+ // This makes cleanups after promoting artifacts easier and does not leave empty directories.
+ return path.join(this._stagingDir, this._generateFsIdFromCoordinates(coordinates));
+ }
+
+ _getTargetPathForArtifact({groupId, artifactId, revision, classifier, extension}) {
+ if (!classifier) {
+ classifier = revision;
+ revision = "";
+ }
+ return path.join(this._artifactsDir,
+ `${groupId}-${artifactId}`.replaceAll(".", "_"), revision, `${classifier}.${extension}`);
+ }
+
+ _getStagingDirForPackage(pkgName, version) {
+ // Staging dir should only contain single files, no directory hierarchy.
+ // This makes cleanups after promoting artifacts easier and does not leave empty directories.
+ return path.join(this._stagingDir, `${pkgName.replaceAll("/", "-")}-${version}`);
+ }
+
+ _getTargetDirForPackage(pkgName, version) {
+ return path.join(this._packagesDir, ...pkgName.split("/"), version);
+ }
+
+ /**
+ * Generate an identifier for an artifact that is safe to use in file names.
+ * Used for naming metadata- and lock-files
+ *
+ * @param {object} parameters
+ * @param {string} parameters.groupId GroupId of the artifact
+ * @param {string} parameters.artifactId ArtifactId of the artifact
+ * @param {string} parameters.extension Extension of the artifact
+ * @param {string} [parameters.classifier] Optional classifier of the artifact
+ * @param {string} [parameters.version] Version of the artifact. Optional if revision is provided
+ * @param {string} [parameters.revision] Optional revision of the artifact
+ * @returns {string} A unique identifier for the provided combination of parameters
+ */
+ _generateFsIdFromCoordinates({groupId, artifactId, version, classifier, extension, revision}) {
+ // Using underscores instead of colons, since the colon is a reserved character for
+ // filenames on Windows and macOS
+ const optionalClassifier = classifier ? `${classifier}.` : "";
+ return `${groupId}_${artifactId}_${revision || version}_${optionalClassifier}${extension}`;
+ }
+
+ /**
+ * Generate an identifier for an artifact that is suitable for logging purposes
+ *
+ * @param {object} parameters
+ * @param {string} parameters.groupId GroupId of the artifact
+ * @param {string} parameters.artifactId ArtifactId of the artifact
+ * @param {string} parameters.version Version of the artifact
+ * @param {string} parameters.extension Extension of the artifact
+ * @param {string} [parameters.classifier] Optional classifier of the artifact
+ * @param {string} [parameters.revision] Optional revision of the artifact
+ * @returns {string} A string with the Maven-typical formatting of the provided coordinates
+ */
+ _generateLogIdFromCoordinates({groupId, artifactId, version, classifier, extension, revision}) {
+ const optionalClassifier = classifier ? `${classifier}.` : "";
+ return `${groupId}:${artifactId}:${revision || version}:${optionalClassifier}${extension}`;
+ }
+}
+
+export default Installer;
diff --git a/packages/project/lib/ui5Framework/maven/Registry.js b/packages/project/lib/ui5Framework/maven/Registry.js
new file mode 100644
index 00000000000..ec5b2293e75
--- /dev/null
+++ b/packages/project/lib/ui5Framework/maven/Registry.js
@@ -0,0 +1,121 @@
+import {getLogger} from "@ui5/logger";
+import fetch from "make-fetch-happen";
+import xml2js from "xml2js";
+import {promisify} from "node:util";
+import {pipeline} from "node:stream/promises";
+import fs from "graceful-fs";
+const log = getLogger("ui5Framework:maven:Registry");
+
+class Registry {
+ /**
+ * @param {object} parameters Parameters
+ * @param {string} parameters.endpointUrl Maven's endpoint URL
+ */
+ constructor({endpointUrl}) {
+ if (!endpointUrl) {
+ throw new Error(`Registry: Missing parameter "endpointUrl"`);
+ }
+ this._endpointUrl = endpointUrl;
+ if (!this._endpointUrl.endsWith("/")) {
+ this._endpointUrl += "/";
+ log.verbose(`Registry: Effective "endpointUrl" resolved to "${this._endpointUrl}"`);
+ }
+ }
+
+ /**
+ * Requests a maven-metadata.xml file from the repository
+ *
+ * @param {object} options options
+ * @param {string} options.groupId
+ * @param {string} options.artifactId
+ * @param {string} [options.version] If given, the version must be a SNAPSHOT version.
+ * In this case, the resulting metadata will list all artifact versions
+ * (and timestamps) deployed for that SNAPSHOT.
+ * If not provided, the resulting metadata will list all versions available for the artifact.
+ */
+ async requestMavenMetadata({groupId, artifactId, version}) {
+ try {
+ const optionalVersion = version ? version + "/" : "";
+ const url = this._endpointUrl +
+ `${groupId.replaceAll(".", "/")}/${artifactId}/${optionalVersion}maven-metadata.xml`;
+
+ log.verbose(`Fetching: ${url}`);
+ const res = await fetch(url);
+ if (!res.ok) {
+ throw new Error(`[HTTP Error] ${res.status} ${res.statusText}`);
+ }
+
+ const parser = new xml2js.Parser({
+ explicitArray: false,
+ ignoreAttrs: true
+ });
+ const readXML = promisify(parser.parseString);
+ const content = await res.buffer();
+ const parsedXml = await readXML(content);
+ if (!parsedXml?.metadata) {
+ throw new Error(
+ `Empty or unexpected response body:\n${content}\nParsed as:\n${JSON.stringify(parsedXml)}`);
+ }
+ return parsedXml.metadata;
+ } catch (err) {
+ if (err.code === "ENOTFOUND") {
+ throw new Error(
+ `Failed to connect to Maven registry at ${this._endpointUrl}. ` +
+ `Please check the correct endpoint URL is maintained and can be reached. ` +
+ `You can change the configured URL using the following command: ` +
+ `'ui5 config set mavenSnapshotEndpointUrl '`);
+
+ // TODO: Allow cacheMode to be set from outside
+ // `You may be able to continue working offline. For this, set --cache-mode to "force"`);
+ // ` or use the --offline flag`); // TODO: Implement --offline flag
+ }
+ throw new Error(
+ `Failed to retrieve maven-metadata.xml for ${groupId}:${artifactId}:${version}: ${err.message}`);
+ }
+ }
+
+ async requestArtifact({groupId, artifactId, version, revision, classifier, extension}, targetPath) {
+ try {
+ // Classifier can be null, e.g. for the default "jar" artifact
+ const optionalClassifier = classifier ? `-${classifier}` : "";
+ const url = this._endpointUrl +
+ `${groupId.replaceAll(".", "/")}/${artifactId}/${version}/` +
+ `${artifactId}-${revision}${optionalClassifier}.${extension}`;
+
+ log.verbose(`Fetching: ${url}`);
+ const res = await fetch(url, {
+ cache: "no-store", // Do not cache these large artifacts. We store them right away anyways
+
+ // Disable usage of shared keep-alive agents.
+ // make-fetch-happen uses a hard-coded 15 seconds freeSocketTimeout
+ // that can be easily reached (Error: Socket timeout) and there doesn't
+ // seem to be another way to disable or increase it.
+ // Also see: https://github.com/node-modules/agentkeepalive/issues/106
+ // The same applies in npm/Registry.js
+ agent: false,
+ });
+ if (!res.ok) {
+ throw new Error(`[HTTP Error] ${res.status} ${res.statusText}`);
+ }
+
+ // Write to target
+ await pipeline(res.body, fs.createWriteStream(targetPath));
+ } catch (err) {
+ if (err.code === "ENOTFOUND") {
+ throw new Error(
+ `Failed to connect to Maven registry at ${this._endpointUrl}. ` +
+ `Please check the correct endpoint URL is maintained and can be reached. ` +
+ `You can change the configured URL using the following command: ` +
+ `'ui5 config set mavenSnapshotEndpointUrl '`);
+
+ // TODO: Allow cacheMode to be set from outside
+ // `You may be able to continue working offline. For this, set --cache-mode to "force"`);
+ // ` or use the --offline flag`); // TODO: Implement --offline flag
+ }
+ throw new Error(`Failed to retrieve artifact ` +
+ `${groupId}:${artifactId}:${version}:${classifier}:${extension} ${err.message}`);
+ }
+ }
+}
+
+export default Registry;
diff --git a/packages/project/lib/ui5Framework/npm/Installer.js b/packages/project/lib/ui5Framework/npm/Installer.js
new file mode 100644
index 00000000000..40d1dae9814
--- /dev/null
+++ b/packages/project/lib/ui5Framework/npm/Installer.js
@@ -0,0 +1,160 @@
+import path from "node:path";
+import {mkdirp} from "../../utils/fs.js";
+import fs from "graceful-fs";
+import {promisify} from "node:util";
+import Registry from "./Registry.js";
+import AbstractInstaller from "../AbstractInstaller.js";
+import {rmrf} from "../../utils/fs.js";
+const stat = promisify(fs.stat);
+const readFile = promisify(fs.readFile);
+const rename = promisify(fs.rename);
+import {getLogger} from "@ui5/logger";
+const log = getLogger("ui5Framework:npm:Installer");
+
+class Installer extends AbstractInstaller {
+ /**
+ * @param {object} parameters Parameters
+ * @param {string} parameters.cwd Current working directory
+ * @param {string} parameters.ui5DataDir UI5 home directory location. This will be used to store packages,
+ * metadata and configuration used by the resolvers.
+ * @param {string} [parameters.packagesDir="${ui5DataDir}/framework/packages"] Where to install packages
+ * @param {string} [parameters.stagingDir="${ui5DataDir}/framework/staging"] The staging directory for the packages
+ * @param {string} [parameters.cacheDir="${ui5DataDir}/framework/cacache"] Where to store temp/cached packages.
+ */
+ constructor({cwd, ui5DataDir, packagesDir, stagingDir, cacheDir}) {
+ super(ui5DataDir);
+ if (!cwd) {
+ throw new Error(`Installer: Missing parameter "cwd"`);
+ }
+ this._packagesDir = packagesDir ?
+ path.resolve(packagesDir) : path.join(ui5DataDir, "framework", "packages");
+
+ log.verbose(`Installing to: ${this._packagesDir}`);
+
+ this._cwd = cwd;
+ this._caCacheDir = cacheDir ?
+ path.resolve(cacheDir) : path.join(ui5DataDir, "framework", "cacache");
+ this._stagingDir = stagingDir ?
+ path.resolve(stagingDir) : path.join(ui5DataDir, "framework", "staging");
+ }
+
+ getRegistry() {
+ if (this._cachedRegistry) {
+ return this._cachedRegistry;
+ }
+ return this._cachedRegistry = new Registry({
+ cwd: this._cwd,
+ cacheDir: this._caCacheDir
+ });
+ }
+
+ async readJson(jsonPath) {
+ return JSON.parse(await readFile(jsonPath, {encoding: "utf8"}));
+ }
+
+ async fetchPackageVersions({pkgName}) {
+ const packument = await this.getRegistry().requestPackagePackument(pkgName);
+ return Object.keys(packument.versions);
+ }
+
+ async fetchPackageDistTags({pkgName}) {
+ const packument = await this.getRegistry().requestPackagePackument(pkgName);
+ return packument["dist-tags"];
+ }
+
+ async fetchPackageManifest({pkgName, version}) {
+ const targetDir = this._getTargetDirForPackage({pkgName, version});
+ try {
+ const pkg = await this.readJson(path.join(targetDir, "package.json"));
+ return {
+ name: pkg.name,
+ dependencies: pkg.dependencies,
+ devDependencies: pkg.devDependencies
+ };
+ } catch (err) {
+ if (err.code === "ENOENT") { // "File or directory does not exist"
+ const manifest = await this.getRegistry().requestPackageManifest(pkgName, version);
+ return {
+ name: manifest.name,
+ dependencies: manifest.dependencies,
+ devDependencies: manifest.devDependencies
+ };
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ async installPackage({pkgName, version}) {
+ const targetDir = this._getTargetDirForPackage({pkgName, version});
+ const installed = await this._packageJsonExists(targetDir);
+ if (!installed) {
+ await this._synchronize(`package-${pkgName}@${version}`, async () => {
+ // check again whether package is now installed
+ const installed = await this._packageJsonExists(targetDir);
+ if (!installed) {
+ const stagingDir = this._getStagingDirForPackage({pkgName, version});
+ log.info(`Installing missing package ${pkgName}...`);
+
+ // Check whether staging dir already exists and remove it
+ if (await this._pathExists(stagingDir)) {
+ log.verbose(`Removing existing staging directory at ${stagingDir}...`);
+ await rmrf(stagingDir);
+ }
+
+ // Check whether target dir already exists and remove it.
+ // A target directory already existing but missing a package.json should
+ // never happen. However, we want to be *really* sure that there is no target
+ // directory so that the rename operation won't have any no trouble.
+ if (await this._pathExists(targetDir)) {
+ log.verbose(`Removing existing target directory at ${targetDir}...`);
+ await rmrf(targetDir);
+ }
+
+ log.verbose(`Installing ${pkgName} in version ${version} to ${stagingDir}...`);
+ await this.getRegistry().extractPackage(pkgName, version, stagingDir);
+
+ // Do not create target dir itself to prevent EPERM error in following rename operation
+ // (https://github.com/UI5/cli/issues/487)
+ await mkdirp(path.dirname(targetDir));
+ log.verbose(`Promoting staging directory from ${stagingDir} to ${targetDir}...`);
+ await rename(stagingDir, targetDir);
+ } else {
+ log.verbose(`Already installed: ${pkgName} in version ${version}`);
+ }
+ });
+ } else {
+ log.verbose(`Already installed: ${pkgName} in version ${version}`);
+ }
+ return {
+ pkgPath: targetDir
+ };
+ }
+
+ async _packageJsonExists(targetDir) {
+ return this._pathExists(path.join(targetDir, "package.json"));
+ }
+
+ async _pathExists(targetPath) {
+ try {
+ await stat(targetPath);
+ return true;
+ } catch (err) {
+ if (err.code === "ENOENT") { // "File or directory does not exist"
+ return false;
+ } else {
+ throw err;
+ }
+ }
+ }
+
+ _getTargetDirForPackage({pkgName, version}) {
+ return path.join(this._packagesDir, ...pkgName.split("/"), version);
+ }
+
+ _getStagingDirForPackage({pkgName, version}) {
+ return path.join(this._stagingDir, `${pkgName.replaceAll("/", "-")}-${version}`);
+ }
+}
+
+export default Installer;
diff --git a/packages/project/lib/ui5Framework/npm/Registry.js b/packages/project/lib/ui5Framework/npm/Registry.js
new file mode 100644
index 00000000000..4d60b4d2011
--- /dev/null
+++ b/packages/project/lib/ui5Framework/npm/Registry.js
@@ -0,0 +1,94 @@
+import {getLogger} from "@ui5/logger";
+const log = getLogger("ui5Framework:npm:Registry");
+
+function logConfig(config, configName) {
+ const configValue = config[configName];
+ if (configValue) {
+ log.verbose(` ${configName}: ${configValue}`);
+ }
+}
+
+class Registry {
+ /**
+ * @param {object} parameters Parameters
+ * @param {string} parameters.cwd Current working directory
+ * @param {string} parameters.cacheDir Cache directory
+ */
+ constructor({cwd, cacheDir}) {
+ if (!cwd) {
+ throw new Error(`Registry: Missing parameter "cwd"`);
+ }
+ if (!cacheDir) {
+ throw new Error(`Registry: Missing parameter "cacheDir"`);
+ }
+ this._cwd = cwd;
+ this._cacheDir = cacheDir;
+ }
+ async requestPackagePackument(pkgName) {
+ const {pacote, pacoteOptions} = await this._getPacote();
+ return pacote.packument(pkgName, pacoteOptions);
+ }
+ async requestPackageManifest(pkgName, version) {
+ const {pacote, pacoteOptions} = await this._getPacote();
+
+ return pacote.manifest(`${pkgName}@${version}`, pacoteOptions);
+ }
+ async extractPackage(pkgName, version, targetDir) {
+ const {pacote, pacoteOptions} = await this._getPacote();
+ try {
+ await pacote.extract(`${pkgName}@${version}`, targetDir, pacoteOptions);
+ } catch (err) {
+ throw new Error(`Failed to extract package ${pkgName}@${version}: ${err.message}`);
+ }
+ }
+
+ async _getPacote() {
+ if (this._pGetPacote) {
+ return this._pGetPacote;
+ }
+ return this._pGetPacote = (async () => {
+ return {
+ pacote: (await import("pacote")).default,
+ pacoteOptions: await this._getPacoteOptions()
+ };
+ })();
+ }
+
+ async _getPacoteOptions() {
+ const {default: Config} = await import("@npmcli/config");
+ const {
+ default: {flatten, definitions, shorthands, defaults},
+ } = await import("@npmcli/config/lib/definitions/index.js");
+
+ const configuration = new Config({
+ cwd: this._cwd,
+ npmPath: this._cwd,
+ definitions,
+ flatten,
+ shorthands,
+ defaults
+ });
+
+ await configuration.load(); // Reads through the configurations
+ const config = configuration.flat; // JSON. Formatted via "flatten"
+
+ // Always use our cache dir instead of the configured one
+ config.cache = this._cacheDir;
+
+ log.verbose(`Using npm configuration (extract):`);
+ // Do not log full configuration as it may contain authentication tokens
+ logConfig(config, "registry");
+ logConfig(config, "@sapui5:registry");
+ logConfig(config, "@openui5:registry");
+ logConfig(config, "proxy");
+ logConfig(config, "httpsProxy");
+ logConfig(config, "globalconfig");
+ logConfig(config, "userconfig");
+ logConfig(config, "cache");
+ logConfig(config, "cwd");
+
+ return config;
+ }
+}
+
+export default Registry;
diff --git a/packages/project/lib/utils/fs.js b/packages/project/lib/utils/fs.js
new file mode 100644
index 00000000000..a8852509b82
--- /dev/null
+++ b/packages/project/lib/utils/fs.js
@@ -0,0 +1,12 @@
+import fs from "graceful-fs";
+import {promisify} from "node:util";
+const mkdir = promisify(fs.mkdir);
+const rm = promisify(fs.rm);
+
+export async function mkdirp(dirPath) {
+ return mkdir(dirPath, {recursive: true});
+}
+
+export async function rmrf(dirPath) {
+ return rm(dirPath, {recursive: true, force: true});
+}
diff --git a/packages/project/lib/validation/ValidationError.js b/packages/project/lib/validation/ValidationError.js
new file mode 100644
index 00000000000..fabdbd6e8a6
--- /dev/null
+++ b/packages/project/lib/validation/ValidationError.js
@@ -0,0 +1,283 @@
+import chalk from "chalk";
+import escapeStringRegExp from "escape-string-regexp";
+
+/**
+ * Error class for validation of project configuration.
+ *
+ * @public
+ * @class
+ * @alias @ui5/project/validation/ValidationError
+ * @extends Error
+ * @hideconstructor
+ */
+class ValidationError extends Error {
+ constructor({errors, project, yaml}) {
+ super();
+
+ /**
+ * ValidationError
+ *
+ * @constant
+ * @default
+ * @type {string}
+ * @readonly
+ * @public
+ */
+ this.name = "ValidationError";
+
+ this.project = project;
+ this.yaml = yaml;
+
+ this.errors = ValidationError.filterErrors(errors);
+
+ /**
+ * Formatted error message
+ *
+ * @type {string}
+ * @readonly
+ * @public
+ */
+ this.message = this.formatErrors();
+
+ Error.captureStackTrace(this, this.constructor);
+ }
+
+ formatErrors() {
+ let separator = "\n\n";
+ if (process.stdout.isTTY) {
+ // Add a horizontal separator line between errors in case a terminal is used
+ separator += chalk.grey.dim("\u2500".repeat(process.stdout.columns || 80));
+ }
+ separator += "\n\n";
+ let message;
+
+ if (this.project) { // ui5-workspace.yaml is project independent, so in that case, no project is available
+ message = chalk.red(`Invalid ui5.yaml configuration for project ${this.project.id}`) + "\n\n";
+ } else {
+ message = chalk.red(`Invalid workspace configuration.`) + "\n\n";
+ }
+
+ message += this.errors.map((error) => {
+ return this.formatError(error);
+ }).join(separator);
+ return message;
+ }
+
+ formatError(error) {
+ let errorMessage = ValidationError.formatMessage(error);
+ if (this.yaml && this.yaml.path && this.yaml.source) {
+ const yamlExtract = ValidationError.getYamlExtract({error, yaml: this.yaml});
+ const errorLines = errorMessage.split("\n");
+ errorLines.splice(1, 0, "\n" + yamlExtract);
+ errorMessage = errorLines.join("\n");
+ }
+ return errorMessage;
+ }
+
+ static formatMessage(error) {
+ if (error.keyword === "errorMessage") {
+ return error.message;
+ }
+
+ let message = "Configuration ";
+ if (error.dataPath) {
+ message += chalk.underline(chalk.red(error.dataPath.substr(1))) + " ";
+ }
+
+ switch (error.keyword) {
+ case "additionalProperties":
+ message += `property ${error.params.additionalProperty} must not be provided here`;
+ break;
+ case "type":
+ message += `must be of type '${error.params.type}'`;
+ break;
+ case "required":
+ message += `must have required property '${error.params.missingProperty}'`;
+ break;
+ case "enum":
+ message += "must be equal to one of the allowed values\n";
+ message += "Allowed values: " + error.params.allowedValues.join(", ");
+ break;
+ default:
+ message += error.message;
+ }
+
+ return message;
+ }
+
+ static _findDuplicateError(error, errorIndex, errors) {
+ const foundIndex = errors.findIndex(($) => {
+ if ($.dataPath !== error.dataPath) {
+ return false;
+ } else if ($.keyword !== error.keyword) {
+ return false;
+ } else if (JSON.stringify($.params) !== JSON.stringify(error.params)) {
+ return false;
+ } else {
+ return true;
+ }
+ });
+ return foundIndex !== errorIndex;
+ }
+
+ static filterErrors(allErrors) {
+ return allErrors.filter((error, i, errors) => {
+ if (error.keyword === "if" || error.keyword === "oneOf") {
+ return false;
+ }
+
+ return !ValidationError._findDuplicateError(error, i, errors);
+ });
+ }
+
+ static analyzeYamlError({error, yaml}) {
+ if (error.dataPath === "" && error.keyword === "required") {
+ // There is no line/column for a missing required property on root level
+ return {line: -1, column: -1};
+ }
+
+ // Skip leading /
+ const objectPath = error.dataPath.substr(1).split("/");
+
+ if (error.keyword === "additionalProperties") {
+ objectPath.push(error.params.additionalProperty);
+ }
+
+ let currentSubstring;
+ let currentIndex;
+ if (yaml.documentIndex) {
+ const matchDocumentSeparator = /^---/gm;
+ let currentDocumentIndex = 0;
+ let document;
+ while ((document = matchDocumentSeparator.exec(yaml.source)) !== null) {
+ // If the first separator is not at the beginning of the file
+ // we are already at document index 1
+ // Using String#trim() to remove any whitespace characters
+ if (currentDocumentIndex === 0 && yaml.source.substring(0, document.index).trim().length > 0) {
+ currentDocumentIndex = 1;
+ }
+
+ if (currentDocumentIndex === yaml.documentIndex) {
+ currentIndex = document.index;
+ currentSubstring = yaml.source.substring(currentIndex);
+ break;
+ }
+
+ currentDocumentIndex++;
+ }
+ // Document could not be found
+ if (!currentSubstring) {
+ return {line: -1, column: -1};
+ }
+ } else {
+ // In case of index 0 or no index, use whole source
+ currentIndex = 0;
+ currentSubstring = yaml.source;
+ }
+
+ const matchArrayElementIndentation = /([ ]*)-/;
+
+ for (let i = 0; i < objectPath.length; i++) {
+ const property = objectPath[i];
+ let newIndex;
+
+ if (isNaN(property)) {
+ // Try to find a property
+
+ // Creating a regular expression that matches the property name a line
+ // except for comments, indicated by a hash sign "#".
+ const propertyRegExp = new RegExp(`^[^#]*?${escapeStringRegExp(property)}`, "m");
+
+ const propertyMatch = propertyRegExp.exec(currentSubstring);
+ if (!propertyMatch) {
+ return {line: -1, column: -1};
+ }
+ newIndex = propertyMatch.index + propertyMatch[0].length;
+ } else {
+ // Try to find the right index within an array definition.
+ // This currently only works for arrays defined with "-" in multiple lines.
+ // Arrays using square brackets are not supported.
+
+ const matchArrayElement = /(^|\r?\n)([ ]*-[^\r\n]*)/g;
+ const arrayIndex = parseInt(property);
+ let a = 0;
+ let firstIndentation = -1;
+ let match;
+ while ((match = matchArrayElement.exec(currentSubstring)) !== null) {
+ const indentationMatch = match[2].match(matchArrayElementIndentation);
+ if (!indentationMatch) {
+ return {line: -1, column: -1};
+ }
+ const currentIndentation = indentationMatch[1].length;
+ if (firstIndentation === -1) {
+ firstIndentation = currentIndentation;
+ } else if (currentIndentation !== firstIndentation) {
+ continue;
+ }
+ if (a === arrayIndex) {
+ // match[1] might be a line-break
+ newIndex = match.index + match[1].length + currentIndentation;
+ break;
+ }
+ a++;
+ }
+ if (!newIndex) {
+ // Could not find array element
+ return {line: -1, column: -1};
+ }
+ }
+ currentIndex += newIndex;
+ currentSubstring = yaml.source.substring(currentIndex);
+ }
+
+ const linesUntilMatch = yaml.source.substring(0, currentIndex).split(/\r?\n/);
+ const line = linesUntilMatch.length;
+ let column = linesUntilMatch[line - 1].length + 1;
+ const lastPathSegment = objectPath[objectPath.length - 1];
+ if (isNaN(lastPathSegment)) {
+ column -= lastPathSegment.length;
+ }
+
+ return {
+ line,
+ column
+ };
+ }
+
+ static getSourceExtract(yamlSource, line, column) {
+ let source = "";
+ const lines = yamlSource.split(/\r?\n/);
+
+ // Using line numbers instead of array indices
+ const startLine = Math.max(line - 2, 1);
+ const endLine = Math.min(line, lines.length);
+ const padLength = String(endLine).length;
+
+ for (let currentLine = startLine; currentLine <= endLine; currentLine++) {
+ const currentLineContent = lines[currentLine - 1];
+ let string = chalk.gray(
+ String(currentLine).padStart(padLength, " ") + ":"
+ ) + " " + currentLineContent + "\n";
+ if (currentLine === line) {
+ string = chalk.bgRed(string);
+ }
+ source += string;
+ }
+
+ source += " ".repeat(column + padLength + 1) + chalk.red("^");
+
+ return source;
+ }
+
+ static getYamlExtract({error, yaml}) {
+ const {line, column} = ValidationError.analyzeYamlError({error, yaml});
+ if (line !== -1 && column !== -1) {
+ return chalk.grey(yaml.path + ":" + line) +
+ "\n\n" + ValidationError.getSourceExtract(yaml.source, line, column);
+ } else {
+ return chalk.grey(yaml.path) + "\n";
+ }
+ }
+}
+
+export default ValidationError;
diff --git a/packages/project/lib/validation/schema/specVersion/kind/extension.json b/packages/project/lib/validation/schema/specVersion/kind/extension.json
new file mode 100644
index 00000000000..274d67748a2
--- /dev/null
+++ b/packages/project/lib/validation/schema/specVersion/kind/extension.json
@@ -0,0 +1,94 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/specVersion/kind/extension.json",
+
+ "type": "object",
+ "required": ["specVersion", "kind", "type", "metadata"],
+ "properties": {
+ "specVersion": { "enum": ["4.0", "3.2", "3.1", "3.0", "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"] },
+ "kind": {
+ "enum": ["extension"]
+ },
+ "type": {
+ "enum": [
+ "task",
+ "server-middleware",
+ "project-shim"
+ ]
+ },
+ "metadata": {
+ "$ref": "#/definitions/metadata"
+ }
+ },
+ "if": {
+ "properties": {
+ "type": {"const": null}
+ },
+ "$comment": "Using 'if' with null and empty 'then' to ensure no other schemas are applied when the property is not set. Otherwise the first 'if' condition might still be met, causing unexpected errors."
+ },
+ "then": {},
+ "else": {
+ "if": {
+ "properties": {
+ "type": {"const": "task"}
+ }
+ },
+ "then": {
+ "$ref": "extension/task.json"
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "type": {"const": "server-middleware"}
+ }
+ },
+ "then": {
+ "$ref": "extension/server-middleware.json"
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "type": {"const": "project-shim"}
+ }
+ },
+ "then": {
+ "$ref": "extension/project-shim.json"
+ }
+ }
+ }
+ },
+ "definitions": {
+ "metadata": {
+ "type": "object",
+ "required": ["name"],
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "copyright": {
+ "type": "string"
+ }
+ }
+ },
+ "metadata-3.0": {
+ "type": "object",
+ "required": ["name"],
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 80,
+ "pattern": "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$",
+ "title": "Extension Name",
+ "description": "Unique identifier for the extension, for example: ui5-task-fearless-rock",
+ "errorMessage": "Not a valid extension 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"
+ },
+ "copyright": {
+ "type": "string"
+ }
+ }
+ }
+ }
+}
diff --git a/packages/project/lib/validation/schema/specVersion/kind/extension/project-shim.json b/packages/project/lib/validation/schema/specVersion/kind/extension/project-shim.json
new file mode 100644
index 00000000000..d7877e3af93
--- /dev/null
+++ b/packages/project/lib/validation/schema/specVersion/kind/extension/project-shim.json
@@ -0,0 +1,137 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/specVersion/kind/extension/project-shim.json",
+
+ "type": "object",
+ "required": ["specVersion", "kind", "type", "metadata", "shims"],
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": {
+ "enum": ["3.0", "3.1", "3.2", "4.0"]
+ },
+ "kind": {
+ "enum": ["extension"]
+ },
+ "type": {
+ "enum": ["project-shim"]
+ },
+ "metadata": {
+ "$ref": "../extension.json#/definitions/metadata-3.0"
+ },
+ "shims": {
+ "$ref": "#/definitions/shims"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": {
+ "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"]
+ },
+ "kind": {
+ "enum": ["extension"]
+ },
+ "type": {
+ "enum": ["project-shim"]
+ },
+ "metadata": {
+ "$ref": "../extension.json#/definitions/metadata"
+ },
+ "shims": {
+ "$ref": "#/definitions/shims"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": {
+ "enum": ["2.0"]
+ },
+ "kind": {
+ "enum": ["extension"]
+ },
+ "type": {
+ "enum": ["project-shim"]
+ },
+ "metadata": {
+ "$ref": "../extension.json#/definitions/metadata"
+ },
+ "shims": {
+ "$ref": "#/definitions/shims"
+ }
+ }
+ }
+ },
+ "definitions": {
+ "shims": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "configurations": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ ".+": {
+ "type": "object"
+ }
+ }
+ },
+ "dependencies": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ ".+": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "collections": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ ".+": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "modules": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/project/lib/validation/schema/specVersion/kind/extension/server-middleware.json b/packages/project/lib/validation/schema/specVersion/kind/extension/server-middleware.json
new file mode 100644
index 00000000000..de4628ecb92
--- /dev/null
+++ b/packages/project/lib/validation/schema/specVersion/kind/extension/server-middleware.json
@@ -0,0 +1,93 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/specVersion/kind/extension/server-middleware.json",
+
+ "type": "object",
+
+ "required": ["specVersion", "kind", "type", "metadata", "middleware"],
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] },
+ "kind": {
+ "enum": ["extension"]
+ },
+ "type": {
+ "enum": ["server-middleware"]
+ },
+ "metadata": {
+ "$ref": "../extension.json#/definitions/metadata-3.0"
+ },
+ "middleware": {
+ "$ref": "#/definitions/middleware"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] },
+ "kind": {
+ "enum": ["extension"]
+ },
+ "type": {
+ "enum": ["server-middleware"]
+ },
+ "metadata": {
+ "$ref": "../extension.json#/definitions/metadata"
+ },
+ "middleware": {
+ "$ref": "#/definitions/middleware"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.0"] },
+ "kind": {
+ "enum": ["extension"]
+ },
+ "type": {
+ "enum": ["server-middleware"]
+ },
+ "metadata": {
+ "$ref": "../extension.json#/definitions/metadata"
+ },
+ "middleware": {
+ "$ref": "#/definitions/middleware"
+ }
+ }
+ }
+ },
+ "definitions": {
+ "middleware": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "path": {
+ "type": "string"
+ }
+ }
+ }
+ }
+}
diff --git a/packages/project/lib/validation/schema/specVersion/kind/extension/task.json b/packages/project/lib/validation/schema/specVersion/kind/extension/task.json
new file mode 100644
index 00000000000..4fb3496a2d4
--- /dev/null
+++ b/packages/project/lib/validation/schema/specVersion/kind/extension/task.json
@@ -0,0 +1,92 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/specVersion/kind/extension/task.json",
+
+ "type": "object",
+ "required": ["specVersion", "kind", "type", "metadata", "task"],
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] },
+ "kind": {
+ "enum": ["extension"]
+ },
+ "type": {
+ "enum": ["task"]
+ },
+ "metadata": {
+ "$ref": "../extension.json#/definitions/metadata-3.0"
+ },
+ "task": {
+ "$ref": "#/definitions/task"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4", "2.5", "2.6"] },
+ "kind": {
+ "enum": ["extension"]
+ },
+ "type": {
+ "enum": ["task"]
+ },
+ "metadata": {
+ "$ref": "../extension.json#/definitions/metadata"
+ },
+ "task": {
+ "$ref": "#/definitions/task"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.0"] },
+ "kind": {
+ "enum": ["extension"]
+ },
+ "type": {
+ "enum": ["task"]
+ },
+ "metadata": {
+ "$ref": "../extension.json#/definitions/metadata"
+ },
+ "task": {
+ "$ref": "#/definitions/task"
+ }
+ }
+ }
+ },
+ "definitions": {
+ "task": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "path": {
+ "type": "string"
+ }
+ }
+ }
+ }
+}
diff --git a/packages/project/lib/validation/schema/specVersion/kind/project.json b/packages/project/lib/validation/schema/specVersion/kind/project.json
new file mode 100644
index 00000000000..d1167eba986
--- /dev/null
+++ b/packages/project/lib/validation/schema/specVersion/kind/project.json
@@ -0,0 +1,856 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/specVersion/kind/project.json",
+
+ "type": "object",
+ "required": ["specVersion", "type"],
+ "properties": {
+ "specVersion": { "enum": ["4.0", "3.2", "3.1", "3.0", "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"] },
+ "kind": {
+ "enum": ["project", null],
+ "$comment": "Using null to allow not defining 'kind' which defaults to project"
+ },
+ "type": {
+ "enum": [
+ "application",
+ "library",
+ "theme-library",
+ "module"
+ ]
+ }
+ },
+ "if": {
+ "properties": {
+ "type": {"const": null}
+ },
+ "$comment": "Using 'if' with null and empty 'then' to ensure no other schemas are applied when the property is not set. Otherwise the first 'if' condition might still be met, causing unexpected errors."
+ },
+ "then": {},
+ "else": {
+ "if": {
+ "properties": {
+ "type": {"const": "application"}
+ }
+ },
+ "then": {
+ "$ref": "project/application.json"
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "type": {"const": "library"}
+ }
+ },
+ "then": {
+ "$ref": "project/library.json"
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "type": {"const": "theme-library"}
+ }
+ },
+ "then": {
+ "$ref": "project/theme-library.json"
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "type": {"const": "module"}
+ }
+ },
+ "then": {
+ "$ref": "project/module.json"
+ }
+ }
+ }
+ }
+ },
+
+ "definitions": {
+ "metadata": {
+ "type": "object",
+ "required": ["name"],
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "copyright": {
+ "type": "string"
+ },
+ "deprecated": {
+ "type": "boolean",
+ "default": false
+ },
+ "sapInternal": {
+ "type": "boolean",
+ "default": false
+ },
+ "allowSapInternal": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ },
+ "metadata-3.0": {
+ "type": "object",
+ "required": ["name"],
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 80,
+ "pattern": "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$",
+ "title": "Project Name",
+ "description": "Unique identifier for the project, for example: organization.product.project",
+ "errorMessage": "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"
+ },
+ "copyright": {
+ "type": "string"
+ },
+ "deprecated": {
+ "type": "boolean",
+ "default": false
+ },
+ "sapInternal": {
+ "type": "boolean",
+ "default": false
+ },
+ "allowSapInternal": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ },
+ "resources-configuration-propertiesFileSourceEncoding": {
+ "enum": ["UTF-8", "ISO-8859-1"],
+ "default": "UTF-8",
+ "title": "Encoding of *.properties files",
+ "description": "By default, the UI5 CLI expects *.properties files to be UTF-8 encoded."
+ },
+ "builder-resources": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "excludes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "builder-bundles": {
+ "type": "array",
+ "additionalProperties": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "bundleDefinition": {
+ "$ref": "#/definitions/builder-bundles-bundleDefinition"
+ },
+ "bundleOptions": {
+ "$ref": "#/definitions/builder-bundles-bundleOptions"
+ }
+ }
+ }
+ },
+ "builder-bundles-2.4": {
+ "type": "array",
+ "additionalProperties": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "bundleDefinition": {
+ "$ref": "#/definitions/builder-bundles-bundleDefinition-2.4"
+ },
+ "bundleOptions": {
+ "$ref": "#/definitions/builder-bundles-bundleOptions"
+ }
+ }
+ }
+ },
+ "builder-bundles-3.0": {
+ "type": "array",
+ "additionalProperties": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "bundleDefinition": {
+ "$ref": "#/definitions/builder-bundles-bundleDefinition-2.4"
+ },
+ "bundleOptions": {
+ "$ref": "#/definitions/builder-bundles-bundleOptions-3.0"
+ }
+ }
+ }
+ },
+ "builder-bundles-3.2": {
+ "type": "array",
+ "additionalProperties": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "bundleDefinition": {
+ "$ref": "#/definitions/builder-bundles-bundleDefinition-3.2"
+ },
+ "bundleOptions": {
+ "$ref": "#/definitions/builder-bundles-bundleOptions-3.0"
+ }
+ }
+ }
+ },
+ "builder-bundles-4.0": {
+ "type": "array",
+ "additionalProperties": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "bundleDefinition": {
+ "$ref": "#/definitions/builder-bundles-bundleDefinition-4.0"
+ },
+ "bundleOptions": {
+ "$ref": "#/definitions/builder-bundles-bundleOptions-4.0"
+ }
+ }
+ }
+ },
+ "builder-bundles-bundleDefinition": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "defaultFileTypes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "sections": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["mode", "filters"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "mode": {
+ "enum": ["raw", "preload", "require", "provided"]
+ },
+ "filters": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "resolve": {
+ "type": "boolean",
+ "default": false
+ },
+ "resolveConditional": {
+ "type": "boolean",
+ "default": false
+ },
+ "renderer": {
+ "type": "boolean",
+ "default": false
+ },
+ "sort": {
+ "type": "boolean",
+ "default": true
+ },
+ "declareRawModules": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ }
+ }
+ }
+ },
+ "builder-bundles-bundleDefinition-2.4": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "defaultFileTypes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "sections": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["mode", "filters"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "mode": {
+ "enum": ["raw", "preload", "require", "provided", "bundleInfo"]
+ },
+ "filters": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "resolve": {
+ "type": "boolean",
+ "default": false
+ },
+ "resolveConditional": {
+ "type": "boolean",
+ "default": false
+ },
+ "renderer": {
+ "type": "boolean",
+ "default": false
+ },
+ "sort": {
+ "type": "boolean",
+ "default": true
+ },
+ "declareRawModules": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ }
+ }
+ }
+ },
+ "builder-bundles-bundleDefinition-3.2": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "defaultFileTypes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "sections": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["mode", "filters"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "mode": {
+ "enum": ["raw", "preload", "require", "provided", "bundleInfo", "depCache"]
+ },
+ "filters": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "resolve": {
+ "type": "boolean",
+ "default": false
+ },
+ "resolveConditional": {
+ "type": "boolean",
+ "default": false
+ },
+ "renderer": {
+ "type": "boolean",
+ "default": false
+ },
+ "sort": {
+ "type": "boolean",
+ "default": true
+ },
+ "declareRawModules": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ }
+ }
+ }
+ },
+ "builder-bundles-bundleDefinition-4.0": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "defaultFileTypes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "sections": {
+ "type": "array",
+ "items": {
+ "if": {
+ "properties": {
+ "mode": {
+ "const": "require"
+ }
+ },
+ "$comment": "Add async prop only if mode = 'require'"
+ },
+ "then": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["mode", "filters"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "mode": {
+ "enum": ["require"]
+ },
+ "filters": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "resolve": {
+ "type": "boolean",
+ "default": false
+ },
+ "resolveConditional": {
+ "type": "boolean",
+ "default": false
+ },
+ "renderer": {
+ "type": "boolean",
+ "default": false
+ },
+ "sort": {
+ "type": "boolean",
+ "default": true
+ },
+ "declareRawModules": {
+ "type": "boolean",
+ "default": false
+ },
+ "async": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ },
+ "else": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["mode", "filters"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "mode": {
+ "enum": ["raw", "preload", "require", "provided", "bundleInfo", "depCache"]
+ },
+ "filters": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "resolve": {
+ "type": "boolean",
+ "default": false
+ },
+ "resolveConditional": {
+ "type": "boolean",
+ "default": false
+ },
+ "renderer": {
+ "type": "boolean",
+ "default": false
+ },
+ "sort": {
+ "type": "boolean",
+ "default": true
+ },
+ "declareRawModules": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "builder-bundles-bundleOptions": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "optimize": {
+ "type": "boolean",
+ "default": false
+ },
+ "decorateBootstrapModule": {
+ "type": "boolean",
+ "default": false
+ },
+ "addTryCatchRestartWrapper": {
+ "type": "boolean",
+ "default": false
+ },
+ "usePredefineCalls": {
+ "type": "boolean",
+ "default": false
+ },
+ "numberOfParts": {
+ "type": "number",
+ "default": 1
+ }
+ }
+ },
+ "builder-bundles-bundleOptions-3.0": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "optimize": {
+ "type": "boolean",
+ "default": true
+ },
+ "decorateBootstrapModule": {
+ "type": "boolean",
+ "default": false
+ },
+ "addTryCatchRestartWrapper": {
+ "type": "boolean",
+ "default": false
+ },
+ "usePredefineCalls": {
+ "type": "boolean",
+ "default": false
+ },
+ "numberOfParts": {
+ "type": "number",
+ "default": 1
+ },
+ "sourceMap": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ },
+ "builder-bundles-bundleOptions-4.0": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "optimize": {
+ "type": "boolean",
+ "default": true
+ },
+ "decorateBootstrapModule": {
+ "type": "boolean",
+ "default": false
+ },
+ "addTryCatchRestartWrapper": {
+ "type": "boolean",
+ "default": false
+ },
+ "numberOfParts": {
+ "type": "number",
+ "default": 1
+ },
+ "sourceMap": {
+ "type": "boolean",
+ "default": true
+ }
+ }
+ },
+ "builder-componentPreload": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "paths": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "namespaces": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "builder-componentPreload-specVersion-2.3": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "paths": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "namespaces": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "excludes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "builder-libraryPreload": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "excludes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "server": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "settings": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "httpPort": {
+ "type": "number"
+ },
+ "httpsPort": {
+ "type": "number"
+ }
+ }
+ },
+ "customMiddleware": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name", "beforeMiddleware"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "mountPath": {
+ "type": "string"
+ },
+ "beforeMiddleware": {
+ "type": "string"
+ },
+ "configuration": {}
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name", "afterMiddleware"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "mountPath": {
+ "type": "string"
+ },
+ "afterMiddleware": {
+ "type": "string"
+ },
+ "configuration": {}
+ }
+ }
+ ]
+ }
+ }
+ }
+ },
+ "framework": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "enum": ["OpenUI5", "SAPUI5"]
+ },
+ "version": {
+ "type": "string",
+ "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
+ "title": "Version",
+ "description": "Framework version to use in this project",
+ "errorMessage": "Not a valid version according to the Semantic Versioning specification (https://semver.org/)"
+ },
+ "libraries": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "optional": {
+ "type": "boolean",
+ "default": false
+ },
+ "development": {
+ "type": "boolean",
+ "default": false
+ }
+ },
+ "if": {
+ "not": {
+ "anyOf": [
+ {
+ "properties": {
+ "optional": {"enum": [false, null]}
+ }
+ },
+ {
+ "properties": {
+ "development": {"enum": [false, null]}
+ }
+ },
+ {
+ "not": {
+ "properties": {
+ "optional": {"type": "boolean"}
+ }
+ }
+ },
+ {
+ "not": {
+ "properties": {
+ "development": {"type": "boolean"}
+ }
+ }
+ }
+ ],
+ "$comment": "Unfortunately it doesn't seem to work to check for both properties to be true, so instead checking for not having any of the properties to other values like false, not defined or any other type."
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "errorMessage": "Either \"development\" or \"optional\" can be true, but not both",
+ "$comment": "Defining a custom error message and only allowing the \"name\" property causes editors to show the custom error on both properties."
+ }
+ }
+ }
+ }
+ },
+ "customTasks": {
+ "type": "array",
+ "items": {
+ "oneOf": [
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name", "beforeTask"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "beforeTask": {
+ "type": "string"
+ },
+ "configuration": {}
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["name", "afterTask"],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "afterTask": {
+ "type": "string"
+ },
+ "configuration": {}
+ }
+ }
+ ]
+ }
+ },
+ "builder-settings": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "includeDependency": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "includeDependencyRegExp": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "includeDependencyTree": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "builder-minification": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "excludes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/project/lib/validation/schema/specVersion/kind/project/application.json b/packages/project/lib/validation/schema/specVersion/kind/project/application.json
new file mode 100644
index 00000000000..46f05b9753b
--- /dev/null
+++ b/packages/project/lib/validation/schema/specVersion/kind/project/application.json
@@ -0,0 +1,608 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/specVersion/kind/project/application.json",
+
+ "type": "object",
+ "required": ["specVersion", "type", "metadata"],
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["4.0"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["4.0"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["application"]
+ },
+ "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": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["3.2"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["3.2"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["application"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata-3.0"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-3.2"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["3.0", "3.1"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["3.0", "3.1"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["application"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata-3.0"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-3.0"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.6"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.6"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["application"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.6"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.5"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.5"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["application"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.5"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.4"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.4"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["application"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.4"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.3"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.3"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["application"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.3"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["application"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.0"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["application"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "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": {
+ "webapp": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+
+ "builder": {
+ "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"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ }
+ }
+ },
+ "builder-specVersion-2.3": {
+ "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"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ }
+ }
+ },
+ "builder-specVersion-2.4": {
+ "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-2.4"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ }
+ }
+ },
+ "builder-specVersion-2.5": {
+ "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-2.4"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ },
+ "settings": {
+ "$ref": "../project.json#/definitions/builder-settings"
+ }
+ }
+ },
+ "builder-specVersion-2.6": {
+ "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-2.4"
+ },
+ "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"
+ }
+ }
+ },
+ "builder-specVersion-3.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-3.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"
+ }
+ }
+ },
+ "builder-specVersion-3.2": {
+ "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-3.2"
+ },
+ "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"
+ }
+ }
+ },
+ "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/lib/validation/schema/specVersion/kind/project/library.json b/packages/project/lib/validation/schema/specVersion/kind/project/library.json
new file mode 100644
index 00000000000..8a039254010
--- /dev/null
+++ b/packages/project/lib/validation/schema/specVersion/kind/project/library.json
@@ -0,0 +1,593 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/specVersion/kind/project/library.json",
+
+ "type": "object",
+ "required": ["specVersion", "type", "metadata"],
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["4.0"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["4.0"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["library"]
+ },
+ "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": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["3.2"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["3.2"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata-3.0"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-3.2"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["3.0", "3.1"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["3.0", "3.1"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata-3.0"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-3.0"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.6"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.6"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.6"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.5"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.5"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.5"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.4"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.4"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.4"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.3"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.3"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.3"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.0"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "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-jsdoc": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "excludes": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "builder": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "resources": {
+ "$ref": "../project.json#/definitions/builder-resources"
+ },
+ "jsdoc": {
+ "$ref": "#/definitions/builder-jsdoc"
+ },
+ "bundles": {
+ "$ref": "../project.json#/definitions/builder-bundles"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ }
+ }
+ },
+ "builder-specVersion-2.3": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "resources": {
+ "$ref": "../project.json#/definitions/builder-resources"
+ },
+ "jsdoc": {
+ "$ref": "#/definitions/builder-jsdoc"
+ },
+ "bundles": {
+ "$ref": "../project.json#/definitions/builder-bundles"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3"
+ },
+ "libraryPreload": {
+ "$ref": "../project.json#/definitions/builder-libraryPreload"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ }
+ }
+ },
+ "builder-specVersion-2.4": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "resources": {
+ "$ref": "../project.json#/definitions/builder-resources"
+ },
+ "jsdoc": {
+ "$ref": "#/definitions/builder-jsdoc"
+ },
+ "bundles": {
+ "$ref": "../project.json#/definitions/builder-bundles-2.4"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3"
+ },
+ "libraryPreload": {
+ "$ref": "../project.json#/definitions/builder-libraryPreload"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ }
+ }
+ },
+ "builder-specVersion-2.5": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "resources": {
+ "$ref": "../project.json#/definitions/builder-resources"
+ },
+ "jsdoc": {
+ "$ref": "#/definitions/builder-jsdoc"
+ },
+ "bundles": {
+ "$ref": "../project.json#/definitions/builder-bundles-2.4"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3"
+ },
+ "libraryPreload": {
+ "$ref": "../project.json#/definitions/builder-libraryPreload"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ },
+ "settings": {
+ "$ref": "../project.json#/definitions/builder-settings"
+ }
+ }
+ },
+ "builder-specVersion-2.6": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "resources": {
+ "$ref": "../project.json#/definitions/builder-resources"
+ },
+ "jsdoc": {
+ "$ref": "#/definitions/builder-jsdoc"
+ },
+ "bundles": {
+ "$ref": "../project.json#/definitions/builder-bundles-2.4"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3"
+ },
+ "libraryPreload": {
+ "$ref": "../project.json#/definitions/builder-libraryPreload"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ },
+ "minification": {
+ "$ref": "../project.json#/definitions/builder-minification"
+ },
+ "settings": {
+ "$ref": "../project.json#/definitions/builder-settings"
+ }
+ }
+ },
+ "builder-specVersion-3.0": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "resources": {
+ "$ref": "../project.json#/definitions/builder-resources"
+ },
+ "jsdoc": {
+ "$ref": "#/definitions/builder-jsdoc"
+ },
+ "bundles": {
+ "$ref": "../project.json#/definitions/builder-bundles-3.0"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3"
+ },
+ "libraryPreload": {
+ "$ref": "../project.json#/definitions/builder-libraryPreload"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ },
+ "minification": {
+ "$ref": "../project.json#/definitions/builder-minification"
+ },
+ "settings": {
+ "$ref": "../project.json#/definitions/builder-settings"
+ }
+ }
+ },
+ "builder-specVersion-3.2": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "resources": {
+ "$ref": "../project.json#/definitions/builder-resources"
+ },
+ "jsdoc": {
+ "$ref": "#/definitions/builder-jsdoc"
+ },
+ "bundles": {
+ "$ref": "../project.json#/definitions/builder-bundles-3.2"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3"
+ },
+ "libraryPreload": {
+ "$ref": "../project.json#/definitions/builder-libraryPreload"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ },
+ "minification": {
+ "$ref": "../project.json#/definitions/builder-minification"
+ },
+ "settings": {
+ "$ref": "../project.json#/definitions/builder-settings"
+ }
+ }
+ },
+ "builder-specVersion-4.0": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "resources": {
+ "$ref": "../project.json#/definitions/builder-resources"
+ },
+ "jsdoc": {
+ "$ref": "#/definitions/builder-jsdoc"
+ },
+ "bundles": {
+ "$ref": "../project.json#/definitions/builder-bundles-4.0"
+ },
+ "componentPreload": {
+ "$ref": "../project.json#/definitions/builder-componentPreload-specVersion-2.3"
+ },
+ "libraryPreload": {
+ "$ref": "../project.json#/definitions/builder-libraryPreload"
+ },
+ "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/lib/validation/schema/specVersion/kind/project/module.json b/packages/project/lib/validation/schema/specVersion/kind/project/module.json
new file mode 100644
index 00000000000..1e38cfcdf0f
--- /dev/null
+++ b/packages/project/lib/validation/schema/specVersion/kind/project/module.json
@@ -0,0 +1,201 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/specVersion/kind/project/module.json",
+
+ "type": "object",
+ "required": ["specVersion", "type", "metadata"],
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["3.1", "3.2", "4.0"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["3.1", "3.2", "4.0"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["module"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata-3.0"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-3.1"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["3.0"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["3.0"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["module"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata-3.0"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.5"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.5", "2.6"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.5", "2.6"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["module"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.5"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["module"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.0"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["module"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "resources": {
+ "$ref": "#/definitions/resources"
+ }
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "resources": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "configuration": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "paths": {
+ "type": "object",
+ "additionalProperties": false,
+ "patternProperties": {
+ ".+": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "builder-specVersion-2.5": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "settings": {
+ "$ref": "../project.json#/definitions/builder-settings"
+ }
+ }
+ },
+ "builder-specVersion-3.1": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "resources": {
+ "$ref": "../project.json#/definitions/builder-resources"
+ },
+ "settings": {
+ "$ref": "../project.json#/definitions/builder-settings"
+ }
+ }
+ }
+ }
+}
diff --git a/packages/project/lib/validation/schema/specVersion/kind/project/theme-library.json b/packages/project/lib/validation/schema/specVersion/kind/project/theme-library.json
new file mode 100644
index 00000000000..db0853b33a2
--- /dev/null
+++ b/packages/project/lib/validation/schema/specVersion/kind/project/theme-library.json
@@ -0,0 +1,175 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/specVersion/kind/project/theme-library.json",
+
+ "type": "object",
+ "required": ["specVersion", "type", "metadata"],
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["3.0", "3.1", "3.2", "4.0"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["theme-library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata-3.0"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "library.json#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.5"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.5", "2.6"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.5", "2.6"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["theme-library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "library.json#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder-specVersion-2.5"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4"] }
+ }
+ },
+ "then": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.1", "2.2", "2.3", "2.4"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["theme-library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "library.json#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ },
+ "customConfiguration": {
+ "type": "object",
+ "additionalProperties": true
+ }
+ }
+ },
+ "else": {
+ "additionalProperties": false,
+ "properties": {
+ "specVersion": { "enum": ["2.0"] },
+ "kind": {
+ "enum": ["project", null]
+ },
+ "type": {
+ "enum": ["theme-library"]
+ },
+ "metadata": {
+ "$ref": "../project.json#/definitions/metadata"
+ },
+ "framework": {
+ "$ref": "../project.json#/definitions/framework"
+ },
+ "resources": {
+ "$ref": "library.json#/definitions/resources"
+ },
+ "builder": {
+ "$ref": "#/definitions/builder"
+ },
+ "server": {
+ "$ref": "../project.json#/definitions/server"
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "builder": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "resources": {
+ "$ref": "../project.json#/definitions/builder-resources"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ }
+ }
+ },
+ "builder-specVersion-2.5": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "resources": {
+ "$ref": "../project.json#/definitions/builder-resources"
+ },
+ "customTasks": {
+ "$ref": "../project.json#/definitions/customTasks"
+ },
+ "settings": {
+ "$ref": "../project.json#/definitions/builder-settings"
+ }
+ }
+ }
+ }
+}
diff --git a/packages/project/lib/validation/schema/specVersion/specVersion.json b/packages/project/lib/validation/schema/specVersion/specVersion.json
new file mode 100644
index 00000000000..1126cb0e8a7
--- /dev/null
+++ b/packages/project/lib/validation/schema/specVersion/specVersion.json
@@ -0,0 +1,37 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/specVersion/2.0.json",
+
+ "type": "object",
+ "required": ["specVersion"],
+ "properties": {
+ "specVersion": { "enum": ["4.0", "3.2", "3.1", "3.0", "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"] },
+ "kind": {
+ "enum": ["project", "extension", null],
+ "$comment": "Using null to allow not defining 'kind' which defaults to project"
+ }
+ },
+ "if": {
+ "properties": {
+ "kind": {
+ "enum": ["project", null],
+ "$comment": "Using null to allow not defining 'kind' which defaults to project"
+ }
+ }
+ },
+ "then": {
+ "$ref": "kind/project.json"
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "kind": {
+ "enum": ["extension"]
+ }
+ }
+ },
+ "then": {
+ "$ref": "kind/extension.json"
+ }
+ }
+}
diff --git a/packages/project/lib/validation/schema/ui5-workspace.json b/packages/project/lib/validation/schema/ui5-workspace.json
new file mode 100644
index 00000000000..41bd129edd0
--- /dev/null
+++ b/packages/project/lib/validation/schema/ui5-workspace.json
@@ -0,0 +1,59 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/ui5-workspace.json",
+ "title": "ui5-workspace.yaml",
+ "description": "Schema for UI5 CLI Workspace Configuration File (ui5-workspace.yaml)",
+ "$comment": "See https://ui5.github.io/cli/",
+ "type": "object",
+ "required": ["specVersion", "metadata", "dependencyManagement"],
+ "properties": {
+ "additionalProperties": false,
+ "specVersion": {
+ "enum": ["workspace/1.0"],
+ "errorMessage": "Unsupported \"specVersion\"\nYour UI5 CLI installation might be outdated.\nSupported specification versions: \"workspace/1.0\"\nFor details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-specification-versions"
+ },
+ "metadata": {
+ "$ref": "#/definitions/metadata"
+ },
+ "dependencyManagement": {
+ "$ref": "#/definitions/dependencyManagement"
+ }
+ },
+ "definitions": {
+ "metadata": {
+ "type": "object",
+ "required": ["name"],
+ "properties": {
+ "additionalProperties": false,
+ "name": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 80,
+ "pattern": "^(?:@[0-9a-z-_.]+\\/)?[a-z][0-9a-z-_.]*$",
+ "title": "Workspace Name",
+ "description": "Identifier for the workspace configuration. Workspaces named 'default' will be used automatically by UI5 CLI",
+ "errorMessage": "Not a valid workspace 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/Workspace/#name"
+ }
+ }
+ },
+ "dependencyManagement": {
+ "type": "object",
+ "properties": {
+ "additionalProperties": false,
+ "resolutions": {
+ "type": "array",
+ "additionalProperties": false,
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "path": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/project/lib/validation/schema/ui5.json b/packages/project/lib/validation/schema/ui5.json
new file mode 100644
index 00000000000..0511c7fdd2a
--- /dev/null
+++ b/packages/project/lib/validation/schema/ui5.json
@@ -0,0 +1,40 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://ui5.sap/schema/ui5.json",
+ "title": "ui5.yaml",
+ "description": "Schema for UI5 CLI Configuration File (ui5.yaml)",
+ "$comment": "See https://ui5.github.io/cli/",
+
+ "type": "object",
+ "required": ["specVersion"],
+ "properties": {
+ "specVersion": {
+ "enum": [
+ "4.0",
+ "3.2", "3.1", "3.0",
+ "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0",
+ "1.1", "1.0", "0.1"
+ ],
+ "errorMessage": "Unsupported \"specVersion\"\nYour UI5 CLI installation might be outdated.\nSupported specification versions: \"4.0\", \"3.2\", \"3.1\", \"3.0\", \"2.6\", \"2.5\", \"2.4\", \"2.3\", \"2.2\", \"2.1\", \"2.0\", \"1.1\", \"1.0\", \"0.1\"\nFor details, see: https://ui5.github.io/cli/pages/Configuration/#specification-versions"
+ }
+ },
+
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["4.0", "3.2", "3.1", "3.0", "2.6", "2.5", "2.4", "2.3", "2.2", "2.1", "2.0"] }
+ }
+ },
+ "then": {
+ "$ref": "specVersion/specVersion.json"
+ },
+ "else": {
+ "if": {
+ "properties": {
+ "specVersion": { "enum": ["1.1", "1.0", "0.1"] }
+ }
+ },
+ "then": {
+ "additionalProperties": true
+ }
+ }
+}
diff --git a/packages/project/lib/validation/validator.js b/packages/project/lib/validation/validator.js
new file mode 100644
index 00000000000..233e792cb0b
--- /dev/null
+++ b/packages/project/lib/validation/validator.js
@@ -0,0 +1,214 @@
+import {fileURLToPath} from "node:url";
+import {readFile} from "node:fs/promises";
+
+/**
+ * @module @ui5/project/validation/validator
+ * @description A collection of validation related APIs
+ * @public
+ */
+
+/**
+ * @enum {string}
+ * @private
+ * @readonly
+ */
+export const SCHEMA_VARIANTS = {
+ "ui5": "ui5.json",
+ "ui5-workspace": "ui5-workspace.json"
+};
+
+class Validator {
+ constructor({Ajv, ajvErrors, schemaName, ajvConfig}) {
+ if (!schemaName || !SCHEMA_VARIANTS[schemaName]) {
+ throw new Error(
+ `"schemaName" is missing or incorrect. The available schemaName variants are ${Object.keys(
+ SCHEMA_VARIANTS
+ ).join(", ")}`
+ );
+ }
+
+ this._schemaName = SCHEMA_VARIANTS[schemaName];
+
+ ajvConfig = Object.assign({
+ allErrors: true,
+ jsonPointers: true,
+ loadSchema: Validator.loadSchema
+ }, ajvConfig);
+ this.ajv = new Ajv(ajvConfig);
+ ajvErrors(this.ajv);
+ }
+
+ _compileSchema() {
+ const schemaName = this._schemaName;
+
+ if (!this._compiling) {
+ this._compiling = Promise.resolve().then(async () => {
+ const schema = await Validator.loadSchema(schemaName);
+ const validate = await this.ajv.compileAsync(schema);
+ return validate;
+ });
+ }
+ return this._compiling;
+ }
+
+ async validate({config, project, yaml}) {
+ const fnValidate = await this._compileSchema();
+ const valid = fnValidate(config);
+ if (!valid) {
+ // Read errors/schema from fnValidate before lazy loading ValidationError module.
+ // Otherwise they might be cleared already.
+ const {errors, schema} = fnValidate;
+ const {default: ValidationError} = await import("./ValidationError.js");
+ throw new ValidationError({
+ errors,
+ schema,
+ project,
+ yaml
+ });
+ }
+ }
+
+ static async loadSchema(schemaPath) {
+ const filePath = schemaPath.replace("http://ui5.sap/schema/", "");
+ const schemaFile = await readFile(
+ fileURLToPath(new URL(`./schema/${filePath}`, import.meta.url)), {encoding: "utf8"}
+ );
+ return JSON.parse(schemaFile);
+ }
+}
+
+const validator = Object.create(null);
+const defaultsValidator = Object.create(null);
+
+async function _validate(schemaName, options) {
+ if (!validator[schemaName]) {
+ validator[schemaName] = (async () => {
+ const {default: Ajv} = await import("ajv");
+ const {default: ajvErrors} = await import("ajv-errors");
+ return new Validator({Ajv, ajvErrors, schemaName});
+ })();
+ }
+
+ const schemaValidator = await validator[schemaName];
+ await schemaValidator.validate(options);
+}
+
+async function _validateAndSetDefaults(schemaName, options) {
+ if (!defaultsValidator[schemaName]) {
+ defaultsValidator[schemaName] = (async () => {
+ const {default: Ajv} = await import("ajv");
+ const {default: ajvErrors} = await import("ajv-errors");
+ return new Validator({Ajv, ajvErrors, ajvConfig: {useDefaults: true}, schemaName});
+ })();
+ }
+
+ // When AJV is configured with useDefaults: true, it may add properties to the
+ // provided configuration that were not initially present. This behavior can
+ // lead to unexpected side effects and potential issues. To avoid these
+ // problems, we create a copy of the configuration. If we need the altered
+ // configuration later, we return this copied version.
+ const optionsCopy = structuredClone(options);
+ const schemaValidator = await defaultsValidator[schemaName];
+ await schemaValidator.validate(optionsCopy);
+
+ return optionsCopy;
+}
+
+/**
+ * Validates the given ui5 configuration.
+ *
+ * @public
+ * @function
+ * @static
+ * @param {object} options
+ * @param {object} options.config UI5 Configuration to validate
+ * @param {object} options.project Project information
+ * @param {string} options.project.id ID of the project
+ * @param {object} [options.yaml] YAML information
+ * @param {string} options.yaml.path Path of the YAML file
+ * @param {string} options.yaml.source Content of the YAML file
+ * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents
+ * @throws {@ui5/project/validation/ValidationError}
+ * Rejects with a {@link @ui5/project/validation/ValidationError ValidationError}
+ * when the validation fails.
+ * @returns {Promise} Returns a Promise that resolves when the validation succeeds
+ */
+export async function validate(options) {
+ await _validate("ui5", options);
+}
+
+/**
+ * Validates the given ui5 configuration and returns default values if none are provided.
+ *
+ * @public
+ * @function
+ * @static
+ * @param {object} options
+ * @param {object} options.config The UI5 Configuration to validate
+ * @param {object} options.project Project information
+ * @param {string} options.project.id ID of the project
+ * @param {object} [options.yaml] YAML information
+ * @param {string} options.yaml.path Path of the YAML file
+ * @param {string} options.yaml.source Content of the YAML file
+ * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents
+ * @throws {module:@ui5/project/validation/ValidationError}
+ * Rejects with a {@link @ui5/project/validation/ValidationError ValidationError}
+ * when the validation fails.
+ * @returns {Promise} Returns a Promise that resolves when the validation succeeds
+ */
+export async function getDefaults(options) {
+ return await _validateAndSetDefaults("ui5", options);
+}
+
+/**
+ * Enhances bundleDefinition by adding missing properties with their respective default values.
+ *
+ * @param {object[]} bundles Bundles to be enhanced
+ * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleDefinition} bundles[].bundleDefinition
+ * Module bundle definition
+ * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [bundles[].bundleOptions]
+ * Module bundle options
+ * @param {module:@ui5/project/specifications/Project} project The project to get metadata from
+ * @returns {Promise} The enhanced BundleDefinition & BundleOptions
+ */
+export async function enhanceBundlesWithDefaults(bundles, project) {
+ const config = {
+ specVersion: `${project.getSpecVersion()}`,
+ type: `${project.getType()}`,
+ metadata: {name: project.getName()},
+ builder: {bundles}
+ };
+ const result = await getDefaults({config, project: {id: project.getName()}});
+
+ return result.config.builder.bundles;
+}
+
+/**
+ * Validates the given ui5-workspace configuration.
+ *
+ * @public
+ * @function
+ * @static
+ * @param {object} options
+ * @param {object} options.config ui5-workspace Configuration to validate
+ * @param {object} [options.yaml] YAML information
+ * @param {string} options.yaml.path Path of the YAML file
+ * @param {string} options.yaml.source Content of the YAML file
+ * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents
+ * @throws {@ui5/project/validation/ValidationError}
+ * Rejects with a {@link @ui5/project/validation/ValidationError ValidationError}
+ * when the validation fails.
+ * @returns {Promise} Returns a Promise that resolves when the validation succeeds
+ */
+export async function validateWorkspace(options) {
+ await _validate("ui5-workspace", options);
+}
+
+export {
+ /**
+ * For testing only!
+ *
+ * @private
+ */
+ Validator as _Validator
+};
diff --git a/packages/project/package-lock.json b/packages/project/package-lock.json
new file mode 100644
index 00000000000..413fb26d7a6
--- /dev/null
+++ b/packages/project/package-lock.json
@@ -0,0 +1,8807 @@
+{
+ "name": "@ui5/project",
+ "version": "4.0.6",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "@ui5/project",
+ "version": "4.0.6",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@npmcli/config": "^10.4.0",
+ "@ui5/fs": "^4.0.2",
+ "@ui5/logger": "^4.0.2",
+ "ajv": "^6.12.6",
+ "ajv-errors": "^1.0.1",
+ "chalk": "^5.6.2",
+ "escape-string-regexp": "^5.0.0",
+ "globby": "^14.1.0",
+ "graceful-fs": "^4.2.11",
+ "js-yaml": "^4.1.0",
+ "lockfile": "^1.0.4",
+ "make-fetch-happen": "^14.0.3",
+ "node-stream-zip": "^1.15.0",
+ "pacote": "^19.0.1",
+ "pretty-hrtime": "^1.0.3",
+ "read-package-up": "^11.0.0",
+ "read-pkg": "^9.0.1",
+ "resolve": "^1.22.10",
+ "semver": "^7.7.2",
+ "xml2js": "^0.6.2",
+ "yesno": "^0.4.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.8.0",
+ "@istanbuljs/esm-loader-hook": "^0.3.0",
+ "ava": "^6.4.1",
+ "chokidar-cli": "^3.0.0",
+ "cross-env": "^7.0.3",
+ "depcheck": "^1.4.7",
+ "docdash": "^2.0.2",
+ "eslint": "^9.36.0",
+ "eslint-config-google": "^0.14.0",
+ "eslint-plugin-ava": "^15.1.0",
+ "eslint-plugin-jsdoc": "^52.0.4",
+ "esmock": "^2.7.3",
+ "globals": "^16.4.0",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-instrument": "^6.0.3",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.2.0",
+ "js-beautify": "^1.15.4",
+ "jsdoc": "^4.0.4",
+ "nyc": "^17.1.0",
+ "open-cli": "^8.0.0",
+ "rimraf": "^6.0.1",
+ "sinon": "^21.0.0",
+ "tap-xunit": "^2.4.1"
+ },
+ "engines": {
+ "node": "^20.11.0 || >=22.0.0",
+ "npm": ">= 8"
+ },
+ "peerDependencies": {
+ "@ui5/builder": "^4.0.11"
+ },
+ "peerDependenciesMeta": {
+ "@ui5/builder": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
+ "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
+ "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
+ "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-compilation-targets": "^7.27.2",
+ "@babel/helper-module-transforms": "^7.28.3",
+ "@babel/helpers": "^7.28.4",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/traverse": "^7.28.4",
+ "@babel/types": "^7.28.4",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/core/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
+ "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.28.3",
+ "@babel/types": "^7.28.2",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-annotate-as-pure": {
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+ "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.27.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
+ "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/compat-data": "^7.27.2",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz",
+ "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/helper-replace-supers": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/traverse": "^7.28.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-member-expression-to-functions": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz",
+ "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
+ "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
+ "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1",
+ "@babel/traverse": "^7.28.3"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-optimise-call-expression": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz",
+ "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
+ "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-replace-supers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz",
+ "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-member-expression-to-functions": "^7.27.1",
+ "@babel/helper-optimise-call-expression": "^7.27.1",
+ "@babel/traverse": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-skip-transparent-expression-wrappers": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz",
+ "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/traverse": "^7.27.1",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
+ "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
+ "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
+ "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/types": "^7.28.4"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-decorators": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.27.1.tgz",
+ "integrity": "sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-jsx": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz",
+ "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-syntax-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz",
+ "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-modules-commonjs": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz",
+ "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-module-transforms": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-typescript": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz",
+ "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-annotate-as-pure": "^7.27.3",
+ "@babel/helper-create-class-features-plugin": "^7.27.1",
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1",
+ "@babel/plugin-syntax-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/preset-typescript": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz",
+ "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1",
+ "@babel/helper-validator-option": "^7.27.1",
+ "@babel/plugin-syntax-jsx": "^7.27.1",
+ "@babel/plugin-transform-modules-commonjs": "^7.27.1",
+ "@babel/plugin-transform-typescript": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.27.2",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
+ "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/parser": "^7.27.2",
+ "@babel/types": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
+ "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.27.1",
+ "@babel/generator": "^7.28.3",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.28.4",
+ "@babel/template": "^7.27.2",
+ "@babel/types": "^7.28.4",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.28.4",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
+ "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@es-joy/jsdoccomment": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.52.0.tgz",
+ "integrity": "sha512-BXuN7BII+8AyNtn57euU2Yxo9yA/KUDNzrpXyi3pfqKmBhhysR6ZWOebFh3vyPoqA3/j1SOvGgucElMGwlXing==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.8",
+ "@typescript-eslint/types": "^8.34.1",
+ "comment-parser": "1.4.1",
+ "esquery": "^1.6.0",
+ "jsdoc-type-pratt-parser": "~4.1.0"
+ },
+ "engines": {
+ "node": ">=20.11.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+ "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+ "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
+ "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.6",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
+ "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.15.2",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
+ "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
+ "dev": true,
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+ "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.0",
+ "minimatch": "^3.1.2",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.36.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.36.0.tgz",
+ "integrity": "sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+ "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
+ "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
+ "dev": true,
+ "dependencies": {
+ "@eslint/core": "^0.15.2",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@isaacs/balanced-match": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
+ "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@isaacs/brace-expansion": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
+ "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
+ "dependencies": {
+ "@isaacs/balanced-match": "^4.0.1"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/@isaacs/cliui": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+ "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+ "dependencies": {
+ "string-width": "^5.1.2",
+ "string-width-cjs": "npm:string-width@^4.2.0",
+ "strip-ansi": "^7.0.1",
+ "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+ "wrap-ansi": "^8.1.0",
+ "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+ },
+ "node_modules/@isaacs/cliui/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@isaacs/fs-minipass": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
+ "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
+ "dependencies": {
+ "minipass": "^7.0.4"
+ },
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/@istanbuljs/esm-loader-hook": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/esm-loader-hook/-/esm-loader-hook-0.3.0.tgz",
+ "integrity": "sha512-lEnYroBUYfNQuJDYrPvre8TSwPZnyIQv9qUT3gACvhr3igZr+BbrdyIcz4+2RnEXZzi12GqkUW600+QQPpIbVg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.8.7",
+ "@babel/plugin-syntax-decorators": "^7.25.9",
+ "@babel/preset-typescript": "^7.26.0",
+ "@istanbuljs/load-nyc-config": "^1.1.0",
+ "@istanbuljs/schema": "^0.1.3",
+ "babel-plugin-istanbul": "^6.0.0",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=16.12.0"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz",
+ "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.3.1",
+ "find-up": "^4.1.0",
+ "get-package-type": "^0.1.0",
+ "js-yaml": "^3.13.1",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@jsdoc/salty": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.9.tgz",
+ "integrity": "sha512-yYxMVH7Dqw6nO0d5NIV8OQWnitU8k6vXH8NtgqAfIa/IUqRMxRv/NUJJ08VEKbAakwxlgBl5PJdrU0dMPStsnw==",
+ "dev": true,
+ "dependencies": {
+ "lodash": "^4.17.21"
+ },
+ "engines": {
+ "node": ">=v12.0.0"
+ }
+ },
+ "node_modules/@mapbox/node-pre-gyp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-2.0.0.tgz",
+ "integrity": "sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==",
+ "dev": true,
+ "dependencies": {
+ "consola": "^3.2.3",
+ "detect-libc": "^2.0.0",
+ "https-proxy-agent": "^7.0.5",
+ "node-fetch": "^2.6.7",
+ "nopt": "^8.0.0",
+ "semver": "^7.5.3",
+ "tar": "^7.4.0"
+ },
+ "bin": {
+ "node-pre-gyp": "bin/node-pre-gyp"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@nodelib/fs.scandir": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+ "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+ "dependencies": {
+ "@nodelib/fs.stat": "2.0.5",
+ "run-parallel": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.stat": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+ "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@nodelib/fs.walk": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+ "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+ "dependencies": {
+ "@nodelib/fs.scandir": "2.1.5",
+ "fastq": "^1.6.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/@npmcli/agent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz",
+ "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.1",
+ "lru-cache": "^10.0.1",
+ "socks-proxy-agent": "^8.0.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/agent/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
+ },
+ "node_modules/@npmcli/config": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/config/-/config-10.4.0.tgz",
+ "integrity": "sha512-0l6f/q/qfB726SWOGIEooh7u6aB1SOgRxGLu7DeJ6Z9Vvq1gG1s3x+Mq+qv9wt0Q0t53mVHIEBokfJZpeaWDyA==",
+ "license": "ISC",
+ "dependencies": {
+ "@npmcli/map-workspaces": "^4.0.1",
+ "@npmcli/package-json": "^6.0.1",
+ "ci-info": "^4.0.0",
+ "ini": "^5.0.0",
+ "nopt": "^8.1.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "walk-up-path": "^4.0.0"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/@npmcli/fs": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz",
+ "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==",
+ "dependencies": {
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/git": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz",
+ "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==",
+ "dependencies": {
+ "@npmcli/promise-spawn": "^8.0.0",
+ "ini": "^5.0.0",
+ "lru-cache": "^10.0.1",
+ "npm-pick-manifest": "^10.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "semver": "^7.3.5",
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/git/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
+ },
+ "node_modules/@npmcli/installed-package-contents": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz",
+ "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==",
+ "dependencies": {
+ "npm-bundled": "^4.0.0",
+ "npm-normalize-package-bin": "^4.0.0"
+ },
+ "bin": {
+ "installed-package-contents": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/map-workspaces": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/map-workspaces/-/map-workspaces-4.0.2.tgz",
+ "integrity": "sha512-mnuMuibEbkaBTYj9HQ3dMe6L0ylYW+s/gfz7tBDMFY/la0w9Kf44P9aLn4/+/t3aTR3YUHKoT6XQL9rlicIe3Q==",
+ "dependencies": {
+ "@npmcli/name-from-folder": "^3.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "glob": "^10.2.2",
+ "minimatch": "^9.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/name-from-folder": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/name-from-folder/-/name-from-folder-3.0.0.tgz",
+ "integrity": "sha512-61cDL8LUc9y80fXn+lir+iVt8IS0xHqEKwPu/5jCjxQTVoSCmkXvw4vbMrzAMtmghz3/AkiBjhHkDKUH+kf7kA==",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/node-gyp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz",
+ "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/package-json": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.2.0.tgz",
+ "integrity": "sha512-rCNLSB/JzNvot0SEyXqWZ7tX2B5dD2a1br2Dp0vSYVo5jh8Z0EZ7lS9TsZ1UtziddB1UfNUaMCc538/HztnJGA==",
+ "dependencies": {
+ "@npmcli/git": "^6.0.0",
+ "glob": "^10.2.2",
+ "hosted-git-info": "^8.0.0",
+ "json-parse-even-better-errors": "^4.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.5.3",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/promise-spawn": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.3.tgz",
+ "integrity": "sha512-Yb00SWaL4F8w+K8YGhQ55+xE4RUNdMHV43WZGsiTM92gS+lC0mGsn7I4hLug7pbao035S6bj3Y3w0cUNGLfmkg==",
+ "dependencies": {
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/redact": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz",
+ "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@npmcli/run-script": {
+ "version": "9.1.0",
+ "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz",
+ "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==",
+ "dependencies": {
+ "@npmcli/node-gyp": "^4.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "@npmcli/promise-spawn": "^8.0.0",
+ "node-gyp": "^11.0.0",
+ "proc-log": "^5.0.0",
+ "which": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@one-ini/wasm": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@one-ini/wasm/-/wasm-0.1.1.tgz",
+ "integrity": "sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==",
+ "dev": true
+ },
+ "node_modules/@pkgjs/parseargs": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+ "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+ "optional": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
+ "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
+ "dev": true,
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@sigstore/bundle": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz",
+ "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.4.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sigstore/core": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz",
+ "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sigstore/protobuf-specs": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.3.tgz",
+ "integrity": "sha512-fk2zjD9117RL9BjqEwF7fwv7Q/P9yGsMV4MUJZ/DocaQJ6+3pKr+syBq1owU5Q5qGw5CUbXzm+4yJ2JVRDQeSA==",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sigstore/sign": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz",
+ "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.0",
+ "make-fetch-happen": "^14.0.2",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sigstore/tuf": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.1.tgz",
+ "integrity": "sha512-eFFvlcBIoGwVkkwmTi/vEQFSva3xs5Ot3WmBcjgjVdiaoelBLQaQ/ZBfhlG0MnG0cmTYScPpk7eDdGDWUcFUmg==",
+ "dependencies": {
+ "@sigstore/protobuf-specs": "^0.4.1",
+ "tuf-js": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sigstore/verify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.1.tgz",
+ "integrity": "sha512-hVJD77oT67aowHxwT4+M6PGOp+E2LtLdTK3+FC0lBO9T7sYwItDMXZ7Z07IDCvR1M717a4axbIWckrW67KMP/w==",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@sindresorhus/merge-streams": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
+ "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
+ "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "13.0.5",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
+ "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1"
+ }
+ },
+ "node_modules/@sinonjs/samsam": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz",
+ "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1",
+ "type-detect": "^4.1.0"
+ }
+ },
+ "node_modules/@sinonjs/samsam/node_modules/type-detect": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz",
+ "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@tokenizer/token": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
+ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
+ "dev": true
+ },
+ "node_modules/@tufjs/canonical-json": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz",
+ "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==",
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/@tufjs/models": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz",
+ "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==",
+ "dependencies": {
+ "@tufjs/canonical-json": "2.0.0",
+ "minimatch": "^9.0.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true
+ },
+ "node_modules/@types/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==",
+ "dev": true
+ },
+ "node_modules/@types/markdown-it": {
+ "version": "14.1.2",
+ "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz",
+ "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==",
+ "dev": true,
+ "dependencies": {
+ "@types/linkify-it": "^5",
+ "@types/mdurl": "^2"
+ }
+ },
+ "node_modules/@types/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==",
+ "dev": true
+ },
+ "node_modules/@types/minimatch": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
+ "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
+ "dev": true
+ },
+ "node_modules/@types/normalize-package-data": {
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
+ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="
+ },
+ "node_modules/@types/parse-json": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
+ "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
+ "dev": true
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.44.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.44.0.tgz",
+ "integrity": "sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@ui5/fs": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@ui5/fs/-/fs-4.0.2.tgz",
+ "integrity": "sha512-0R7eb9xEMswvkN2wIiyYJtQY83evQJ7LQhTnRf5Ms0o2R29twGLP4XewqH+IoGWyT3T4SuDNTWmUU2UaTRY4zg==",
+ "dependencies": {
+ "@ui5/logger": "^4.0.1",
+ "clone": "^2.1.2",
+ "escape-string-regexp": "^5.0.0",
+ "globby": "^14.1.0",
+ "graceful-fs": "^4.2.11",
+ "micromatch": "^4.0.8",
+ "minimatch": "^10.0.3",
+ "pretty-hrtime": "^1.0.3",
+ "random-int": "^3.0.0"
+ },
+ "engines": {
+ "node": "^20.11.0 || >=22.0.0",
+ "npm": ">= 8"
+ }
+ },
+ "node_modules/@ui5/fs/node_modules/minimatch": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
+ "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
+ "dependencies": {
+ "@isaacs/brace-expansion": "^5.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@ui5/logger": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@ui5/logger/-/logger-4.0.2.tgz",
+ "integrity": "sha512-uscDCQyHFeenh4r2RbYuffTMn6IQdcNC1tXrQ4BF+apAFjmDGP11IHdAwVCKwxgyPrIC17HT2gub3ZugGM8kpQ==",
+ "dependencies": {
+ "chalk": "^5.6.0",
+ "cli-progress": "^3.12.0",
+ "figures": "^6.1.0"
+ },
+ "engines": {
+ "node": "^20.11.0 || >=22.0.0",
+ "npm": ">= 8"
+ }
+ },
+ "node_modules/@vercel/nft": {
+ "version": "0.29.4",
+ "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.29.4.tgz",
+ "integrity": "sha512-6lLqMNX3TuycBPABycx7A9F1bHQR7kiQln6abjFbPrf5C/05qHM9M5E4PeTE59c7z8g6vHnx1Ioihb2AQl7BTA==",
+ "dev": true,
+ "dependencies": {
+ "@mapbox/node-pre-gyp": "^2.0.0",
+ "@rollup/pluginutils": "^5.1.3",
+ "acorn": "^8.6.0",
+ "acorn-import-attributes": "^1.9.5",
+ "async-sema": "^3.1.1",
+ "bindings": "^1.4.0",
+ "estree-walker": "2.0.2",
+ "glob": "^10.4.5",
+ "graceful-fs": "^4.2.9",
+ "node-gyp-build": "^4.2.2",
+ "picomatch": "^4.0.2",
+ "resolve-from": "^5.0.0"
+ },
+ "bin": {
+ "nft": "out/cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.21",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.21.tgz",
+ "integrity": "sha512-8i+LZ0vf6ZgII5Z9XmUvrCyEzocvWT+TeR2VBUVlzIH6Tyv57E20mPZ1bCS+tbejgUgmjrEh7q/0F0bibskAmw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.28.3",
+ "@vue/shared": "3.5.21",
+ "entities": "^4.5.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.21",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.21.tgz",
+ "integrity": "sha512-jNtbu/u97wiyEBJlJ9kmdw7tAr5Vy0Aj5CgQmo+6pxWNQhXZDPsRr1UWPN4v3Zf82s2H3kF51IbzZ4jMWAgPlQ==",
+ "dev": true,
+ "dependencies": {
+ "@vue/compiler-core": "3.5.21",
+ "@vue/shared": "3.5.21"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.21",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz",
+ "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.28.3",
+ "@vue/compiler-core": "3.5.21",
+ "@vue/compiler-dom": "3.5.21",
+ "@vue/compiler-ssr": "3.5.21",
+ "@vue/shared": "3.5.21",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.18",
+ "postcss": "^8.5.6",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.21",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.21.tgz",
+ "integrity": "sha512-vKQ5olH5edFZdf5ZrlEgSO1j1DMA4u23TVK5XR1uMhvwnYvVdDF0nHXJUblL/GvzlShQbjhZZ2uvYmDlAbgo9w==",
+ "dev": true,
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.21",
+ "@vue/shared": "3.5.21"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.21",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.21.tgz",
+ "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==",
+ "dev": true
+ },
+ "node_modules/abbrev": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz",
+ "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "dev": true,
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+ "dev": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-import-attributes": {
+ "version": "1.9.5",
+ "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz",
+ "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^8"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/acorn-walk": {
+ "version": "8.3.4",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
+ "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.11.0"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/aggregate-error": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz",
+ "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==",
+ "dev": true,
+ "dependencies": {
+ "clean-stack": "^2.0.0",
+ "indent-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/aggregate-error/node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.12.6",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ajv-errors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz",
+ "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==",
+ "peerDependencies": {
+ "ajv": ">=5.0.0"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
+ "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
+ "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/anymatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/append-transform": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-2.0.0.tgz",
+ "integrity": "sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==",
+ "dev": true,
+ "dependencies": {
+ "default-require-extensions": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/archy": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz",
+ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==",
+ "dev": true
+ },
+ "node_modules/are-docs-informative": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz",
+ "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ },
+ "node_modules/array-differ": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz",
+ "integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array-find-index": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+ "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/arrgv": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/arrgv/-/arrgv-1.0.2.tgz",
+ "integrity": "sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/arrify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-3.0.0.tgz",
+ "integrity": "sha512-tLkvA81vQG/XqE2mjDkGQHoOINtMHtysSnemrmoGe6PydDPMRbVugqyk4A6V/WDWEfm3l+0d8anA9r8cv/5Jaw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/async-sema": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz",
+ "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==",
+ "dev": true
+ },
+ "node_modules/ava": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/ava/-/ava-6.4.1.tgz",
+ "integrity": "sha512-vxmPbi1gZx9zhAjHBgw81w/iEDKcrokeRk/fqDTyA2DQygZ0o+dUGRHFOtX8RA5N0heGJTTsIk7+xYxitDb61Q==",
+ "dev": true,
+ "dependencies": {
+ "@vercel/nft": "^0.29.4",
+ "acorn": "^8.15.0",
+ "acorn-walk": "^8.3.4",
+ "ansi-styles": "^6.2.1",
+ "arrgv": "^1.0.2",
+ "arrify": "^3.0.0",
+ "callsites": "^4.2.0",
+ "cbor": "^10.0.9",
+ "chalk": "^5.4.1",
+ "chunkd": "^2.0.1",
+ "ci-info": "^4.3.0",
+ "ci-parallel-vars": "^1.0.1",
+ "cli-truncate": "^4.0.0",
+ "code-excerpt": "^4.0.0",
+ "common-path-prefix": "^3.0.0",
+ "concordance": "^5.0.4",
+ "currently-unhandled": "^0.4.1",
+ "debug": "^4.4.1",
+ "emittery": "^1.2.0",
+ "figures": "^6.1.0",
+ "globby": "^14.1.0",
+ "ignore-by-default": "^2.1.0",
+ "indent-string": "^5.0.0",
+ "is-plain-object": "^5.0.0",
+ "is-promise": "^4.0.0",
+ "matcher": "^5.0.0",
+ "memoize": "^10.1.0",
+ "ms": "^2.1.3",
+ "p-map": "^7.0.3",
+ "package-config": "^5.0.0",
+ "picomatch": "^4.0.2",
+ "plur": "^5.1.0",
+ "pretty-ms": "^9.2.0",
+ "resolve-cwd": "^3.0.0",
+ "stack-utils": "^2.0.6",
+ "strip-ansi": "^7.1.0",
+ "supertap": "^3.0.1",
+ "temp-dir": "^3.0.0",
+ "write-file-atomic": "^6.0.0",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "ava": "entrypoints/cli.mjs"
+ },
+ "engines": {
+ "node": "^18.18 || ^20.8 || ^22 || ^23 || >=24"
+ },
+ "peerDependencies": {
+ "@ava/typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "@ava/typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/babel-plugin-istanbul": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz",
+ "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.0.0",
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-instrument": "^5.0.4",
+ "test-exclude": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz",
+ "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.12.3",
+ "@babel/parser": "^7.14.7",
+ "@istanbuljs/schema": "^0.1.2",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^6.3.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/babel-plugin-istanbul/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.8.6",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz",
+ "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==",
+ "dev": true,
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.js"
+ }
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
+ "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/bindings": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+ "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+ "dev": true,
+ "dependencies": {
+ "file-uri-to-path": "1.0.0"
+ }
+ },
+ "node_modules/bluebird": {
+ "version": "3.7.2",
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
+ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==",
+ "dev": true
+ },
+ "node_modules/blueimp-md5": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz",
+ "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==",
+ "dev": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+ "dependencies": {
+ "balanced-match": "^1.0.0"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+ "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+ "dependencies": {
+ "fill-range": "^7.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.26.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz",
+ "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "baseline-browser-mapping": "^2.8.3",
+ "caniuse-lite": "^1.0.30001741",
+ "electron-to-chromium": "^1.5.218",
+ "node-releases": "^2.0.21",
+ "update-browserslist-db": "^1.1.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/bundle-name": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz",
+ "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==",
+ "dev": true,
+ "dependencies": {
+ "run-applescript": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cacache": {
+ "version": "19.0.1",
+ "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz",
+ "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==",
+ "dependencies": {
+ "@npmcli/fs": "^4.0.0",
+ "fs-minipass": "^3.0.0",
+ "glob": "^10.2.2",
+ "lru-cache": "^10.0.1",
+ "minipass": "^7.0.3",
+ "minipass-collect": "^2.0.1",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "p-map": "^7.0.2",
+ "ssri": "^12.0.0",
+ "tar": "^7.4.3",
+ "unique-filename": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/cacache/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
+ },
+ "node_modules/caching-transform": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz",
+ "integrity": "sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==",
+ "dev": true,
+ "dependencies": {
+ "hasha": "^5.0.0",
+ "make-dir": "^3.0.0",
+ "package-hash": "^4.0.0",
+ "write-file-atomic": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/caching-transform/node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/caching-transform/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/caching-transform/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/caching-transform/node_modules/write-file-atomic": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz",
+ "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "is-typedarray": "^1.0.0",
+ "signal-exit": "^3.0.2",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "node_modules/callsite": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/callsite/-/callsite-1.0.0.tgz",
+ "integrity": "sha512-0vdNRFXn5q+dtOqjfFtmtlI9N2eVZ7LMyEV2iKC5mEEFvSg/69Ml6b/WU2qF8W1nLRa0wiSrDT3Y5jOHZCwKPQ==",
+ "dev": true,
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz",
+ "integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "5.3.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001743",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz",
+ "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ]
+ },
+ "node_modules/catharsis": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz",
+ "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==",
+ "dev": true,
+ "dependencies": {
+ "lodash": "^4.17.15"
+ },
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/cbor": {
+ "version": "10.0.11",
+ "resolved": "https://registry.npmjs.org/cbor/-/cbor-10.0.11.tgz",
+ "integrity": "sha512-vIwORDd/WyB8Nc23o2zNN5RrtFGlR6Fca61TtjkUXueI3Jf2DOZDl1zsshvBntZ3wZHBM9ztjnkXSmzQDaq3WA==",
+ "dev": true,
+ "dependencies": {
+ "nofilter": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
+ "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
+ "dev": true,
+ "dependencies": {
+ "anymatch": "~3.1.2",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.2",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.6.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/chokidar-cli": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chokidar-cli/-/chokidar-cli-3.0.0.tgz",
+ "integrity": "sha512-xVW+Qeh7z15uZRxHOkP93Ux8A0xbPzwK4GaqD8dQOYc34TlkqUhVSS59fK36DOp5WdJlrRzlYSy02Ht99FjZqQ==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": "^3.5.2",
+ "lodash.debounce": "^4.0.8",
+ "lodash.throttle": "^4.1.1",
+ "yargs": "^13.3.0"
+ },
+ "bin": {
+ "chokidar": "index.js"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/ansi-regex": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz",
+ "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/cliui": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^3.1.0",
+ "strip-ansi": "^5.2.0",
+ "wrap-ansi": "^5.1.0"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/emoji-regex": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+ "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==",
+ "dev": true
+ },
+ "node_modules/chokidar-cli/node_modules/find-up": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/locate-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^3.0.0",
+ "path-exists": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/p-locate": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/string-width": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^7.0.1",
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/strip-ansi": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/wrap-ansi": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.0",
+ "string-width": "^3.0.0",
+ "strip-ansi": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "dev": true
+ },
+ "node_modules/chokidar-cli/node_modules/yargs": {
+ "version": "13.3.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+ "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^5.0.0",
+ "find-up": "^3.0.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^3.0.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^13.1.2"
+ }
+ },
+ "node_modules/chokidar-cli/node_modules/yargs-parser": {
+ "version": "13.1.2",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ }
+ },
+ "node_modules/chownr": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
+ "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chunkd": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/chunkd/-/chunkd-2.0.1.tgz",
+ "integrity": "sha512-7d58XsFmOq0j6el67Ug9mHf9ELUXsQXYJBkyxhH/k+6Ke0qXRnv0kbemx+Twc6fRJ07C49lcbdgm9FL1Ei/6SQ==",
+ "dev": true
+ },
+ "node_modules/ci-info": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
+ "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/sibiraj-s"
+ }
+ ],
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ci-parallel-vars": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/ci-parallel-vars/-/ci-parallel-vars-1.0.1.tgz",
+ "integrity": "sha512-uvzpYrpmidaoxvIQHM+rKSrigjOe9feHYbw4uOI2gdfe1C3xIlxO+kVXq83WQWNniTf8bAxVpy+cQeFQsMERKg==",
+ "dev": true
+ },
+ "node_modules/clean-stack": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
+ "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/cli-progress": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/cli-progress/-/cli-progress-3.12.0.tgz",
+ "integrity": "sha512-tRkV3HJ1ASwm19THiiLIXLO7Im7wlTuKnvkYaTkyoAPefqjNg7W7DHKUlGRxy9vxDvbyCYQkQozvptuMkGCg8A==",
+ "dependencies": {
+ "string-width": "^4.2.3"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/cli-truncate": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz",
+ "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==",
+ "dev": true,
+ "dependencies": {
+ "slice-ansi": "^5.0.0",
+ "string-width": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cli-truncate/node_modules/emoji-regex": {
+ "version": "10.5.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.5.0.tgz",
+ "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==",
+ "dev": true
+ },
+ "node_modules/cli-truncate/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/cliui/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/cliui/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/clone": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz",
+ "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
+ "engines": {
+ "node": ">=0.8"
+ }
+ },
+ "node_modules/code-excerpt": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/code-excerpt/-/code-excerpt-4.0.0.tgz",
+ "integrity": "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==",
+ "dev": true,
+ "dependencies": {
+ "convert-to-spaces": "^2.0.1"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/commander": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
+ "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/comment-parser": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz",
+ "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/common-path-prefix": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz",
+ "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==",
+ "dev": true
+ },
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "node_modules/concordance": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/concordance/-/concordance-5.0.4.tgz",
+ "integrity": "sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==",
+ "dev": true,
+ "dependencies": {
+ "date-time": "^3.1.0",
+ "esutils": "^2.0.3",
+ "fast-diff": "^1.2.0",
+ "js-string-escape": "^1.0.1",
+ "lodash": "^4.17.15",
+ "md5-hex": "^3.0.1",
+ "semver": "^7.3.2",
+ "well-known-symbols": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10.18.0 <11 || >=12.14.0 <13 || >=14"
+ }
+ },
+ "node_modules/config-chain": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz",
+ "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==",
+ "dev": true,
+ "dependencies": {
+ "ini": "^1.3.4",
+ "proto-list": "~1.2.1"
+ }
+ },
+ "node_modules/config-chain/node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true
+ },
+ "node_modules/consola": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz",
+ "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==",
+ "dev": true,
+ "engines": {
+ "node": "^14.18.0 || >=16.10.0"
+ }
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true
+ },
+ "node_modules/convert-to-spaces": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz",
+ "integrity": "sha512-rcQ1bsQO9799wq24uE5AM2tAILy4gXGIK/njFWcVQkGNZ96edlpY+A7bjwvzjYvLDyzmG1MmMLZhpcsb+klNMQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "dev": true
+ },
+ "node_modules/cosmiconfig": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz",
+ "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==",
+ "dev": true,
+ "dependencies": {
+ "@types/parse-json": "^4.0.0",
+ "import-fresh": "^3.2.1",
+ "parse-json": "^5.0.0",
+ "path-type": "^4.0.0",
+ "yaml": "^1.10.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/cross-spawn/node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ },
+ "node_modules/cross-spawn/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/crypto-random-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz",
+ "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/crypto-random-string/node_modules/type-fest": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz",
+ "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/currently-unhandled": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+ "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==",
+ "dev": true,
+ "dependencies": {
+ "array-find-index": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/date-time": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/date-time/-/date-time-3.1.0.tgz",
+ "integrity": "sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==",
+ "dev": true,
+ "dependencies": {
+ "time-zone": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true
+ },
+ "node_modules/default-browser": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz",
+ "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==",
+ "dev": true,
+ "dependencies": {
+ "bundle-name": "^4.1.0",
+ "default-browser-id": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-browser-id": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
+ "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/default-require-extensions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz",
+ "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==",
+ "dev": true,
+ "dependencies": {
+ "strip-bom": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/define-lazy-prop": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
+ "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/depcheck": {
+ "version": "1.4.7",
+ "resolved": "https://registry.npmjs.org/depcheck/-/depcheck-1.4.7.tgz",
+ "integrity": "sha512-1lklS/bV5chOxwNKA/2XUUk/hPORp8zihZsXflr8x0kLwmcZ9Y9BsS6Hs3ssvA+2wUVbG0U2Ciqvm1SokNjPkA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.23.0",
+ "@babel/traverse": "^7.23.2",
+ "@vue/compiler-sfc": "^3.3.4",
+ "callsite": "^1.0.0",
+ "camelcase": "^6.3.0",
+ "cosmiconfig": "^7.1.0",
+ "debug": "^4.3.4",
+ "deps-regex": "^0.2.0",
+ "findup-sync": "^5.0.0",
+ "ignore": "^5.2.4",
+ "is-core-module": "^2.12.0",
+ "js-yaml": "^3.14.1",
+ "json5": "^2.2.3",
+ "lodash": "^4.17.21",
+ "minimatch": "^7.4.6",
+ "multimatch": "^5.0.0",
+ "please-upgrade-node": "^3.2.0",
+ "readdirp": "^3.6.0",
+ "require-package-name": "^2.0.1",
+ "resolve": "^1.22.3",
+ "resolve-from": "^5.0.0",
+ "semver": "^7.5.4",
+ "yargs": "^16.2.0"
+ },
+ "bin": {
+ "depcheck": "bin/depcheck.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/depcheck/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/depcheck/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/depcheck/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/depcheck/node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/depcheck/node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/depcheck/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/depcheck/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/depcheck/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/depcheck/node_modules/minimatch": {
+ "version": "7.4.6",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-7.4.6.tgz",
+ "integrity": "sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/depcheck/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/depcheck/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/depcheck/node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/depcheck/node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/deps-regex": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/deps-regex/-/deps-regex-0.2.0.tgz",
+ "integrity": "sha512-PwuBojGMQAYbWkMXOY9Pd/NWCDNHVH12pnS7WHqZkTSeMESe4hwnKKRp0yR87g37113x4JPbo/oIvXY+s/f56Q==",
+ "dev": true
+ },
+ "node_modules/detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz",
+ "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diff": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz",
+ "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/docdash": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/docdash/-/docdash-2.0.2.tgz",
+ "integrity": "sha512-3SDDheh9ddrwjzf6dPFe1a16M6ftstqTNjik2+1fx46l24H9dD2osT2q9y+nBEC1wWz4GIqA48JmicOLQ0R8xA==",
+ "dev": true,
+ "dependencies": {
+ "@jsdoc/salty": "^0.2.1"
+ }
+ },
+ "node_modules/duplexer": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
+ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
+ "dev": true
+ },
+ "node_modules/eastasianwidth": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+ "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
+ },
+ "node_modules/editorconfig": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-1.0.4.tgz",
+ "integrity": "sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==",
+ "dev": true,
+ "dependencies": {
+ "@one-ini/wasm": "0.1.1",
+ "commander": "^10.0.0",
+ "minimatch": "9.0.1",
+ "semver": "^7.5.3"
+ },
+ "bin": {
+ "editorconfig": "bin/editorconfig"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/editorconfig/node_modules/minimatch": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.1.tgz",
+ "integrity": "sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.222",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz",
+ "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==",
+ "dev": true
+ },
+ "node_modules/emittery": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/emittery/-/emittery-1.2.0.tgz",
+ "integrity": "sha512-KxdRyyFcS85pH3dnU8Y5yFUm2YJdaHwcBZWrfG8o89ZY9a13/f9itbN+YG3ELbBo9Pg5zvIozstmuV8bX13q6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/emittery?sponsor=1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "optional": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "node_modules/enhance-visitors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/enhance-visitors/-/enhance-visitors-1.0.0.tgz",
+ "integrity": "sha512-+29eJLiUixTEDRaZ35Vu8jP3gPLNcQQkQkOQjLp2X+6cZGGPDD/uasbFzvLsJKnGZnvmyZ0srxudwOtskHeIDA==",
+ "dev": true,
+ "dependencies": {
+ "lodash": "^4.13.1"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/env-paths": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
+ "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/err-code": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz",
+ "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="
+ },
+ "node_modules/error-ex": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
+ "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
+ "dev": true,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es6-error": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
+ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==",
+ "dev": true
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
+ "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.36.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.36.0.tgz",
+ "integrity": "sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.1",
+ "@eslint/config-array": "^0.21.0",
+ "@eslint/config-helpers": "^0.3.1",
+ "@eslint/core": "^0.15.2",
+ "@eslint/eslintrc": "^3.3.1",
+ "@eslint/js": "9.36.0",
+ "@eslint/plugin-kit": "^0.3.5",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^8.4.0",
+ "eslint-visitor-keys": "^4.2.1",
+ "espree": "^10.4.0",
+ "esquery": "^1.5.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-google": {
+ "version": "0.14.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz",
+ "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "peerDependencies": {
+ "eslint": ">=5.16.0"
+ }
+ },
+ "node_modules/eslint-plugin-ava": {
+ "version": "15.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-ava/-/eslint-plugin-ava-15.1.0.tgz",
+ "integrity": "sha512-+6Zxk1uYW3mf7lxCLWIQsFYgn3hfuCMbsKc0MtqfloOz1F6fiV5/PaWEaLgkL1egrSQmnyR7vOFP1wSPJbVUbw==",
+ "dev": true,
+ "dependencies": {
+ "enhance-visitors": "^1.0.0",
+ "eslint-utils": "^3.0.0",
+ "espree": "^9.0.0",
+ "espurify": "^2.1.1",
+ "import-modules": "^2.1.0",
+ "micro-spelling-correcter": "^1.1.1",
+ "pkg-dir": "^5.0.0",
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18 || >=20"
+ },
+ "peerDependencies": {
+ "eslint": ">=9"
+ }
+ },
+ "node_modules/eslint-plugin-ava/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-plugin-ava/node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-plugin-jsdoc": {
+ "version": "52.0.4",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-52.0.4.tgz",
+ "integrity": "sha512-be5OzGlLExvcK13Il3noU7/v7WmAQGenTmCaBKf1pwVtPOb6X+PGFVnJad0QhMj4KKf45XjE4hbsBxv25q1fTg==",
+ "dev": true,
+ "dependencies": {
+ "@es-joy/jsdoccomment": "~0.52.0",
+ "are-docs-informative": "^0.0.2",
+ "comment-parser": "1.4.1",
+ "debug": "^4.4.1",
+ "escape-string-regexp": "^4.0.0",
+ "espree": "^10.4.0",
+ "esquery": "^1.6.0",
+ "parse-imports-exports": "^0.2.4",
+ "semver": "^7.7.2",
+ "spdx-expression-parse": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=20.11.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-utils": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz",
+ "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^2.0.0"
+ },
+ "engines": {
+ "node": "^10.0.0 || ^12.0.0 || >= 14.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=5"
+ }
+ },
+ "node_modules/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz",
+ "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/eslint/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/eslint/node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/eslint/node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/eslint/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint/node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/esmock": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/esmock/-/esmock-2.7.3.tgz",
+ "integrity": "sha512-/M/YZOjgyLaVoY6K83pwCsGE1AJQnj4S4GyXLYgi/Y79KL8EeW6WU7Rmjc89UO7jv6ec8+j34rKeWOfiLeEu0A==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.16.0"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true,
+ "bin": {
+ "esparse": "bin/esparse.js",
+ "esvalidate": "bin/esvalidate.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/espurify": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/espurify/-/espurify-2.1.1.tgz",
+ "integrity": "sha512-zttWvnkhcDyGOhSH4vO2qCBILpdCMv/MX8lp4cqgRkQoDRGK2oZxi2GfWhlP2dIXmk7BaKeOTuzbHhyC68o8XQ==",
+ "dev": true
+ },
+ "node_modules/esquery": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+ "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "dev": true
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/events-to-array": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/events-to-array/-/events-to-array-1.1.2.tgz",
+ "integrity": "sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==",
+ "dev": true
+ },
+ "node_modules/expand-tilde": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz",
+ "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==",
+ "dev": true,
+ "dependencies": {
+ "homedir-polyfill": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/exponential-backoff": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.2.tgz",
+ "integrity": "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA=="
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
+ },
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "dev": true
+ },
+ "node_modules/fast-glob": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+ "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+ "dependencies": {
+ "@nodelib/fs.stat": "^2.0.2",
+ "@nodelib/fs.walk": "^1.2.3",
+ "glob-parent": "^5.1.2",
+ "merge2": "^1.3.0",
+ "micromatch": "^4.0.8"
+ },
+ "engines": {
+ "node": ">=8.6.0"
+ }
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true
+ },
+ "node_modules/fastq": {
+ "version": "1.19.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+ "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/figures": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz",
+ "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==",
+ "dependencies": {
+ "is-unicode-supported": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/file-type": {
+ "version": "18.7.0",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-18.7.0.tgz",
+ "integrity": "sha512-ihHtXRzXEziMrQ56VSgU7wkxh55iNchFkosu7Y9/S+tXHdKyrGjVK0ujbqNnsxzea+78MaLhN6PGmfYSAv1ACw==",
+ "dev": true,
+ "dependencies": {
+ "readable-web-to-node-stream": "^3.0.2",
+ "strtok3": "^7.0.0",
+ "token-types": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/file-type?sponsor=1"
+ }
+ },
+ "node_modules/file-uri-to-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+ "dev": true
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-cache-dir": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
+ "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+ "dev": true,
+ "dependencies": {
+ "commondir": "^1.0.1",
+ "make-dir": "^3.0.2",
+ "pkg-dir": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+ }
+ },
+ "node_modules/find-cache-dir/node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/find-cache-dir/node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-cache-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up-simple": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz",
+ "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/findup-sync": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz",
+ "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==",
+ "dev": true,
+ "dependencies": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^4.0.3",
+ "micromatch": "^4.0.4",
+ "resolve-dir": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 10.13.0"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+ "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+ "dev": true
+ },
+ "node_modules/foreground-child": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+ "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
+ "dependencies": {
+ "cross-spawn": "^7.0.6",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/fromentries": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz",
+ "integrity": "sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/fs-minipass": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz",
+ "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
+ "integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-package-type": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz",
+ "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/get-stdin": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz",
+ "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/glob": {
+ "version": "10.4.5",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+ "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+ "dependencies": {
+ "foreground-child": "^3.1.0",
+ "jackspeak": "^3.1.2",
+ "minimatch": "^9.0.4",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^1.11.1"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/global-modules": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz",
+ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==",
+ "dev": true,
+ "dependencies": {
+ "global-prefix": "^1.0.1",
+ "is-windows": "^1.0.1",
+ "resolve-dir": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz",
+ "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.2",
+ "homedir-polyfill": "^1.0.1",
+ "ini": "^1.3.4",
+ "is-windows": "^1.0.1",
+ "which": "^1.2.14"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/global-prefix/node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true
+ },
+ "node_modules/global-prefix/node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/global-prefix/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/globals": {
+ "version": "16.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz",
+ "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-14.1.0.tgz",
+ "integrity": "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==",
+ "dependencies": {
+ "@sindresorhus/merge-streams": "^2.1.0",
+ "fast-glob": "^3.3.3",
+ "ignore": "^7.0.3",
+ "path-type": "^6.0.0",
+ "slash": "^5.1.0",
+ "unicorn-magic": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globby/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/globby/node_modules/path-type": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz",
+ "integrity": "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hasha": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz",
+ "integrity": "sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==",
+ "dev": true,
+ "dependencies": {
+ "is-stream": "^2.0.0",
+ "type-fest": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/homedir-polyfill": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
+ "dev": true,
+ "dependencies": {
+ "parse-passwd": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/hosted-git-info": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz",
+ "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==",
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/hosted-git-info/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true
+ },
+ "node_modules/http-cache-semantics": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
+ "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "optional": true,
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/ignore-by-default": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-2.1.0.tgz",
+ "integrity": "sha512-yiWd4GVmJp0Q6ghmM2B/V3oZGRmjrKLXvHR3TE1nfoXsmoggllfZUQe74EN0fJdPFZu2NIvNdrMMLm3OsV7Ohw==",
+ "dev": true,
+ "engines": {
+ "node": ">=10 <11 || >=12 <13 || >=14"
+ }
+ },
+ "node_modules/ignore-walk": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz",
+ "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==",
+ "dependencies": {
+ "minimatch": "^9.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/import-fresh/node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/import-modules": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/import-modules/-/import-modules-2.1.0.tgz",
+ "integrity": "sha512-8HEWcnkbGpovH9yInoisxaSoIg9Brbul+Ju3Kqe2UsYDUBJD/iQjSgEj0zPcTDPKfPp2fs5xlv1i+JSye/m1/A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz",
+ "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/index-to-position": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz",
+ "integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "node_modules/ini": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz",
+ "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/ip-address": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
+ "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/irregular-plurals": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz",
+ "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+ "dev": true
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz",
+ "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==",
+ "dev": true,
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz",
+ "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-inside-container": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz",
+ "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==",
+ "dev": true,
+ "dependencies": {
+ "is-docker": "^3.0.0"
+ },
+ "bin": {
+ "is-inside-container": "cli.js"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-object": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
+ "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-promise": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+ "dev": true
+ },
+ "node_modules/is-stream": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+ "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
+ "dev": true
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
+ "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-windows": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
+ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-wsl": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
+ "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==",
+ "dev": true,
+ "dependencies": {
+ "is-inside-container": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "dev": true
+ },
+ "node_modules/isexe": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
+ "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-hook": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-3.0.0.tgz",
+ "integrity": "sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==",
+ "dev": true,
+ "dependencies": {
+ "append-transform": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-instrument": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
+ "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
+ "dev": true,
+ "dependencies": {
+ "@babel/core": "^7.23.9",
+ "@babel/parser": "^7.23.9",
+ "@istanbuljs/schema": "^0.1.3",
+ "istanbul-lib-coverage": "^3.2.0",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-processinfo": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz",
+ "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==",
+ "dev": true,
+ "dependencies": {
+ "archy": "^1.0.0",
+ "cross-spawn": "^7.0.3",
+ "istanbul-lib-coverage": "^3.2.0",
+ "p-map": "^3.0.0",
+ "rimraf": "^3.0.0",
+ "uuid": "^8.3.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-processinfo/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/istanbul-lib-processinfo/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/istanbul-lib-processinfo/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/istanbul-lib-processinfo/node_modules/p-map": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
+ "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
+ "dev": true,
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-processinfo/node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-lib-source-maps": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz",
+ "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^4.1.1",
+ "istanbul-lib-coverage": "^3.0.0",
+ "source-map": "^0.6.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jackspeak": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+ "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ },
+ "optionalDependencies": {
+ "@pkgjs/parseargs": "^0.11.0"
+ }
+ },
+ "node_modules/js-beautify": {
+ "version": "1.15.4",
+ "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
+ "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
+ "dev": true,
+ "dependencies": {
+ "config-chain": "^1.1.13",
+ "editorconfig": "^1.0.4",
+ "glob": "^10.4.2",
+ "js-cookie": "^3.0.5",
+ "nopt": "^7.2.1"
+ },
+ "bin": {
+ "css-beautify": "js/bin/css-beautify.js",
+ "html-beautify": "js/bin/html-beautify.js",
+ "js-beautify": "js/bin/js-beautify.js"
+ },
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-beautify/node_modules/abbrev": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
+ "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
+ "dev": true,
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/js-beautify/node_modules/nopt": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
+ "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
+ "dev": true,
+ "dependencies": {
+ "abbrev": "^2.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/js-string-escape": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz",
+ "integrity": "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+ "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/js2xmlparser": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz",
+ "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==",
+ "dev": true,
+ "dependencies": {
+ "xmlcreate": "^2.0.4"
+ }
+ },
+ "node_modules/jsdoc": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-4.0.4.tgz",
+ "integrity": "sha512-zeFezwyXeG4syyYHbvh1A967IAqq/67yXtXvuL5wnqCkFZe8I0vKfm+EO+YEvLguo6w9CDUbrAXVtJSHh2E8rw==",
+ "dev": true,
+ "dependencies": {
+ "@babel/parser": "^7.20.15",
+ "@jsdoc/salty": "^0.2.1",
+ "@types/markdown-it": "^14.1.1",
+ "bluebird": "^3.7.2",
+ "catharsis": "^0.9.0",
+ "escape-string-regexp": "^2.0.0",
+ "js2xmlparser": "^4.0.2",
+ "klaw": "^3.0.0",
+ "markdown-it": "^14.1.0",
+ "markdown-it-anchor": "^8.6.7",
+ "marked": "^4.0.10",
+ "mkdirp": "^1.0.4",
+ "requizzle": "^0.2.3",
+ "strip-json-comments": "^3.1.0",
+ "underscore": "~1.13.2"
+ },
+ "bin": {
+ "jsdoc": "jsdoc.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/jsdoc-type-pratt-parser": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.1.0.tgz",
+ "integrity": "sha512-Hicd6JK5Njt2QB6XYFS7ok9e37O8AYk3jTcppG4YVQnYjOemymvTcmc7OWsmq/Qqj5TdRFO5/x/tIPmBeRtGHg==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/jsdoc/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "node_modules/json-parse-even-better-errors": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz",
+ "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
+ "engines": [
+ "node >= 0.2.0"
+ ]
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/klaw": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz",
+ "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.9"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lines-and-columns": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
+ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
+ "dev": true
+ },
+ "node_modules/linkify-it": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz",
+ "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==",
+ "dev": true,
+ "dependencies": {
+ "uc.micro": "^2.0.0"
+ }
+ },
+ "node_modules/load-json-file": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-7.0.1.tgz",
+ "integrity": "sha512-Gnxj3ev3mB5TkVBGad0JM6dmLiQL+o0t23JPBZ9sd+yvSLk05mFoqKBw5N8gbbkU4TNXyqCgIrl/VM17OgUIgQ==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/lockfile": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/lockfile/-/lockfile-1.0.4.tgz",
+ "integrity": "sha512-cvbTwETRfsFh4nHsL1eGWapU1XFi5Ot9E85sWAwia7Y7EgB7vfqcZhTKZ+l7hCGxSPoushMv5GKhT5PdLv03WA==",
+ "dependencies": {
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "node_modules/lockfile/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "dev": true
+ },
+ "node_modules/lodash.debounce": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+ "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
+ "dev": true
+ },
+ "node_modules/lodash.flattendeep": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz",
+ "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==",
+ "dev": true
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true
+ },
+ "node_modules/lodash.throttle": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz",
+ "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==",
+ "dev": true
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.19",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
+ "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-fetch-happen": {
+ "version": "14.0.3",
+ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz",
+ "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==",
+ "dependencies": {
+ "@npmcli/agent": "^3.0.0",
+ "cacache": "^19.0.1",
+ "http-cache-semantics": "^4.1.1",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^4.0.0",
+ "minipass-flush": "^1.0.5",
+ "minipass-pipeline": "^1.2.4",
+ "negotiator": "^1.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "ssri": "^12.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/markdown-it": {
+ "version": "14.1.0",
+ "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz",
+ "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^2.0.1",
+ "entities": "^4.4.0",
+ "linkify-it": "^5.0.0",
+ "mdurl": "^2.0.0",
+ "punycode.js": "^2.3.1",
+ "uc.micro": "^2.1.0"
+ },
+ "bin": {
+ "markdown-it": "bin/markdown-it.mjs"
+ }
+ },
+ "node_modules/markdown-it-anchor": {
+ "version": "8.6.7",
+ "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.7.tgz",
+ "integrity": "sha512-FlCHFwNnutLgVTflOYHPW2pPcl2AACqVzExlkGQNsi4CJgqOHN7YTgDd4LuhgN1BFO3TS0vLAruV1Td6dwWPJA==",
+ "dev": true,
+ "peerDependencies": {
+ "@types/markdown-it": "*",
+ "markdown-it": "*"
+ }
+ },
+ "node_modules/marked": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz",
+ "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==",
+ "dev": true,
+ "bin": {
+ "marked": "bin/marked.js"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/matcher": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/matcher/-/matcher-5.0.0.tgz",
+ "integrity": "sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^5.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/md5-hex": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/md5-hex/-/md5-hex-3.0.1.tgz",
+ "integrity": "sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==",
+ "dev": true,
+ "dependencies": {
+ "blueimp-md5": "^2.10.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/mdurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",
+ "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==",
+ "dev": true
+ },
+ "node_modules/memoize": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.1.0.tgz",
+ "integrity": "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==",
+ "dev": true,
+ "dependencies": {
+ "mimic-function": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/memoize?sponsor=1"
+ }
+ },
+ "node_modules/meow": {
+ "version": "12.1.1",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz",
+ "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==",
+ "dev": true,
+ "engines": {
+ "node": ">=16.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/merge2": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+ "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/micro-spelling-correcter": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/micro-spelling-correcter/-/micro-spelling-correcter-1.1.1.tgz",
+ "integrity": "sha512-lkJ3Rj/mtjlRcHk6YyCbvZhyWTOzdBvTHsxMmZSk5jxN1YyVSQ+JETAom55mdzfcyDrY/49Z7UCW760BK30crg==",
+ "dev": true
+ },
+ "node_modules/micromatch": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+ "dependencies": {
+ "braces": "^3.0.3",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "node_modules/micromatch/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/mimic-function": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz",
+ "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "9.0.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+ "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+ "dependencies": {
+ "brace-expansion": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/minipass": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+ "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minipass-collect": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz",
+ "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.17"
+ }
+ },
+ "node_modules/minipass-fetch": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz",
+ "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==",
+ "dependencies": {
+ "minipass": "^7.0.3",
+ "minipass-sized": "^1.0.3",
+ "minizlib": "^3.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ },
+ "optionalDependencies": {
+ "encoding": "^0.1.13"
+ }
+ },
+ "node_modules/minipass-flush": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz",
+ "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/minipass-flush/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-flush/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/minipass-pipeline": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz",
+ "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-pipeline/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/minipass-sized": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz",
+ "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/minipass-sized/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/minizlib": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz",
+ "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==",
+ "dependencies": {
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
+ "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
+ "bin": {
+ "mkdirp": "bin/cmd.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/multimatch": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz",
+ "integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==",
+ "dev": true,
+ "dependencies": {
+ "@types/minimatch": "^3.0.3",
+ "array-differ": "^3.0.0",
+ "array-union": "^2.1.0",
+ "arrify": "^2.0.1",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/multimatch/node_modules/arrify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
+ "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/multimatch/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/multimatch/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "node_modules/negotiator": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "dev": true,
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/node-gyp": {
+ "version": "11.4.2",
+ "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.4.2.tgz",
+ "integrity": "sha512-3gD+6zsrLQH7DyYOUIutaauuXrcyxeTPyQuZQCQoNPZMHMMS5m4y0xclNpvYzoK3VNzuyxT6eF4mkIL4WSZ1eQ==",
+ "dependencies": {
+ "env-paths": "^2.2.0",
+ "exponential-backoff": "^3.1.1",
+ "graceful-fs": "^4.2.6",
+ "make-fetch-happen": "^14.0.3",
+ "nopt": "^8.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "tar": "^7.4.3",
+ "tinyglobby": "^0.2.12",
+ "which": "^5.0.0"
+ },
+ "bin": {
+ "node-gyp": "bin/node-gyp.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/node-gyp-build": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
+ "dev": true,
+ "bin": {
+ "node-gyp-build": "bin.js",
+ "node-gyp-build-optional": "optional.js",
+ "node-gyp-build-test": "build-test.js"
+ }
+ },
+ "node_modules/node-preload": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz",
+ "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==",
+ "dev": true,
+ "dependencies": {
+ "process-on-spawn": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.21",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz",
+ "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==",
+ "dev": true
+ },
+ "node_modules/node-stream-zip": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz",
+ "integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw==",
+ "engines": {
+ "node": ">=0.12.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/antelle"
+ }
+ },
+ "node_modules/nofilter": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz",
+ "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.19"
+ }
+ },
+ "node_modules/nopt": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz",
+ "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==",
+ "dependencies": {
+ "abbrev": "^3.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/normalize-package-data": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz",
+ "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==",
+ "dependencies": {
+ "hosted-git-info": "^7.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-license": "^3.0.4"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/normalize-package-data/node_modules/hosted-git-info": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
+ "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
+ "dependencies": {
+ "lru-cache": "^10.0.1"
+ },
+ "engines": {
+ "node": "^16.14.0 || >=18.0.0"
+ }
+ },
+ "node_modules/normalize-package-data/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/npm-bundled": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz",
+ "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==",
+ "dependencies": {
+ "npm-normalize-package-bin": "^4.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-install-checks": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.2.tgz",
+ "integrity": "sha512-z9HJBCYw9Zr8BqXcllKIs5nI+QggAImbBdHphOzVYrz2CB4iQ6FzWyKmlqDZua+51nAu7FcemlbTc9VgQN5XDQ==",
+ "dependencies": {
+ "semver": "^7.1.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-normalize-package-bin": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz",
+ "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-package-arg": {
+ "version": "12.0.2",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz",
+ "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==",
+ "dependencies": {
+ "hosted-git-info": "^8.0.0",
+ "proc-log": "^5.0.0",
+ "semver": "^7.3.5",
+ "validate-npm-package-name": "^6.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-packlist": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz",
+ "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==",
+ "dependencies": {
+ "ignore-walk": "^7.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-pick-manifest": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz",
+ "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==",
+ "dependencies": {
+ "npm-install-checks": "^7.1.0",
+ "npm-normalize-package-bin": "^4.0.0",
+ "npm-package-arg": "^12.0.0",
+ "semver": "^7.3.5"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/npm-registry-fetch": {
+ "version": "18.0.2",
+ "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz",
+ "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==",
+ "dependencies": {
+ "@npmcli/redact": "^3.0.0",
+ "jsonparse": "^1.3.1",
+ "make-fetch-happen": "^14.0.0",
+ "minipass": "^7.0.2",
+ "minipass-fetch": "^4.0.0",
+ "minizlib": "^3.0.1",
+ "npm-package-arg": "^12.0.0",
+ "proc-log": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/nyc": {
+ "version": "17.1.0",
+ "resolved": "https://registry.npmjs.org/nyc/-/nyc-17.1.0.tgz",
+ "integrity": "sha512-U42vQ4czpKa0QdI1hu950XuNhYqgoM+ZF1HT+VuUHL9hPfDPVvNQyltmMqdE9bUHMVa+8yNbc3QKTj8zQhlVxQ==",
+ "dev": true,
+ "dependencies": {
+ "@istanbuljs/load-nyc-config": "^1.0.0",
+ "@istanbuljs/schema": "^0.1.2",
+ "caching-transform": "^4.0.0",
+ "convert-source-map": "^1.7.0",
+ "decamelize": "^1.2.0",
+ "find-cache-dir": "^3.2.0",
+ "find-up": "^4.1.0",
+ "foreground-child": "^3.3.0",
+ "get-package-type": "^0.1.0",
+ "glob": "^7.1.6",
+ "istanbul-lib-coverage": "^3.0.0",
+ "istanbul-lib-hook": "^3.0.0",
+ "istanbul-lib-instrument": "^6.0.2",
+ "istanbul-lib-processinfo": "^2.0.2",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-lib-source-maps": "^4.0.0",
+ "istanbul-reports": "^3.0.2",
+ "make-dir": "^3.0.0",
+ "node-preload": "^0.2.1",
+ "p-map": "^3.0.0",
+ "process-on-spawn": "^1.0.0",
+ "resolve-from": "^5.0.0",
+ "rimraf": "^3.0.0",
+ "signal-exit": "^3.0.2",
+ "spawn-wrap": "^2.0.0",
+ "test-exclude": "^6.0.0",
+ "yargs": "^15.0.2"
+ },
+ "bin": {
+ "nyc": "bin/nyc.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/nyc/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/nyc/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/nyc/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/nyc/node_modules/cliui": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+ "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+ "dev": true,
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^6.2.0"
+ }
+ },
+ "node_modules/nyc/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/nyc/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
+ "node_modules/nyc/node_modules/convert-source-map": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
+ "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
+ "dev": true
+ },
+ "node_modules/nyc/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/nyc/node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/nyc/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/nyc/node_modules/p-map": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz",
+ "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==",
+ "dev": true,
+ "dependencies": {
+ "aggregate-error": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/nyc/node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/nyc/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/nyc/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/nyc/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/nyc/node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/nyc/node_modules/y18n": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz",
+ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==",
+ "dev": true
+ },
+ "node_modules/nyc/node_modules/yargs": {
+ "version": "15.4.1",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz",
+ "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^6.0.0",
+ "decamelize": "^1.2.0",
+ "find-up": "^4.1.0",
+ "get-caller-file": "^2.0.1",
+ "require-directory": "^2.1.1",
+ "require-main-filename": "^2.0.0",
+ "set-blocking": "^2.0.0",
+ "string-width": "^4.2.0",
+ "which-module": "^2.0.0",
+ "y18n": "^4.0.0",
+ "yargs-parser": "^18.1.2"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/nyc/node_modules/yargs-parser": {
+ "version": "18.1.3",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+ "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+ "dev": true,
+ "dependencies": {
+ "camelcase": "^5.0.0",
+ "decamelize": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/open": {
+ "version": "10.2.0",
+ "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz",
+ "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==",
+ "dev": true,
+ "dependencies": {
+ "default-browser": "^5.2.1",
+ "define-lazy-prop": "^3.0.0",
+ "is-inside-container": "^1.0.0",
+ "wsl-utils": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/open-cli": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/open-cli/-/open-cli-8.0.0.tgz",
+ "integrity": "sha512-3muD3BbfLyzl+aMVSEfn2FfOqGdPYR0O4KNnxXsLEPE2q9OSjBfJAaB6XKbrUzLgymoSMejvb5jpXJfru/Ko2A==",
+ "dev": true,
+ "dependencies": {
+ "file-type": "^18.7.0",
+ "get-stdin": "^9.0.0",
+ "meow": "^12.1.1",
+ "open": "^10.0.0",
+ "tempy": "^3.1.0"
+ },
+ "bin": {
+ "open-cli": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-map": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz",
+ "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/package-config": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/package-config/-/package-config-5.0.0.tgz",
+ "integrity": "sha512-GYTTew2slBcYdvRHqjhwaaydVMvn/qrGC323+nKclYioNSLTDUM/lGgtGTgyHVtYcozb+XkE8CNhwcraOmZ9Mg==",
+ "dev": true,
+ "dependencies": {
+ "find-up-simple": "^1.0.0",
+ "load-json-file": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/package-hash": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz",
+ "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.1.15",
+ "hasha": "^5.0.0",
+ "lodash.flattendeep": "^4.4.0",
+ "release-zalgo": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/package-json-from-dist": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
+ },
+ "node_modules/pacote": {
+ "version": "19.0.1",
+ "resolved": "https://registry.npmjs.org/pacote/-/pacote-19.0.1.tgz",
+ "integrity": "sha512-zIpxWAsr/BvhrkSruspG8aqCQUUrWtpwx0GjiRZQhEM/pZXrigA32ElN3vTcCPUDOFmHr6SFxwYrvVUs5NTEUg==",
+ "dependencies": {
+ "@npmcli/git": "^6.0.0",
+ "@npmcli/installed-package-contents": "^3.0.0",
+ "@npmcli/package-json": "^6.0.0",
+ "@npmcli/promise-spawn": "^8.0.0",
+ "@npmcli/run-script": "^9.0.0",
+ "cacache": "^19.0.0",
+ "fs-minipass": "^3.0.0",
+ "minipass": "^7.0.2",
+ "npm-package-arg": "^12.0.0",
+ "npm-packlist": "^9.0.0",
+ "npm-pick-manifest": "^10.0.0",
+ "npm-registry-fetch": "^18.0.0",
+ "proc-log": "^5.0.0",
+ "promise-retry": "^2.0.1",
+ "sigstore": "^3.0.0",
+ "ssri": "^12.0.0",
+ "tar": "^6.1.11"
+ },
+ "bin": {
+ "pacote": "bin/index.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/pacote/node_modules/chownr": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
+ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pacote/node_modules/minizlib": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz",
+ "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==",
+ "dependencies": {
+ "minipass": "^3.0.0",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/pacote/node_modules/minizlib/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pacote/node_modules/tar": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
+ "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
+ "dependencies": {
+ "chownr": "^2.0.0",
+ "fs-minipass": "^2.0.0",
+ "minipass": "^5.0.0",
+ "minizlib": "^2.1.1",
+ "mkdirp": "^1.0.3",
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pacote/node_modules/tar/node_modules/fs-minipass": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz",
+ "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==",
+ "dependencies": {
+ "minipass": "^3.0.0"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/pacote/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": {
+ "version": "3.3.6",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz",
+ "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==",
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pacote/node_modules/tar/node_modules/minipass": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz",
+ "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pacote/node_modules/yallist": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
+ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parent-module/node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse-imports-exports": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz",
+ "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==",
+ "dev": true,
+ "dependencies": {
+ "parse-statements": "1.0.11"
+ }
+ },
+ "node_modules/parse-json": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
+ "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
+ "dev": true,
+ "dependencies": {
+ "@babel/code-frame": "^7.0.0",
+ "error-ex": "^1.3.1",
+ "json-parse-even-better-errors": "^2.3.0",
+ "lines-and-columns": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-json/node_modules/json-parse-even-better-errors": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
+ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
+ "dev": true
+ },
+ "node_modules/parse-ms": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz",
+ "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parse-passwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz",
+ "integrity": "sha512-1Y1A//QUXEZK7YKz+rD9WydcE1+EuPr6ZBgKecAB8tmoW6UFv0NREVJe1p+jRxtThkcbbKkfwIbWJe/IeE6m2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/parse-statements": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz",
+ "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==",
+ "dev": true
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
+ },
+ "node_modules/path-scurry": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+ "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "dependencies": {
+ "lru-cache": "^10.2.0",
+ "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ },
+ "engines": {
+ "node": ">=16 || 14 >=14.18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/path-scurry/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="
+ },
+ "node_modules/path-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/peek-readable": {
+ "version": "5.4.2",
+ "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-5.4.2.tgz",
+ "integrity": "sha512-peBp3qZyuS6cNIJ2akRNG1uo1WJ1d0wTxg/fxMdZ0BqCVhx242bSFHM9eNqflfJVS9SsgkzgT/1UgnsurBOTMg==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
+ "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==",
+ "dev": true,
+ "dependencies": {
+ "find-up": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pkg-dir/node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/please-upgrade-node": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
+ "integrity": "sha512-gQR3WpIgNIKwBMVLkpMUeR3e1/E1y42bqDQZfql+kDeXd8COYfM8PQA4X6y7a8u9Ua9FHmsrrmirW2vHs45hWg==",
+ "dev": true,
+ "dependencies": {
+ "semver-compare": "^1.0.0"
+ }
+ },
+ "node_modules/plur": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz",
+ "integrity": "sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==",
+ "dev": true,
+ "dependencies": {
+ "irregular-plurals": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/pretty-hrtime": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz",
+ "integrity": "sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/pretty-ms": {
+ "version": "9.3.0",
+ "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz",
+ "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==",
+ "dev": true,
+ "dependencies": {
+ "parse-ms": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/proc-log": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz",
+ "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "dev": true
+ },
+ "node_modules/process-on-spawn": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.1.0.tgz",
+ "integrity": "sha512-JOnOPQ/8TZgjs1JIH/m9ni7FfimjNa/PRx7y/Wb5qdItsnhO0jE4AT7fC0HjC28DUQWDr50dwSYZLdRMlqDq3Q==",
+ "dev": true,
+ "dependencies": {
+ "fromentries": "^1.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/promise-retry": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz",
+ "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==",
+ "dependencies": {
+ "err-code": "^2.0.2",
+ "retry": "^0.12.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/proto-list": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
+ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
+ "dev": true
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/punycode.js": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
+ "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/queue-microtask": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/random-int": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/random-int/-/random-int-3.1.0.tgz",
+ "integrity": "sha512-h8CRz8cpvzj0hC/iH/1Gapgcl2TQ6xtnCpyOI5WvWfXf/yrDx2DOU+tD9rX23j36IF11xg1KqB9W11Z18JPMdw==",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-package-up": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz",
+ "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==",
+ "dependencies": {
+ "find-up-simple": "^1.0.0",
+ "read-pkg": "^9.0.0",
+ "type-fest": "^4.6.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-package-up/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz",
+ "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==",
+ "dependencies": {
+ "@types/normalize-package-data": "^2.4.3",
+ "normalize-package-data": "^6.0.0",
+ "parse-json": "^8.0.0",
+ "type-fest": "^4.6.0",
+ "unicorn-magic": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg/node_modules/parse-json": {
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
+ "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
+ "dependencies": {
+ "@babel/code-frame": "^7.26.2",
+ "index-to-position": "^1.1.0",
+ "type-fest": "^4.39.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg/node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/read-pkg/node_modules/unicorn-magic": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
+ "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "dev": true,
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/readable-web-to-node-stream": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
+ "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==",
+ "dev": true,
+ "dependencies": {
+ "readable-stream": "^4.7.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.6.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
+ "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
+ "dev": true,
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/readdirp/node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/release-zalgo": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz",
+ "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==",
+ "dev": true,
+ "dependencies": {
+ "es6-error": "^4.0.1"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-main-filename": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+ "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
+ "dev": true
+ },
+ "node_modules/require-package-name": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/require-package-name/-/require-package-name-2.0.1.tgz",
+ "integrity": "sha512-uuoJ1hU/k6M0779t3VMVIYpb2VMJk05cehCaABFhXaibcbvfgR8wKiozLjVFSzJPmQMRqIcO0HMyTFqfV09V6Q==",
+ "dev": true
+ },
+ "node_modules/requizzle": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.4.tgz",
+ "integrity": "sha512-JRrFk1D4OQ4SqovXOgdav+K8EAhSB/LJZqCz8tbX0KObcdeM15Ss59ozWMBWmmINMagCwmqn4ZNryUGpBsl6Jw==",
+ "dev": true,
+ "dependencies": {
+ "lodash": "^4.17.21"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.10",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
+ "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
+ "dependencies": {
+ "is-core-module": "^2.16.0",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/resolve-cwd": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
+ "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
+ "dev": true,
+ "dependencies": {
+ "resolve-from": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resolve-dir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz",
+ "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==",
+ "dev": true,
+ "dependencies": {
+ "expand-tilde": "^2.0.0",
+ "global-modules": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/retry": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz",
+ "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/reusify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+ "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+ "engines": {
+ "iojs": ">=1.0.0",
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
+ "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^11.0.0",
+ "package-json-from-dist": "^1.0.0"
+ },
+ "bin": {
+ "rimraf": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/glob": {
+ "version": "11.0.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
+ "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^3.3.1",
+ "jackspeak": "^4.1.1",
+ "minimatch": "^10.0.3",
+ "minipass": "^7.1.2",
+ "package-json-from-dist": "^1.0.0",
+ "path-scurry": "^2.0.0"
+ },
+ "bin": {
+ "glob": "dist/esm/bin.mjs"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/jackspeak": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
+ "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
+ "dev": true,
+ "dependencies": {
+ "@isaacs/cliui": "^8.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/lru-cache": {
+ "version": "11.2.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.1.tgz",
+ "integrity": "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ==",
+ "dev": true,
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/rimraf/node_modules/minimatch": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
+ "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
+ "dev": true,
+ "dependencies": {
+ "@isaacs/brace-expansion": "^5.0.0"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/rimraf/node_modules/path-scurry": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
+ "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
+ "dev": true,
+ "dependencies": {
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/run-applescript": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz",
+ "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/run-parallel": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+ "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "queue-microtask": "^1.2.2"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "optional": true
+ },
+ "node_modules/sax": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz",
+ "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
+ },
+ "node_modules/semver": {
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/semver-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
+ "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==",
+ "dev": true
+ },
+ "node_modules/serialize-error": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-7.0.1.tgz",
+ "integrity": "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.13.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/serialize-error/node_modules/type-fest": {
+ "version": "0.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
+ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/set-blocking": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
+ "dev": true
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/sigstore": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz",
+ "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==",
+ "dependencies": {
+ "@sigstore/bundle": "^3.1.0",
+ "@sigstore/core": "^2.0.0",
+ "@sigstore/protobuf-specs": "^0.4.0",
+ "@sigstore/sign": "^3.1.0",
+ "@sigstore/tuf": "^3.1.0",
+ "@sigstore/verify": "^2.1.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/sinon": {
+ "version": "21.0.0",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-21.0.0.tgz",
+ "integrity": "sha512-TOgRcwFPbfGtpqvZw+hyqJDvqfapr1qUlOizROIk4bBLjlsjlB00Pg6wMFXNtJRpu+eCZuVOaLatG7M8105kAw==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.1",
+ "@sinonjs/fake-timers": "^13.0.5",
+ "@sinonjs/samsam": "^8.0.1",
+ "diff": "^7.0.0",
+ "supports-color": "^7.2.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/sinon"
+ }
+ },
+ "node_modules/slash": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
+ "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz",
+ "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^6.0.0",
+ "is-fullwidth-code-point": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/smart-buffer": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz",
+ "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==",
+ "engines": {
+ "node": ">= 6.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks": {
+ "version": "2.8.7",
+ "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
+ "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==",
+ "dependencies": {
+ "ip-address": "^10.0.1",
+ "smart-buffer": "^4.2.0"
+ },
+ "engines": {
+ "node": ">= 10.0.0",
+ "npm": ">= 3.0.0"
+ }
+ },
+ "node_modules/socks-proxy-agent": {
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz",
+ "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "^4.3.4",
+ "socks": "^2.8.3"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/spawn-wrap": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz",
+ "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==",
+ "dev": true,
+ "dependencies": {
+ "foreground-child": "^2.0.0",
+ "is-windows": "^1.0.2",
+ "make-dir": "^3.0.0",
+ "rimraf": "^3.0.0",
+ "signal-exit": "^3.0.2",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/spawn-wrap/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/spawn-wrap/node_modules/foreground-child": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
+ "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/spawn-wrap/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/spawn-wrap/node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true
+ },
+ "node_modules/spawn-wrap/node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/spawn-wrap/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/spawn-wrap/node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "deprecated": "Rimraf versions prior to v4 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/spawn-wrap/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/spawn-wrap/node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/spawn-wrap/node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/spdx-correct": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
+ "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
+ "dependencies": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-correct/node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-exceptions": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
+ "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="
+ },
+ "node_modules/spdx-expression-parse": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz",
+ "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==",
+ "dev": true,
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/spdx-license-ids": {
+ "version": "3.0.22",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.22.tgz",
+ "integrity": "sha512-4PRT4nh1EImPbt2jASOKHX7PB7I+e4IWNLvkKFDxNhJlfjbYlleYQh285Z/3mPTHSAK/AvdMmw5BNNuYH8ShgQ=="
+ },
+ "node_modules/sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+ "dev": true
+ },
+ "node_modules/ssri": {
+ "version": "12.0.0",
+ "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz",
+ "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==",
+ "dependencies": {
+ "minipass": "^7.0.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/stack-utils": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz",
+ "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==",
+ "dev": true,
+ "dependencies": {
+ "escape-string-regexp": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/stack-utils/node_modules/escape-string-regexp": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz",
+ "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs": {
+ "name": "string-width",
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/string-width/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
+ "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
+ "dependencies": {
+ "ansi-regex": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/strip-ansi-cjs": {
+ "name": "strip-ansi",
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz",
+ "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/strtok3": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-7.1.1.tgz",
+ "integrity": "sha512-mKX8HA/cdBqMKUr0MMZAFssCkIGoZeSCMXgnt79yKxNFguMLVFgRe6wB+fsL0NmoHDbeyZXczy7vEPSoo3rkzg==",
+ "dev": true,
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0",
+ "peek-readable": "^5.1.3"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/supertap": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/supertap/-/supertap-3.0.1.tgz",
+ "integrity": "sha512-u1ZpIBCawJnO+0QePsEiOknOfCRq0yERxiAchT0i4li0WHNUJbf0evXXSXOcCAR4M8iMDoajXYmstm/qO81Isw==",
+ "dev": true,
+ "dependencies": {
+ "indent-string": "^5.0.0",
+ "js-yaml": "^3.14.1",
+ "serialize-error": "^7.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/supertap/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/supertap/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/tap-parser": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tap-parser/-/tap-parser-1.2.2.tgz",
+ "integrity": "sha512-uXKcosa0qoSjeh73dhmX+OpJvpigDxUciOhBcbGUKtmwzEFJjUT1Ql5dpg4M9I1UjXT9b+6n1W05FB8QmKossA==",
+ "dev": true,
+ "dependencies": {
+ "events-to-array": "^1.0.1",
+ "inherits": "~2.0.1",
+ "js-yaml": "^3.2.7"
+ },
+ "bin": {
+ "tap-parser": "bin/cmd.js"
+ },
+ "optionalDependencies": {
+ "readable-stream": "^2"
+ }
+ },
+ "node_modules/tap-parser/node_modules/argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "dependencies": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "node_modules/tap-parser/node_modules/js-yaml": {
+ "version": "3.14.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
+ "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
+ "dev": true,
+ "dependencies": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/tap-parser/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/tap-parser/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "optional": true
+ },
+ "node_modules/tap-parser/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/tap-xunit": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/tap-xunit/-/tap-xunit-2.4.1.tgz",
+ "integrity": "sha512-qcZStDtjjYjMKAo7QNiCtOW256g3tuSyCSe5kNJniG1Q2oeOExJq4vm8CwboHZURpkXAHvtqMl4TVL7mcbMVVA==",
+ "dev": true,
+ "dependencies": {
+ "duplexer": "~0.1.1",
+ "minimist": "~1.2.0",
+ "tap-parser": "~1.2.2",
+ "through2": "~2.0.0",
+ "xmlbuilder": "~4.2.0",
+ "xtend": "~4.0.0"
+ },
+ "bin": {
+ "tap-xunit": "bin/tap-xunit",
+ "txunit": "bin/tap-xunit"
+ }
+ },
+ "node_modules/tar": {
+ "version": "7.4.3",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz",
+ "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==",
+ "dependencies": {
+ "@isaacs/fs-minipass": "^4.0.0",
+ "chownr": "^3.0.0",
+ "minipass": "^7.1.2",
+ "minizlib": "^3.0.1",
+ "mkdirp": "^3.0.1",
+ "yallist": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tar/node_modules/mkdirp": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
+ "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
+ "bin": {
+ "mkdirp": "dist/cjs/src/bin.js"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/tar/node_modules/yallist": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
+ "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/temp-dir": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz",
+ "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.16"
+ }
+ },
+ "node_modules/tempy": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz",
+ "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==",
+ "dev": true,
+ "dependencies": {
+ "is-stream": "^3.0.0",
+ "temp-dir": "^3.0.0",
+ "type-fest": "^2.12.2",
+ "unique-string": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/tempy/node_modules/is-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz",
+ "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/tempy/node_modules/type-fest": {
+ "version": "2.19.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
+ "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12.20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+ "dev": true,
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/test-exclude/node_modules/brace-expansion": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+ "dev": true,
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/test-exclude/node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "deprecated": "Glob versions prior to v9 are no longer supported",
+ "dev": true,
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.1.1",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/test-exclude/node_modules/minimatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+ "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/through2": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
+ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
+ "dev": true,
+ "dependencies": {
+ "readable-stream": "~2.3.6",
+ "xtend": "~4.0.1"
+ }
+ },
+ "node_modules/through2/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "dev": true,
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/through2/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "node_modules/through2/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/time-zone": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/time-zone/-/time-zone-1.0.0.tgz",
+ "integrity": "sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/token-types": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-5.0.1.tgz",
+ "integrity": "sha512-Y2fmSnZjQdDb9W4w4r1tswlMHylzWIeOKpx0aZH9BgGtACHhrk3OkT52AzwcuqTRBZtvvnTjDBh8eynMulu8Vg==",
+ "dev": true,
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0",
+ "ieee754": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "dev": true
+ },
+ "node_modules/tuf-js": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.1.0.tgz",
+ "integrity": "sha512-3T3T04WzowbwV2FDiGXBbr81t64g1MUGGJRgT4x5o97N+8ArdhVCAF9IxFrxuSJmM3E5Asn7nKHkao0ibcZXAg==",
+ "dependencies": {
+ "@tufjs/models": "3.0.1",
+ "debug": "^4.4.1",
+ "make-fetch-happen": "^14.0.3"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/type-fest": {
+ "version": "0.8.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
+ "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "dev": true,
+ "dependencies": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
+ "node_modules/uc.micro": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
+ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
+ "dev": true
+ },
+ "node_modules/underscore": {
+ "version": "1.13.7",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",
+ "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==",
+ "dev": true
+ },
+ "node_modules/unicorn-magic": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz",
+ "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/unique-filename": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz",
+ "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==",
+ "dependencies": {
+ "unique-slug": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/unique-slug": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz",
+ "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==",
+ "dependencies": {
+ "imurmurhash": "^0.1.4"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/unique-string": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz",
+ "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==",
+ "dev": true,
+ "dependencies": {
+ "crypto-random-string": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
+ "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "dev": true,
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dependencies": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
+ "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
+ "dependencies": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "node_modules/validate-npm-package-name": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz",
+ "integrity": "sha512-IUoow1YUtvoBBC06dXs8bR8B9vuA3aJfmQNKMoaPG/OFsPmoQvw8xh+6Ye25Gx9DQhoEom3Pcu9MKHerm/NpUQ==",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/walk-up-path": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz",
+ "integrity": "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A==",
+ "license": "ISC",
+ "engines": {
+ "node": "20 || >=22"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "dev": true
+ },
+ "node_modules/well-known-symbols": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/well-known-symbols/-/well-known-symbols-2.0.0.tgz",
+ "integrity": "sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dev": true,
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz",
+ "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==",
+ "dependencies": {
+ "isexe": "^3.1.1"
+ },
+ "bin": {
+ "node-which": "bin/which.js"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/which-module": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+ "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==",
+ "dev": true
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+ "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+ "dependencies": {
+ "ansi-styles": "^6.1.0",
+ "string-width": "^5.0.1",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs": {
+ "name": "wrap-ansi",
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/emoji-regex": {
+ "version": "9.2.2",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+ "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+ "dependencies": {
+ "eastasianwidth": "^0.2.0",
+ "emoji-regex": "^9.2.2",
+ "strip-ansi": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "node_modules/write-file-atomic": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-6.0.0.tgz",
+ "integrity": "sha512-GmqrO8WJ1NuzJ2DrziEI2o57jKAVIQNf8a18W3nCYU3H7PNWqCCVTeH6/NQE93CIllIgQS98rrmVkYgTX9fFJQ==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
+ "node_modules/wsl-utils": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
+ "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==",
+ "dev": true,
+ "dependencies": {
+ "is-wsl": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/xml2js": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz",
+ "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==",
+ "dependencies": {
+ "sax": ">=0.6.0",
+ "xmlbuilder": "~11.0.0"
+ },
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/xml2js/node_modules/xmlbuilder": {
+ "version": "11.0.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/xmlbuilder": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-4.2.1.tgz",
+ "integrity": "sha512-oEePiEefhQhAeUnwRnIBLBWmk/fsWWbQ53EEWsRuzECbQ3m5o/Esmq6H47CYYwSLW+Ynt0rS9hd0pd2ogMAWjg==",
+ "dev": true,
+ "dependencies": {
+ "lodash": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/xmlcreate": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz",
+ "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==",
+ "dev": true
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true
+ },
+ "node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yesno": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/yesno/-/yesno-0.4.0.tgz",
+ "integrity": "sha512-tdBxmHvbXPBKYIg81bMCB7bVeDmHkRzk5rVJyYYXurwKkHq/MCd8rz4HSJUP7hW0H2NlXiq8IFiWvYKEHhlotA=="
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ }
+}
diff --git a/packages/project/package.json b/packages/project/package.json
new file mode 100644
index 00000000000..b72d0d254ce
--- /dev/null
+++ b/packages/project/package.json
@@ -0,0 +1,179 @@
+{
+ "name": "@ui5/project",
+ "version": "4.0.6",
+ "description": "UI5 CLI - Project ",
+ "author": {
+ "name": "SAP SE",
+ "email": "openui5@sap.com",
+ "url": "https://www.sap.com"
+ },
+ "license": "Apache-2.0",
+ "keywords": [
+ "openui5",
+ "sapui5",
+ "ui5",
+ "build",
+ "development",
+ "tool"
+ ],
+ "type": "module",
+ "exports": {
+ "./config/Configuration": "./lib/config/Configuration.js",
+ "./specifications/Specification": "./lib/specifications/Specification.js",
+ "./specifications/SpecificationVersion": "./lib/specifications/SpecificationVersion.js",
+ "./ui5Framework/Sapui5MavenSnapshotResolver": "./lib/ui5Framework/Sapui5MavenSnapshotResolver.js",
+ "./ui5Framework/Openui5Resolver": "./lib/ui5Framework/Openui5Resolver.js",
+ "./ui5Framework/Sapui5Resolver": "./lib/ui5Framework/Sapui5Resolver.js",
+ "./ui5Framework/maven/CacheMode": "./lib/ui5Framework/maven/CacheMode.js",
+ "./validation/validator": "./lib/validation/validator.js",
+ "./validation/ValidationError": "./lib/validation/ValidationError.js",
+ "./graph/ProjectGraph": "./lib/graph/ProjectGraph.js",
+ "./graph/projectGraphBuilder": "./lib/graph/projectGraphBuilder.js",
+ "./graph": "./lib/graph/graph.js",
+ "./package.json": "./package.json"
+ },
+ "engines": {
+ "node": "^20.11.0 || >=22.0.0",
+ "npm": ">= 8"
+ },
+ "scripts": {
+ "test": "npm run lint && npm run jsdoc-generate && npm run coverage && npm run depcheck",
+ "test-azure": "npm run coverage-xunit",
+ "lint": "eslint ./",
+ "unit": "rimraf test/tmp && ava",
+ "unit-verbose": "rimraf test/tmp && cross-env UI5_LOG_LVL=verbose ava --verbose --serial",
+ "unit-watch": "npm run unit -- --watch",
+ "unit-xunit": "rimraf test/tmp && ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\" --tap | tap-xunit --dontUseCommentsAsTestNames=true > test-results.xml",
+ "unit-inspect": "cross-env UI5_LOG_LVL=verbose ava debug --break",
+ "coverage": "rimraf test/tmp && nyc ava --node-arguments=\"--experimental-loader=@istanbuljs/esm-loader-hook\"",
+ "coverage-xunit": "nyc --reporter=text --reporter=text-summary --reporter=cobertura npm run unit-xunit",
+ "jsdoc": "npm run jsdoc-generate && open-cli jsdocs/index.html",
+ "jsdoc-generate": "jsdoc -c ./jsdoc.json -t $(node -p 'path.dirname(require.resolve(\"docdash\"))') ./lib/ || (echo 'Error during JSDoc generation! Check log.' && exit 1)",
+ "jsdoc-watch": "npm run jsdoc && chokidar \"./lib/**/*.js\" -c \"npm run jsdoc-generate\"",
+ "preversion": "npm test",
+ "version": "git-chglog --sort semver --next-tag v$npm_package_version -o CHANGELOG.md v4.0.0.. && git add CHANGELOG.md",
+ "prepublishOnly": "git push --follow-tags",
+ "release-note": "git-chglog --sort semver -c .chglog/release-config.yml v$npm_package_version",
+ "depcheck": "depcheck --ignores @ui5/project,docdash,@istanbuljs/esm-loader-hook,rimraf"
+ },
+ "files": [
+ "CHANGELOG.md",
+ "CONTRIBUTING.md",
+ "jsdoc.json",
+ "lib/**",
+ "LICENSES/**",
+ ".reuse/**"
+ ],
+ "ava": {
+ "files": [
+ "test/lib/**/*.js",
+ "!test/**/__helper__/**"
+ ],
+ "nodeArguments": [
+ "--loader=esmock",
+ "--no-warnings"
+ ],
+ "workerThreads": false
+ },
+ "nyc": {
+ "reporter": [
+ "lcov",
+ "text",
+ "text-summary"
+ ],
+ "exclude": [
+ "docs/**",
+ "jsdocs/**",
+ "coverage/**",
+ "test/**",
+ ".eslintrc.cjs",
+ "jsdoc-plugin.cjs"
+ ],
+ "check-coverage": true,
+ "statements": 90,
+ "branches": 85,
+ "functions": 90,
+ "lines": 90,
+ "watermarks": {
+ "statements": [
+ 70,
+ 90
+ ],
+ "branches": [
+ 70,
+ 90
+ ],
+ "functions": [
+ 70,
+ 90
+ ],
+ "lines": [
+ 70,
+ 90
+ ]
+ },
+ "cache": true,
+ "all": true
+ },
+ "repository": {
+ "type": "git",
+ "url": "git@github.com:SAP/ui5-project.git"
+ },
+ "dependencies": {
+ "@npmcli/config": "^10.4.0",
+ "@ui5/fs": "^4.0.2",
+ "@ui5/logger": "^4.0.2",
+ "ajv": "^6.12.6",
+ "ajv-errors": "^1.0.1",
+ "chalk": "^5.6.2",
+ "escape-string-regexp": "^5.0.0",
+ "globby": "^14.1.0",
+ "graceful-fs": "^4.2.11",
+ "js-yaml": "^4.1.0",
+ "lockfile": "^1.0.4",
+ "make-fetch-happen": "^14.0.3",
+ "node-stream-zip": "^1.15.0",
+ "pacote": "^19.0.1",
+ "pretty-hrtime": "^1.0.3",
+ "read-package-up": "^11.0.0",
+ "read-pkg": "^9.0.1",
+ "resolve": "^1.22.10",
+ "semver": "^7.7.2",
+ "xml2js": "^0.6.2",
+ "yesno": "^0.4.0"
+ },
+ "peerDependencies": {
+ "@ui5/builder": "^4.0.11"
+ },
+ "peerDependenciesMeta": {
+ "@ui5/builder": {
+ "optional": true
+ }
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.8.0",
+ "@istanbuljs/esm-loader-hook": "^0.3.0",
+ "ava": "^6.4.1",
+ "chokidar-cli": "^3.0.0",
+ "cross-env": "^7.0.3",
+ "depcheck": "^1.4.7",
+ "docdash": "^2.0.2",
+ "eslint": "^9.36.0",
+ "eslint-config-google": "^0.14.0",
+ "eslint-plugin-ava": "^15.1.0",
+ "eslint-plugin-jsdoc": "^52.0.4",
+ "esmock": "^2.7.3",
+ "globals": "^16.4.0",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-instrument": "^6.0.3",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.2.0",
+ "js-beautify": "^1.15.4",
+ "jsdoc": "^4.0.4",
+ "nyc": "^17.1.0",
+ "open-cli": "^8.0.0",
+ "rimraf": "^6.0.1",
+ "sinon": "^21.0.0",
+ "tap-xunit": "^2.4.1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/lib/extensionModule.js b/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/lib/extensionModule.js
new file mode 100644
index 00000000000..c7a5c0758dd
--- /dev/null
+++ b/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/lib/extensionModule.js
@@ -0,0 +1,2 @@
+export default () => "extension module";
+export function determineRequiredDependencies () { return "required dependencies function" };
diff --git a/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/package.json b/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/package.json
new file mode 100644
index 00000000000..97c63b1660f
--- /dev/null
+++ b/packages/project/test/fixtures/application.a.aliases/node_modules/extension.a.esm.alias/package.json
@@ -0,0 +1,5 @@
+{
+ "name": "extension.a",
+ "type": "module",
+ "version": "1.0.0"
+}
diff --git a/packages/project/test/fixtures/application.a.aliases/package.json b/packages/project/test/fixtures/application.a.aliases/package.json
new file mode 100644
index 00000000000..b15c70dfe0a
--- /dev/null
+++ b/packages/project/test/fixtures/application.a.aliases/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "application.a.aliases",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application",
+ "main": "index.html",
+ "dependencies": {
+ "extension.a.esm.alias": "file:../extension.a.esm"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.a.aliases/ui5.yaml b/packages/project/test/fixtures/application.a.aliases/ui5.yaml
new file mode 100644
index 00000000000..82579b7d86a
--- /dev/null
+++ b/packages/project/test/fixtures/application.a.aliases/ui5.yaml
@@ -0,0 +1,23 @@
+---
+specVersion: "3.1"
+type: application
+metadata:
+ name: application.a.aliases
+
+--- # Everything below this line could also be put into the ui5.yaml of a standalone extension module
+specVersion: "3.1"
+kind: extension
+type: project-shim
+metadata:
+ name: my.application.thirdparty
+shims:
+ configurations:
+ extension.a.esm.alias: # name as defined in package.json
+ specVersion: "3.1"
+ type: module # Use module type
+ metadata:
+ name: extension.a.esm.alias
+ resources:
+ configuration:
+ paths:
+ /resources/my/application/thirdparty/: "" # map root directory of lodash module
diff --git a/packages/project/test/fixtures/application.a.aliases/webapp/index.html b/packages/project/test/fixtures/application.a.aliases/webapp/index.html
new file mode 100644
index 00000000000..1b8755901bf
--- /dev/null
+++ b/packages/project/test/fixtures/application.a.aliases/webapp/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Application A
+
+
+
+
+
diff --git a/packages/project/test/fixtures/application.a.aliases/webapp/manifest.json b/packages/project/test/fixtures/application.a.aliases/webapp/manifest.json
new file mode 100644
index 00000000000..d33902df74e
--- /dev/null
+++ b/packages/project/test/fixtures/application.a.aliases/webapp/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}}"
+ }
+}
diff --git a/packages/project/test/fixtures/application.a.aliases/webapp/test.js b/packages/project/test/fixtures/application.a.aliases/webapp/test.js
new file mode 100644
index 00000000000..a3df410c341
--- /dev/null
+++ b/packages/project/test/fixtures/application.a.aliases/webapp/test.js
@@ -0,0 +1,5 @@
+function test(paramA) {
+ var variableA = paramA;
+ console.log(variableA);
+}
+test();
diff --git a/packages/project/test/fixtures/application.a/middleware.a.js b/packages/project/test/fixtures/application.a/middleware.a.js
new file mode 100644
index 00000000000..ea41b01de46
--- /dev/null
+++ b/packages/project/test/fixtures/application.a/middleware.a.js
@@ -0,0 +1 @@
+module.exports = function () {};
diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.a/package.json b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/package.json
new file mode 100644
index 00000000000..2179673d41d
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/library.a/src/library/a/.library b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/src/library/a/.library
new file mode 100644
index 00000000000..25c8603f31a
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/application.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/application.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/application.a/node_modules/collection/library.a/test/library/a/Test.html b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/library.a/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/collection/library.a/ui5.yaml
new file mode 100644
index 00000000000..8d4784313c3
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/library.b/package.json b/packages/project/test/fixtures/application.a/node_modules/collection/library.b/package.json
new file mode 100644
index 00000000000..2a0243b1683
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/library.b/src/library/b/.library b/packages/project/test/fixtures/application.a/node_modules/collection/library.b/src/library/b/.library
new file mode 100644
index 00000000000..36052acebdc
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/library.b/test/library/b/Test.html b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/library.b/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/collection/library.b/ui5.yaml
new file mode 100644
index 00000000000..b2fe5be59ee
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/library.c/package.json b/packages/project/test/fixtures/application.a/node_modules/collection/library.c/package.json
new file mode 100644
index 00000000000..64ac75d6ffe
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/library.c/src/library/c/.library b/packages/project/test/fixtures/application.a/node_modules/collection/library.c/src/library/c/.library
new file mode 100644
index 00000000000..4180ce2af2f
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/application.a/node_modules/collection/library.c/test/LibraryC/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.a/node_modules/collection/library.c/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/collection/library.c/ui5.yaml
new file mode 100644
index 00000000000..7c5e38a7fc1
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/node_modules/library.d/package.json b/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/application.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/application.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/application.a/node_modules/collection/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/collection/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/package.json b/packages/project/test/fixtures/application.a/node_modules/collection/package.json
new file mode 100644
index 00000000000..81b948438bd
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/collection/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/collection/ui5.yaml
new file mode 100644
index 00000000000..e47048de6a7
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.a/node_modules/library.d/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.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/application.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/application.a/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.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/application.a/node_modules/library.d/package.json b/packages/project/test/fixtures/application.a/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.a/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.a/package.json b/packages/project/test/fixtures/application.a/package.json
new file mode 100644
index 00000000000..7da37b86a56
--- /dev/null
+++ b/packages/project/test/fixtures/application.a/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "application.a",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application",
+ "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/application.a/task.a.js b/packages/project/test/fixtures/application.a/task.a.js
new file mode 100644
index 00000000000..ea41b01de46
--- /dev/null
+++ b/packages/project/test/fixtures/application.a/task.a.js
@@ -0,0 +1 @@
+module.exports = function () {};
diff --git a/packages/project/test/fixtures/application.a/ui5-test-configPath.yaml b/packages/project/test/fixtures/application.a/ui5-test-configPath.yaml
new file mode 100644
index 00000000000..a50b3c48b99
--- /dev/null
+++ b/packages/project/test/fixtures/application.a/ui5-test-configPath.yaml
@@ -0,0 +1,7 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.a
+customConfiguration:
+ configPathTest: true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.a/ui5-test-corrupt.yaml b/packages/project/test/fixtures/application.a/ui5-test-corrupt.yaml
new file mode 100644
index 00000000000..ecce9d7e78b
--- /dev/null
+++ b/packages/project/test/fixtures/application.a/ui5-test-corrupt.yaml
@@ -0,0 +1 @@
+|-\nfoo\nbar
diff --git a/packages/project/test/fixtures/application.a/ui5-test-empty.yaml b/packages/project/test/fixtures/application.a/ui5-test-empty.yaml
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.a/ui5-test-error.yaml b/packages/project/test/fixtures/application.a/ui5-test-error.yaml
new file mode 100644
index 00000000000..f5ad909d259
--- /dev/null
+++ b/packages/project/test/fixtures/application.a/ui5-test-error.yaml
@@ -0,0 +1,7 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.a
+xyz:
+ foo: true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.a/ui5.yaml b/packages/project/test/fixtures/application.a/ui5.yaml
new file mode 100644
index 00000000000..b9dde7b16b2
--- /dev/null
+++ b/packages/project/test/fixtures/application.a/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.a
diff --git a/packages/project/test/fixtures/application.a/webapp/index.html b/packages/project/test/fixtures/application.a/webapp/index.html
new file mode 100644
index 00000000000..77b0207cc80
--- /dev/null
+++ b/packages/project/test/fixtures/application.a/webapp/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Application A
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.a/webapp/manifest.json b/packages/project/test/fixtures/application.a/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/application.a/webapp/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/application.a/webapp/test.js b/packages/project/test/fixtures/application.a/webapp/test.js
new file mode 100644
index 00000000000..a3df410c341
--- /dev/null
+++ b/packages/project/test/fixtures/application.a/webapp/test.js
@@ -0,0 +1,5 @@
+function test(paramA) {
+ var variableA = paramA;
+ console.log(variableA);
+}
+test();
diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.a/package.json b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/package.json
new file mode 100644
index 00000000000..2179673d41d
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/library.a/src/library/a/.library b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/src/library/a/.library
new file mode 100644
index 00000000000..25c8603f31a
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/application.b/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/application.b/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/application.b/node_modules/collection/library.a/test/library/a/Test.html b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/test/library/a/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.a/ui5.yaml b/packages/project/test/fixtures/application.b/node_modules/collection/library.a/ui5.yaml
new file mode 100644
index 00000000000..8d4784313c3
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/library.b/package.json b/packages/project/test/fixtures/application.b/node_modules/collection/library.b/package.json
new file mode 100644
index 00000000000..2a0243b1683
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/library.b/src/library/b/.library b/packages/project/test/fixtures/application.b/node_modules/collection/library.b/src/library/b/.library
new file mode 100644
index 00000000000..36052acebdc
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/library.b/test/library/b/Test.html b/packages/project/test/fixtures/application.b/node_modules/collection/library.b/test/library/b/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.b/ui5.yaml b/packages/project/test/fixtures/application.b/node_modules/collection/library.b/ui5.yaml
new file mode 100644
index 00000000000..b2fe5be59ee
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/library.c/package.json b/packages/project/test/fixtures/application.b/node_modules/collection/library.c/package.json
new file mode 100644
index 00000000000..64ac75d6ffe
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/library.c/src/library/c/.library b/packages/project/test/fixtures/application.b/node_modules/collection/library.c/src/library/c/.library
new file mode 100644
index 00000000000..4180ce2af2f
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/application.b/node_modules/collection/library.c/test/LibraryC/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.b/node_modules/collection/library.c/ui5.yaml b/packages/project/test/fixtures/application.b/node_modules/collection/library.c/ui5.yaml
new file mode 100644
index 00000000000..7c5e38a7fc1
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/node_modules/library.d/package.json b/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/application.b/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/application.b/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/application.b/node_modules/collection/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.b/node_modules/collection/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/collection/package.json b/packages/project/test/fixtures/application.b/node_modules/collection/package.json
new file mode 100644
index 00000000000..81b948438bd
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.b/node_modules/library.d/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.b/node_modules/library.d/main/src/library/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.b/node_modules/library.d/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.b/node_modules/library.d/package.json b/packages/project/test/fixtures/application.b/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.b/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/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/application.b/package.json b/packages/project/test/fixtures/application.b/package.json
new file mode 100644
index 00000000000..0243e3a9001
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "application.b",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application",
+ "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/application.b/ui5.yaml b/packages/project/test/fixtures/application.b/ui5.yaml
new file mode 100644
index 00000000000..7b5e5dd2359
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.b
diff --git a/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n.properties b/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n.properties
new file mode 100644
index 00000000000..d93c8a39c0e
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n.properties
@@ -0,0 +1 @@
+title=embedded-i18n
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n_de.properties b/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n_de.properties
new file mode 100644
index 00000000000..e513333c842
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/webapp/embedded/i18n/i18n_de.properties
@@ -0,0 +1 @@
+title=embedded-i18n_de
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.b/webapp/embedded/i18n_fr.properties b/packages/project/test/fixtures/application.b/webapp/embedded/i18n_fr.properties
new file mode 100644
index 00000000000..85e162740f9
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/webapp/embedded/i18n_fr.properties
@@ -0,0 +1 @@
+title=embedded-i18n_fr-wrong
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.b/webapp/embedded/manifest.json b/packages/project/test/fixtures/application.b/webapp/embedded/manifest.json
new file mode 100644
index 00000000000..5ef0c362425
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/webapp/embedded/manifest.json
@@ -0,0 +1,13 @@
+{
+ "_version": "1.1.0",
+ "sap.app": {
+ "_version": "1.1.0",
+ "id": "id1.embedded",
+ "type": "component",
+ "applicationVersion": {
+ "version": "1.2.2"
+ },
+ "embeddedBy": "../",
+ "title": "{{title}}"
+ }
+}
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.b/webapp/i18n.properties b/packages/project/test/fixtures/application.b/webapp/i18n.properties
new file mode 100644
index 00000000000..88b84f602f5
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/webapp/i18n.properties
@@ -0,0 +1 @@
+title=app-i18n-wrong
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.b/webapp/i18n/i18n.properties b/packages/project/test/fixtures/application.b/webapp/i18n/i18n.properties
new file mode 100644
index 00000000000..575fb20d0c3
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/webapp/i18n/i18n.properties
@@ -0,0 +1 @@
+title=app-i18n
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.b/webapp/i18n/i18n_de.properties b/packages/project/test/fixtures/application.b/webapp/i18n/i18n_de.properties
new file mode 100644
index 00000000000..f5885803892
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/webapp/i18n/i18n_de.properties
@@ -0,0 +1 @@
+title=app-i18n_de
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.b/webapp/i18n/l10n.properties b/packages/project/test/fixtures/application.b/webapp/i18n/l10n.properties
new file mode 100644
index 00000000000..88b84f602f5
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/webapp/i18n/l10n.properties
@@ -0,0 +1 @@
+title=app-i18n-wrong
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.b/webapp/manifest.json b/packages/project/test/fixtures/application.b/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/application.b/webapp/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/application.c/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.c/node_modules/library.d/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/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/application.c/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.c/node_modules/library.d/main/src/library/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/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/application.c/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.c/node_modules/library.d/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.c/node_modules/library.d/package.json b/packages/project/test/fixtures/application.c/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/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/application.c/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.c/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/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/application.c/node_modules/library.e/node_modules/library.d/package.json b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/node_modules/library.e/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/application.c/node_modules/library.e/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/src/library/d/.library
new file mode 100644
index 00000000000..21251d1bbba
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/node_modules/library.e/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/application.c/node_modules/library.e/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..7b731df83f6
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/node_modules/library.e/node_modules/library.d/ui5.yaml
@@ -0,0 +1,3 @@
+---
+name: library.d
+type: library
diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/package.json b/packages/project/test/fixtures/application.c/node_modules/library.e/package.json
new file mode 100644
index 00000000000..07ed6e5f4bf
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/node_modules/library.e/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.e",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library",
+ "devDependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.c/node_modules/library.e/src/library/e/.library
new file mode 100644
index 00000000000..26ff954f7b1
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/node_modules/library.e/src/library/e/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.e
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library E
+
+
diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.c/node_modules/library.e/test/library/e/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.c/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.c/node_modules/library.e/ui5.yaml
new file mode 100644
index 00000000000..88ba07e82dd
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/node_modules/library.e/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.e
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/application.c/package.json b/packages/project/test/fixtures/application.c/package.json
new file mode 100644
index 00000000000..1cd37e82d67
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "application.c",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - test for dev dependencies. Optional dep gets resolved through root project",
+ "main": "index.html",
+ "dependencies": {
+ "library.e": "file:../library.e",
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.c/src/manifest.json b/packages/project/test/fixtures/application.c/src/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/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/application.c/ui5.yaml b/packages/project/test/fixtures/application.c/ui5.yaml
new file mode 100644
index 00000000000..fd28471bad3
--- /dev/null
+++ b/packages/project/test/fixtures/application.c/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.c
+resources:
+ configuration:
+ paths:
+ webapp: src
diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/src/library/d/.library b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/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/application.c2/node_modules/library.d-depender/main/src/library/d/some.js b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/src/library/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/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/application.c2/node_modules/library.d-depender/main/test/library/d/Test.html b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/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/application.c2/node_modules/library.d-depender/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/src/library/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/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/application.c2/node_modules/library.d-depender/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/package.json b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/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/application.c2/node_modules/library.d-depender/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/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/application.c2/node_modules/library.d-depender/package.json b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/package.json
new file mode 100644
index 00000000000..9f88fc95f0c
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.d-depender",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library",
+ "dependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/ui5.yaml b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/ui5.yaml
new file mode 100644
index 00000000000..51744218833
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.d-depender/ui5.yaml
@@ -0,0 +1,11 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.d-depender
+resources:
+ configuration:
+ paths:
+ src: main/src
+ test: main/test
+
diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/package.json b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/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/application.c2/node_modules/library.e/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/src/library/d/.library
new file mode 100644
index 00000000000..21251d1bbba
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/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/application.c2/node_modules/library.e/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..e05b61880d8
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/node_modules/library.d/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.d
diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/package.json b/packages/project/test/fixtures/application.c2/node_modules/library.e/package.json
new file mode 100644
index 00000000000..07ed6e5f4bf
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.e",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library",
+ "devDependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.c2/node_modules/library.e/src/library/e/.library
new file mode 100644
index 00000000000..26ff954f7b1
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/src/library/e/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.e
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library E
+
+
diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.c2/node_modules/library.e/test/library/e/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.c2/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.c2/node_modules/library.e/ui5.yaml
new file mode 100644
index 00000000000..99dbbf3bcc3
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/node_modules/library.e/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.e
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/application.c2/package.json b/packages/project/test/fixtures/application.c2/package.json
new file mode 100644
index 00000000000..7af4231b145
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "application.c2",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - test for dev dependencies. Optional dep gets resolved through other project",
+ "main": "index.html",
+ "dependencies": {
+ "library.e": "file:../library.e",
+ "library.d-depender": "file:../library.d-depender"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.c2/src/manifest.json b/packages/project/test/fixtures/application.c2/src/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/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/application.c2/ui5.yaml b/packages/project/test/fixtures/application.c2/ui5.yaml
new file mode 100644
index 00000000000..c982fa9dc26
--- /dev/null
+++ b/packages/project/test/fixtures/application.c2/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.c2
+resources:
+ configuration:
+ paths:
+ webapp: src
diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/.library b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/.library
new file mode 100644
index 00000000000..42efe5f9d84
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.d-depender
+ SAP SE
+ Some fancy copyright
+ ${version}
+
+ Library D
+
+
diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/some.js b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/src/library/d-depender/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/test/library/d/Test.html b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/package.json b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/package.json
new file mode 100644
index 00000000000..9f88fc95f0c
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.d-depender",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library",
+ "dependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/ui5.yaml b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/ui5.yaml
new file mode 100644
index 00000000000..51744218833
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/node_modules/library.d-depender/ui5.yaml
@@ -0,0 +1,11 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.d-depender
+resources:
+ configuration:
+ paths:
+ src: main/src
+ test: main/test
+
diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.c3/node_modules/library.d/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/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/application.c3/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.c3/node_modules/library.d/main/src/library/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/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/application.c3/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.c3/node_modules/library.d/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.d/package.json b/packages/project/test/fixtures/application.c3/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/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/application.c3/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.c3/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/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/application.c3/node_modules/library.e/package.json b/packages/project/test/fixtures/application.c3/node_modules/library.e/package.json
new file mode 100644
index 00000000000..07ed6e5f4bf
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/node_modules/library.e/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.e",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library",
+ "devDependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.c3/node_modules/library.e/src/library/e/.library
new file mode 100644
index 00000000000..26ff954f7b1
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/node_modules/library.e/src/library/e/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.e
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library E
+
+
diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.c3/node_modules/library.e/test/library/e/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.c3/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.c3/node_modules/library.e/ui5.yaml
new file mode 100644
index 00000000000..99dbbf3bcc3
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/node_modules/library.e/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.e
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/application.c3/package.json b/packages/project/test/fixtures/application.c3/package.json
new file mode 100644
index 00000000000..b51a0f27e23
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "application.c3",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - test for dev dependencies. Optional dep gets resolved through other project (but got hoisted)",
+ "main": "index.html",
+ "dependencies": {
+ "library.e": "file:../library.e",
+ "library.d-depender": "file:../library.d-depender"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.c3/src/manifest.json b/packages/project/test/fixtures/application.c3/src/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/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/application.c3/ui5.yaml b/packages/project/test/fixtures/application.c3/ui5.yaml
new file mode 100644
index 00000000000..3cecacec12e
--- /dev/null
+++ b/packages/project/test/fixtures/application.c3/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.c3
+resources:
+ configuration:
+ paths:
+ webapp: src
diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/package.json b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.d/node_modules/library.e/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/application.d/node_modules/library.e/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/src/library/d/.library
new file mode 100644
index 00000000000..21251d1bbba
--- /dev/null
+++ b/packages/project/test/fixtures/application.d/node_modules/library.e/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/application.d/node_modules/library.e/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..e05b61880d8
--- /dev/null
+++ b/packages/project/test/fixtures/application.d/node_modules/library.e/node_modules/library.d/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.d
diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/package.json b/packages/project/test/fixtures/application.d/node_modules/library.e/package.json
new file mode 100644
index 00000000000..9ce874ff55a
--- /dev/null
+++ b/packages/project/test/fixtures/application.d/node_modules/library.e/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.e",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for dev dependencies",
+ "devDependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/.library
new file mode 100644
index 00000000000..26ff954f7b1
--- /dev/null
+++ b/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.e
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library E
+
+
diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/some.js b/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/application.d/node_modules/library.e/src/library/e/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.d/node_modules/library.e/test/library/e/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.d/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.d/node_modules/library.e/ui5.yaml
new file mode 100644
index 00000000000..99dbbf3bcc3
--- /dev/null
+++ b/packages/project/test/fixtures/application.d/node_modules/library.e/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.e
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/application.d/package.json b/packages/project/test/fixtures/application.d/package.json
new file mode 100644
index 00000000000..e99f770892b
--- /dev/null
+++ b/packages/project/test/fixtures/application.d/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "application.d",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - test for dev dependencies",
+ "main": "index.html",
+ "dependencies": {
+ "library.e": "file:../library.e"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.d/ui5.yaml b/packages/project/test/fixtures/application.d/ui5.yaml
new file mode 100644
index 00000000000..1b43352b18a
--- /dev/null
+++ b/packages/project/test/fixtures/application.d/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.d
diff --git a/packages/project/test/fixtures/application.d/webapp/manifest.json b/packages/project/test/fixtures/application.d/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/application.d/webapp/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/application.e/node_modules/library.e/package.json b/packages/project/test/fixtures/application.e/node_modules/library.e/package.json
new file mode 100644
index 00000000000..07ed6e5f4bf
--- /dev/null
+++ b/packages/project/test/fixtures/application.e/node_modules/library.e/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.e",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library",
+ "devDependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.e/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.e/node_modules/library.e/src/library/e/.library
new file mode 100644
index 00000000000..20c99070048
--- /dev/null
+++ b/packages/project/test/fixtures/application.e/node_modules/library.e/src/library/e/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.d
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library E
+
+
diff --git a/packages/project/test/fixtures/application.e/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.e/node_modules/library.e/test/library/e/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.e/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.e/node_modules/library.e/ui5.yaml
new file mode 100644
index 00000000000..3852a732d57
--- /dev/null
+++ b/packages/project/test/fixtures/application.e/node_modules/library.e/ui5.yaml
@@ -0,0 +1,11 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.e
+builder:
+ configuration:
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/application.e/package.json b/packages/project/test/fixtures/application.e/package.json
new file mode 100644
index 00000000000..50f927fcc52
--- /dev/null
+++ b/packages/project/test/fixtures/application.e/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "application.d",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - test for circular dependencies",
+ "main": "index.html",
+ "dependencies": {
+ "library.f": "file:../library.f",
+ "library.g": "file:../library.g"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.e/ui5.yaml b/packages/project/test/fixtures/application.e/ui5.yaml
new file mode 100644
index 00000000000..97537e5ca03
--- /dev/null
+++ b/packages/project/test/fixtures/application.e/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.e
diff --git a/packages/project/test/fixtures/application.e/webapp/manifest.json b/packages/project/test/fixtures/application.e/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/application.e/webapp/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/application.f/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.f/node_modules/library.d/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/application.f/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/application.f/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.f/node_modules/library.d/main/src/library/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/application.f/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/application.f/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.f/node_modules/library.d/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.f/node_modules/library.d/package.json b/packages/project/test/fixtures/application.f/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.f/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/application.f/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.f/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/application.f/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/application.f/node_modules/library.e/package.json b/packages/project/test/fixtures/application.f/node_modules/library.e/package.json
new file mode 100644
index 00000000000..9ce874ff55a
--- /dev/null
+++ b/packages/project/test/fixtures/application.f/node_modules/library.e/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.e",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for dev dependencies",
+ "devDependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.f/node_modules/library.e/src/library/e/.library b/packages/project/test/fixtures/application.f/node_modules/library.e/src/library/e/.library
new file mode 100644
index 00000000000..26ff954f7b1
--- /dev/null
+++ b/packages/project/test/fixtures/application.f/node_modules/library.e/src/library/e/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.e
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library E
+
+
diff --git a/packages/project/test/fixtures/application.f/node_modules/library.e/test/library/e/Test.html b/packages/project/test/fixtures/application.f/node_modules/library.e/test/library/e/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.f/node_modules/library.e/ui5.yaml b/packages/project/test/fixtures/application.f/node_modules/library.e/ui5.yaml
new file mode 100644
index 00000000000..99dbbf3bcc3
--- /dev/null
+++ b/packages/project/test/fixtures/application.f/node_modules/library.e/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.e
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/application.f/package.json b/packages/project/test/fixtures/application.f/package.json
new file mode 100644
index 00000000000..68a71be8c58
--- /dev/null
+++ b/packages/project/test/fixtures/application.f/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "application.f",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - test for ui5-dependencies configuration",
+ "main": "index.html",
+ "dependencies": {
+ "library.d": "file:../library.d",
+ "library.e": "file:../library.e"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "ui5": {
+ "dependencies": [
+ "library.d"
+ ]
+ }
+}
diff --git a/packages/project/test/fixtures/application.f/ui5.yaml b/packages/project/test/fixtures/application.f/ui5.yaml
new file mode 100644
index 00000000000..3df51b3a974
--- /dev/null
+++ b/packages/project/test/fixtures/application.f/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.f
diff --git a/packages/project/test/fixtures/application.f/webapp/manifest.json b/packages/project/test/fixtures/application.f/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/application.f/webapp/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/application.g/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/application.g/node_modules/library.d/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/application.g/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/application.g/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/application.g/node_modules/library.d/main/src/library/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/application.g/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/application.g/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/application.g/node_modules/library.d/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/application.g/node_modules/library.d/package.json b/packages/project/test/fixtures/application.g/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/application.g/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/application.g/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/application.g/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/application.g/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/application.g/package.json b/packages/project/test/fixtures/application.g/package.json
new file mode 100644
index 00000000000..41d1ea32cf2
--- /dev/null
+++ b/packages/project/test/fixtures/application.g/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "application.g",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - test for npm optionalDependencies",
+ "main": "index.html",
+ "optionalDependencies": {
+ "library.d": "file:../library.d",
+ "library.nonexistent": "file:../library.nonexistent"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/application.g/ui5.yaml b/packages/project/test/fixtures/application.g/ui5.yaml
new file mode 100644
index 00000000000..d4e5b20f996
--- /dev/null
+++ b/packages/project/test/fixtures/application.g/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.g
diff --git a/packages/project/test/fixtures/application.g/webapp/manifest.json b/packages/project/test/fixtures/application.g/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/application.g/webapp/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/application.h/pom.xml b/packages/project/test/fixtures/application.h/pom.xml
new file mode 100644
index 00000000000..478ebc85c8c
--- /dev/null
+++ b/packages/project/test/fixtures/application.h/pom.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+ 4.0.0
+
+
+
+
+ com.sap.test
+ application.h
+ 1.0.0
+ war
+
+
+
+
+ application.h
+ Simple SAPUI5 based application
+
+
+
+
+
+
+ application.h
+
+
+
+
+
diff --git a/packages/project/test/fixtures/application.h/projectDependencies-missing-id.yaml b/packages/project/test/fixtures/application.h/projectDependencies-missing-id.yaml
new file mode 100644
index 00000000000..1eee899301b
--- /dev/null
+++ b/packages/project/test/fixtures/application.h/projectDependencies-missing-id.yaml
@@ -0,0 +1,7 @@
+---
+version: "0.0.1"
+path: "../application.a"
+dependencies:
+- id: static-library.e
+ version: "0.0.1"
+ path: "../library.e"
diff --git a/packages/project/test/fixtures/application.h/projectDependencies-missing-path.yaml b/packages/project/test/fixtures/application.h/projectDependencies-missing-path.yaml
new file mode 100644
index 00000000000..8604cc66aba
--- /dev/null
+++ b/packages/project/test/fixtures/application.h/projectDependencies-missing-path.yaml
@@ -0,0 +1,7 @@
+---
+id: static-application.a
+version: "0.0.1"
+path: "../application.a"
+dependencies:
+- id: static-library.e
+ version: "0.0.1"
diff --git a/packages/project/test/fixtures/application.h/projectDependencies-missing-version.yaml b/packages/project/test/fixtures/application.h/projectDependencies-missing-version.yaml
new file mode 100644
index 00000000000..3cecad748ba
--- /dev/null
+++ b/packages/project/test/fixtures/application.h/projectDependencies-missing-version.yaml
@@ -0,0 +1,7 @@
+---
+id: static-application.a
+path: "../application.a"
+dependencies:
+- id: static-library.e
+ version: "0.0.1"
+ path: "../library.e"
diff --git a/packages/project/test/fixtures/application.h/projectDependencies.yaml b/packages/project/test/fixtures/application.h/projectDependencies.yaml
new file mode 100644
index 00000000000..b06c3121380
--- /dev/null
+++ b/packages/project/test/fixtures/application.h/projectDependencies.yaml
@@ -0,0 +1,8 @@
+---
+id: static-application.a
+version: "0.0.1"
+path: "../application.a"
+dependencies:
+- id: static-library.e
+ version: "0.0.1"
+ path: "../library.e"
diff --git a/packages/project/test/fixtures/application.h/webapp-project.artifactId/manifest.json b/packages/project/test/fixtures/application.h/webapp-project.artifactId/manifest.json
new file mode 100644
index 00000000000..7de6072ce82
--- /dev/null
+++ b/packages/project/test/fixtures/application.h/webapp-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/application.h/webapp-properties.appId/manifest.json b/packages/project/test/fixtures/application.h/webapp-properties.appId/manifest.json
new file mode 100644
index 00000000000..e1515df7025
--- /dev/null
+++ b/packages/project/test/fixtures/application.h/webapp-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/application.h/webapp-properties.componentName/manifest.json b/packages/project/test/fixtures/application.h/webapp-properties.componentName/manifest.json
new file mode 100644
index 00000000000..7d63e359cdf
--- /dev/null
+++ b/packages/project/test/fixtures/application.h/webapp-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/application.h/webapp/Component.js b/packages/project/test/fixtures/application.h/webapp/Component.js
new file mode 100644
index 00000000000..cb9bd406864
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.h/webapp/manifest.json b/packages/project/test/fixtures/application.h/webapp/manifest.json
new file mode 100644
index 00000000000..32b7e4a8458
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.h/webapp/sectionsA/section1.js b/packages/project/test/fixtures/application.h/webapp/sectionsA/section1.js
new file mode 100644
index 00000000000..ac4a8129651
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.h/webapp/sectionsA/section2.js b/packages/project/test/fixtures/application.h/webapp/sectionsA/section2.js
new file mode 100644
index 00000000000..e009c828602
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.h/webapp/sectionsA/section3.js b/packages/project/test/fixtures/application.h/webapp/sectionsA/section3.js
new file mode 100644
index 00000000000..5fd9349d49b
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.h/webapp/sectionsB/section1.js b/packages/project/test/fixtures/application.h/webapp/sectionsB/section1.js
new file mode 100644
index 00000000000..ac4a8129651
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.h/webapp/sectionsB/section2.js b/packages/project/test/fixtures/application.h/webapp/sectionsB/section2.js
new file mode 100644
index 00000000000..e009c828602
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/application.h/webapp/sectionsB/section3.js b/packages/project/test/fixtures/application.h/webapp/sectionsB/section3.js
new file mode 100644
index 00000000000..5fd9349d49b
--- /dev/null
+++ b/packages/project/test/fixtures/application.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/fixtures/build-manifest/application.a/.ui5/build-manifest.json b/packages/project/test/fixtures/build-manifest/application.a/.ui5/build-manifest.json
new file mode 100644
index 00000000000..03ff08f24bd
--- /dev/null
+++ b/packages/project/test/fixtures/build-manifest/application.a/.ui5/build-manifest.json
@@ -0,0 +1,42 @@
+{
+ "project": {
+ "specVersion": "2.3",
+ "type": "application",
+ "metadata": {
+ "name": "application.a"
+ },
+ "resources": {
+ "configuration": {
+ "paths": {
+ "webapp": "resources/id1"
+ }
+ }
+ }
+ },
+ "buildManifest": {
+ "manifestVersion": "0.1",
+ "timestamp": "2022-05-04T12:45:30.024Z",
+ "versions": {
+ "builderVersion": "3.0.0",
+ "projectVersion": "3.0.0",
+ "fsVersion": "3.0.0"
+ },
+ "buildConfig": {
+ "selfContained": false,
+ "jsdoc": false,
+ "includedTasks": [],
+ "excludedTasks": []
+ },
+ "id": "application.a",
+ "version": "0.2.0",
+ "namespace": "id1",
+ "tags": {
+ "/resources/id1/test.js": {
+ "ui5:HasDebugVariant": true
+ },
+ "/resources/id1/test-dbg.js": {
+ "ui5:IsDebugVariant": true
+ }
+ }
+ }
+}
diff --git a/packages/project/test/fixtures/build-manifest/application.a/package.json b/packages/project/test/fixtures/build-manifest/application.a/package.json
new file mode 100644
index 00000000000..b5401c1e6e9
--- /dev/null
+++ b/packages/project/test/fixtures/build-manifest/application.a/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "application.a-archive",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application",
+ "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/build-manifest/application.a/resources/id1/index.html b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/index.html
new file mode 100644
index 00000000000..77b0207cc80
--- /dev/null
+++ b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Application A
+
+
+
+
+
\ No newline at end of file
diff --git a/packages/project/test/fixtures/build-manifest/application.a/resources/id1/manifest.json b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/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/build-manifest/application.a/resources/id1/test-dbg.js b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/test-dbg.js
new file mode 100644
index 00000000000..a3df410c341
--- /dev/null
+++ b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/test-dbg.js
@@ -0,0 +1,5 @@
+function test(paramA) {
+ var variableA = paramA;
+ console.log(variableA);
+}
+test();
diff --git a/packages/project/test/fixtures/build-manifest/application.a/resources/id1/test.js b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/test.js
new file mode 100644
index 00000000000..a3df410c341
--- /dev/null
+++ b/packages/project/test/fixtures/build-manifest/application.a/resources/id1/test.js
@@ -0,0 +1,5 @@
+function test(paramA) {
+ var variableA = paramA;
+ console.log(variableA);
+}
+test();
diff --git a/packages/project/test/fixtures/build-manifest/library.e/.ui5/build-manifest.json b/packages/project/test/fixtures/build-manifest/library.e/.ui5/build-manifest.json
new file mode 100644
index 00000000000..5205a51a854
--- /dev/null
+++ b/packages/project/test/fixtures/build-manifest/library.e/.ui5/build-manifest.json
@@ -0,0 +1,43 @@
+{
+ "project": {
+ "specVersion": "2.3",
+ "type": "library",
+ "metadata": {
+ "name": "library.e"
+ },
+ "resources": {
+ "configuration": {
+ "paths": {
+ "src": "resources",
+ "test": "test-resources"
+ }
+ }
+ }
+ },
+ "buildManifest": {
+ "manifestVersion": "0.1",
+ "timestamp": "2022-05-06T09:54:29.051Z",
+ "versions": {
+ "builderVersion": "3.0.0",
+ "projectVersion": "3.0.0",
+ "fsVersion": "3.0.0"
+ },
+ "buildConfig": {
+ "selfContained": false,
+ "jsdoc": false,
+ "includedTasks": [],
+ "excludedTasks": []
+ },
+ "id": "library.e",
+ "version": "1.0.0",
+ "namespace": "library/e",
+ "tags": {
+ "/resources/library/e/some.js": {
+ "ui5:HasDebugVariant": true
+ },
+ "/resources/library/e/some-dbg.js": {
+ "ui5:IsDebugVariant": true
+ }
+ }
+ }
+}
diff --git a/packages/project/test/fixtures/build-manifest/library.e/package.json b/packages/project/test/fixtures/build-manifest/library.e/package.json
new file mode 100644
index 00000000000..9ce874ff55a
--- /dev/null
+++ b/packages/project/test/fixtures/build-manifest/library.e/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.e",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for dev dependencies",
+ "devDependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/.library b/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/.library
new file mode 100644
index 00000000000..c1f37d77222
--- /dev/null
+++ b/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.e
+ SAP SE
+ Some fancy copyright
+ ${version}
+
+ Library E
+
+
diff --git a/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/some.js b/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/build-manifest/library.e/resources/library/e/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/build-manifest/library.e/test-resources/library/e/Test.html b/packages/project/test/fixtures/build-manifest/library.e/test-resources/library/e/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/collection.b/library.a/package.json b/packages/project/test/fixtures/collection.b/library.a/package.json
new file mode 100644
index 00000000000..aec498f7283
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/library.a/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "library.a",
+ "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/collection.b/library.a/src/library/a/.library b/packages/project/test/fixtures/collection.b/library.a/src/library/a/.library
new file mode 100644
index 00000000000..ef0ea1065bc
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/library.a/src/library/a/.library
@@ -0,0 +1,17 @@
+
+
+
+ library.a
+ SAP SE
+ Some fancy copyright ${currentYear}
+ ${version}
+
+ Library A
+
+
+
+ library.d
+
+
+
+
diff --git a/packages/project/test/fixtures/collection.b/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/collection.b/library.a/src/library/a/themes/base/library.source.less
new file mode 100644
index 00000000000..ff0f1d5e3df
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/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/collection.b/library.a/test/library/a/Test.html b/packages/project/test/fixtures/collection.b/library.a/test/library/a/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/collection.b/library.a/ui5.yaml b/packages/project/test/fixtures/collection.b/library.a/ui5.yaml
new file mode 100644
index 00000000000..8d4784313c3
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/library.a/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.a
diff --git a/packages/project/test/fixtures/collection.b/library.b/package.json b/packages/project/test/fixtures/collection.b/library.b/package.json
new file mode 100644
index 00000000000..2a0243b1683
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/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/collection.b/library.b/src/library/b/.library b/packages/project/test/fixtures/collection.b/library.b/src/library/b/.library
new file mode 100644
index 00000000000..7128151f3f4
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/library.b/src/library/b/.library
@@ -0,0 +1,17 @@
+
+
+
+ library.b
+ SAP SE
+ Some fancy copyright ${currentYear}
+ ${version}
+
+ Library B
+
+
+
+ library.d
+
+
+
+
diff --git a/packages/project/test/fixtures/collection.b/library.b/test/library/b/Test.html b/packages/project/test/fixtures/collection.b/library.b/test/library/b/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/collection.b/library.b/ui5.yaml b/packages/project/test/fixtures/collection.b/library.b/ui5.yaml
new file mode 100644
index 00000000000..b2fe5be59ee
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/library.b/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.b
diff --git a/packages/project/test/fixtures/collection.b/library.c/package.json b/packages/project/test/fixtures/collection.b/library.c/package.json
new file mode 100644
index 00000000000..64ac75d6ffe
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/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/collection.b/library.c/src/library/c/.library b/packages/project/test/fixtures/collection.b/library.c/src/library/c/.library
new file mode 100644
index 00000000000..4180ce2af2f
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/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/collection.b/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/collection.b/library.c/test/LibraryC/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/collection.b/library.c/ui5.yaml b/packages/project/test/fixtures/collection.b/library.c/ui5.yaml
new file mode 100644
index 00000000000..7c5e38a7fc1
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/library.c/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.c
diff --git a/packages/project/test/fixtures/collection.b/package.json b/packages/project/test/fixtures/collection.b/package.json
new file mode 100644
index 00000000000..25e37da0426
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "collection",
+ "version": "1.0.0",
+ "description": "Simple Collection used for Workspace testing",
+ "dependencies": {
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "ui5": {
+ "workspaces": [
+ "library.{a,b}",
+ "*c",
+ "sub-*"
+ ]
+ }
+}
diff --git a/packages/project/test/fixtures/collection.b/sub-collection/package.json b/packages/project/test/fixtures/collection.b/sub-collection/package.json
new file mode 100644
index 00000000000..046177300dc
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/sub-collection/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "sub-collection",
+ "version": "1.0.0",
+ "description": "Sub-Collection package",
+ "ui5": {
+ "workspaces": [
+ "../../library.d"
+ ]
+ }
+}
diff --git a/packages/project/test/fixtures/collection.b/sub-empty/.keep b/packages/project/test/fixtures/collection.b/sub-empty/.keep
new file mode 100644
index 00000000000..a550add6add
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/sub-empty/.keep
@@ -0,0 +1,2 @@
+This file is a stand-in for an empty project directory.
+This directory, even though matching the npm workspace configuration pattern, should be ignored by UI5 CLI.
diff --git a/packages/project/test/fixtures/collection.b/test.js b/packages/project/test/fixtures/collection.b/test.js
new file mode 100644
index 00000000000..d063db1e726
--- /dev/null
+++ b/packages/project/test/fixtures/collection.b/test.js
@@ -0,0 +1,4 @@
+import {globby} from 'globby';
+
+const paths = await globby(["library.a"]);
+console.log("paths")
diff --git a/packages/project/test/fixtures/collection/library.a/package.json b/packages/project/test/fixtures/collection/library.a/package.json
new file mode 100644
index 00000000000..aec498f7283
--- /dev/null
+++ b/packages/project/test/fixtures/collection/library.a/package.json
@@ -0,0 +1,9 @@
+{
+ "name": "library.a",
+ "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/collection/library.a/src/library/a/.library b/packages/project/test/fixtures/collection/library.a/src/library/a/.library
new file mode 100644
index 00000000000..ef0ea1065bc
--- /dev/null
+++ b/packages/project/test/fixtures/collection/library.a/src/library/a/.library
@@ -0,0 +1,17 @@
+
+
+
+ library.a
+ SAP SE
+ Some fancy copyright ${currentYear}
+ ${version}
+
+ Library A
+
+
+
+ library.d
+
+
+
+
diff --git a/packages/project/test/fixtures/collection/library.a/src/library/a/themes/base/library.source.less b/packages/project/test/fixtures/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/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/collection/library.a/test/library/a/Test.html b/packages/project/test/fixtures/collection/library.a/test/library/a/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/collection/library.a/ui5.yaml b/packages/project/test/fixtures/collection/library.a/ui5.yaml
new file mode 100644
index 00000000000..8d4784313c3
--- /dev/null
+++ b/packages/project/test/fixtures/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/collection/library.b/package.json b/packages/project/test/fixtures/collection/library.b/package.json
new file mode 100644
index 00000000000..2a0243b1683
--- /dev/null
+++ b/packages/project/test/fixtures/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/collection/library.b/src/library/b/.library b/packages/project/test/fixtures/collection/library.b/src/library/b/.library
new file mode 100644
index 00000000000..7128151f3f4
--- /dev/null
+++ b/packages/project/test/fixtures/collection/library.b/src/library/b/.library
@@ -0,0 +1,17 @@
+
+
+
+ library.b
+ SAP SE
+ Some fancy copyright ${currentYear}
+ ${version}
+
+ Library B
+
+
+
+ library.d
+
+
+
+
diff --git a/packages/project/test/fixtures/collection/library.b/test/library/b/Test.html b/packages/project/test/fixtures/collection/library.b/test/library/b/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/collection/library.b/ui5.yaml b/packages/project/test/fixtures/collection/library.b/ui5.yaml
new file mode 100644
index 00000000000..b2fe5be59ee
--- /dev/null
+++ b/packages/project/test/fixtures/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/collection/library.c/package.json b/packages/project/test/fixtures/collection/library.c/package.json
new file mode 100644
index 00000000000..64ac75d6ffe
--- /dev/null
+++ b/packages/project/test/fixtures/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/collection/library.c/src/library/c/.library b/packages/project/test/fixtures/collection/library.c/src/library/c/.library
new file mode 100644
index 00000000000..4180ce2af2f
--- /dev/null
+++ b/packages/project/test/fixtures/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/collection/library.c/test/LibraryC/Test.html b/packages/project/test/fixtures/collection/library.c/test/LibraryC/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/collection/library.c/ui5.yaml b/packages/project/test/fixtures/collection/library.c/ui5.yaml
new file mode 100644
index 00000000000..7c5e38a7fc1
--- /dev/null
+++ b/packages/project/test/fixtures/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/collection/node_modules/library.d/package.json b/packages/project/test/fixtures/collection/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/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/collection/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/collection/node_modules/library.d/src/library/d/.library
new file mode 100644
index 00000000000..21251d1bbba
--- /dev/null
+++ b/packages/project/test/fixtures/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/collection/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/collection/node_modules/library.d/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/collection/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/collection/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/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/collection/package.json b/packages/project/test/fixtures/collection/package.json
new file mode 100644
index 00000000000..24849dbe4a8
--- /dev/null
+++ b/packages/project/test/fixtures/collection/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "collection",
+ "version": "1.0.0",
+ "description": "Simple Collection",
+ "dependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "workspaces": [
+ "library.a",
+ "library.b",
+ "library.c"
+ ]
+}
diff --git a/packages/project/test/fixtures/collection/test.js b/packages/project/test/fixtures/collection/test.js
new file mode 100644
index 00000000000..d063db1e726
--- /dev/null
+++ b/packages/project/test/fixtures/collection/test.js
@@ -0,0 +1,4 @@
+import {globby} from 'globby';
+
+const paths = await globby(["library.a"]);
+console.log("paths")
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/package.json
new file mode 100644
index 00000000000..5077e3ac9f1
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@ui5-internal/application.cycle.a",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - Test for cyclic UI5 dependencies",
+ "main": "index.html",
+ "dependencies": {
+ "@ui5-internal/component.cycle.a": "file:../component.cycle.a"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/ui5.yaml
new file mode 100644
index 00000000000..7501387233f
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.cycle.a
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/index.html
new file mode 100644
index 00000000000..daa631f7ba8
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Application Cycle A
+
+
+
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/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/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/test.js
new file mode 100644
index 00000000000..a3df410c341
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.a/webapp/test.js
@@ -0,0 +1,5 @@
+function test(paramA) {
+ var variableA = paramA;
+ console.log(variableA);
+}
+test();
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/package.json
new file mode 100644
index 00000000000..68458d55e78
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@ui5-internal/application.cycle.b",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - Test for cyclic npm (non-UI5) dependencies - Cycle on second level via dev dependency",
+ "main": "index.html",
+ "dependencies": {
+ "@ui5-internal/module.d": "file:../module.d",
+ "@ui5-internal/module.e": "file:../module.e"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/ui5.yaml
new file mode 100644
index 00000000000..33e181aa9bb
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/ui5.yaml
@@ -0,0 +1,23 @@
+---
+specVersion: "2.2"
+type: application
+metadata:
+ name: application.cycle.b
+---
+specVersion: "2.2"
+kind: extension
+type: project-shim
+metadata:
+ name: application.cycle.b-shim
+shims:
+ configurations:
+ module.d:
+ specVersion: "2.2"
+ type: module
+ metadata:
+ name: module.d
+ module.e:
+ specVersion: "2.2"
+ type: module
+ metadata:
+ name: module.e
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/index.html
new file mode 100644
index 00000000000..fb845822a8e
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Application Cycle B
+
+
+
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/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/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/test.js
new file mode 100644
index 00000000000..a3df410c341
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.b/webapp/test.js
@@ -0,0 +1,5 @@
+function test(paramA) {
+ var variableA = paramA;
+ console.log(variableA);
+}
+test();
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/package.json
new file mode 100644
index 00000000000..f7ca7ac8d93
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@ui5-internal/application.cycle.c",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - Test for cyclic npm (non-UI5) dependencies - Cycle on third level",
+ "_ui5_test_comment": "This scenario can't be tested using file: URLs as npm can't create cyclic symlinks.",
+ "_ui5_test_comment2": "However publishing to and installing from a registry works.",
+ "main": "index.html",
+ "dependencies": {
+ "@ui5-internal/module.f": "^1.0.0",
+ "@ui5-internal/module.g": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/ui5.yaml
new file mode 100644
index 00000000000..18e66978522
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.cycle.c
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/index.html
new file mode 100644
index 00000000000..a845ac4258e
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Application Cycle C
+
+
+
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/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/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/test.js
new file mode 100644
index 00000000000..a3df410c341
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.c/webapp/test.js
@@ -0,0 +1,5 @@
+function test(paramA) {
+ var variableA = paramA;
+ console.log(variableA);
+}
+test();
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/package.json
new file mode 100644
index 00000000000..cf2ff767d66
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "@ui5-internal/application.cycle.d",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - Test for cyclic npm (non-UI5) dependencies - Heavily influenced by npm package 'es6-map'",
+ "_ui5_test_comment": "This scenario can't be tested using file: URLs as npm can't create cyclic symlinks.",
+ "_ui5_test_comment2": "However publishing to and installing from a registry works.",
+ "main": "index.html",
+ "dependencies": {
+ "@ui5-internal/module.h": "^1.0.0",
+ "@ui5-internal/module.i": "^1.0.0",
+ "@ui5-internal/module.j": "^1.0.0",
+ "@ui5-internal/module.k": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/ui5.yaml
new file mode 100644
index 00000000000..72d79ae25db
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.cycle.d
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/index.html
new file mode 100644
index 00000000000..aefe63db523
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Application Cycle D
+
+
+
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/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/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/test.js
new file mode 100644
index 00000000000..a3df410c341
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.d/webapp/test.js
@@ -0,0 +1,5 @@
+function test(paramA) {
+ var variableA = paramA;
+ console.log(variableA);
+}
+test();
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/package.json
new file mode 100644
index 00000000000..b2b7bc31eef
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@ui5-internal/application.cycle.e",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - Test for cyclic npm (non-UI5) dependencies - Indirect cycle via devDependency (pending dev)",
+ "main": "index.html",
+ "dependencies": {
+ "@ui5-internal/module.l": "^1.0.0",
+ "@ui5-internal/module.m": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/ui5.yaml
new file mode 100644
index 00000000000..c3d85496b83
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.cycle.e
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/index.html
new file mode 100644
index 00000000000..43cb37dc30b
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Application Cycle E
+
+
+
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/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/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/test.js
new file mode 100644
index 00000000000..a3df410c341
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.e/webapp/test.js
@@ -0,0 +1,5 @@
+function test(paramA) {
+ var variableA = paramA;
+ console.log(variableA);
+}
+test();
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/package.json
new file mode 100644
index 00000000000..f48e64bb3ed
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@ui5-internal/application.cycle.f",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - Test for cyclic npm UI5 dependencies - Cycle via projectPreprocessor deduplication",
+ "main": "index.html",
+ "dependencies": {
+ "@ui5-internal/library.cycle.c": "^1.0.0",
+ "@ui5-internal/library.cycle.d": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/ui5.yaml
new file mode 100644
index 00000000000..6c39c5b3f56
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.cycle.f
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/index.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/index.html
new file mode 100644
index 00000000000..38e2bf69eee
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Application Cycle F
+
+
+
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/manifest.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/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/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/test.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/test.js
new file mode 100644
index 00000000000..a3df410c341
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/application.cycle.f/webapp/test.js
@@ -0,0 +1,5 @@
+function test(paramA) {
+ var variableA = paramA;
+ console.log(variableA);
+}
+test();
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/package.json
new file mode 100644
index 00000000000..1a930a22dac
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "@ui5-internal/component.cycle.a",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - Test for cyclic dependencies",
+ "dependencies": {
+ "@ui5-internal/library.cycle.a": "file:../library.cycle.a",
+ "@ui5-internal/library.cycle.b": "file:../library.cycle.b"
+ },
+ "devDependencies": {
+ "@ui5-internal/application.cycle.a": "file:../application.cycle.a"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/.library
new file mode 100644
index 00000000000..e2eb7bd4c7c
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/.library
@@ -0,0 +1,11 @@
+
+
+
+ component.cycle.a
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Component Cycle A
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/src/component/cycle/a/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/test/component/cycle/a/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/test/component/cycle/a/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/ui5.yaml
new file mode 100644
index 00000000000..f63b38ea3ba
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/component.cycle.a/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: component.cycle.a
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/package.json
new file mode 100644
index 00000000000..2b6d08c4f2b
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/library.cycle.a",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - Test for cyclic dependencies",
+ "devDependencies": {
+ "@ui5-internal/component.cycle.a": "file:../component.cycle.a"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/.library
new file mode 100644
index 00000000000..f9cfdf7313d
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/.library
@@ -0,0 +1,11 @@
+
+
+
+ cycle.a
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library Cycle A
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/src/cycle/a/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/test/cycle/a/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/test/cycle/a/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/ui5.yaml
new file mode 100644
index 00000000000..5c42ee2e188
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.a/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.cycle.a
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/package.json
new file mode 100644
index 00000000000..303a3ad4cbc
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/library.cycle.b",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - Test for cyclic dependencies",
+ "devDependencies": {
+ "@ui5-internal/component.cycle.a": "file:../component.cycle.a"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/.library
new file mode 100644
index 00000000000..d7b129ae80e
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/.library
@@ -0,0 +1,11 @@
+
+
+
+ cycle.b
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library Cycle B
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/src/cycle/b/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/test/cycle/b/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/test/cycle/b/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/ui5.yaml
new file mode 100644
index 00000000000..fb6a25de09a
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.b/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.cycle.b
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/package.json
new file mode 100644
index 00000000000..32a39279058
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@ui5-internal/library.cycle.d",
+ "version": "0.9.0",
+ "description": "Simple SAPUI5 based library - Test for cyclic npm UI5 dependencies - Cycle via projectPreprocessor deduplication",
+ "dependencies": {
+ "@ui5-internal/library.cycle.c": "^1.0.0",
+ "@ui5-internal/library.cycle.e": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library
new file mode 100644
index 00000000000..af0aa5c6492
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.cycle.b
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library Cycle D
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/test/cycle/d/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/test/cycle/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/ui5.yaml
new file mode 100644
index 00000000000..52bb969a878
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/node_modules/@ui5-internal/library.cycle.d/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.cycle.d
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/package.json
new file mode 100644
index 00000000000..491f90eaf79
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/library.cycle.c",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - Test for cyclic npm UI5 dependencies - Cycle via projectPreprocessor deduplication",
+ "dependencies": {
+ "@ui5-internal/library.cycle.d": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/.library
new file mode 100644
index 00000000000..773fb893c19
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.cycle.b
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library Cycle C
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/src/cycle/c/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/test/cycle/c/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/test/cycle/c/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/ui5.yaml
new file mode 100644
index 00000000000..f6b4f2ac1e9
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.c/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.cycle.c
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/package.json
new file mode 100644
index 00000000000..b0b7a230c40
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@ui5-internal/library.cycle.d",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - Test for cyclic npm UI5 dependencies - Cycle via projectPreprocessor deduplication",
+ "dependencies": {
+ "@ui5-internal/library.cycle.c": "^1.0.0",
+ "@ui5-internal/library.cycle.e": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library
new file mode 100644
index 00000000000..af0aa5c6492
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.cycle.b
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library Cycle D
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/src/cycle/d/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/test/cycle/d/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/test/cycle/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/ui5.yaml
new file mode 100644
index 00000000000..52bb969a878
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.d/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.cycle.d
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/package.json
new file mode 100644
index 00000000000..9d5562b40ed
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/library.cycle.e",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - Test for cyclic npm UI5 dependencies - Cycle via projectPreprocessor deduplication",
+ "dependencies": {
+ "@ui5-internal/library.cycle.c": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/.library b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/.library
new file mode 100644
index 00000000000..dc182abd9b5
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.cycle.b
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library Cycle E
+
+
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/some.js b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/src/cycle/e/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/test/cycle/e/Test.html b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/test/cycle/e/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/ui5.yaml
new file mode 100644
index 00000000000..d20d4477a28
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/library.cycle.e/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.cycle.e
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/package.json
new file mode 100644
index 00000000000..5b7d5e167ac
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.a/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/module.a",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - cycle via module.c",
+ "dependencies": {
+ "@ui5-internal/module.b": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/package.json
new file mode 100644
index 00000000000..ab0bc9b7d03
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.b/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/module.b",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies",
+ "dependencies": {
+ "@ui5-internal/module.c": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/package.json
new file mode 100644
index 00000000000..630afc451bc
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/module.c",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies- cycle via module.a",
+ "dependencies": {
+ "@ui5-internal/module.a": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/ui5.yaml
new file mode 100644
index 00000000000..f79d97826f9
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.c/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: module
+metadata:
+ name: module.c
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/package.json
new file mode 100644
index 00000000000..35546680df3
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/module.d",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - cycle with module.e",
+ "dependencies": {
+ "@ui5-internal/module.e": "file:../module.e"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/ui5.yaml
new file mode 100644
index 00000000000..c65780ec58d
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.d/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: module
+metadata:
+ name: module.d
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/package.json
new file mode 100644
index 00000000000..72763a7dcad
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/module.e",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - cycle with module.d",
+ "devDependencies": {
+ "@ui5-internal/module.d": "file:../module.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/ui5.yaml
new file mode 100644
index 00000000000..e6487041ace
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.e/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: module
+metadata:
+ name: module.e
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/package.json
new file mode 100644
index 00000000000..dc4677fa6b1
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/module.f",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - cycle via module.c",
+ "dependencies": {
+ "@ui5-internal/module.a": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/ui5.yaml
new file mode 100644
index 00000000000..5834bf479ab
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.f/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: module
+metadata:
+ name: module.f
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/.npmrc
new file mode 100644
index 00000000000..32165d31db9
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/.npmrc
@@ -0,0 +1 @@
+registry=http://127.0.0.1:99999/
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/package.json
new file mode 100644
index 00000000000..71d0fd3468e
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/module.g",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - cycle via module.c",
+ "dependencies": {
+ "@ui5-internal/module.a": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/ui5.yaml
new file mode 100644
index 00000000000..dfadd749b12
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.g/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: module
+metadata:
+ name: module.g
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/package.json
new file mode 100644
index 00000000000..82ab89eae2e
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "@ui5-internal/module.h",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - Like npm package 'es6-symbol'",
+ "dependencies": {
+ "@ui5-internal/module.i": "^1.0.0",
+ "@ui5-internal/module.j": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/ui5.yaml
new file mode 100644
index 00000000000..d80eec70ea4
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.h/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: module
+metadata:
+ name: module.h
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/package.json
new file mode 100644
index 00000000000..62588d1f5f9
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/module.i",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - Like npm package 'es5-ext'",
+ "dependencies": {
+ "@ui5-internal/module.k": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/ui5.yaml
new file mode 100644
index 00000000000..d2872d2544a
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.i/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: module
+metadata:
+ name: module.i
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/package.json
new file mode 100644
index 00000000000..cbf6388cb19
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/module.j",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - Like npm package 'd'",
+ "dependencies": {
+ "@ui5-internal/module.i": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/ui5.yaml
new file mode 100644
index 00000000000..e9cb9133d6f
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.j/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: module
+metadata:
+ name: module.j
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/package.json
new file mode 100644
index 00000000000..e930465f54c
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/package.json
@@ -0,0 +1,13 @@
+{
+ "name": "@ui5-internal/module.k",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies - Like npm package 'es6-iterator'",
+ "dependencies": {
+ "@ui5-internal/module.h": "^1.0.0",
+ "@ui5-internal/module.i": "^1.0.0",
+ "@ui5-internal/module.j": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/ui5.yaml
new file mode 100644
index 00000000000..6c763884743
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.k/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: module
+metadata:
+ name: module.k
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/package.json
new file mode 100644
index 00000000000..29c6723878c
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/module.l",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies",
+ "devDependencies": {
+ "@ui5-internal/module.m": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/ui5.yaml
new file mode 100644
index 00000000000..9c50648e65e
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.l/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: module
+metadata:
+ name: module.l
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/.npmrc b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/.npmrc
new file mode 100644
index 00000000000..79a2e805cae
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/.npmrc
@@ -0,0 +1,3 @@
+registry=http://127.0.0.1:99999/
+ignore-scripts=true
+offline=true
\ No newline at end of file
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/package.json b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/package.json
new file mode 100644
index 00000000000..017b5e13cf0
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "@ui5-internal/module.m",
+ "version": "1.0.0",
+ "description": "Simple npm module - Test for cyclic npm (non-UI5) dependencies",
+ "dependencies": {
+ "@ui5-internal/module.l": "^1.0.0"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/ui5.yaml b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/ui5.yaml
new file mode 100644
index 00000000000..5f02be6185b
--- /dev/null
+++ b/packages/project/test/fixtures/cyclic-deps/node_modules/@ui5-internal/module.m/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: module
+metadata:
+ name: module.m
diff --git a/packages/project/test/fixtures/err.application.a/package.json b/packages/project/test/fixtures/err.application.a/package.json
new file mode 100644
index 00000000000..17af8f18fe5
--- /dev/null
+++ b/packages/project/test/fixtures/err.application.a/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "err.application.a",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based application - test for missing dependencies",
+ "main": "index.html",
+ "dependencies": {
+ "library.xx": "file:../not.existing"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/err.application.a/ui5.yaml b/packages/project/test/fixtures/err.application.a/ui5.yaml
new file mode 100644
index 00000000000..00090cd96ea
--- /dev/null
+++ b/packages/project/test/fixtures/err.application.a/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: err.app.a
diff --git a/packages/project/test/fixtures/err.application.a/webapp/index.html b/packages/project/test/fixtures/err.application.a/webapp/index.html
new file mode 100644
index 00000000000..d86c19d3d0c
--- /dev/null
+++ b/packages/project/test/fixtures/err.application.a/webapp/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Error Application A
+
+
+
+
+
diff --git a/packages/project/test/fixtures/err.application.a/webapp/manifest.json b/packages/project/test/fixtures/err.application.a/webapp/manifest.json
new file mode 100644
index 00000000000..781945df9dc
--- /dev/null
+++ b/packages/project/test/fixtures/err.application.a/webapp/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/err.application.a/webapp/test.js b/packages/project/test/fixtures/err.application.a/webapp/test.js
new file mode 100644
index 00000000000..a3df410c341
--- /dev/null
+++ b/packages/project/test/fixtures/err.application.a/webapp/test.js
@@ -0,0 +1,5 @@
+function test(paramA) {
+ var variableA = paramA;
+ console.log(variableA);
+}
+test();
diff --git a/packages/project/test/fixtures/extension.a.esm/lib/extensionModule.js b/packages/project/test/fixtures/extension.a.esm/lib/extensionModule.js
new file mode 100644
index 00000000000..c7a5c0758dd
--- /dev/null
+++ b/packages/project/test/fixtures/extension.a.esm/lib/extensionModule.js
@@ -0,0 +1,2 @@
+export default () => "extension module";
+export function determineRequiredDependencies () { return "required dependencies function" };
diff --git a/packages/project/test/fixtures/extension.a.esm/package.json b/packages/project/test/fixtures/extension.a.esm/package.json
new file mode 100644
index 00000000000..872a636590d
--- /dev/null
+++ b/packages/project/test/fixtures/extension.a.esm/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "extension.a",
+ "type": "module"
+}
diff --git a/packages/project/test/fixtures/extension.a/lib/extensionModule.js b/packages/project/test/fixtures/extension.a/lib/extensionModule.js
new file mode 100644
index 00000000000..44b74979801
--- /dev/null
+++ b/packages/project/test/fixtures/extension.a/lib/extensionModule.js
@@ -0,0 +1,2 @@
+module.exports = () => "extension module";
+module.exports.determineRequiredDependencies = () => "required dependencies function";
diff --git a/packages/project/test/fixtures/extension.a/package.json b/packages/project/test/fixtures/extension.a/package.json
new file mode 100644
index 00000000000..c3f37004f9f
--- /dev/null
+++ b/packages/project/test/fixtures/extension.a/package.json
@@ -0,0 +1,4 @@
+{
+ "name": "extension.a",
+ "version": "1.0.0"
+}
diff --git a/packages/project/test/fixtures/extension.a/ui5.yaml b/packages/project/test/fixtures/extension.a/ui5.yaml
new file mode 100644
index 00000000000..1ae473d86b2
--- /dev/null
+++ b/packages/project/test/fixtures/extension.a/ui5.yaml
@@ -0,0 +1,8 @@
+---
+specVersion: "2.0"
+kind: extension
+type: task
+metadata:
+ name: extension.a-ui5-yaml
+task:
+ path: lib/extensionModule.js
diff --git a/packages/project/test/fixtures/fsInterface/foo.txt b/packages/project/test/fixtures/fsInterface/foo.txt
new file mode 100644
index 00000000000..538451fc5e6
--- /dev/null
+++ b/packages/project/test/fixtures/fsInterface/foo.txt
@@ -0,0 +1 @@
+content of /foo.txt
\ No newline at end of file
diff --git a/packages/project/test/fixtures/glob/application.a/package.json b/packages/project/test/fixtures/glob/application.a/package.json
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.a/ui5.yaml b/packages/project/test/fixtures/glob/application.a/ui5.yaml
new file mode 100644
index 00000000000..b9dde7b16b2
--- /dev/null
+++ b/packages/project/test/fixtures/glob/application.a/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.a
diff --git a/packages/project/test/fixtures/glob/application.a/webapp/index.html b/packages/project/test/fixtures/glob/application.a/webapp/index.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.a/webapp/test.js b/packages/project/test/fixtures/glob/application.a/webapp/test.js
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.b/package.json b/packages/project/test/fixtures/glob/application.b/package.json
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.b/ui5.yaml b/packages/project/test/fixtures/glob/application.b/ui5.yaml
new file mode 100644
index 00000000000..7b5e5dd2359
--- /dev/null
+++ b/packages/project/test/fixtures/glob/application.b/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: application
+metadata:
+ name: application.b
diff --git a/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n/i18n.properties b/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n/i18n.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n/i18n_de.properties b/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n/i18n_de.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n_fr.properties b/packages/project/test/fixtures/glob/application.b/webapp/embedded/i18n_fr.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.b/webapp/embedded/manifest.json b/packages/project/test/fixtures/glob/application.b/webapp/embedded/manifest.json
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.b/webapp/i18n.properties b/packages/project/test/fixtures/glob/application.b/webapp/i18n.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.b/webapp/i18n/i18n.properties b/packages/project/test/fixtures/glob/application.b/webapp/i18n/i18n.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.b/webapp/i18n/i18n_de.properties b/packages/project/test/fixtures/glob/application.b/webapp/i18n/i18n_de.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.b/webapp/i18n/l10n.properties b/packages/project/test/fixtures/glob/application.b/webapp/i18n/l10n.properties
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/application.b/webapp/manifest.json b/packages/project/test/fixtures/glob/application.b/webapp/manifest.json
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/glob/package.json b/packages/project/test/fixtures/glob/package.json
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/init-application/package.json b/packages/project/test/fixtures/init-application/package.json
new file mode 100644
index 00000000000..63696d06cbd
--- /dev/null
+++ b/packages/project/test/fixtures/init-application/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "init-application",
+ "version": "1.0.0",
+ "private": true,
+ "description": "",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "UNLICENSED"
+}
diff --git a/packages/project/test/fixtures/init-application/webapp/.gitkeep b/packages/project/test/fixtures/init-application/webapp/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/init-invalid-no-package-json/webapp/.gitkeep b/packages/project/test/fixtures/init-invalid-no-package-json/webapp/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/init-invalid-webapp-src/package.json b/packages/project/test/fixtures/init-invalid-webapp-src/package.json
new file mode 100644
index 00000000000..a9eff2a7949
--- /dev/null
+++ b/packages/project/test/fixtures/init-invalid-webapp-src/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "init-invalid",
+ "version": "1.0.0",
+ "private": true,
+ "description": "",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "UNLICENSED"
+}
diff --git a/packages/project/test/fixtures/init-invalid-webapp-src/src/.gitkeep b/packages/project/test/fixtures/init-invalid-webapp-src/src/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/init-invalid-webapp-src/webapp/.gitkeep b/packages/project/test/fixtures/init-invalid-webapp-src/webapp/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/init-library/package.json b/packages/project/test/fixtures/init-library/package.json
new file mode 100644
index 00000000000..5037d8cbea4
--- /dev/null
+++ b/packages/project/test/fixtures/init-library/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "init-library",
+ "version": "1.0.0",
+ "private": true,
+ "description": "",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "UNLICENSED"
+}
diff --git a/packages/project/test/fixtures/init-library/src/.gitkeep b/packages/project/test/fixtures/init-library/src/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/init-library/test/.gitkeep b/packages/project/test/fixtures/init-library/test/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/lbt/modules/bundle.js b/packages/project/test/fixtures/lbt/modules/bundle.js
new file mode 100644
index 00000000000..2ee4601c9a3
--- /dev/null
+++ b/packages/project/test/fixtures/lbt/modules/bundle.js
@@ -0,0 +1,12 @@
+sap.ui.predefine("sap/m/CheckBox",["sap/ui/core/Control"],function(o){"use strict";var n=o.extend("sap.m.CheckBox");return n});
+sap.ui.predefine("sap/ui/core/Core",[],function(){"use strict";return {}});
+sap.ui.predefine("todo/Component",["sap/ui/core/UIComponent"],function(e){"use strict";return e.extend("todo.Component",{metadata:{manifest:"json"}})});
+sap.ui.predefine("todo/controller/App.controller",["sap/ui/core/mvc/Controller"],function(e){"use strict";return e.extend("todo.controller.App")});
+jQuery.sap.registerPreloadedModules({
+"version":"2.0",
+"modules":{
+ "sap/m/messagebundle.properties":'#This is the resource bundle for the SAPUI5 sar\r\n',
+ "todo/manifest.json":'{"sap.app":{"id":"todo","type":"application"},"sap.ui5":{"rootView":{"viewName":"todo.view.App","type":"XML","id":"app"},"models":{"i18n":{"type":"sap.ui.model.resource.ResourceModel","settings":{"bundleName":"todo.i18n.messageBundle"}},"":{"type":"sap.ui.model.json.JSONModel","settings":"/model/todoitems.json"}},"resources":{"css":[{"uri":"css/styles.css"}]}}}',
+ "todo/model/todoitems.json":'{"newTodo":"","todos":[{"title":"Start this app","completed":true},{"title":"Learn OpenUI5","completed":false}],"itemsRemovable":true,"someCompleted":true,"completedCount":1}',
+ "todo/view/App.view.xml":'
\n'
+}});
diff --git a/packages/project/test/fixtures/lbt/modules/declare_function_expr_scope.js b/packages/project/test/fixtures/lbt/modules/declare_function_expr_scope.js
new file mode 100644
index 00000000000..661eb022477
--- /dev/null
+++ b/packages/project/test/fixtures/lbt/modules/declare_function_expr_scope.js
@@ -0,0 +1,43 @@
+// uses a function expr + additional arguments to create a scope
+// Note that this scope style is old-fashioned and results in a JSLint warning!
+(function($1, window) {
+
+ //declares module sap.ui.testmodule
+ jQuery.sap.declare("sap.ui.testmodule");
+
+ // top level statements in the scope
+ jQuery.sap.require("top.require.void");
+ var x = jQuery.sap.require("top.require.var");
+ x = jQuery.sap.require("top.require.assign");
+ var xs = sap.ui.requireSync("top/requireSync/var");
+ xs = sap.ui.requireSync("top/requireSync/assign");
+
+ // a block with require statements
+ {
+ jQuery.sap.require("block.require.void");
+ var z = jQuery.sap.require("block.require.var");
+ z = jQuery.sap.require("block.require.assign");
+ var zs = sap.ui.requireSync("block/requireSync/var");
+ zs = sap.ui.requireSync("block/requireSync/assign");
+ }
+
+ // a nested function invocation with require statements
+ (function() {
+ jQuery.sap.require("nested.scope.require.void");
+ var z = jQuery.sap.require("nested.scope.require.var");
+ z = jQuery.sap.require("nested.scope.require.assign");
+ var zs = sap.ui.requireSync("nested/scope/requireSync/var");
+ zs = sap.ui.requireSync("nested/scope/requireSync/assign");
+ }());
+
+ //a nested function expression with require statements
+ (function() {
+ jQuery.sap.require("nested.scope2.require.void");
+ var z = jQuery.sap.require("nested.scope2.require.var");
+ z = jQuery.sap.require("nested.scope2.require.assign");
+ var zs = sap.ui.requireSync("nested/scope2/requireSync/var");
+ zs = sap.ui.requireSync("nested/scope2/requireSync/assign");
+ })();
+
+})(jQuery, this);
+
diff --git a/packages/project/test/fixtures/lbt/modules/declare_function_invocation_scope.js b/packages/project/test/fixtures/lbt/modules/declare_function_invocation_scope.js
new file mode 100644
index 00000000000..a1ba6cbba41
--- /dev/null
+++ b/packages/project/test/fixtures/lbt/modules/declare_function_invocation_scope.js
@@ -0,0 +1,41 @@
+//uses a function invocation expr to create a scope
+(function($2, window){
+
+ //declares module sap.ui.testmodule
+ jQuery.sap.declare("sap.ui.testmodule");
+
+ // top level statements in the scope
+ jQuery.sap.require("top.require.void");
+ var x = jQuery.sap.require("top.require.var");
+ x = jQuery.sap.require("top.require.assign");
+ var xs = sap.ui.requireSync("top/requireSync/var");
+ xs = sap.ui.requireSync("top/requireSync/assign");
+
+ // a block with require statements
+ {
+ jQuery.sap.require("block.require.void");
+ var z = jQuery.sap.require("block.require.var");
+ z = jQuery.sap.require("block.require.assign");
+ var zs = sap.ui.requireSync("block/requireSync/var");
+ zs = sap.ui.requireSync("block/requireSync/assign");
+ }
+
+ // a nested function invocation with require statements
+ (function() {
+ jQuery.sap.require("nested.scope.require.void");
+ var z = jQuery.sap.require("nested.scope.require.var");
+ z = jQuery.sap.require("nested.scope.require.assign");
+ var zs = sap.ui.requireSync("nested/scope/requireSync/var");
+ zs = sap.ui.requireSync("nested/scope/requireSync/assign");
+ }());
+
+ //a nested function expression with require statements
+ (function() {
+ jQuery.sap.require("nested.scope2.require.void");
+ var z = jQuery.sap.require("nested.scope2.require.var");
+ z = jQuery.sap.require("nested.scope2.require.assign");
+ var zs = sap.ui.requireSync("nested/scope2/requireSync/var");
+ zs = sap.ui.requireSync("nested/scope2/requireSync/assign");
+ })();
+
+}(jQuery, this));
diff --git a/packages/project/test/fixtures/lbt/modules/declare_toplevel.js b/packages/project/test/fixtures/lbt/modules/declare_toplevel.js
new file mode 100644
index 00000000000..f849f88de0d
--- /dev/null
+++ b/packages/project/test/fixtures/lbt/modules/declare_toplevel.js
@@ -0,0 +1,36 @@
+//declares module sap.ui.testmodule
+jQuery.sap.declare("sap.ui.testmodule");
+
+// top level statements in the scope
+jQuery.sap.require("top.require.void");
+var x = jQuery.sap.require("top.require.var");
+x = jQuery.sap.require("top.require.assign");
+var xs = sap.ui.requireSync("top/requireSync/var");
+xs = sap.ui.requireSync("top/requireSync/assign");
+
+// a block with require statements
+{
+ jQuery.sap.require("block.require.void");
+ var z = jQuery.sap.require("block.require.var");
+ z = jQuery.sap.require("block.require.assign");
+ var zs = sap.ui.requireSync("block/requireSync/var");
+ zs = sap.ui.requireSync("block/requireSync/assign");
+}
+
+// a nested function invocation with require statements
+(function() {
+ jQuery.sap.require("nested.scope.require.void");
+ var z = jQuery.sap.require("nested.scope.require.var");
+ z = jQuery.sap.require("nested.scope.require.assign");
+ var zs = sap.ui.requireSync("nested/scope/requireSync/var");
+ zs = sap.ui.requireSync("nested/scope/requireSync/assign");
+}());
+
+//a nested function expression with require statements
+(function() {
+ jQuery.sap.require("nested.scope2.require.void");
+ var z = jQuery.sap.require("nested.scope2.require.var");
+ z = jQuery.sap.require("nested.scope2.require.assign");
+ var zs = sap.ui.requireSync("nested/scope2/requireSync/var");
+ zs = sap.ui.requireSync("nested/scope2/requireSync/assign");
+})();
diff --git a/packages/project/test/fixtures/lbt/modules/define_toplevel_named.js b/packages/project/test/fixtures/lbt/modules/define_toplevel_named.js
new file mode 100644
index 00000000000..4f3f19cbcce
--- /dev/null
+++ b/packages/project/test/fixtures/lbt/modules/define_toplevel_named.js
@@ -0,0 +1,4 @@
+//declares module sap.ui.testmodule
+sap.ui.define("sap/ui/testmodule", ["define/arg1","define/arg2"], function(Strings,Dom,Something) {
+
+});
diff --git a/packages/project/test/fixtures/lbt/modules/define_toplevel_unnamed.js b/packages/project/test/fixtures/lbt/modules/define_toplevel_unnamed.js
new file mode 100644
index 00000000000..245c0697a0c
--- /dev/null
+++ b/packages/project/test/fixtures/lbt/modules/define_toplevel_unnamed.js
@@ -0,0 +1,4 @@
+//declares module sap.ui.testmodule
+sap.ui.define(["define/arg1","define/arg2"], function(Strings,Dom,Something) {
+
+});
diff --git a/packages/project/test/fixtures/lbt/modules/not_a_module.js b/packages/project/test/fixtures/lbt/modules/not_a_module.js
new file mode 100644
index 00000000000..786cc378cb2
--- /dev/null
+++ b/packages/project/test/fixtures/lbt/modules/not_a_module.js
@@ -0,0 +1 @@
+// this file does not contain a UI5 module definition
\ No newline at end of file
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/package.json b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/package.json
new file mode 100644
index 00000000000..d8f009d42d7
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.f",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "dependencies": {
+ "library.g": "file:../library.g"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/ui5.yaml
new file mode 100644
index 00000000000..52c17922bfd
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/node_modules/library.f/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.f
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/package.json b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/package.json
new file mode 100644
index 00000000000..d90b72ffa99
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "legacy.library.x",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - legacy library missing ui5.yaml",
+ "devDependencies": {
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/.library b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/.library
new file mode 100644
index 00000000000..af7a248dd3c
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/.library
@@ -0,0 +1,11 @@
+
+
+
+ legacy.library.x
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Legacy Library X
+
+
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/some.js b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/src/legacy/library/x/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/test/legacy/library/x/Test.html b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.x/test/legacy/library/x/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/package.json b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/package.json
new file mode 100644
index 00000000000..d8f009d42d7
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.f",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "dependencies": {
+ "library.g": "file:../library.g"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/ui5.yaml
new file mode 100644
index 00000000000..52c17922bfd
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/node_modules/library.f/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.f
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/package.json b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/package.json
new file mode 100644
index 00000000000..a46d2e8f452
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "legacy.library.y",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - legacy library missing ui5.yaml",
+ "devDependencies": {
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/.library b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/.library
new file mode 100644
index 00000000000..6c70bac8492
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/.library
@@ -0,0 +1,11 @@
+
+
+
+ legacy.library.y
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Legacy Library Y
+
+
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/some.js b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/src/legacy/library/y/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/test/legacy/library/y/Test.html b/packages/project/test/fixtures/legacy.collection.a/src/legacy.library.y/test/legacy/library/y/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/package.json b/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/package.json
new file mode 100644
index 00000000000..d8f009d42d7
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.f",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "dependencies": {
+ "library.g": "file:../library.g"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/ui5.yaml
new file mode 100644
index 00000000000..52c17922bfd
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.library.a/node_modules/library.f/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.f
diff --git a/packages/project/test/fixtures/legacy.library.a/package.json b/packages/project/test/fixtures/legacy.library.a/package.json
new file mode 100644
index 00000000000..d88840fbbb0
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.library.a/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "legacy.library.a",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - legacy library missing ui5.yaml",
+ "devDependencies": {
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/.library b/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/.library
new file mode 100644
index 00000000000..432c95e95db
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/.library
@@ -0,0 +1,11 @@
+
+
+
+ legacy.library.a
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Legacy Library A
+
+
diff --git a/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/some.js b/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.library.a/src/legacy/library/a/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/legacy.library.a/test/legacy/library/a/Test.html b/packages/project/test/fixtures/legacy.library.a/test/legacy/library/a/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/package.json b/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/package.json
new file mode 100644
index 00000000000..d8f009d42d7
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.f",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "dependencies": {
+ "library.g": "file:../library.g"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/ui5.yaml
new file mode 100644
index 00000000000..52c17922bfd
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.library.b/node_modules/library.f/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.f
diff --git a/packages/project/test/fixtures/legacy.library.b/package.json b/packages/project/test/fixtures/legacy.library.b/package.json
new file mode 100644
index 00000000000..643d8019f7d
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.library.b/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "legacy.library.b",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - legacy library missing ui5.yaml",
+ "devDependencies": {
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/.library b/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/.library
new file mode 100644
index 00000000000..0a8645fd7d1
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/.library
@@ -0,0 +1,11 @@
+
+
+
+ legacy.library.b
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Legacy Library B
+
+
diff --git a/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/some.js b/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/legacy.library.b/src/legacy/library/b/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/legacy.library.b/test/legacy/library/b/Test.html b/packages/project/test/fixtures/legacy.library.b/test/legacy/library/b/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/main/src/library/d/.library b/packages/project/test/fixtures/library.d-adtl-deps/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/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/library.d-adtl-deps/main/src/library/d/some.js b/packages/project/test/fixtures/library.d-adtl-deps/main/src/library/d/some.js
new file mode 100644
index 00000000000..719155d1e6d
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/main/src/library/d/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/main/test/library/d/Test.html b/packages/project/test/fixtures/library.d-adtl-deps/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/package.json b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/package.json
new file mode 100644
index 00000000000..d8f009d42d7
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.f",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "dependencies": {
+ "library.g": "file:../library.g"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/.library b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/.library
new file mode 100644
index 00000000000..c45172d48b8
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.f
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library F
+
+
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/some.js b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/src/library/f/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/ui5.yaml
new file mode 100644
index 00000000000..52c17922bfd
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.f/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.f
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/package.json b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/package.json
new file mode 100644
index 00000000000..fff39011db1
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.g",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "devDependencies": {
+ "library.f": "file:../library.f"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/.library b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/.library
new file mode 100644
index 00000000000..4d884278e90
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.g
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library G
+
+
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/some.js b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/src/library/g/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/ui5.yaml b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/ui5.yaml
new file mode 100644
index 00000000000..a20d2d4991c
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/node_modules/library.g/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.g
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/package.json b/packages/project/test/fixtures/library.d-adtl-deps/package.json
new file mode 100644
index 00000000000..10cd27bded1
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.d",
+ "version": "2.0.0",
+ "description": "Version of library.d that has additional dependencies defined. Used for testing UI5 Workspace resolutions",
+ "dependencies": {
+ "library.f": "file:../library.f"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/library.d-adtl-deps/ui5.yaml b/packages/project/test/fixtures/library.d-adtl-deps/ui5.yaml
new file mode 100644
index 00000000000..9c8d48e1127
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-adtl-deps/ui5.yaml
@@ -0,0 +1,9 @@
+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/library.d-depender/main/src/library/d/.library b/packages/project/test/fixtures/library.d-depender/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-depender/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/library.d-depender/main/src/library/d/some.js b/packages/project/test/fixtures/library.d-depender/main/src/library/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-depender/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/library.d-depender/main/test/library/d/Test.html b/packages/project/test/fixtures/library.d-depender/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/src/library/d/.library b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-depender/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/library.d-depender/node_modules/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/src/library/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-depender/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/library.d-depender/node_modules/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/library.d-depender/node_modules/library.d/package.json b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-depender/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/library.d-depender/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/library.d-depender/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-depender/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/library.d-depender/package.json b/packages/project/test/fixtures/library.d-depender/package.json
new file mode 100644
index 00000000000..9f88fc95f0c
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-depender/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.d-depender",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library",
+ "dependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/library.d-depender/ui5.yaml b/packages/project/test/fixtures/library.d-depender/ui5.yaml
new file mode 100644
index 00000000000..51744218833
--- /dev/null
+++ b/packages/project/test/fixtures/library.d-depender/ui5.yaml
@@ -0,0 +1,11 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.d-depender
+resources:
+ configuration:
+ paths:
+ src: main/src
+ test: main/test
+
diff --git a/packages/project/test/fixtures/library.d/main/src/library/d/.library b/packages/project/test/fixtures/library.d/main/src/library/d/.library
new file mode 100644
index 00000000000..53c2d14c9d6
--- /dev/null
+++ b/packages/project/test/fixtures/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/library.d/main/src/library/d/some.js b/packages/project/test/fixtures/library.d/main/src/library/d/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/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/library.d/main/test/library/d/Test.html b/packages/project/test/fixtures/library.d/main/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/library.d/package.json b/packages/project/test/fixtures/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/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/library.d/ui5.yaml b/packages/project/test/fixtures/library.d/ui5.yaml
new file mode 100644
index 00000000000..a47c1f64c3d
--- /dev/null
+++ b/packages/project/test/fixtures/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/library.e/node_modules/library.d/package.json b/packages/project/test/fixtures/library.e/node_modules/library.d/package.json
new file mode 100644
index 00000000000..90c75040abe
--- /dev/null
+++ b/packages/project/test/fixtures/library.e/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/library.e/node_modules/library.d/src/library/d/.library b/packages/project/test/fixtures/library.e/node_modules/library.d/src/library/d/.library
new file mode 100644
index 00000000000..21251d1bbba
--- /dev/null
+++ b/packages/project/test/fixtures/library.e/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/library.e/node_modules/library.d/test/library/d/Test.html b/packages/project/test/fixtures/library.e/node_modules/library.d/test/library/d/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/library.e/node_modules/library.d/ui5.yaml b/packages/project/test/fixtures/library.e/node_modules/library.d/ui5.yaml
new file mode 100644
index 00000000000..7b731df83f6
--- /dev/null
+++ b/packages/project/test/fixtures/library.e/node_modules/library.d/ui5.yaml
@@ -0,0 +1,3 @@
+---
+name: library.d
+type: library
diff --git a/packages/project/test/fixtures/library.e/package.json b/packages/project/test/fixtures/library.e/package.json
new file mode 100644
index 00000000000..9ce874ff55a
--- /dev/null
+++ b/packages/project/test/fixtures/library.e/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.e",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for dev dependencies",
+ "devDependencies": {
+ "library.d": "file:../library.d"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/library.e/src/library/e/.library b/packages/project/test/fixtures/library.e/src/library/e/.library
new file mode 100644
index 00000000000..26ff954f7b1
--- /dev/null
+++ b/packages/project/test/fixtures/library.e/src/library/e/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.e
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library E
+
+
diff --git a/packages/project/test/fixtures/library.e/src/library/e/some.js b/packages/project/test/fixtures/library.e/src/library/e/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/library.e/src/library/e/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/library.e/test/library/e/Test.html b/packages/project/test/fixtures/library.e/test/library/e/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/library.e/ui5-workspace.yaml b/packages/project/test/fixtures/library.e/ui5-workspace.yaml
new file mode 100644
index 00000000000..7e8fd36b8f1
--- /dev/null
+++ b/packages/project/test/fixtures/library.e/ui5-workspace.yaml
@@ -0,0 +1,13 @@
+specVersion: workspace/1.0
+metadata:
+ name: config-a
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
+---
+specVersion: workspace/1.0
+metadata:
+ name: config-b
+dependencyManagement:
+ resolutions:
+ - path: ../library.x
diff --git a/packages/project/test/fixtures/library.e/ui5.yaml b/packages/project/test/fixtures/library.e/ui5.yaml
new file mode 100644
index 00000000000..88ba07e82dd
--- /dev/null
+++ b/packages/project/test/fixtures/library.e/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.e
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/fixtures/library.f/node_modules/library.g/package.json b/packages/project/test/fixtures/library.f/node_modules/library.g/package.json
new file mode 100644
index 00000000000..fff39011db1
--- /dev/null
+++ b/packages/project/test/fixtures/library.f/node_modules/library.g/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.g",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "devDependencies": {
+ "library.f": "file:../library.f"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/library.f/node_modules/library.g/ui5.yaml b/packages/project/test/fixtures/library.f/node_modules/library.g/ui5.yaml
new file mode 100644
index 00000000000..a20d2d4991c
--- /dev/null
+++ b/packages/project/test/fixtures/library.f/node_modules/library.g/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.g
diff --git a/packages/project/test/fixtures/library.f/package.json b/packages/project/test/fixtures/library.f/package.json
new file mode 100644
index 00000000000..d8f009d42d7
--- /dev/null
+++ b/packages/project/test/fixtures/library.f/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.f",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "dependencies": {
+ "library.g": "file:../library.g"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/library.f/src/library/f/.library b/packages/project/test/fixtures/library.f/src/library/f/.library
new file mode 100644
index 00000000000..c45172d48b8
--- /dev/null
+++ b/packages/project/test/fixtures/library.f/src/library/f/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.f
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library F
+
+
diff --git a/packages/project/test/fixtures/library.f/src/library/f/some.js b/packages/project/test/fixtures/library.f/src/library/f/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/library.f/src/library/f/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/library.f/ui5.yaml b/packages/project/test/fixtures/library.f/ui5.yaml
new file mode 100644
index 00000000000..52c17922bfd
--- /dev/null
+++ b/packages/project/test/fixtures/library.f/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.f
diff --git a/packages/project/test/fixtures/library.g/node_modules/library.f/package.json b/packages/project/test/fixtures/library.g/node_modules/library.f/package.json
new file mode 100644
index 00000000000..d8f009d42d7
--- /dev/null
+++ b/packages/project/test/fixtures/library.g/node_modules/library.f/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.f",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "dependencies": {
+ "library.g": "file:../library.g"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/library.g/node_modules/library.f/ui5.yaml b/packages/project/test/fixtures/library.g/node_modules/library.f/ui5.yaml
new file mode 100644
index 00000000000..52c17922bfd
--- /dev/null
+++ b/packages/project/test/fixtures/library.g/node_modules/library.f/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.f
diff --git a/packages/project/test/fixtures/library.g/package.json b/packages/project/test/fixtures/library.g/package.json
new file mode 100644
index 00000000000..fff39011db1
--- /dev/null
+++ b/packages/project/test/fixtures/library.g/package.json
@@ -0,0 +1,11 @@
+{
+ "name": "library.g",
+ "version": "1.0.0",
+ "description": "Simple SAPUI5 based library - test for circular dependencies",
+ "devDependencies": {
+ "library.f": "file:../library.f"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ }
+}
diff --git a/packages/project/test/fixtures/library.g/src/library/g/.library b/packages/project/test/fixtures/library.g/src/library/g/.library
new file mode 100644
index 00000000000..4d884278e90
--- /dev/null
+++ b/packages/project/test/fixtures/library.g/src/library/g/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.g
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library G
+
+
diff --git a/packages/project/test/fixtures/library.g/src/library/g/some.js b/packages/project/test/fixtures/library.g/src/library/g/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/library.g/src/library/g/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/library.g/ui5.yaml b/packages/project/test/fixtures/library.g/ui5.yaml
new file mode 100644
index 00000000000..a20d2d4991c
--- /dev/null
+++ b/packages/project/test/fixtures/library.g/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.g
diff --git a/packages/project/test/fixtures/library.h/corrupt-ui5-workspace.yaml b/packages/project/test/fixtures/library.h/corrupt-ui5-workspace.yaml
new file mode 100644
index 00000000000..ecce9d7e78b
--- /dev/null
+++ b/packages/project/test/fixtures/library.h/corrupt-ui5-workspace.yaml
@@ -0,0 +1 @@
+|-\nfoo\nbar
diff --git a/packages/project/test/fixtures/library.h/custom-ui5-workspace.yaml b/packages/project/test/fixtures/library.h/custom-ui5-workspace.yaml
new file mode 100644
index 00000000000..177dea5f67c
--- /dev/null
+++ b/packages/project/test/fixtures/library.h/custom-ui5-workspace.yaml
@@ -0,0 +1,15 @@
+specVersion: workspace/1.0
+metadata:
+ name: library-d
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
+---
+specVersion: workspace/1.0
+metadata:
+ name: all-libraries
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
+ - path: ../library.e
+ - path: ../library.f
diff --git a/packages/project/test/fixtures/library.h/empty-ui5-workspace.yaml b/packages/project/test/fixtures/library.h/empty-ui5-workspace.yaml
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/library.h/invalid-ui5-workspace.yaml b/packages/project/test/fixtures/library.h/invalid-ui5-workspace.yaml
new file mode 100644
index 00000000000..ca4ad00f4d3
--- /dev/null
+++ b/packages/project/test/fixtures/library.h/invalid-ui5-workspace.yaml
@@ -0,0 +1,6 @@
+specVersion: wörkspace/1.0
+metadata:
+ name: default
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
diff --git a/packages/project/test/fixtures/library.h/src/.library b/packages/project/test/fixtures/library.h/src/.library
new file mode 100644
index 00000000000..8de6bd2ebba
--- /dev/null
+++ b/packages/project/test/fixtures/library.h/src/.library
@@ -0,0 +1,11 @@
+
+
+
+ library.h
+ SAP SE
+ ${copyright}
+ ${version}
+
+ Library G
+
+
diff --git a/packages/project/test/fixtures/library.h/src/manifest.json b/packages/project/test/fixtures/library.h/src/manifest.json
new file mode 100644
index 00000000000..2279cb6ce3d
--- /dev/null
+++ b/packages/project/test/fixtures/library.h/src/manifest.json
@@ -0,0 +1,26 @@
+{
+ "_version": "1.21.0",
+ "sap.app": {
+ "id": "library.h",
+ "type": "library",
+ "embeds": [],
+ "applicationVersion": {
+ "version": "1.0.0"
+ },
+ "title": "Library H",
+ "description": "Library H"
+ },
+ "sap.ui": {
+ "technology": "UI5",
+ "supportedThemes": []
+ },
+ "sap.ui5": {
+ "dependencies": {
+ "minUI5Version": "1.0",
+ "libs": {}
+ },
+ "library": {
+ "i18n": false
+ }
+ }
+}
diff --git a/packages/project/test/fixtures/library.h/src/some.js b/packages/project/test/fixtures/library.h/src/some.js
new file mode 100644
index 00000000000..81e73436075
--- /dev/null
+++ b/packages/project/test/fixtures/library.h/src/some.js
@@ -0,0 +1,4 @@
+/*!
+ * ${copyright}
+ */
+console.log('HelloWorld');
\ No newline at end of file
diff --git a/packages/project/test/fixtures/library.h/ui5-workspace.yaml b/packages/project/test/fixtures/library.h/ui5-workspace.yaml
new file mode 100644
index 00000000000..ac0ea1c35ba
--- /dev/null
+++ b/packages/project/test/fixtures/library.h/ui5-workspace.yaml
@@ -0,0 +1,15 @@
+specVersion: workspace/1.0
+metadata:
+ name: default
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
+---
+specVersion: workspace/1.0
+metadata:
+ name: all-libraries
+dependencyManagement:
+ resolutions:
+ - path: ../library.d
+ - path: ../library.e
+ - path: ../library.f
diff --git a/packages/project/test/fixtures/library.h/ui5.yaml b/packages/project/test/fixtures/library.h/ui5.yaml
new file mode 100644
index 00000000000..cbea83db544
--- /dev/null
+++ b/packages/project/test/fixtures/library.h/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.6"
+type: library
+metadata:
+ name: library.h
diff --git a/packages/project/test/fixtures/module.a/dev/devTools.js b/packages/project/test/fixtures/module.a/dev/devTools.js
new file mode 100644
index 00000000000..e035bfaeab6
--- /dev/null
+++ b/packages/project/test/fixtures/module.a/dev/devTools.js
@@ -0,0 +1 @@
+console.log("dev dev dev");
diff --git a/packages/project/test/fixtures/module.a/dist/index.js b/packages/project/test/fixtures/module.a/dist/index.js
new file mode 100644
index 00000000000..019c0f4bc8e
--- /dev/null
+++ b/packages/project/test/fixtures/module.a/dist/index.js
@@ -0,0 +1 @@
+console.log("Hello World!");
diff --git a/packages/project/test/fixtures/module.a/ui5.yaml b/packages/project/test/fixtures/module.a/ui5.yaml
new file mode 100644
index 00000000000..af957cf1ee8
--- /dev/null
+++ b/packages/project/test/fixtures/module.a/ui5.yaml
@@ -0,0 +1,5 @@
+---
+specVersion: "2.6"
+type: module
+metadata:
+ name: module.a
diff --git a/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme
new file mode 100644
index 00000000000..4c62f26114c
--- /dev/null
+++ b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theme
@@ -0,0 +1,9 @@
+
+
+
+ my_theme
+ me
+ ${copyright}
+ ${version}
+
+
\ No newline at end of file
diff --git a/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming
new file mode 100644
index 00000000000..83b6c785a87
--- /dev/null
+++ b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/.theming
@@ -0,0 +1,27 @@
+{
+ "sEntity": "Theme",
+ "sId": "sap_belize",
+ "oExtends": "base",
+ "sVendor": "SAP",
+ "aBundled": ["sap_belize_plus"],
+ "mCssScopes": {
+ "library": {
+ "sBaseFile": "library",
+ "sEmbeddingMethod": "APPEND",
+ "aScopes": [
+ {
+ "sLabel": "Contrast",
+ "sSelector": "sapContrast",
+ "sEmbeddedFile": "sap_belize_plus.library",
+ "sEmbeddedCompareFile": "library",
+ "sThemeIdSuffix": "Contrast",
+ "sThemability": "PUBLIC",
+ "aThemabilityFilter": [
+ "Color"
+ ],
+ "rExcludeSelector": "\\.sapContrastPlus\\W"
+ }
+ ]
+ }
+ }
+}
diff --git a/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less
new file mode 100644
index 00000000000..d3286002bfe
--- /dev/null
+++ b/packages/project/test/fixtures/theme.library.e/src/theme/library/e/themes/my_theme/library.source.less
@@ -0,0 +1,9 @@
+/*!
+ * ${copyright}
+ */
+
+@mycolor: blue;
+
+.sapUiBody {
+ background-color: @mycolor;
+}
diff --git a/packages/project/test/fixtures/theme.library.e/test/theme/library/e/Test.html b/packages/project/test/fixtures/theme.library.e/test/theme/library/e/Test.html
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/project/test/fixtures/theme.library.e/ui5.yaml b/packages/project/test/fixtures/theme.library.e/ui5.yaml
new file mode 100644
index 00000000000..cf89c2432e8
--- /dev/null
+++ b/packages/project/test/fixtures/theme.library.e/ui5.yaml
@@ -0,0 +1,9 @@
+---
+specVersion: "1.1"
+type: theme-library
+metadata:
+ name: theme.library.e
+ copyright: |-
+ UI development toolkit for HTML5 (OpenUI5)
+ * (c) Copyright 2009-xxx SAP SE or an SAP affiliate company.
+ * Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
diff --git a/packages/project/test/lib/build/ProjectBuilder.js b/packages/project/test/lib/build/ProjectBuilder.js
new file mode 100644
index 00000000000..64b36ab85e9
--- /dev/null
+++ b/packages/project/test/lib/build/ProjectBuilder.js
@@ -0,0 +1,812 @@
+import test from "ava";
+import sinonGlobal from "sinon";
+import path from "node:path";
+import esmock from "esmock";
+import {setLogLevel} from "@ui5/logger";
+import OutputStyleEnum from "../../../lib/build/helpers/ProjectBuilderOutputStyle.js";
+
+function noop() {}
+
+function getMockProject(type, id = "b") {
+ return {
+ getName: () => "project." + id,
+ getNamespace: () => "project/" + id,
+ getType: () => type,
+ getCopyright: noop,
+ getVersion: noop,
+ getReader: () => "reader",
+ getWorkspace: () => "workspace",
+ };
+}
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+ t.context.getRootNameStub = sinon.stub().returns("project.a");
+ t.context.getRootTypeStub = sinon.stub().returns("application");
+ t.context.taskRepository = "taskRepository";
+ t.context.graph = {
+ getRoot: () => {
+ return {
+ getName: t.context.getRootNameStub,
+ getType: t.context.getRootTypeStub,
+ };
+ },
+ isSealed: sinon.stub().returns(true),
+ getProjectNames: sinon.stub().returns([
+ "project.a",
+ "project.b",
+ "project.c",
+ ]),
+ getSize: sinon.stub().returns(3),
+ getDependencies: sinon.stub().returns([]).withArgs("project.a").returns(["project.b"]),
+ traverseBreadthFirst: async (start, callback) => {
+ if (callback) {
+ await callback({
+ project: getMockProject("library", "c")
+ });
+ return;
+ }
+ await start({
+ project: getMockProject("library", "a")
+ });
+ await start({
+ project: getMockProject("library", "c")
+ });
+ await start({
+ project: getMockProject("library", "b")
+ });
+ },
+ traverseDepthFirst: async (callback) => {
+ await callback({
+ project: getMockProject("library", "a")
+ });
+ await callback({
+ project: getMockProject("library", "b")
+ });
+ await callback({
+ project: getMockProject("library", "c")
+ });
+ },
+ getProject: sinon.stub().callsFake((projectName) => {
+ return getMockProject(...projectName.split("."));
+ })
+ };
+
+ t.context.ProjectBuilder = await esmock("../../../lib/build/ProjectBuilder.js");
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test("Missing graph parameters", (t) => {
+ const {ProjectBuilder} = t.context;
+ const err1 = t.throws(() => {
+ new ProjectBuilder({});
+ });
+ t.is(err1.message, "Missing parameter 'graph'",
+ "Threw with expected error message");
+
+ const err2 = t.throws(() => {
+ new ProjectBuilder({graph: "graph"});
+ });
+ t.is(err2.message, "Missing parameter 'taskRepository'",
+ "Threw with expected error message");
+});
+
+test("build", async (t) => {
+ const {graph, taskRepository, ProjectBuilder, sinon} = t.context;
+
+ const builder = new ProjectBuilder({graph, taskRepository});
+
+ const filterProjectStub = sinon.stub().returns(true);
+ const getProjectFilterStub = sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub);
+
+ const requiresBuildStub = sinon.stub().returns(true);
+ const runTasksStub = sinon.stub().resolves();
+ const projectBuildContextMock = {
+ getTaskRunner: () => {
+ return {
+ runTasks: runTasksStub,
+ };
+ },
+ requiresBuild: requiresBuildStub,
+ getProject: sinon.stub().returns(getMockProject("library"))
+ };
+ const createRequiredBuildContextsStub = sinon.stub(builder, "_createRequiredBuildContexts")
+ .resolves(new Map().set("project.a", projectBuildContextMock));
+
+ const registerCleanupSigHooksStub = sinon.stub(builder, "_registerCleanupSigHooks").returns("cleanup sig hooks");
+
+ const writeResultsStub = sinon.stub(builder, "_writeResults").resolves();
+ const deregisterCleanupSigHooksStub = sinon.stub(builder, "_deregisterCleanupSigHooks");
+ const executeCleanupTasksStub = sinon.stub(builder, "_executeCleanupTasks").resolves();
+
+ await builder.build({
+ destPath: "dest/path",
+ includedDependencies: ["dep a"],
+ excludedDependencies: ["dep b"]
+ });
+
+ t.is(getProjectFilterStub.callCount, 1, "_getProjectFilter got called once");
+ t.deepEqual(getProjectFilterStub.getCall(0).args[0], {
+ explicitIncludes: ["dep a"],
+ explicitExcludes: ["dep b"],
+ dependencyIncludes: undefined
+ }, "_getProjectFilter got called with correct arguments");
+
+ t.is(createRequiredBuildContextsStub.callCount, 1, "_createRequiredBuildContexts got called once");
+ t.deepEqual(createRequiredBuildContextsStub.getCall(0).args[0], [
+ "project.a", "project.b", "project.c"
+ ], "_createRequiredBuildContexts got called with correct arguments");
+
+ t.is(requiresBuildStub.callCount, 1, "ProjectBuildContext#requiresBuild got called once");
+ t.is(registerCleanupSigHooksStub.callCount, 1, "_registerCleanupSigHooksStub got called once");
+
+ t.is(runTasksStub.callCount, 1, "TaskRunner#runTasks got called once");
+
+ t.is(writeResultsStub.callCount, 1, "_writeResults got called once");
+ t.is(writeResultsStub.getCall(0).args[0], projectBuildContextMock,
+ "_writeResults got called with correct first argument");
+ t.is(writeResultsStub.getCall(0).args[1]._fsBasePath, path.resolve("dest/path") + path.sep,
+ "_writeResults got called with correct second argument");
+
+ t.is(deregisterCleanupSigHooksStub.callCount, 1, "_deregisterCleanupSigHooks got called once");
+ t.is(deregisterCleanupSigHooksStub.getCall(0).args[0], "cleanup sig hooks",
+ "_deregisterCleanupSigHooks got called with correct arguments");
+ t.is(executeCleanupTasksStub.callCount, 1, "_executeCleanupTasksStub got called once");
+});
+
+test("build: Missing dest parameter", async (t) => {
+ const {graph, taskRepository, ProjectBuilder} = t.context;
+
+ const builder = new ProjectBuilder({graph, taskRepository});
+
+ const err = await t.throwsAsync(builder.build({
+ destPath: "dest/path",
+ dependencyIncludes: "dependencyIncludes",
+ includedDependencies: ["dep a"],
+ excludedDependencies: ["dep b"]
+ }));
+
+ t.is(err.message,
+ "Parameter 'dependencyIncludes' can't be used in conjunction " +
+ "with parameters 'includedDependencies' or 'excludedDependencies",
+ "Threw with expected error message");
+});
+
+test("build: Too many dependency parameters", async (t) => {
+ const {graph, taskRepository, ProjectBuilder} = t.context;
+
+ const builder = new ProjectBuilder({graph, taskRepository});
+
+ const err = await t.throwsAsync(builder.build({
+ includedDependencies: ["dep a"],
+ excludedDependencies: ["dep b"]
+ }));
+
+ t.is(err.message, "Missing parameter 'destPath'", "Threw with expected error message");
+});
+
+test("build: createBuildManifest in conjunction with dependencies", async (t) => {
+ const {graph, taskRepository, ProjectBuilder, sinon} = t.context;
+ t.context.getRootTypeStub = sinon.stub().returns("library");
+ const builder = new ProjectBuilder({graph, taskRepository,
+ buildConfig: {
+ createBuildManifest: true
+ }
+ });
+
+ const filterProjectStub = sinon.stub().returns(true);
+ sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub);
+ const err = await t.throwsAsync(builder.build({
+ destPath: "dest/path",
+ includedDependencies: ["dep a"]
+ }));
+
+ t.is(err.message,
+ "It is currently not supported to request the creation of a build manifest while " +
+ "including any dependencies into the build result",
+ "Threw with expected error message");
+});
+
+test("build: Failure", async (t) => {
+ const {graph, taskRepository, ProjectBuilder, sinon} = t.context;
+
+ const builder = new ProjectBuilder({graph, taskRepository});
+
+ const filterProjectStub = sinon.stub().returns(true);
+ sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub);
+
+ const requiresBuildStub = sinon.stub().returns(true);
+ const runTasksStub = sinon.stub().rejects(new Error("Some Error"));
+ const projectBuildContextMock = {
+ requiresBuild: requiresBuildStub,
+ getTaskRunner: () => {
+ return {
+ runTasks: runTasksStub
+ };
+ },
+ getProject: sinon.stub().returns(getMockProject("library"))
+ };
+ sinon.stub(builder, "_createRequiredBuildContexts")
+ .resolves(new Map().set("project.a", projectBuildContextMock));
+
+ sinon.stub(builder, "_registerCleanupSigHooks").returns("cleanup sig hooks");
+ const writeResultsStub = sinon.stub(builder, "_writeResults").resolves();
+ const deregisterCleanupSigHooksStub = sinon.stub(builder, "_deregisterCleanupSigHooks");
+ const executeCleanupTasksStub = sinon.stub(builder, "_executeCleanupTasks").resolves();
+
+ const err = await t.throwsAsync(builder.build({
+ destPath: "dest/path",
+ includedDependencies: ["dep a"],
+ excludedDependencies: ["dep b"]
+ }));
+
+ t.is(err.message, "Some Error", "Threw with expected error message");
+
+ t.is(writeResultsStub.callCount, 0, "_writeResults did not get called");
+
+ t.is(deregisterCleanupSigHooksStub.callCount, 1, "_deregisterCleanupSigHooks got called once");
+ t.is(deregisterCleanupSigHooksStub.getCall(0).args[0], "cleanup sig hooks",
+ "_deregisterCleanupSigHooks got called with correct arguments");
+ t.is(executeCleanupTasksStub.callCount, 1, "_executeCleanupTasksStub got called once");
+});
+
+test.serial("build: Multiple projects", async (t) => {
+ const {graph, taskRepository, sinon} = t.context;
+
+ const buildLoggerMock = {
+ isLevelEnabled: sinon.stub(),
+ setProjects: sinon.stub(),
+ startProjectBuild: sinon.stub(),
+ endProjectBuild: sinon.stub(),
+ skipProjectBuild: sinon.stub(),
+
+ info: sinon.stub(),
+ verbose: sinon.stub(),
+ error: sinon.stub(),
+ };
+ // Function acts as constructor for our class mock
+ function CreateBuildLoggerMock(moduleName) {
+ t.is(moduleName, "ProjectBuilder", "BuildLogger created with expected moduleName");
+ return buildLoggerMock;
+ }
+ const ProjectBuilder = await esmock("../../../lib/build/ProjectBuilder.js", {
+ "@ui5/logger/internal/loggers/Build": CreateBuildLoggerMock
+ });
+
+ const builder = new ProjectBuilder({graph, taskRepository});
+
+ const filterProjectStub = sinon.stub().returns(true).onFirstCall().returns(false);
+ const getProjectFilterStub = sinon.stub(builder, "_getProjectFilter").resolves(filterProjectStub);
+
+ const requiresBuildAStub = sinon.stub().returns(true);
+ const requiresBuildBStub = sinon.stub().returns(false);
+ const requiresBuildCStub = sinon.stub().returns(true);
+ const getBuildMetadataStub = sinon.stub().returns({
+ timestamp: "2022-07-28T12:00:00.000Z",
+ age: "xx days"
+ });
+ const runTasksStub = sinon.stub().resolves();
+ const projectBuildContextMockA = {
+ getTaskRunner: () => {
+ return {
+ runTasks: runTasksStub
+ };
+ },
+ requiresBuild: requiresBuildAStub,
+ getProject: sinon.stub().returns(getMockProject("library", "a"))
+ };
+ const projectBuildContextMockB = {
+ getTaskRunner: () => {
+ return {
+ runTasks: runTasksStub
+ };
+ },
+ getBuildMetadata: getBuildMetadataStub,
+ requiresBuild: requiresBuildBStub,
+ getProject: sinon.stub().returns(getMockProject("library", "b"))
+ };
+ const projectBuildContextMockC = {
+ getTaskRunner: () => {
+ return {
+ runTasks: runTasksStub
+ };
+ },
+ requiresBuild: requiresBuildCStub,
+ getProject: sinon.stub().returns(getMockProject("library", "c"))
+ };
+ const createRequiredBuildContextsStub = sinon.stub(builder, "_createRequiredBuildContexts")
+ .resolves(new Map()
+ .set("project.a", projectBuildContextMockA)
+ .set("project.b", projectBuildContextMockB)
+ .set("project.c", projectBuildContextMockC)
+ );
+
+ const registerCleanupSigHooksStub = sinon.stub(builder, "_registerCleanupSigHooks").returns("cleanup sig hooks");
+ const writeResultsStub = sinon.stub(builder, "_writeResults").resolves();
+ const deregisterCleanupSigHooksStub = sinon.stub(builder, "_deregisterCleanupSigHooks");
+ const executeCleanupTasksStub = sinon.stub(builder, "_executeCleanupTasks").resolves();
+
+ setLogLevel("verbose");
+ await builder.build({
+ destPath: path.join("dest", "path"),
+ dependencyIncludes: "dependencyIncludes"
+ });
+ setLogLevel("info");
+
+ t.is(getProjectFilterStub.callCount, 1, "_getProjectFilter got called once");
+ t.deepEqual(getProjectFilterStub.getCall(0).args[0], {
+ explicitIncludes: [],
+ explicitExcludes: [],
+ dependencyIncludes: "dependencyIncludes"
+ }, "_getProjectFilter got called with correct arguments");
+
+ t.is(createRequiredBuildContextsStub.callCount, 1, "_createRequiredBuildContexts got called once");
+ t.deepEqual(createRequiredBuildContextsStub.getCall(0).args[0], [
+ "project.b", "project.c"
+ ], "_createRequiredBuildContexts got called with correct arguments");
+
+ t.is(requiresBuildAStub.callCount, 1, "TaskRunner#requiresBuild got called once times for library.a");
+ t.is(requiresBuildBStub.callCount, 1, "TaskRunner#requiresBuild got called once times for library.b");
+ t.is(requiresBuildCStub.callCount, 1, "TaskRunner#requiresBuild got called once times for library.c");
+ t.is(registerCleanupSigHooksStub.callCount, 1, "_registerCleanupSigHooksStub got called once");
+
+ t.is(runTasksStub.callCount, 2, "TaskRunner#runTasks got called twice"); // library.b does not require a build
+
+ t.is(writeResultsStub.callCount, 2, "_writeResults got called twice"); // library.a has not been requested
+ t.is(writeResultsStub.getCall(0).args[0], projectBuildContextMockB,
+ "_writeResults got called with correct first argument");
+ t.is(writeResultsStub.getCall(0).args[1]._fsBasePath, path.resolve("dest/path") + path.sep,
+ "_writeResults got called with correct second argument");
+ t.is(writeResultsStub.getCall(1).args[0], projectBuildContextMockC,
+ "_writeResults got called with correct first argument");
+ t.is(writeResultsStub.getCall(1).args[1]._fsBasePath, path.resolve("dest/path") + path.sep,
+ "_writeResults got called with correct second argument");
+
+ t.is(deregisterCleanupSigHooksStub.callCount, 1, "_deregisterCleanupSigHooks got called once");
+ t.is(deregisterCleanupSigHooksStub.getCall(0).args[0], "cleanup sig hooks",
+ "_deregisterCleanupSigHooks got called with correct arguments");
+ t.is(executeCleanupTasksStub.callCount, 1, "_executeCleanupTasksStub got called once");
+
+ t.is(buildLoggerMock.setProjects.callCount, 1, "BuildLogger#setProjects got called once");
+ t.deepEqual(buildLoggerMock.setProjects.firstCall.firstArg, [
+ "project.a",
+ "project.b",
+ "project.c",
+ ], "BuildLogger#setProjects got called with expected argument");
+ t.is(buildLoggerMock.startProjectBuild.callCount, 2,
+ "BuildLogger#startProjectBuild got called twice");
+ t.is(buildLoggerMock.startProjectBuild.getCall(0).firstArg, "project.a",
+ "BuildLogger#startProjectBuild got called with expected argument on first call");
+ t.is(buildLoggerMock.startProjectBuild.getCall(1).firstArg, "project.c",
+ "BuildLogger#startProjectBuild got called with expected argument on second call");
+ t.is(buildLoggerMock.endProjectBuild.callCount, 2,
+ "BuildLogger#endProjectBuild got called twice");
+ t.is(buildLoggerMock.endProjectBuild.getCall(0).firstArg, "project.a",
+ "BuildLogger#endProjectBuild got called with expected argument on first call");
+ t.is(buildLoggerMock.endProjectBuild.getCall(1).firstArg, "project.c",
+ "BuildLogger#endProjectBuild got called with expected argument on second call");
+ t.is(buildLoggerMock.skipProjectBuild.callCount, 1,
+ "BuildLogger#skipProjectBuild got called once");
+ t.is(buildLoggerMock.skipProjectBuild.getCall(0).firstArg, "project.b",
+ "BuildLogger#skipProjectBuild got called with expected argument");
+});
+
+test("_createRequiredBuildContexts", async (t) => {
+ const {graph, taskRepository, ProjectBuilder, sinon} = t.context;
+
+ const builder = new ProjectBuilder({graph, taskRepository});
+
+ const requiresBuildStub = sinon.stub().returns(true);
+ const getRequiredDependenciesStub = sinon.stub()
+ .returns(new Set())
+ .onFirstCall().returns(new Set(["project.b"])); // required dependency of project.a
+
+ const projectBuildContextMock = {
+ requiresBuild: requiresBuildStub,
+ getTaskRunner: () => {
+ return {
+ getRequiredDependencies: getRequiredDependenciesStub
+ };
+ }
+ };
+ const createProjectContextStub = sinon.stub(builder._buildContext, "createProjectContext")
+ .returns(projectBuildContextMock);
+ const projectBuildContexts = await builder._createRequiredBuildContexts(["project.a", "project.c"]);
+
+ t.is(requiresBuildStub.callCount, 3, "TaskRunner#requiresBuild got called three times");
+ t.is(getRequiredDependenciesStub.callCount, 3, "TaskRunner#getRequiredDependencies got called three times");
+
+ t.deepEqual(Object.fromEntries(projectBuildContexts), {
+ "project.a": projectBuildContextMock,
+ "project.b": projectBuildContextMock, // is a required dependency of project.a
+ "project.c": projectBuildContextMock,
+ }, "Returned expected project build contexts");
+
+ t.is(createProjectContextStub.callCount, 3, "BuildContext#createProjectContextStub got called three times");
+ t.is(createProjectContextStub.getCall(0).args[0].project.getName(), "project.a",
+ "First call to BuildContext#createProjectContextStub with expected project");
+ t.is(createProjectContextStub.getCall(1).args[0].project.getName(), "project.c",
+ "Second call to BuildContext#createProjectContextStub with expected project");
+ t.is(createProjectContextStub.getCall(2).args[0].project.getName(), "project.b",
+ "Third call to BuildContext#createProjectContextStub with expected project");
+});
+
+test.serial("_getProjectFilter with dependencyIncludes", async (t) => {
+ const {graph, taskRepository, sinon} = t.context;
+ const composeProjectListStub = sinon.stub().returns({
+ includedDependencies: ["project.b", "project.c"],
+ excludedDependencies: ["project.d", "project.e", "project.a"],
+ });
+ const ProjectBuilder = await esmock("../../../lib/build/ProjectBuilder.js", {
+ "../../../lib/build/helpers/composeProjectList.js": composeProjectListStub
+ });
+
+ const builder = new ProjectBuilder({graph, taskRepository});
+
+ const filterProject = await builder._getProjectFilter({
+ dependencyIncludes: "dependencyIncludes",
+ explicitIncludes: "explicitIncludes",
+ explicitExcludes: "explicitExcludes",
+ });
+
+ t.is(composeProjectListStub.callCount, 1, "composeProjectList got called once");
+ t.is(composeProjectListStub.getCall(0).args[0], graph,
+ "composeProjectList got called with correct graph argument");
+ t.is(composeProjectListStub.getCall(0).args[1], "dependencyIncludes",
+ "composeProjectList got called with correct include/exclude argument");
+
+ t.true(filterProject("project.a"), "project.a (root project) is always allowed");
+ t.true(filterProject("project.b"), "project.b is allowed");
+ t.true(filterProject("project.c"), "project.c is allowed");
+ t.false(filterProject("project.d"), "project.d is not allowed");
+ t.false(filterProject("project.e"), "project.e is not allowed");
+});
+
+test.serial("_getProjectFilter with explicit include/exclude", async (t) => {
+ const {graph, taskRepository, sinon} = t.context;
+ const composeProjectListStub = sinon.stub().returns({
+ includedDependencies: ["project.b", "project.c"],
+ excludedDependencies: ["project.d", "project.e", "project.a"],
+ });
+ const ProjectBuilder = await esmock("../../../lib/build/ProjectBuilder.js", {
+ "../../../lib/build/helpers/composeProjectList.js": composeProjectListStub
+ });
+
+ const builder = new ProjectBuilder({graph, taskRepository});
+
+ const filterProject = await builder._getProjectFilter({
+ explicitIncludes: "explicitIncludes",
+ explicitExcludes: "explicitExcludes",
+ });
+
+ t.is(composeProjectListStub.callCount, 1, "composeProjectList got called once");
+ t.is(composeProjectListStub.getCall(0).args[0], graph,
+ "composeProjectList got called with correct graph argument");
+ t.deepEqual(composeProjectListStub.getCall(0).args[1], {
+ includeDependencyTree: "explicitIncludes",
+ excludeDependencyTree: "explicitExcludes",
+ }, "composeProjectList got called with correct include/exclude argument");
+
+ t.true(filterProject("project.a"), "project.a (root project) is always allowed");
+ t.true(filterProject("project.b"), "project.b is allowed");
+ t.true(filterProject("project.c"), "project.c is allowed");
+ t.false(filterProject("project.d"), "project.d is not allowed");
+ t.false(filterProject("project.e"), "project.e is not allowed");
+});
+
+test("_writeResults", async (t) => {
+ const {ProjectBuilder, sinon} = t.context;
+ t.context.getRootTypeStub = sinon.stub().returns("library");
+ const {graph, taskRepository} = t.context;
+ const builder = new ProjectBuilder({
+ graph, taskRepository,
+ buildConfig: {
+ createBuildManifest: false,
+ otherBuildConfig: "yes"
+ }
+ });
+
+ const mockResources = [{
+ _resourceName: "resource.a",
+ getPath: () => "resource.a"
+ }, {
+ _resourceName: "resource.b",
+ getPath: () => "resource.b"
+ }, {
+ _resourceName: "resource.c",
+ getPath: () => "resource.c"
+ }];
+ const byGlobStub = sinon.stub().resolves(mockResources);
+ const getReaderStub = sinon.stub().returns({
+ byGlob: byGlobStub
+ });
+ const mockProject = getMockProject("library", "c");
+ mockProject.getReader = getReaderStub;
+
+ const getTagStub = sinon.stub().returns(false).onFirstCall().returns(true);
+ const projectBuildContextMock = {
+ getProject: () => mockProject,
+ getTaskUtil: () => {
+ return {
+ isRootProject: () => false,
+ getTag: getTagStub,
+ STANDARD_TAGS: {
+ OmitFromBuildResult: "OmitFromBuildResultTag"
+ }
+ };
+ }
+ };
+ const writerMock = {
+ write: sinon.stub().resolves()
+ };
+
+ await builder._writeResults(projectBuildContextMock, writerMock);
+
+ t.is(getReaderStub.callCount, 1, "One reader requested");
+ t.deepEqual(getReaderStub.getCall(0).args[0], {
+ style: "dist"
+ }, "Reader requested expected style");
+
+ t.is(byGlobStub.callCount, 1, "One byGlob call");
+ t.is(byGlobStub.getCall(0).args[0], "/**/*", "byGlob called with expected pattern");
+
+ t.is(getTagStub.callCount, 3, "TaskUtil#getTag got called three times");
+ t.is(getTagStub.getCall(0).args[0], mockResources[0], "TaskUtil#getTag called with first resource");
+ t.is(getTagStub.getCall(0).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value");
+ t.is(getTagStub.getCall(1).args[0], mockResources[1], "TaskUtil#getTag called with second resource");
+ t.is(getTagStub.getCall(1).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value");
+ t.is(getTagStub.getCall(2).args[0], mockResources[2], "TaskUtil#getTag called with third resource");
+ t.is(getTagStub.getCall(2).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value");
+
+ t.is(writerMock.write.callCount, 2, "Write got called twice");
+ t.is(writerMock.write.getCall(0).args[0], mockResources[1], "Write got called with second resource");
+ t.is(writerMock.write.getCall(1).args[0], mockResources[2], "Write got called with third resource");
+});
+
+test.serial("_writeResults: Create build manifest", async (t) => {
+ const {sinon} = t.context;
+ t.context.getRootTypeStub = sinon.stub().returns("library");
+ const {graph, taskRepository} = t.context;
+
+ const createBuildManifestStub = sinon.stub().returns({"build": "manifest"});
+ const createResourceStub = sinon.stub().returns("build manifest resource");
+ const ProjectBuilder = await esmock.p("../../../lib/build/ProjectBuilder.js", {
+ "../../../lib/build/helpers/createBuildManifest.js": createBuildManifestStub,
+ "@ui5/fs/resourceFactory": {
+ createResource: createResourceStub
+ }
+ });
+
+ const builder = new ProjectBuilder({
+ graph, taskRepository,
+ buildConfig: {
+ createBuildManifest: true,
+ otherBuildConfig: "yes"
+ }
+ });
+
+ const mockResources = [{
+ _resourceName: "resource.a",
+ getPath: () => "resource.a"
+ }, {
+ _resourceName: "resource.b",
+ getPath: () => "resource.b"
+ }, {
+ _resourceName: "resource.c",
+ getPath: () => "resource.c"
+ }];
+ const byGlobStub = sinon.stub().resolves(mockResources);
+ const getReaderStub = sinon.stub().returns({
+ byGlob: byGlobStub
+ });
+ const mockProject = getMockProject("library", "c");
+ mockProject.getReader = getReaderStub;
+
+ const getTagStub = sinon.stub().returns(false).onFirstCall().returns(true);
+ const projectBuildContextMock = {
+ getProject: () => mockProject,
+ getTaskUtil: () => {
+ return {
+ isRootProject: () => true,
+ getTag: getTagStub,
+ STANDARD_TAGS: {
+ OmitFromBuildResult: "OmitFromBuildResultTag"
+ }
+ };
+ }
+ };
+ const writerMock = {
+ write: sinon.stub().resolves()
+ };
+
+ await builder._writeResults(projectBuildContextMock, writerMock);
+
+ t.is(getReaderStub.callCount, 1, "One reader requested");
+ t.deepEqual(getReaderStub.getCall(0).args[0], {
+ style: "buildtime"
+ }, "Reader requested expected style");
+
+ t.is(byGlobStub.callCount, 1, "One byGlob call");
+ t.is(byGlobStub.getCall(0).args[0], "/**/*", "byGlob called with expected pattern");
+
+ t.is(createBuildManifestStub.callCount, 1, "createBuildManifest got called once");
+ t.is(createBuildManifestStub.getCall(0).args[0], mockProject,
+ "createBuildManifest got called with correct project");
+ t.deepEqual(createBuildManifestStub.getCall(0).args[1], {
+ createBuildManifest: true,
+ outputStyle: OutputStyleEnum.Default,
+ cssVariables: false,
+ excludedTasks: [],
+ includedTasks: [],
+ jsdoc: false,
+ selfContained: false,
+ }, "createBuildManifest got called with correct build configuration");
+
+ t.is(createResourceStub.callCount, 1, "One resource has been created");
+ t.deepEqual(createResourceStub.getCall(0).args[0], {
+ path: "/.ui5/build-manifest.json",
+ string: `{
+ "build": "manifest"
+}`
+ }, "Build manifest resource has been created with correct arguments");
+
+ t.is(getTagStub.callCount, 3, "TaskUtil#getTag got called three times");
+ t.is(getTagStub.getCall(0).args[0], mockResources[0], "TaskUtil#getTag called with first resource");
+ t.is(getTagStub.getCall(0).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value");
+ t.is(getTagStub.getCall(1).args[0], mockResources[1], "TaskUtil#getTag called with second resource");
+ t.is(getTagStub.getCall(1).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value");
+ t.is(getTagStub.getCall(2).args[0], mockResources[2], "TaskUtil#getTag called with third resource");
+ t.is(getTagStub.getCall(2).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value");
+
+ t.is(writerMock.write.callCount, 3, "Write got called three times");
+ t.is(writerMock.write.getCall(0).args[0], "build manifest resource", "Write got called with build manifest");
+ t.is(writerMock.write.getCall(1).args[0], mockResources[1], "Write got called with second resource");
+ t.is(writerMock.write.getCall(2).args[0], mockResources[2], "Write got called with third resource");
+
+ esmock.purge(ProjectBuilder);
+});
+
+test.serial("_writeResults: Flat build output", async (t) => {
+ const {sinon, ProjectBuilder} = t.context;
+ t.context.getRootTypeStub = sinon.stub().returns("library");
+ const {graph, taskRepository} = t.context;
+
+ const builder = new ProjectBuilder({
+ graph, taskRepository,
+ buildConfig: {
+ outputStyle: OutputStyleEnum.Flat,
+ otherBuildConfig: "yes"
+ }
+ });
+
+ const mockResources = [{
+ _resourceName: "resource.a",
+ getPath: () => "resource.a"
+ }, {
+ _resourceName: "resource.b",
+ getPath: () => "resource.b"
+ }, {
+ _resourceName: "resource.c",
+ getPath: () => "resource.c"
+ }];
+ const byGlobStub = sinon.stub().resolves(mockResources);
+ const getReaderStub = sinon.stub().returns({
+ byGlob: byGlobStub
+ });
+ const mockProject = getMockProject("library", "a");
+ mockProject.getReader = getReaderStub;
+
+ const getTagStub = sinon.stub().returns(false).onFirstCall().returns(true);
+ const projectBuildContextMock = {
+ getProject: () => mockProject,
+ getTaskUtil: () => {
+ return {
+ isRootProject: () => true,
+ getTag: getTagStub,
+ STANDARD_TAGS: {
+ OmitFromBuildResult: "OmitFromBuildResultTag"
+ }
+ };
+ }
+ };
+ const writerMock = {
+ write: sinon.stub().resolves()
+ };
+
+ await builder._writeResults(projectBuildContextMock, writerMock);
+
+ t.is(getReaderStub.callCount, 2, "One reader requested");
+ t.deepEqual(getReaderStub.getCall(0).args[0], {
+ style: "flat"
+ }, "Reader requested expected style");
+
+ t.is(byGlobStub.callCount, 2, "One byGlob call");
+ t.is(byGlobStub.getCall(0).args[0], "/**/*", "byGlob called with expected pattern");
+
+ t.is(getTagStub.callCount, 3, "TaskUtil#getTag got called three times");
+ t.is(getTagStub.getCall(0).args[0], mockResources[0], "TaskUtil#getTag called with first resource");
+ t.is(getTagStub.getCall(0).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value");
+ t.is(getTagStub.getCall(1).args[0], mockResources[1], "TaskUtil#getTag called with second resource");
+ t.is(getTagStub.getCall(1).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value");
+ t.is(getTagStub.getCall(2).args[0], mockResources[2], "TaskUtil#getTag called with third resource");
+ t.is(getTagStub.getCall(2).args[1], "OmitFromBuildResultTag", "TaskUtil#getTag called with correct tag value");
+
+ t.is(writerMock.write.callCount, 2, "Write got called twice");
+ t.is(writerMock.write.getCall(0).args[0], mockResources[1], "Write got called with second resource");
+ t.is(writerMock.write.getCall(1).args[0], mockResources[2], "Write got called with third resource");
+});
+
+
+test("_executeCleanupTasks", async (t) => {
+ const {graph, taskRepository, ProjectBuilder, sinon} = t.context;
+ const builder = new ProjectBuilder({graph, taskRepository});
+
+ const executeCleanupTasksStub = sinon.stub(builder._buildContext, "executeCleanupTasks");
+ await builder._executeCleanupTasks();
+ t.is(executeCleanupTasksStub.callCount, 1, "BuildContext#executeCleanupTasks got called once");
+ t.deepEqual(executeCleanupTasksStub.getCall(0).args, [undefined],
+ "BuildContext#executeCleanupTasks got called with correct arguments");
+
+ // reset stub
+ executeCleanupTasksStub.reset();
+ // Call with enforcement flag
+ await builder._executeCleanupTasks(true);
+ t.is(executeCleanupTasksStub.callCount, 1, "BuildContext#executeCleanupTasks got called once");
+ t.deepEqual(executeCleanupTasksStub.getCall(0).args, [true],
+ "BuildContext#executeCleanupTasks got called with correct arguments");
+});
+
+test("instantiate new logger for every ProjectBuilder", async (t) => {
+ function CreateBuildLoggerMock(moduleName) {
+ t.is(moduleName, "ProjectBuilder", "BuildLogger created with expected moduleName");
+ return {};
+ }
+
+ const {graph, taskRepository, sinon} = t.context;
+ const createBuildLoggerMockSpy = sinon.spy(CreateBuildLoggerMock);
+ const ProjectBuilder = await esmock("../../../lib/build/ProjectBuilder.js", {
+ "@ui5/logger/internal/loggers/Build": createBuildLoggerMockSpy
+ });
+
+ new ProjectBuilder({graph, taskRepository});
+ new ProjectBuilder({graph, taskRepository});
+
+ t.is(createBuildLoggerMockSpy.callCount, 2, "BuildLogger is instantiated for every ProjectBuilder instance");
+});
+
+
+function getProcessListenerCount() {
+ return ["SIGHUP", "SIGINT", "SIGTERM", "SIGBREAK"].map((eventName) => {
+ return process.listenerCount(eventName);
+ });
+}
+test("_registerCleanupSigHooks/_deregisterCleanupSigHooks", (t) => {
+ const listenersBefore = getProcessListenerCount();
+
+ const {graph, taskRepository, ProjectBuilder} = t.context;
+ const builder = new ProjectBuilder({graph, taskRepository});
+
+ const signals = builder._registerCleanupSigHooks();
+
+ t.deepEqual(Object.keys(signals), ["SIGHUP", "SIGINT", "SIGTERM", "SIGBREAK"],
+ "Returned four signal listeners");
+
+ t.deepEqual(getProcessListenerCount(), listenersBefore.map((x) => x+1),
+ "For every signal one new listener got registered");
+
+ builder._deregisterCleanupSigHooks(signals);
+
+ t.deepEqual(getProcessListenerCount(), listenersBefore,
+ "All signal listeners got de-registered");
+});
+
+test("_getElapsedTime", (t) => {
+ const {graph, taskRepository, ProjectBuilder} = t.context;
+ const builder = new ProjectBuilder({graph, taskRepository});
+
+ const res = builder._getElapsedTime(process.hrtime());
+ t.truthy(res, "Returned a value");
+});
diff --git a/packages/project/test/lib/build/TaskRunner.js b/packages/project/test/lib/build/TaskRunner.js
new file mode 100644
index 00000000000..73c9e187648
--- /dev/null
+++ b/packages/project/test/lib/build/TaskRunner.js
@@ -0,0 +1,1580 @@
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import {setLogLevel} from "@ui5/logger";
+setLogLevel("perf");
+
+function noop() {}
+function emptyarray() {
+ return [];
+}
+
+const buildConfig = {
+ selfContained: false,
+ jsdoc: false,
+ includedTasks: [],
+ excludedTasks: []
+};
+
+function getMockProject(type) {
+ return {
+ getName: () => "project.b",
+ getNamespace: () => "project/b",
+ getType: () => type,
+ getPropertiesFileSourceEncoding: noop,
+ getCopyright: noop,
+ getVersion: noop,
+ getMinificationExcludes: emptyarray,
+ getSpecVersion: () => {
+ return {
+ gte: () => false
+ };
+ },
+ getComponentPreloadPaths: () => [
+ "project/b/**/Component.js"
+ ],
+ getComponentPreloadNamespaces: emptyarray,
+ getComponentPreloadExcludes: emptyarray,
+ getLibraryPreloadExcludes: emptyarray,
+ getBundles: () => [{
+ bundleDefinition: {
+ name: "project/b/sectionsA/customBundle.js",
+ defaultFileTypes: [".js"],
+ sections: [{
+ mode: "preload",
+ filters: [
+ "project/b/sectionsA/",
+ "!project/b/sectionsA/section2**",
+ ]
+ }],
+ sort: true
+ },
+ bundleOptions: {
+ optimize: true,
+ usePredefinedCalls: true
+ }
+ }],
+ getCachebusterSignatureType: noop,
+ getCustomTasks: () => [],
+ hasBuildManifest: () => false,
+ getWorkspace: () => "workspace",
+ isFrameworkProject: () => false
+ };
+}
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.taskUtil = {
+ isRootProject: sinon.stub().returns(true),
+ getBuildOption: sinon.stub(),
+ getProject: sinon.stub(),
+ getDependencies: sinon.stub().returns(["dep.a", "dep.b"]),
+ getInterface: sinon.stub(),
+ };
+ t.context.taskUtil.getInterface.returns(t.context.taskUtil);
+
+ t.context.taskRepository = {
+ getTask: sinon.stub().callsFake(async (taskName) => {
+ throw new Error(`taskRepository: Unknown Task ${taskName}`);
+ }),
+ getAllTaskNames: sinon.stub().returns(["replaceVersion"]),
+ getRemovedTaskNames: sinon.stub().returns(["removedTask"]),
+ };
+
+ t.context.customTaskSpecVersionGteStub = sinon.stub().returns(true);
+ t.context.getRequiredDependenciesCallbackStub = sinon.stub().resolves(null);
+ t.context.customTask = {
+ getName: () => "custom task name",
+ getSpecVersion: () => {
+ return {
+ gte: t.context.customTaskSpecVersionGteStub
+ };
+ },
+ getRequiredDependenciesCallback: t.context.getRequiredDependenciesCallbackStub,
+ };
+
+ t.context.graph = {
+ getRoot: () => {
+ return {
+ getName: () => "graph-root"
+ };
+ },
+ getExtension: sinon.stub().returns(t.context.customTask),
+ traverseBreadthFirst: sinon.stub(),
+ getTransitiveDependencies: sinon.stub().returns(["dep.a", "dep.b", "dep.c"])
+ };
+
+ t.context.logger = {
+ getLogger: sinon.stub().returns("group logger")
+ };
+
+ t.context.projectBuildLogger = {
+ setTasks: sinon.stub(),
+ startTask: sinon.stub(),
+ endTask: sinon.stub(),
+ verbose: sinon.stub(),
+ perf: sinon.stub(),
+ isLevelEnabled: sinon.stub().returns(true),
+ };
+
+ t.context.resourceFactory = {
+ createReaderCollection: sinon.stub()
+ .returns("reader collection")
+ };
+
+ t.context.TaskRunner = await esmock("../../../lib/build/TaskRunner.js", {
+ "@ui5/logger": t.context.logger,
+ "@ui5/fs/resourceFactory": t.context.resourceFactory
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test("Missing parameters", (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ t.throws(() => {
+ new TaskRunner({
+ graph,
+ taskUtil,
+ taskRepository,
+ log: projectBuildLogger,
+ buildConfig
+ });
+ }, {
+ message: "TaskRunner: One or more mandatory parameters not provided"
+ }, "Threw with expected error message for missing project parameter");
+ t.throws(() => {
+ new TaskRunner({
+ project: getMockProject("application"),
+ taskUtil,
+ taskRepository,
+ log: projectBuildLogger,
+ buildConfig
+ });
+ }, {
+ message: "TaskRunner: One or more mandatory parameters not provided"
+ }, "Threw with expected error message for missing graph parameter");
+ t.throws(() => {
+ new TaskRunner({
+ project: getMockProject("application"),
+ graph,
+ taskRepository,
+ log: projectBuildLogger,
+ buildConfig
+ });
+ }, {
+ message: "TaskRunner: One or more mandatory parameters not provided"
+ }, "Threw with expected error message for missing taskUtil parameter");
+ t.throws(() => {
+ new TaskRunner({
+ project: getMockProject("application"),
+ graph,
+ taskUtil,
+ log: projectBuildLogger,
+ buildConfig
+ });
+ }, {
+ message: "TaskRunner: One or more mandatory parameters not provided"
+ }, "Threw with expected error message for missing taskRepository parameter");
+ t.throws(() => {
+ new TaskRunner({
+ project: getMockProject("application"),
+ graph,
+ taskUtil,
+ taskRepository,
+ buildConfig
+ });
+ }, {
+ message: "TaskRunner: One or more mandatory parameters not provided"
+ }, "Threw with expected error message for missing log parameter");
+ t.throws(() => {
+ new TaskRunner({
+ project: getMockProject("application"),
+ graph,
+ taskUtil,
+ taskRepository,
+ log: projectBuildLogger,
+ });
+ }, {
+ message: "TaskRunner: One or more mandatory parameters not provided"
+ }, "Threw with expected error message for missing buildConfig parameter");
+});
+
+test("_initTasks: Project of type 'application'", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskRunner = new TaskRunner({
+ project: getMockProject("application"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+ t.deepEqual(taskRunner._taskExecutionOrder, [
+ "escapeNonAsciiCharacters",
+ "replaceCopyright",
+ "replaceVersion",
+ "minify",
+ "enhanceManifest",
+ "generateFlexChangesBundle",
+ "generateComponentPreload",
+ "generateStandaloneAppBundle",
+ "transformBootstrapHtml",
+ "generateBundle",
+ "generateVersionInfo",
+ "generateCachebusterInfo",
+ "generateApiIndex",
+ "generateResourcesJson"
+ ], "Correct standard tasks");
+});
+
+test("_initTasks: Project of type 'library'", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskRunner = new TaskRunner({
+ project: getMockProject("library"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ t.deepEqual(taskRunner._taskExecutionOrder, [
+ "escapeNonAsciiCharacters",
+ "replaceCopyright",
+ "replaceVersion",
+ "replaceBuildtime",
+ "generateJsdoc",
+ "executeJsdocSdkTransformation",
+ "minify",
+ "generateLibraryManifest",
+ "enhanceManifest",
+ "generateComponentPreload",
+ "generateLibraryPreload",
+ "generateBundle",
+ "buildThemes",
+ "generateThemeDesignerResources",
+ "generateResourcesJson"
+ ], "Correct standard tasks");
+});
+
+test("_initTasks: Project of type 'library' (framework project)", async (t) => {
+ const {graph, taskUtil, taskRepository, projectBuildLogger, TaskRunner} = t.context;
+
+ const project = getMockProject("library");
+ project.isFrameworkProject = () => true;
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ t.deepEqual(taskRunner._taskExecutionOrder, [
+ "escapeNonAsciiCharacters",
+ "replaceCopyright",
+ "replaceVersion",
+ "replaceBuildtime",
+ "generateJsdoc",
+ "executeJsdocSdkTransformation",
+ "minify",
+ "generateLibraryManifest",
+ "enhanceManifest",
+ "generateComponentPreload",
+ "generateLibraryPreload",
+ "generateBundle",
+ "buildThemes",
+ "generateThemeDesignerResources",
+ "generateResourcesJson"
+ ], "Correct standard tasks");
+});
+
+test("_initTasks: Project of type 'theme-library'", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskRunner = new TaskRunner({
+ project: getMockProject("theme-library"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ t.deepEqual(taskRunner._taskExecutionOrder, [
+ "replaceCopyright",
+ "replaceVersion",
+ "buildThemes",
+ "generateThemeDesignerResources",
+ "generateResourcesJson"
+ ], "Correct standard tasks");
+});
+
+test("_initTasks: Project of type 'theme-library' (framework project)", async (t) => {
+ const {graph, taskUtil, taskRepository, projectBuildLogger, TaskRunner} = t.context;
+
+ const project = getMockProject("theme-library");
+ project.isFrameworkProject = () => true;
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ t.deepEqual(taskRunner._taskExecutionOrder, [
+ "replaceCopyright",
+ "replaceVersion",
+ "buildThemes",
+ "generateThemeDesignerResources",
+ "generateResourcesJson"
+ ], "Correct standard tasks");
+});
+
+test("_initTasks: Project of type 'module'", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskRunner = new TaskRunner({
+ project: getMockProject("module"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ t.deepEqual(taskRunner._taskExecutionOrder, [], "Correct standard tasks");
+});
+
+test("_initTasks: Unknown project type", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskRunner = new TaskRunner({
+ project: getMockProject("pony"), graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ const err = await t.throwsAsync(taskRunner._initTasks());
+
+ t.is(err.message, "Unknown project type pony", "Threw with expected error message");
+});
+
+test("_initTasks: Custom tasks", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("application");
+ project.getCustomTasks = () => [
+ {name: "myTask", afterTask: "minify"},
+ {name: "myOtherTask", beforeTask: "replaceVersion"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+ t.deepEqual(taskRunner._taskExecutionOrder, [
+ "escapeNonAsciiCharacters",
+ "replaceCopyright",
+ "myOtherTask",
+ "replaceVersion",
+ "minify",
+ "myTask",
+ "enhanceManifest",
+ "generateFlexChangesBundle",
+ "generateComponentPreload",
+ "generateStandaloneAppBundle",
+ "transformBootstrapHtml",
+ "generateBundle",
+ "generateVersionInfo",
+ "generateCachebusterInfo",
+ "generateApiIndex",
+ "generateResourcesJson"
+ ], "Custom tasks are inserted correctly");
+});
+
+test("_initTasks: Custom tasks with no standard tasks", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("module");
+ project.getCustomTasks = () => [
+ {name: "myTask"},
+ {name: "myOtherTask", beforeTask: "myTask"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+ t.deepEqual(taskRunner._taskExecutionOrder, [
+ "myOtherTask",
+ "myTask",
+ ], "ApplicationBuilder is still instantiated with standard tasks");
+});
+
+test("_initTasks: Custom tasks with no standard tasks and second task defining no before-/afterTask", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("module");
+ project.getCustomTasks = () => [
+ {name: "myTask"},
+ {name: "myOtherTask"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ const err = await t.throwsAsync(async () => {
+ await taskRunner._initTasks();
+ });
+ t.is(err.message,
+ `Custom task definition myOtherTask of project project.b defines neither a ` +
+ `"beforeTask" nor an "afterTask" parameter. One must be defined.`,
+ "Threw with expected error message");
+});
+
+test("_initTasks: Custom tasks with both, before- and afterTask reference", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("application");
+ project.getCustomTasks = () => [
+ {name: "myTask", beforeTask: "minify", afterTask: "replaceVersion"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ const err = await t.throwsAsync(async () => {
+ await taskRunner._initTasks();
+ });
+ t.is(err.message,
+ `Custom task definition myTask of project project.b defines both ` +
+ `"beforeTask" and "afterTask" parameters. Only one must be defined.`,
+ "Threw with expected error message");
+});
+
+test("_initTasks: Custom tasks with no before-/afterTask reference", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("application");
+ project.getCustomTasks = () => [
+ {name: "myTask"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ const err = await t.throwsAsync(async () => {
+ await taskRunner._initTasks();
+ });
+ t.is(err.message,
+ `Custom task definition myTask of project project.b defines neither a ` +
+ `"beforeTask" nor an "afterTask" parameter. One must be defined.`,
+ "Threw with expected error message");
+});
+
+test("_initTasks: Custom tasks without name", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("application");
+ project.getCustomTasks = () => [
+ {name: ""}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ const err = await t.throwsAsync(async () => {
+ await taskRunner._initTasks();
+ });
+ t.is(err.message,
+ `Missing name for custom task in configuration of project project.b`,
+ "Threw with expected error message");
+});
+
+test("_initTasks: Custom task with name of standard tasks", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("application");
+ project.getCustomTasks = () => [
+ {name: "replaceVersion", afterTask: "minify"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ const err = await t.throwsAsync(async () => {
+ await taskRunner._initTasks();
+ });
+ t.is(err.message,
+ "Custom task configuration of project project.b references standard task replaceVersion. " +
+ "Only custom tasks must be provided here.",
+ "Threw with expected error message");
+});
+
+test("_initTasks: Multiple custom tasks with same name", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("application");
+ project.getCustomTasks = () => [
+ {name: "myTask", afterTask: "minify"},
+ {name: "myTask", afterTask: "myTask"},
+ {name: "myTask", afterTask: "minify"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+ t.deepEqual(taskRunner._taskExecutionOrder, [
+ "escapeNonAsciiCharacters",
+ "replaceCopyright",
+ "replaceVersion",
+ "minify",
+ "myTask--3",
+ "myTask",
+ "myTask--2",
+ "enhanceManifest",
+ "generateFlexChangesBundle",
+ "generateComponentPreload",
+ "generateStandaloneAppBundle",
+ "transformBootstrapHtml",
+ "generateBundle",
+ "generateVersionInfo",
+ "generateCachebusterInfo",
+ "generateApiIndex",
+ "generateResourcesJson"
+ ], "Custom tasks are inserted correctly");
+});
+
+test("_initTasks: Custom tasks with unknown beforeTask", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("application");
+ project.getCustomTasks = () => [
+ {name: "myTask", beforeTask: "unknownTask"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ const err = await t.throwsAsync(async () => {
+ await taskRunner._initTasks();
+ });
+ t.is(err.message,
+ "Could not find task unknownTask, referenced by custom task myTask, " +
+ "to be scheduled for project project.b",
+ "Threw with expected error message");
+});
+
+test("_initTasks: Custom tasks with unknown afterTask", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("application");
+ project.getCustomTasks = () => [
+ {name: "myTask", afterTask: "unknownTask"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ const err = await t.throwsAsync(async () => {
+ await taskRunner._initTasks();
+ });
+ t.is(err.message,
+ "Could not find task unknownTask, referenced by custom task myTask, " +
+ "to be scheduled for project project.b",
+ "Threw with expected error message");
+});
+
+test("_initTasks: Custom tasks is unknown", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ graph.getExtension.returns(undefined);
+ const project = getMockProject("application");
+ project.getCustomTasks = () => [
+ {name: "myTask", afterTask: "minify"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ const err = await t.throwsAsync(async () => {
+ await taskRunner._initTasks();
+ });
+ t.is(err.message,
+ "Could not find custom task myTask, referenced by project project.b in project " +
+ "graph with root node graph-root",
+ "Threw with expected error message");
+});
+
+test("_initTasks: Custom tasks with removed beforeTask", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("application");
+ project.getCustomTasks = () => [
+ {name: "myTask", beforeTask: "removedTask"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ const err = await t.throwsAsync(async () => {
+ await taskRunner._initTasks();
+ });
+ t.is(err.message,
+ `Standard task removedTask, referenced by custom task myTask in project project.b, ` +
+ `has been removed in this version of UI5 CLI and can't be referenced anymore. ` +
+ `Please see the migration guide at https://ui5.github.io/cli/updates/migrate-v3/`,
+ "Threw with expected error message");
+});
+
+test("_initTasks: Create dependencies reader for all dependencies", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context;
+ const project = getMockProject("application");
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+ t.is(graph.traverseBreadthFirst.callCount, 1, "ProjectGraph#traverseBreadthFirst called once");
+ t.is(graph.traverseBreadthFirst.getCall(0).args[0], "project.b",
+ "ProjectGraph#traverseBreadthFirst called with correct project name for start");
+ const traversalCallback = graph.traverseBreadthFirst.getCall(0).args[1];
+
+ // Call with root project should be ignored
+ await traversalCallback({
+ project: {
+ getName: () => "project.b",
+ getReader: () => "project.b reader",
+ }
+ });
+ await traversalCallback({
+ project: {
+ getName: () => "dep.a",
+ getReader: () => "dep.a reader",
+ }
+ });
+ await traversalCallback({
+ project: {
+ getName: () => "dep.b",
+ getReader: () => "dep.b reader",
+ }
+ });
+ await traversalCallback({
+ project: {
+ getName: () => "transitive.dep.a",
+ getReader: () => "transitive.dep.a reader",
+ }
+ });
+ t.is(resourceFactory.createReaderCollection.callCount, 1, "createReaderCollection got called once");
+ t.deepEqual(resourceFactory.createReaderCollection.getCall(0).args[0], {
+ name: "Dependency reader collection of project project.b",
+ readers: [
+ "dep.a reader", "dep.b reader", "transitive.dep.a reader"
+ ]
+ }, "createReaderCollection got called with correct arguments");
+});
+
+test("Custom task is called correctly", async (t) => {
+ const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskStub = sinon.stub();
+ const specVersionGteStub = sinon.stub().returns(false);
+ const mockSpecVersion = {
+ toString: () => "2.6",
+ gte: specVersionGteStub
+ };
+
+ const getRequiredDependenciesCallbackStub = sinon.stub().resolves(undefined);
+ graph.getExtension.returns({
+ getTask: () => taskStub,
+ getSpecVersion: () => mockSpecVersion,
+ getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub
+ });
+ t.context.taskUtil.getInterface.returns("taskUtil interface");
+ const project = getMockProject("module");
+ project.getCustomTasks = () => [
+ {name: "myTask", configuration: "configuration"}
+ ];
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map");
+ t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.a", "dep.b"]),
+ "Custom tasks requires all dependencies by default");
+ const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies");
+ await taskRunner._tasks["myTask"].task();
+
+ t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice");
+ t.is(specVersionGteStub.getCall(0).args[0], "3.0",
+ "SpecificationVersion#gte got called with correct arguments on first call");
+ t.is(specVersionGteStub.getCall(1).args[0], "3.0",
+ "SpecificationVersion#gte got called with correct arguments on second call");
+
+ t.is(createDependencyReaderStub.callCount, 1, "_createDependenciesReader got called once");
+ t.deepEqual(createDependencyReaderStub.getCall(0).args[0],
+ new Set(["dep.a", "dep.b"]),
+ "_createDependenciesReader got called with correct arguments");
+
+ t.is(taskStub.callCount, 1, "Task got called once");
+ t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument");
+ t.deepEqual(taskStub.getCall(0).args[0], {
+ workspace: "workspace",
+ dependencies: "dependencies",
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ configuration: "configuration",
+ },
+ taskUtil: "taskUtil interface"
+ }, "Task got called with one argument");
+
+ t.is(taskUtil.getInterface.callCount, 1, "taskUtil#getInterface got called once");
+ t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion,
+ "taskUtil#getInterface got called with correct argument");
+});
+
+test("Custom task with legacy spec version", async (t) => {
+ const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskStub = sinon.stub();
+ const specVersionGteStub = sinon.stub().returns(false);
+ const mockSpecVersion = {
+ toString: () => "1.0",
+ gte: specVersionGteStub
+ };
+ const getRequiredDependenciesCallbackStub = sinon.stub().resolves(undefined);
+ graph.getExtension.returns({
+ getTask: () => taskStub,
+ getSpecVersion: () => mockSpecVersion,
+ getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub
+ });
+ t.context.taskUtil.getInterface.returns(undefined); // simulating no taskUtil for old specVersion
+ const project = getMockProject("module");
+ project.getCustomTasks = () => [
+ {name: "myTask", configuration: "configuration"}
+ ];
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map");
+ t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.a", "dep.b"]),
+ "Custom tasks requires all dependencies by default");
+
+ const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies");
+ await taskRunner._tasks["myTask"].task();
+
+ t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice");
+ t.is(specVersionGteStub.getCall(0).args[0], "3.0",
+ "SpecificationVersion#gte got called with correct arguments on first call");
+ t.is(specVersionGteStub.getCall(1).args[0], "3.0",
+ "SpecificationVersion#gte got called with correct arguments on second call");
+
+ t.is(createDependencyReaderStub.callCount, 1, "_createDependenciesReader got called once");
+ t.deepEqual(createDependencyReaderStub.getCall(0).args[0],
+ new Set(["dep.a", "dep.b"]),
+ "_createDependenciesReader got called with correct arguments");
+
+ t.is(taskStub.callCount, 1, "Task got called once");
+ t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument");
+ t.deepEqual(taskStub.getCall(0).args[0], {
+ workspace: "workspace",
+ dependencies: "dependencies",
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ configuration: "configuration",
+ }
+ }, "Task got called with one argument");
+
+ t.is(taskUtil.getInterface.callCount, 1, "taskUtil#getInterface got called once");
+ t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion,
+ "taskUtil#getInterface got called with correct argument");
+});
+
+test("Custom task with legacy spec version and requiredDependenciesCallback", async (t) => {
+ const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskStub = sinon.stub();
+ const specVersionGteStub = sinon.stub().returns(false);
+ const mockSpecVersion = {
+ toString: () => "1.0",
+ gte: specVersionGteStub
+ };
+ const requiredDependenciesCallbackStub = sinon.stub().resolves(new Set(["dep.b"]));
+ const getRequiredDependenciesCallbackStub = sinon.stub().resolves(requiredDependenciesCallbackStub);
+ graph.getExtension.returns({
+ getTask: () => taskStub,
+ getSpecVersion: () => mockSpecVersion,
+ getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub
+ });
+ t.context.taskUtil.getInterface.returns(undefined); // simulating no taskUtil for old specVersion
+ const project = getMockProject("module");
+ project.getCustomTasks = () => [
+ {name: "myTask", configuration: "configuration"}
+ ];
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map");
+ t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.b"]),
+ "Custom tasks requires all dependencies by default");
+
+ t.is(requiredDependenciesCallbackStub.callCount, 1, "requiredDependenciesCallback got called once");
+ t.deepEqual(requiredDependenciesCallbackStub.getCall(0).args[0], {
+ availableDependencies: new Set(["dep.a", "dep.b"]),
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ configuration: "configuration",
+ taskName: "myTask"
+ }
+ }, "requiredDependenciesCallback got called with expected arguments");
+
+ const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies");
+ await taskRunner._tasks["myTask"].task();
+
+ t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice");
+ t.is(specVersionGteStub.getCall(0).args[0], "3.0",
+ "SpecificationVersion#gte got called with correct arguments on first call");
+ t.is(specVersionGteStub.getCall(1).args[0], "3.0",
+ "SpecificationVersion#gte got called with correct arguments on second call");
+
+ t.is(createDependencyReaderStub.callCount, 1, "_createDependenciesReader got called once");
+ t.deepEqual(createDependencyReaderStub.getCall(0).args[0],
+ new Set(["dep.b"]),
+ "_createDependenciesReader got called with correct arguments");
+
+ t.is(taskStub.callCount, 1, "Task got called once");
+ t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument");
+ t.deepEqual(taskStub.getCall(0).args[0], {
+ workspace: "workspace",
+ dependencies: "dependencies",
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ configuration: "configuration",
+ }
+ }, "Task got called with one argument");
+
+ t.is(taskUtil.getInterface.callCount, 1, "taskUtil#getInterface got called once");
+ t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion,
+ "taskUtil#getInterface got called with correct argument");
+});
+
+test("Custom task with specVersion 3.0", async (t) => {
+ const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskStub = sinon.stub();
+ const specVersionGteStub = sinon.stub().returns(true);
+ const mockSpecVersion = {
+ toString: () => "3.0",
+ gte: specVersionGteStub
+ };
+
+ const requiredDependenciesCallbackStub = sinon.stub().resolves(new Set(["dep.b"]));
+ const getRequiredDependenciesCallbackStub = sinon.stub()
+ .resolves(requiredDependenciesCallbackStub);
+
+ graph.getExtension.returns({
+ getTask: () => taskStub,
+ getSpecVersion: () => mockSpecVersion,
+ getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub
+ });
+
+ const project = getMockProject("module");
+ project.getCustomTasks = () => [
+ {name: "myTask", configuration: "configuration"}
+ ];
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ t.is(requiredDependenciesCallbackStub.callCount, 1, "requiredDependenciesCallback got called once");
+ const requiredDependenciesCallbackArgs = requiredDependenciesCallbackStub.getCall(0).args[0];
+
+ t.is(typeof requiredDependenciesCallbackArgs.getProject, "function", "getProject function provided");
+ requiredDependenciesCallbackArgs.getProject("some.project");
+ t.is(taskUtil.getProject.callCount, 1, "taskUtil.getProject got called once");
+ t.is(taskUtil.getProject.getCall(0).args[0], "some.project",
+ "taskUtil.getProject got called with expected arguments");
+ requiredDependenciesCallbackArgs.getProject = "getProject function";
+
+ t.is(typeof requiredDependenciesCallbackArgs.getDependencies, "function", "getDependencies function provided");
+ requiredDependenciesCallbackArgs.getDependencies("some.project");
+ t.is(taskUtil.getDependencies.callCount, 2, "taskUtil.getDependencies got called twice");
+ t.is(taskUtil.getDependencies.getCall(1).args[0], "some.project",
+ "taskUtil.getDependencies got called with expected arguments");
+ requiredDependenciesCallbackArgs.getDependencies = "getDependencies function";
+
+ t.deepEqual(requiredDependenciesCallbackArgs, {
+ availableDependencies: new Set(["dep.a", "dep.b"]),
+ getProject: "getProject function",
+ getDependencies: "getDependencies function",
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ taskName: "myTask",
+ configuration: "configuration",
+ }
+ }, "requiredDependenciesCallback got called with expected arguments");
+
+ t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map");
+ t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.b"]),
+ "Custom tasks requires all dependencies by default");
+ const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies");
+ await taskRunner._tasks["myTask"].task();
+
+ t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice");
+ t.is(specVersionGteStub.getCall(0).args[0], "3.0",
+ "SpecificationVersion#gte got called with correct arguments on first call");
+ t.is(specVersionGteStub.getCall(1).args[0], "3.0",
+ "SpecificationVersion#gte got called with correct arguments on second call");
+
+ t.is(taskUtil.getInterface.callCount, 2, "taskUtil#getInterface got called twice");
+ t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion,
+ "taskUtil#getInterface got called with correct argument on first call");
+ t.is(taskUtil.getInterface.getCall(1).args[0], mockSpecVersion,
+ "taskUtil#getInterface got called with correct argument on second call");
+
+ t.is(createDependencyReaderStub.callCount, 1, "_createDependenciesReader got called once");
+ t.deepEqual(createDependencyReaderStub.getCall(0).args[0],
+ new Set(["dep.b"]),
+ "_createDependenciesReader got called with correct arguments");
+
+ t.is(taskStub.callCount, 1, "Task got called once");
+ t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument");
+ t.deepEqual(taskStub.getCall(0).args[0], {
+ workspace: "workspace",
+ dependencies: "dependencies",
+ log: "group logger",
+ taskUtil,
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ taskName: "myTask", // specVersion 3.0 feature
+ configuration: "configuration",
+ },
+ }, "Task got called with one argument");
+});
+
+test("Custom task with specVersion 3.0 and no requiredDependenciesCallback", async (t) => {
+ const {sinon, graph, taskUtil, taskRepository, projectBuildLogger, TaskRunner} = t.context;
+ const taskStub = sinon.stub();
+ const specVersionGteStub = sinon.stub().returns(true);
+ const mockSpecVersion = {
+ toString: () => "3.0",
+ gte: specVersionGteStub
+ };
+
+ const getRequiredDependenciesCallbackStub = sinon.stub().resolves(undefined);
+
+ graph.getExtension.returns({
+ getName: () => "custom task name",
+ getTask: () => taskStub,
+ getSpecVersion: () => mockSpecVersion,
+ getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub
+ });
+
+ const project = getMockProject("module");
+ project.getCustomTasks = () => [
+ {name: "myTask", configuration: "configuration"}
+ ];
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ t.truthy(taskRunner._tasks["myTask"], "Custom tasks has been added to task map");
+ t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(),
+ "Custom tasks requires no dependencies by default");
+ const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies");
+ await taskRunner._tasks["myTask"].task();
+
+ t.is(specVersionGteStub.callCount, 2, "SpecificationVersion#gte got called twice");
+ t.is(specVersionGteStub.getCall(0).args[0], "3.0",
+ "SpecificationVersion#gte got called with correct arguments on first call");
+ t.is(specVersionGteStub.getCall(1).args[0], "3.0",
+ "SpecificationVersion#gte got called with correct arguments on second call");
+
+ t.is(taskUtil.getInterface.callCount, 1, "taskUtil#getInterface got called once");
+ t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersion,
+ "taskUtil#getInterface got called with correct argument on first call");
+
+ t.is(createDependencyReaderStub.callCount, 0, "_createDependenciesReader did not get called");
+
+ t.is(taskStub.callCount, 1, "Task got called once");
+ t.is(taskStub.getCall(0).args.length, 1, "Task got called with one argument");
+ t.deepEqual(taskStub.getCall(0).args[0], {
+ workspace: "workspace",
+ log: "group logger",
+ taskUtil,
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ taskName: "myTask", // specVersion 3.0 feature
+ configuration: "configuration",
+ },
+ }, "Task got called with one argument");
+});
+
+test("Multiple custom tasks with same name are called correctly", async (t) => {
+ const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskStubA = sinon.stub();
+ const taskStubB = sinon.stub();
+ const taskStubC = sinon.stub();
+ const taskStubD = sinon.stub();
+ const mockSpecVersionA = {
+ toString: () => "2.5",
+ gte: () => false
+ };
+ const mockSpecVersionB = {
+ toString: () => "2.6",
+ gte: () => false
+ };
+ const mockSpecVersionC = {
+ toString: () => "3.0",
+ gte: () => true
+ };
+ const mockSpecVersionD = {
+ toString: () => "3.0",
+ gte: () => true
+ };
+ const requiredDependenciesCallbackStubA = sinon.stub().resolves(new Set(["dep.b"]));
+ const requiredDependenciesCallbackStubD = sinon.stub().resolves(new Set(["dep.a"]));
+ const getRequiredDependenciesCallbackStub = sinon.stub()
+ .resolves(null)
+ .onCall(0).resolves(requiredDependenciesCallbackStubA)
+ .onCall(3).resolves(requiredDependenciesCallbackStubD);
+
+ graph.getExtension.onFirstCall().returns({
+ getName: () => "Task Name A",
+ getTask: () => taskStubA,
+ getSpecVersion: () => mockSpecVersionA,
+ getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub
+ });
+ graph.getExtension.onSecondCall().returns({
+ getName: () => "Task Name B",
+ getTask: () => taskStubB,
+ getSpecVersion: () => mockSpecVersionB,
+ getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub
+ });
+ graph.getExtension.onThirdCall().returns({
+ getName: () => "Task Name C",
+ getTask: () => taskStubC,
+ getSpecVersion: () => mockSpecVersionC,
+ getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub
+ });
+ graph.getExtension.onCall(3).returns({
+ getName: () => "Task Name D",
+ getTask: () => taskStubD,
+ getSpecVersion: () => mockSpecVersionD,
+ getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub
+ });
+ const project = getMockProject("module");
+ project.getCustomTasks = () => [
+ {name: "myTask", configuration: "cat"},
+ {name: "myTask", afterTask: "myTask", configuration: "dog"},
+ {name: "myTask", afterTask: "myTask", configuration: "bird"},
+ {name: "myTask", afterTask: "myTask", configuration: "bird"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ // getRequiredDependenciesCallbackStub is only called for specVersion >= 3.0
+ t.is(getRequiredDependenciesCallbackStub.callCount, 4,
+ "getRequiredDependenciesCallback stub was called for all tasks");
+ t.is(requiredDependenciesCallbackStubA.callCount, 1,
+ "requiredDependenciesCallback stub for task A was called once");
+ t.is(requiredDependenciesCallbackStubD.callCount, 1,
+ "requiredDependenciesCallback stub for Task D stub was called once");
+
+ t.truthy(taskRunner._tasks["myTask"], "Custom tasks A has been added to task map");
+ t.truthy(taskRunner._tasks["myTask--2"], "Custom tasks B has been added to task map");
+ t.truthy(taskRunner._tasks["myTask--3"], "Custom tasks C has been added to task map");
+ t.truthy(taskRunner._tasks["myTask--4"], "Custom tasks D has been added to task map");
+ t.deepEqual(taskRunner._tasks["myTask"].requiredDependencies, new Set(["dep.b"]),
+ "Custom tasks with legacy specVersion and requiredDependenciesCallback defines " +
+ "required dependencies");
+ t.deepEqual(taskRunner._tasks["myTask--2"].requiredDependencies, new Set(["dep.a", "dep.b"]),
+ "Custom tasks with legacy specVersion require all dependencies by default");
+ t.deepEqual(taskRunner._tasks["myTask--3"].requiredDependencies, new Set([]),
+ "Custom tasks with specVersion 3.0 but no requiredDependenciesCallback " +
+ "require no dependencies by default");
+ t.deepEqual(taskRunner._tasks["myTask--4"].requiredDependencies, new Set(["dep.a"]),
+ "Custom tasks with specVersion 3.0 and requiredDependenciesCallback defines " +
+ "required dependencies");
+
+ // "Last in is the first out"
+ t.deepEqual(taskRunner._taskExecutionOrder, [
+ "myTask",
+ "myTask--4",
+ "myTask--3",
+ "myTask--2",
+ ], "Correct order of custom tasks");
+
+ const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies");
+ await taskRunner.runTasks();
+
+ t.is(projectBuildLogger.setTasks.callCount, 1, "ProjectBuildLogger#setTask got called once");
+ t.deepEqual(projectBuildLogger.setTasks.firstCall.firstArg, [
+ "myTask",
+ "myTask--4",
+ "myTask--3",
+ "myTask--2",
+ ], "ProjectBuildLogger#setTask got called with expected argument");
+
+ t.is(projectBuildLogger.startTask.callCount, 4, "ProjectBuildLogger#startTask got called four times");
+ t.deepEqual(projectBuildLogger.startTask.getCalls().map((call) => call.firstArg), [
+ "myTask",
+ "myTask--4",
+ "myTask--3",
+ "myTask--2",
+ ], "ProjectBuildLogger#startTask got called with expected arguments");
+ t.is(projectBuildLogger.endTask.callCount, 4, "ProjectBuildLogger#endTask got called four times");
+ t.deepEqual(projectBuildLogger.endTask.getCalls().map((call) => call.firstArg), [
+ "myTask",
+ "myTask--4",
+ "myTask--3",
+ "myTask--2",
+ ], "ProjectBuildLogger#endTask got called with expected arguments");
+
+ t.is(taskUtil.getInterface.callCount, 5, "taskUtil#getInterface got called three times");
+ t.is(taskUtil.getInterface.getCall(0).args[0], mockSpecVersionD,
+ "taskUtil#getInterface got called with correct argument on first call");
+ t.is(taskUtil.getInterface.getCall(1).args[0], mockSpecVersionA,
+ "taskUtil#getInterface got called with correct argument on second call");
+ t.is(taskUtil.getInterface.getCall(2).args[0], mockSpecVersionD,
+ "taskUtil#getInterface got called with correct argument on third call");
+ t.is(taskUtil.getInterface.getCall(3).args[0], mockSpecVersionC,
+ "taskUtil#getInterface got called with correct argument on fourth call");
+ t.is(taskUtil.getInterface.getCall(4).args[0], mockSpecVersionB,
+ "taskUtil#getInterface got called with correct argument on fifth call");
+
+ t.is(createDependencyReaderStub.callCount, 3, "_createDependenciesReader got called three times");
+ t.deepEqual(createDependencyReaderStub.getCall(0).args[0],
+ new Set(["dep.b"]),
+ "_createDependenciesReader got called with correct arguments on first call");
+ t.deepEqual(createDependencyReaderStub.getCall(1).args[0],
+ new Set(["dep.a"]),
+ "_createDependenciesReader got called with correct arguments on second call");
+ t.deepEqual(createDependencyReaderStub.getCall(2).args[0],
+ new Set(["dep.a", "dep.b"]),
+ "_createDependenciesReader got called with correct arguments on third call");
+
+ t.is(taskStubA.callCount, 1, "Task A got called once");
+ t.is(taskStubA.getCall(0).args.length, 1, "Task A got called with one argument");
+ t.deepEqual(taskStubA.getCall(0).args[0], {
+ workspace: "workspace",
+ dependencies: "dependencies",
+ taskUtil,
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ configuration: "cat",
+ }
+ }, "Task A got called with one argument");
+
+ t.is(taskStubB.callCount, 1, "Task B got called once");
+ t.is(taskStubB.getCall(0).args.length, 1, "Task B got called with one argument");
+ t.deepEqual(taskStubB.getCall(0).args[0], {
+ workspace: "workspace",
+ dependencies: "dependencies",
+ taskUtil,
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ configuration: "dog",
+ }
+ }, "Task B got called with one argument");
+
+ t.is(taskStubC.callCount, 1, "Task C got called once");
+ t.is(taskStubC.getCall(0).args.length, 1, "Task C got called with one argument");
+ t.deepEqual(taskStubC.getCall(0).args[0], {
+ workspace: "workspace",
+ log: "group logger",
+ taskUtil,
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ taskName: "myTask--3",
+ configuration: "bird",
+ }
+ }, "Task C got called with one argument");
+
+ t.is(taskStubD.callCount, 1, "Task D got called once");
+ t.is(taskStubD.getCall(0).args.length, 1, "Task D got called with one argument");
+ t.deepEqual(taskStubD.getCall(0).args[0], {
+ workspace: "workspace",
+ dependencies: "dependencies",
+ log: "group logger",
+ taskUtil,
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ taskName: "myTask--4",
+ configuration: "bird",
+ }
+ }, "Task D got called with one argument");
+});
+
+test("Custom task: requiredDependenciesCallback returns unknown dependency", async (t) => {
+ const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskStub = sinon.stub();
+ const specVersionGteStub = sinon.stub().returns(true);
+ const mockSpecVersion = {
+ toString: () => "3.0",
+ gte: specVersionGteStub
+ };
+
+ const requiredDependenciesCallbackStub = sinon.stub().resolves(new Set(["dep.b", "other.dep"]));
+ const getRequiredDependenciesCallbackStub = sinon.stub()
+ .resolves(requiredDependenciesCallbackStub);
+
+ graph.getExtension.returns({
+ getName: () => "custom.task.a",
+ getTask: () => taskStub,
+ getSpecVersion: () => mockSpecVersion,
+ getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub
+ });
+
+ const project = getMockProject("module");
+ project.getCustomTasks = () => [
+ {name: "myTask", configuration: "configuration"}
+ ];
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await t.throwsAsync(taskRunner._initTasks(), {
+ message:
+ `'determineRequiredDependencies' callback function of custom task custom.task.a ` +
+ `of project project.b must resolve with a subset of the the direct dependencies of the project. ` +
+ `other.dep is not a direct dependency of the project.`
+ }, "Threw with expected error message");
+});
+
+
+test("Custom task: requiredDependenciesCallback returns Array instead of Set", async (t) => {
+ const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskStub = sinon.stub();
+ const specVersionGteStub = sinon.stub().returns(true);
+ const mockSpecVersion = {
+ toString: () => "3.0",
+ gte: specVersionGteStub
+ };
+
+ const requiredDependenciesCallbackStub = sinon.stub().resolves(["dep.b"]);
+ const getRequiredDependenciesCallbackStub = sinon.stub()
+ .resolves(requiredDependenciesCallbackStub);
+
+ graph.getExtension.returns({
+ getName: () => "custom.task.a",
+ getTask: () => taskStub,
+ getSpecVersion: () => mockSpecVersion,
+ getRequiredDependenciesCallback: getRequiredDependenciesCallbackStub
+ });
+
+ const project = getMockProject("module");
+ project.getCustomTasks = () => [
+ {name: "myTask", configuration: "configuration"}
+ ];
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await t.throwsAsync(taskRunner._initTasks(), {
+ message:
+ `'determineRequiredDependencies' callback function of custom task custom.task.a ` +
+ `of project project.b must resolve with Set.`
+ }, "Threw with expected error message");
+});
+
+test("Custom task attached to a disabled task", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger, sinon, customTask} = t.context;
+
+ const project = getMockProject("application");
+ const customTaskFnStub = sinon.stub();
+ project.getBundles = emptyarray;
+ project.getCustomTasks = () => [
+ {name: "myTask", afterTask: "generateBundle", configuration: "dog"}
+ ];
+
+ taskRepository.getTask = sinon.stub().returns({task: sinon.stub()});
+ customTask.getTask = () => customTaskFnStub;
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+
+ await taskRunner.runTasks();
+
+ const setTasksArgs = projectBuildLogger.setTasks.firstCall.args[0];
+ t.true(setTasksArgs.includes("myTask"), "Custom task 'myTask' is queried");
+ t.is(customTaskFnStub.calledOnce, true, "Custom task 'myTask' is executed");
+ t.false(setTasksArgs.includes("generateBundle"),
+ "generateBundle standard task is excluded from the execution list");
+
+ t.deepEqual(
+ setTasksArgs,
+ [
+ "escapeNonAsciiCharacters",
+ "replaceCopyright",
+ "replaceVersion",
+ "minify",
+ "enhanceManifest",
+ "generateFlexChangesBundle",
+ "generateComponentPreload",
+ "myTask",
+ ],
+ "Correct tasks execution");
+});
+
+test.serial("_addTask", async (t) => {
+ const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+
+ const taskStub = sinon.stub();
+ taskRepository.getTask.withArgs("standardTask").resolves({
+ task: taskStub
+ });
+
+ const project = getMockProject("module");
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ taskRunner._addTask("standardTask");
+
+ t.truthy(taskRunner._tasks["standardTask"], "Task has been added to task map");
+ t.deepEqual(taskRunner._tasks["standardTask"].requiredDependencies, new Set(),
+ "By default, no dependencies required");
+ t.truthy(taskRunner._tasks["standardTask"].task, "Task function got set correctly");
+ t.deepEqual(taskRunner._taskExecutionOrder, ["standardTask"], "Task got added to execution order");
+
+ await taskRunner._tasks["standardTask"].task({
+ workspace: "workspace",
+ dependencies: "dependencies",
+ });
+
+ t.is(taskRepository.getTask.callCount, 1, "taskRepository#getTask got called once");
+ t.is(taskRepository.getTask.getCall(0).args[0], "standardTask",
+ "taskRepository#getTask got called with correct argument");
+ t.is(taskStub.callCount, 1, "Task got called once");
+ t.deepEqual(taskStub.getCall(0).args[0], {
+ workspace: "workspace",
+ // No dependencies
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b"
+ },
+ taskUtil
+ }, "Task got called with correct arguments");
+});
+
+test.serial("_addTask with options", async (t) => {
+ const {sinon, graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const taskStub = sinon.stub();
+ const project = getMockProject("module");
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ taskRunner._addTask("standardTask", {
+ requiresDependencies: true,
+ options: {
+ myTaskOption: "cat",
+ },
+ taskFunction: taskStub
+ });
+
+ t.truthy(taskRunner._tasks["standardTask"], "Task has been added to task map");
+ t.deepEqual(taskRunner._tasks["standardTask"].requiredDependencies, new Set(["dep.a", "dep.b"]),
+ "All dependencies required");
+ t.truthy(taskRunner._tasks["standardTask"].task, "Task function got set correctly");
+ t.deepEqual(taskRunner._taskExecutionOrder, ["standardTask"], "Task got added to execution order");
+
+ const createDependencyReaderStub = sinon.stub(taskRunner, "_createDependenciesReader").resolves("dependencies");
+ await taskRunner._tasks["standardTask"].task({
+ workspace: "workspace",
+ dependencies: "dependencies",
+ });
+
+ t.is(taskRepository.getTask.callCount, 0, "taskRepository#getTask did not get called");
+ t.is(createDependencyReaderStub.callCount, 0, "_createDependenciesReader did not get called");
+
+ t.is(taskStub.callCount, 1, "Task got called once");
+ t.deepEqual(taskStub.getCall(0).args[0], {
+ workspace: "workspace",
+ dependencies: taskRunner._allDependenciesReader,
+ options: {
+ projectName: "project.b",
+ projectNamespace: "project/b",
+ myTaskOption: "cat"
+ },
+ taskUtil
+ }, "Task got called with correct arguments");
+});
+
+test("_addTask: Duplicate task", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("module");
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ taskRunner._addTask("standardTask", {
+ taskFunction: () => {}
+ });
+
+ const err = t.throws(() => {
+ taskRunner._addTask("standardTask", {
+ taskFunction: () => {}
+ });
+ });
+ t.is(err.message, "Failed to add duplicate task standardTask for project project.b",
+ "Threw with expected error message");
+});
+
+test("_addTask: Task already added to execution order", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("module");
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+
+ taskRunner._taskExecutionOrder.push("standardTask");
+ const err = t.throws(() => {
+ taskRunner._addTask("standardTask", {
+ taskFunction: () => {}
+ });
+ });
+ t.is(err.message,
+ "Failed to add task standardTask for project project.b. It has already been scheduled for execution",
+ "Threw with expected error message");
+});
+
+test("getRequiredDependencies: Custom Task", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("module");
+ project.getCustomTasks = () => [
+ {name: "myTask"}
+ ];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ t.deepEqual(await taskRunner.getRequiredDependencies(), new Set([]),
+ "Project with custom task >= specVersion 3.0 and no requiredDependenciesCallback " +
+ "requires no dependencies");
+});
+
+test("getRequiredDependencies: Default application", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("application");
+ project.getBundles = () => [];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ t.deepEqual(await taskRunner.getRequiredDependencies(), new Set([]),
+ "Default application project does not require dependencies");
+});
+
+test("getRequiredDependencies: Default library", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("library");
+ project.getBundles = () => [];
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ t.deepEqual(await taskRunner.getRequiredDependencies(), new Set(["dep.a", "dep.b"]),
+ "Default library project requires dependencies");
+});
+
+test("getRequiredDependencies: Default theme-library", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("theme-library");
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ t.deepEqual(await taskRunner.getRequiredDependencies(), new Set(["dep.a", "dep.b"]),
+ "Default theme-library project requires dependencies");
+});
+
+test("getRequiredDependencies: Default module", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, projectBuildLogger} = t.context;
+ const project = getMockProject("module");
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ t.deepEqual(await taskRunner.getRequiredDependencies(), new Set([]),
+ "Default module project does not require dependencies");
+});
+
+test("_createDependenciesReader", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context;
+ const project = getMockProject("module");
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+ graph.traverseBreadthFirst.reset(); // Ignore the call in initTask
+ resourceFactory.createReaderCollection.reset(); // Ignore the call in initTask
+ resourceFactory.createReaderCollection.returns("custom reader collection");
+ const res = await taskRunner._createDependenciesReader(new Set(["dep.a"]));
+
+ t.is(graph.traverseBreadthFirst.callCount, 1, "ProjectGraph#traverseBreadthFirst got called once");
+ t.is(graph.traverseBreadthFirst.getCall(0).args[0], "project.b",
+ "ProjectGraph#traverseBreadthFirst called with correct project name for start");
+
+ const traversalCallback = graph.traverseBreadthFirst.getCall(0).args[1];
+
+ // Call with root project should be ignored
+ await traversalCallback({
+ project: {
+ getName: () => "project.b",
+ getReader: () => "project.b reader",
+ }
+ });
+ await traversalCallback({
+ project: {
+ getName: () => "dep.a",
+ getReader: () => "dep.a reader",
+ }
+ });
+ await traversalCallback({
+ project: {
+ getName: () => "dep.b",
+ getReader: () => "dep.b reader",
+ }
+ });
+ await traversalCallback({
+ project: {
+ getName: () => "dep.c",
+ getReader: () => "dep.c reader",
+ }
+ });
+ await traversalCallback({
+ project: {
+ // Will be ignored as it is no (transitive) dependency of the project
+ getName: () => "other project",
+ getReader: () => "other project reader",
+ }
+ });
+ t.is(resourceFactory.createReaderCollection.callCount, 1, "createReaderCollection got called once");
+ t.deepEqual(resourceFactory.createReaderCollection.getCall(0).args[0], {
+ name: "Reduced dependency reader collection of project project.b",
+ readers: [
+ "dep.a reader", "dep.b reader", "dep.c reader"
+ ]
+ }, "createReaderCollection got called with correct arguments");
+ t.is(res, "custom reader collection", "Returned expected value");
+});
+
+test("_createDependenciesReader: All dependencies required", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context;
+ const project = getMockProject("module");
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+ graph.traverseBreadthFirst.reset(); // Ignore the call in initTask
+ resourceFactory.createReaderCollection.reset(); // Ignore the call in initTask
+ resourceFactory.createReaderCollection.returns("custom reader collection");
+ const res = await taskRunner._createDependenciesReader(new Set(["dep.a", "dep.b"]));
+ t.is(graph.traverseBreadthFirst.callCount, 0, "ProjectGraph#traverseBreadthFirst did not get called again");
+ t.is(resourceFactory.createReaderCollection.callCount, 0, "createReaderCollection did not get called again");
+ t.is(res, "reader collection", "Shared (all-)dependency reader returned");
+});
+
+test("_createDependenciesReader: No dependencies required", async (t) => {
+ const {graph, taskUtil, taskRepository, TaskRunner, resourceFactory, projectBuildLogger} = t.context;
+ const project = getMockProject("module");
+
+ const taskRunner = new TaskRunner({
+ project, graph, taskUtil, taskRepository, log: projectBuildLogger, buildConfig
+ });
+ await taskRunner._initTasks();
+ graph.traverseBreadthFirst.reset(); // Ignore the call in initTask
+ resourceFactory.createReaderCollection.reset(); // Ignore the call in initTask
+ resourceFactory.createReaderCollection.returns("custom reader collection");
+ const res = await taskRunner._createDependenciesReader(new Set());
+ t.is(graph.traverseBreadthFirst.callCount, 1, "ProjectGraph#traverseBreadthFirst got called once");
+ t.is(resourceFactory.createReaderCollection.callCount, 1, "createReaderCollection got called once");
+ t.deepEqual(resourceFactory.createReaderCollection.getCall(0).args[0].readers, [],
+ "createReaderCollection got called with no readers");
+ t.is(res, "custom reader collection", "Shared (all-)dependency reader returned");
+});
+
diff --git a/packages/project/test/lib/build/definitions/application.js b/packages/project/test/lib/build/definitions/application.js
new file mode 100644
index 00000000000..742d398e988
--- /dev/null
+++ b/packages/project/test/lib/build/definitions/application.js
@@ -0,0 +1,521 @@
+import test from "ava";
+import sinon from "sinon";
+import application from "../../../../lib/build/definitions/application.js";
+
+function emptyarray() {
+ return [];
+}
+
+function getMockProject() {
+ return {
+ getName: () => "project.b",
+ getNamespace: () => "project/b",
+ getType: () => "application",
+ getPropertiesFileSourceEncoding: () => "UTF-412",
+ getCopyright: () => "copyright",
+ getVersion: () => "version",
+ getSpecVersion: () => {
+ return {
+ toString: () => "2.6",
+ gte: () => true
+ };
+ },
+ 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 = application({
+ 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: []
+ }
+ },
+ generateStandaloneAppBundle: {
+ requiresDependencies: true
+ },
+ transformBootstrapHtml: {},
+ generateBundle: {
+ taskFunction: null
+ },
+ generateVersionInfo: {
+ requiresDependencies: true,
+ options: {
+ rootProject: project,
+ pattern: "/resources/**/.library"
+ }
+ },
+ generateCachebusterInfo: {
+ options: {
+ signatureType: "PONY"
+ }
+ },
+ generateApiIndex: {
+ requiresDependencies: true
+ },
+ generateResourcesJson: {
+ requiresDependencies: true
+ }
+ }, "Correct task definitions");
+
+ t.is(taskUtil.getBuildOption.callCount, 0, "taskUtil#getBuildOption has not been called");
+});
+
+test("Standard build with legacy spec version", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ project.getSpecVersion = () => {
+ return {
+ toString: () => "0.1",
+ gte: () => false
+ };
+ };
+ const generateBundleTaskStub = sinon.stub();
+ getTask.returns({
+ task: generateBundleTaskStub
+ });
+
+ const tasks = application({
+ 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: []
+ }
+ },
+ generateStandaloneAppBundle: {
+ requiresDependencies: true
+ },
+ transformBootstrapHtml: {},
+ generateBundle: {
+ taskFunction: null
+ },
+ generateVersionInfo: {
+ requiresDependencies: true,
+ options: {
+ rootProject: project,
+ pattern: "/resources/**/.library"
+ }
+ },
+ generateCachebusterInfo: {
+ options: {
+ signatureType: "PONY"
+ }
+ },
+ generateApiIndex: {
+ requiresDependencies: true
+ },
+ generateResourcesJson: {
+ requiresDependencies: true
+ }
+ }, "Correct task definitions");
+});
+
+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,
+ usePredefineCalls: 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,
+ usePredefineCalls: true,
+ addTryCatchRestartWrapper: false,
+ decorateBootstrapModule: true,
+ numberOfParts: 1,
+ }
+ }];
+
+ const generateBundleTaskStub = sinon.stub();
+ getTask.returns({
+ task: generateBundleTaskStub
+ });
+
+ const tasks = application({
+ 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"
+ ]
+ }
+ },
+ generateStandaloneAppBundle: {
+ requiresDependencies: true
+ },
+ transformBootstrapHtml: {},
+ generateBundle: {
+ requiresDependencies: true,
+ taskFunction: generateBundleTaskDefinition.taskFunction
+ },
+ generateVersionInfo: {
+ requiresDependencies: true,
+ options: {
+ rootProject: project,
+ pattern: "/resources/**/.library"
+ }
+ },
+ generateCachebusterInfo: {
+ options: {
+ signatureType: "PONY"
+ }
+ },
+ generateApiIndex: {
+ requiresDependencies: true
+ },
+ 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,
+ usePredefineCalls: true,
+ addTryCatchRestartWrapper: false,
+ decorateBootstrapModule: true,
+ numberOfParts: 1,
+ }
+ }
+ }, "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,
+ usePredefineCalls: true,
+ addTryCatchRestartWrapper: false,
+ decorateBootstrapModule: true,
+ numberOfParts: 1,
+ }
+ }
+ }, "generateBundle task got called with correct arguments");
+});
+
+test("Minification excludes", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ project.getMinificationExcludes = () => ["**.html"];
+
+ const tasks = application({
+ project, taskUtil, getTask
+ });
+
+ const taskDefinition = tasks.get("minify");
+ t.deepEqual(taskDefinition, {
+ options: {
+ pattern: [
+ "/**/*.js",
+ "!**/*.support.js",
+ "!/resources/**.html",
+ ]
+ }
+ }, "Correct minify task definition");
+});
+
+test("Minification excludes not applied for legacy specVersion", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ project.getSpecVersion = () => {
+ return {
+ toString: () => "2.5",
+ gte: () => false
+ };
+ };
+ project.getMinificationExcludes = () => ["**.html"];
+
+ const tasks = application({
+ project, taskUtil, getTask
+ });
+
+ const taskDefinition = tasks.get("minify");
+ t.deepEqual(taskDefinition, {
+ options: {
+ pattern: [
+ "/**/*.js",
+ "!**/*.support.js",
+ ]
+ }
+ }, "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,
+ usePredefineCalls: true,
+ addTryCatchRestartWrapper: false,
+ decorateBootstrapModule: true,
+ numberOfParts: 1,
+ }
+ }];
+
+ project.getComponentPreloadPaths = () => [
+ "project/b/**/Component.js",
+ "project/b/**/SubComponent.js"
+ ];
+ project.getComponentPreloadExcludes = () => ["project/b/dir/**"];
+
+ const tasks = application({
+ 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 = application({
+ 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 = application({
+ 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");
+});
diff --git a/packages/project/test/lib/build/definitions/library.js b/packages/project/test/lib/build/definitions/library.js
new file mode 100644
index 00000000000..121e8951442
--- /dev/null
+++ b/packages/project/test/lib/build/definitions/library.js
@@ -0,0 +1,743 @@
+import test from "ava";
+import sinon from "sinon";
+import library from "../../../../lib/build/definitions/library.js";
+
+function emptyarray() {
+ return [];
+}
+
+function getMockProject() {
+ return {
+ getName: () => "project.b",
+ getNamespace: () => "project/b",
+ getType: () => "library",
+ getPropertiesFileSourceEncoding: () => "UTF-412",
+ getCopyright: () => "copyright",
+ getVersion: () => "version",
+ getSpecVersion: () => {
+ return {
+ toString: () => "2.6",
+ gte: () => true
+ };
+ },
+ getMinificationExcludes: emptyarray,
+ getComponentPreloadPaths: emptyarray,
+ getComponentPreloadNamespaces: emptyarray,
+ getComponentPreloadExcludes: emptyarray,
+ getLibraryPreloadExcludes: emptyarray,
+ getBundles: emptyarray,
+ getCachebusterSignatureType: () => "PONY",
+ getJsdocExcludes: () => [],
+ getCustomTasks: emptyarray,
+ isFrameworkProject: () => false
+ };
+}
+
+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", async (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ project.getJsdocExcludes = () => ["**.html"];
+
+ const generateJsdocTaskStub = sinon.stub();
+ getTask.returns({
+ task: generateJsdocTaskStub
+ });
+
+ const tasks = library({
+ project, taskUtil, getTask
+ });
+ const generateJsdocTaskDefinition = tasks.get("generateJsdoc");
+ t.deepEqual(Object.fromEntries(tasks), {
+ escapeNonAsciiCharacters: {
+ options: {
+ encoding: "UTF-412", pattern: "/**/*.properties"
+ }
+ },
+ replaceCopyright: {
+ options: {
+ copyright: "copyright",
+ pattern: "/**/*.{js,library,css,less,theme,html}"
+ }
+ },
+ replaceVersion: {
+ options: {
+ version: "version",
+ pattern: "/**/*.{js,json,library,css,less,theme,html}"
+ }
+ },
+ replaceBuildtime: {
+ options: {
+ pattern: "/resources/sap/ui/{Global,core/Core}.js"
+ }
+ },
+ generateJsdoc: {
+ requiresDependencies: true,
+ taskFunction: generateJsdocTaskDefinition.taskFunction
+ },
+ executeJsdocSdkTransformation: {
+ requiresDependencies: true,
+ options: {
+ dotLibraryPattern: "/resources/**/*.library"
+ }
+ },
+ minify: {
+ options: {
+ pattern: [
+ "/resources/**/*.js",
+ "!**/*.support.js",
+ ]
+ }
+ },
+ generateLibraryManifest: {},
+ enhanceManifest: {},
+ generateLibraryPreload: {
+ options: {
+ excludes: [], skipBundles: []
+ }
+ },
+ buildThemes: {
+ requiresDependencies: true,
+ options: {
+ projectName: "project.b",
+ librariesPattern: undefined,
+ themesPattern: undefined,
+ inputPattern: "/resources/project/b/themes/*/library.source.less",
+ cssVariables: undefined
+ }
+ },
+ generateBundle: {
+ taskFunction: null
+ },
+ generateComponentPreload: {
+ taskFunction: null
+ },
+ generateThemeDesignerResources: {
+ taskFunction: null
+ },
+ generateResourcesJson: {
+ requiresDependencies: true
+ }
+ }, "Correct task definitions");
+
+
+ await generateJsdocTaskDefinition.taskFunction({
+ workspace: "workspace",
+ dependencies: "dependencies",
+ taskUtil,
+ options: {
+ projectName: "projectName"
+ }
+ });
+
+ t.is(generateJsdocTaskStub.callCount, 1, "generateJsdoc task got called once");
+ t.deepEqual(generateJsdocTaskStub.getCall(0).args[0], {
+ workspace: "workspace",
+ dependencies: "dependencies",
+ taskUtil,
+ options: {
+ projectName: "projectName",
+ namespace: "project/b",
+ version: "version",
+ pattern: [
+ "/resources/**/*.js",
+ "!/resources/**.html"
+ ],
+ }
+ }, "generateBundle task got called with correct arguments");
+
+ t.is(taskUtil.getBuildOption.callCount, 1, "taskUtil#getBuildOption got called once");
+ t.is(taskUtil.getBuildOption.getCall(0).args[0], "cssVariables",
+ "taskUtil#getBuildOption got called with correct argument");
+});
+
+test("Standard build (framework project)", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+
+ project.isFrameworkProject = () => true;
+
+ const generateJsdocTaskStub = sinon.stub();
+ getTask.returns({
+ task: generateJsdocTaskStub
+ });
+
+ const tasks = library({
+ project, taskUtil, getTask
+ });
+
+ t.deepEqual(tasks.get("generateThemeDesignerResources"), {
+ requiresDependencies: true, options: {
+ version: "version"
+ }
+ });
+});
+
+test("Standard build with legacy spec version", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ project.getSpecVersion = () => {
+ return {
+ toString: () => "0.1",
+ gte: () => false
+ };
+ };
+
+ const tasks = library({
+ project, taskUtil, getTask
+ });
+ const generateJsdocTaskDefinition = tasks.get("generateJsdoc");
+ t.deepEqual(Object.fromEntries(tasks), {
+ escapeNonAsciiCharacters: {
+ options: {
+ encoding: "UTF-412", pattern: "/**/*.properties"
+ }
+ },
+ replaceCopyright: {
+ options: {
+ copyright: "copyright",
+ pattern: "/**/*.{js,library,css,less,theme,html}"
+ }
+ },
+ replaceVersion: {
+ options: {
+ version: "version",
+ pattern: "/**/*.{js,json,library,css,less,theme,html}"
+ }
+ },
+ replaceBuildtime: {
+ options: {
+ pattern: "/resources/sap/ui/{Global,core/Core}.js"
+ }
+ },
+ generateJsdoc: {
+ requiresDependencies: true,
+ taskFunction: generateJsdocTaskDefinition.taskFunction
+ },
+ executeJsdocSdkTransformation: {
+ requiresDependencies: true,
+ options: {
+ dotLibraryPattern: "/resources/**/*.library"
+ }
+ },
+ minify: {
+ options: {
+ pattern: [
+ "/resources/**/*.js",
+ "!**/*.support.js",
+ ]
+ }
+ },
+ generateLibraryManifest: {},
+ enhanceManifest: {},
+ generateLibraryPreload: {
+ options: {
+ excludes: [], skipBundles: []
+ }
+ },
+ buildThemes: {
+ requiresDependencies: true,
+ options: {
+ projectName: "project.b",
+ librariesPattern: undefined,
+ themesPattern: undefined,
+ inputPattern: "/resources/project/b/themes/*/library.source.less",
+ cssVariables: undefined
+ }
+ },
+ generateBundle: {
+ taskFunction: null
+ },
+ generateComponentPreload: {
+ taskFunction: null
+ },
+ generateThemeDesignerResources: {
+ taskFunction: null
+ },
+ generateResourcesJson: {
+ requiresDependencies: true
+ }
+ }, "Correct task definitions");
+});
+
+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,
+ usePredefineCalls: 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,
+ usePredefineCalls: true,
+ addTryCatchRestartWrapper: false,
+ decorateBootstrapModule: true,
+ numberOfParts: 1,
+ }
+ }];
+
+ const generateBundleTaskStub = sinon.stub();
+ getTask.returns({
+ task: generateBundleTaskStub
+ });
+
+ const tasks = library({
+ project, taskUtil, getTask
+ });
+ const generateJsdocTaskDefinition = tasks.get("generateJsdoc");
+ const generateBundleTaskDefinition = tasks.get("generateBundle");
+
+ t.deepEqual(Object.fromEntries(tasks), {
+ escapeNonAsciiCharacters: {
+ options: {
+ encoding: "UTF-412", pattern: "/**/*.properties"
+ }
+ },
+ replaceCopyright: {
+ options: {
+ copyright: "copyright",
+ pattern: "/**/*.{js,library,css,less,theme,html}"
+ }
+ },
+ replaceVersion: {
+ options: {
+ version: "version",
+ pattern: "/**/*.{js,json,library,css,less,theme,html}"
+ }
+ },
+ replaceBuildtime: {
+ options: {
+ pattern: "/resources/sap/ui/{Global,core/Core}.js"
+ }
+ },
+ generateJsdoc: {
+ requiresDependencies: true,
+ taskFunction: generateJsdocTaskDefinition.taskFunction
+ },
+ executeJsdocSdkTransformation: {
+ requiresDependencies: true,
+ options: {
+ dotLibraryPattern: "/resources/**/*.library"
+ }
+ },
+ minify: {
+ options: {
+ pattern: [
+ "/resources/**/*.js",
+ "!**/*.support.js",
+ ]
+ }
+ },
+ generateLibraryManifest: {},
+ enhanceManifest: {},
+ generateLibraryPreload: {
+ options: {
+ excludes: [],
+ skipBundles: [
+ "project/b/sectionsA/customBundle.js",
+ "project/b/sectionsB/customBundle.js",
+ ]
+ }
+ },
+ generateBundle: {
+ requiresDependencies: true,
+ taskFunction: generateBundleTaskDefinition.taskFunction
+ },
+ buildThemes: {
+ requiresDependencies: true,
+ options: {
+ projectName: "project.b",
+ librariesPattern: undefined,
+ themesPattern: undefined,
+ inputPattern: "/resources/project/b/themes/*/library.source.less",
+ cssVariables: undefined
+ }
+ },
+ generateComponentPreload: {
+ taskFunction: null
+ },
+ generateThemeDesignerResources: {
+ taskFunction: null
+ },
+ 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,
+ usePredefineCalls: true,
+ addTryCatchRestartWrapper: false,
+ decorateBootstrapModule: true,
+ numberOfParts: 1,
+ }
+ }
+ }, "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,
+ usePredefineCalls: true,
+ addTryCatchRestartWrapper: false,
+ decorateBootstrapModule: true,
+ numberOfParts: 1,
+ }
+ }
+ }, "generateBundle task got called with correct arguments");
+});
+
+test("Minification excludes", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ project.getMinificationExcludes = () => ["**.html"];
+
+ const tasks = library({
+ project, taskUtil, getTask
+ });
+
+ const taskDefinition = tasks.get("minify");
+ t.deepEqual(taskDefinition, {
+ options: {
+ pattern: [
+ "/resources/**/*.js",
+ "!**/*.support.js",
+ "!/resources/**.html",
+ ]
+ }
+ }, "Correct minify task definition");
+});
+
+test("Minification excludes not applied for legacy specVersion", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ project.getSpecVersion = () => {
+ return {
+ toString: () => "2.5",
+ gte: () => false
+ };
+ };
+ project.getMinificationExcludes = () => ["**.html"];
+
+ const tasks = library({
+ project, taskUtil, getTask
+ });
+
+ const taskDefinition = tasks.get("minify");
+ t.deepEqual(taskDefinition, {
+ options: {
+ pattern: [
+ "/resources/**/*.js",
+ "!**/*.support.js",
+ ]
+ }
+ }, "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,
+ usePredefineCalls: true,
+ addTryCatchRestartWrapper: false,
+ decorateBootstrapModule: true,
+ numberOfParts: 1,
+ }
+ }];
+
+ project.getComponentPreloadPaths = () => [
+ "project/b/**/Component.js",
+ "project/b/**/SubComponent.js"
+ ];
+ project.getComponentPreloadExcludes = () => ["project/b/dir/**"];
+
+ const tasks = library({
+ 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 = library({
+ 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("generateLibraryPreload with excludes", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ project.getLibraryPreloadExcludes = () => ["project/b/dir/**"];
+
+ const tasks = library({
+ project, taskUtil, getTask
+ });
+
+ const taskDefinition = tasks.get("generateLibraryPreload");
+ t.deepEqual(taskDefinition, {
+ options: {
+ excludes: ["project/b/dir/**"],
+ skipBundles: []
+ }
+ }, "Correct generateLibraryPreload task definition");
+});
+
+test("buildThemes: Project is not root", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ taskUtil.isRootProject.returns(false);
+
+ const tasks = library({
+ project, taskUtil, getTask
+ });
+
+ const taskDefinition = tasks.get("buildThemes");
+ t.deepEqual(taskDefinition, {
+ requiresDependencies: true,
+ options: {
+ projectName: "project.b",
+ librariesPattern: "/resources/**/(*.library|library.js)",
+ themesPattern: "/resources/sap/ui/core/themes/*",
+ inputPattern: "/resources/project/b/themes/*/library.source.less",
+ cssVariables: undefined
+ }
+ }, "Correct buildThemes task definition");
+});
+test("buildThemes: CSS Variables enabled", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ taskUtil.getBuildOption.returns(true);
+
+ const tasks = library({
+ project, taskUtil, getTask
+ });
+
+ const taskDefinition = tasks.get("buildThemes");
+ t.deepEqual(taskDefinition, {
+ requiresDependencies: true,
+ options: {
+ projectName: "project.b",
+ librariesPattern: undefined,
+ themesPattern: undefined,
+ inputPattern: "/resources/project/b/themes/*/library.source.less",
+ cssVariables: true
+ }
+ }, "Correct buildThemes task definition");
+
+ t.is(taskUtil.getBuildOption.callCount, 1, "taskUtil#getBuildOption got called once");
+ t.is(taskUtil.getBuildOption.getCall(0).args[0], "cssVariables",
+ "taskUtil#getBuildOption got called with correct argument");
+});
+
+test("Standard build: nulled taskFunction to skip tasks", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ project.getJsdocExcludes = () => ["**.html"];
+
+ const tasks = library({
+ project, taskUtil, getTask
+ });
+ const generateComponentPreloadTaskDefinition = tasks.get("generateComponentPreload");
+ const generateBundleTaskDefinition = tasks.get("generateBundle");
+ const generateThemeDesignerResourcesTaskDefinition = tasks.get("generateThemeDesignerResources");
+ t.deepEqual(Object.fromEntries(tasks), {
+ escapeNonAsciiCharacters: {
+ options: {
+ encoding: "UTF-412", pattern: "/**/*.properties"
+ }
+ },
+ replaceCopyright: {
+ options: {
+ copyright: "copyright",
+ pattern: "/**/*.{js,library,css,less,theme,html}"
+ }
+ },
+ replaceVersion: {
+ options: {
+ version: "version",
+ pattern: "/**/*.{js,json,library,css,less,theme,html}"
+ }
+ },
+ replaceBuildtime: {
+ options: {
+ pattern: "/resources/sap/ui/{Global,core/Core}.js"
+ }
+ },
+ generateJsdoc: {
+ requiresDependencies: true,
+ taskFunction: async () => {},
+ },
+ executeJsdocSdkTransformation: {
+ requiresDependencies: true,
+ options: {
+ dotLibraryPattern: "/resources/**/*.library"
+ }
+ },
+ minify: {
+ options: {
+ pattern: [
+ "/resources/**/*.js",
+ "!**/*.support.js",
+ ]
+ }
+ },
+ generateLibraryManifest: {},
+ enhanceManifest: {},
+ generateLibraryPreload: {
+ options: {
+ excludes: [], skipBundles: []
+ }
+ },
+ buildThemes: {
+ requiresDependencies: true,
+ options: {
+ projectName: "project.b",
+ librariesPattern: undefined,
+ themesPattern: undefined,
+ inputPattern: "/resources/project/b/themes/*/library.source.less",
+ cssVariables: undefined
+ }
+ },
+ generateBundle: {
+ taskFunction: null
+ },
+ generateComponentPreload: {
+ taskFunction: null
+ },
+ generateThemeDesignerResources: {
+ taskFunction: null
+ },
+ generateResourcesJson: {
+ requiresDependencies: true
+ }
+ }, "Correct task definitions");
+
+ t.is(generateComponentPreloadTaskDefinition.taskFunction, null, "taskFunction is explicitly set to null");
+ t.is(generateBundleTaskDefinition.taskFunction, null, "taskFunction is explicitly set to null");
+ t.is(generateThemeDesignerResourcesTaskDefinition.taskFunction, null, "taskFunction is explicitly set to null");
+});
diff --git a/packages/project/test/lib/build/definitions/module.js b/packages/project/test/lib/build/definitions/module.js
new file mode 100644
index 00000000000..0a74ff702fa
--- /dev/null
+++ b/packages/project/test/lib/build/definitions/module.js
@@ -0,0 +1,7 @@
+import test from "ava";
+import moduleDefinition from "../../../../lib/build/definitions/module.js";
+
+test("Standard build", (t) => {
+ const tasks = moduleDefinition({});
+ t.is(tasks.size, 0, "No tasks returned");
+});
diff --git a/packages/project/test/lib/build/definitions/themeLibrary.js b/packages/project/test/lib/build/definitions/themeLibrary.js
new file mode 100644
index 00000000000..2da2457b538
--- /dev/null
+++ b/packages/project/test/lib/build/definitions/themeLibrary.js
@@ -0,0 +1,171 @@
+import test from "ava";
+import sinon from "sinon";
+import themeLibrary from "../../../../lib/build/definitions/themeLibrary.js";
+
+function emptyarray() {
+ return [];
+}
+
+function getMockProject() {
+ return {
+ getName: () => "project.b",
+ getNamespace: () => "project/b",
+ getType: () => "theme-library",
+ getCopyright: () => "copyright",
+ getVersion: () => "version",
+ getSpecVersion: () => {
+ return {
+ toString: () => "2.6"
+ };
+ },
+ getMinificationExcludes: emptyarray,
+ getComponentPreloadPaths: emptyarray,
+ getComponentPreloadNamespaces: emptyarray,
+ getComponentPreloadExcludes: emptyarray,
+ getLibraryPreloadExcludes: emptyarray,
+ getBundles: emptyarray,
+ getCachebusterSignatureType: () => "PONY",
+ getCustomTasks: emptyarray,
+ isFrameworkProject: () => false
+ };
+}
+
+test.beforeEach((t) => {
+ t.context.taskUtil = {
+ isRootProject: sinon.stub().returns(true),
+ getBuildOption: sinon.stub(),
+ getInterface: sinon.stub()
+ };
+
+ t.context.project = getMockProject();
+ t.context.getTask = sinon.stub();
+});
+
+test("Standard build", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+
+ const tasks = themeLibrary({
+ project, taskUtil, getTask
+ });
+ const generateThemeDesignerResourcesTaskFunction = tasks.get("generateThemeDesignerResources");
+ t.deepEqual(Object.fromEntries(tasks), {
+ replaceCopyright: {
+ options: {
+ copyright: "copyright",
+ pattern: "/resources/**/*.{less,theme}"
+ }
+ },
+ replaceVersion: {
+ options: {
+ version: "version",
+ pattern: "/resources/**/*.{less,theme}"
+ }
+ },
+ buildThemes: {
+ requiresDependencies: true,
+ options: {
+ projectName: "project.b",
+ librariesPattern: undefined,
+ themesPattern: undefined,
+ inputPattern: "/resources/**/themes/*/library.source.less",
+ cssVariables: undefined
+ }
+ },
+ generateResourcesJson: {
+ requiresDependencies: true
+ },
+ generateThemeDesignerResources: {
+ taskFunction: null
+ }
+ }, "Correct task definitions");
+
+ t.is(taskUtil.getBuildOption.callCount, 1, "taskUtil#getBuildOption got called once");
+ t.is(taskUtil.getBuildOption.getCall(0).args[0], "cssVariables",
+ "taskUtil#getBuildOption got called with correct argument");
+
+ t.is(generateThemeDesignerResourcesTaskFunction.taskFunction, null, "taskFunction is explicitly set to null");
+});
+
+test("Standard build (framework project)", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+
+ project.isFrameworkProject = () => true;
+
+ const tasks = themeLibrary({
+ project, taskUtil, getTask
+ });
+
+ t.deepEqual(tasks.get("generateThemeDesignerResources"), {
+ requiresDependencies: true, options: {
+ version: "version"
+ }
+ });
+});
+
+test("Standard build for non root project", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ taskUtil.isRootProject.returns(false);
+
+ const tasks = themeLibrary({
+ project, taskUtil, getTask
+ });
+ t.deepEqual(Object.fromEntries(tasks), {
+ replaceCopyright: {
+ options: {
+ copyright: "copyright",
+ pattern: "/resources/**/*.{less,theme}"
+ }
+ },
+ replaceVersion: {
+ options: {
+ version: "version",
+ pattern: "/resources/**/*.{less,theme}"
+ }
+ },
+ buildThemes: {
+ requiresDependencies: true,
+ options: {
+ projectName: "project.b",
+ librariesPattern: "/resources/**/(*.library|library.js)",
+ themesPattern: "/resources/sap/ui/core/themes/*",
+ inputPattern: "/resources/**/themes/*/library.source.less",
+ cssVariables: undefined
+ }
+ },
+ generateResourcesJson: {
+ requiresDependencies: true
+ },
+ generateThemeDesignerResources: {
+ taskFunction: null
+ }
+ }, "Correct task definitions");
+
+ t.is(taskUtil.getBuildOption.callCount, 1, "taskUtil#getBuildOption got called once");
+ t.is(taskUtil.getBuildOption.getCall(0).args[0], "cssVariables",
+ "taskUtil#getBuildOption got called with correct argument");
+});
+
+test("CSS variables enabled", (t) => {
+ const {project, taskUtil, getTask} = t.context;
+ taskUtil.getBuildOption.returns(true);
+
+ const tasks = themeLibrary({
+ project, taskUtil, getTask
+ });
+
+ const taskDefinition = tasks.get("buildThemes");
+ t.deepEqual(taskDefinition, {
+ requiresDependencies: true,
+ options: {
+ projectName: "project.b",
+ librariesPattern: undefined,
+ themesPattern: undefined,
+ inputPattern: "/resources/**/themes/*/library.source.less",
+ cssVariables: true
+ }
+ }, "Correct buildThemes task definition");
+
+ t.is(taskUtil.getBuildOption.callCount, 1, "taskUtil#getBuildOption got called once");
+ t.is(taskUtil.getBuildOption.getCall(0).args[0], "cssVariables",
+ "taskUtil#getBuildOption got called with correct argument");
+});
diff --git a/packages/project/test/lib/build/definitions/utils.js b/packages/project/test/lib/build/definitions/utils.js
new file mode 100644
index 00000000000..7cea787ceef
--- /dev/null
+++ b/packages/project/test/lib/build/definitions/utils.js
@@ -0,0 +1,18 @@
+import test from "ava";
+import {enhancePatternWithExcludes} from "../../../../lib/build/definitions/_utils.js";
+
+test("enhancePatternWithExcludes", (t) => {
+ const patterns = ["/default/pattern", "!/other/pattern"];
+ const excludes = ["a", "!b", "c", "!d"];
+
+ enhancePatternWithExcludes(patterns, excludes, "/prefix/");
+
+ t.deepEqual(patterns, [
+ "/default/pattern",
+ "!/other/pattern",
+ "!/prefix/a",
+ "/prefix/b",
+ "!/prefix/c",
+ "/prefix/d"
+ ]);
+});
diff --git a/packages/project/test/lib/build/helpers/BuildContext.js b/packages/project/test/lib/build/helpers/BuildContext.js
new file mode 100644
index 00000000000..cc09a7cd870
--- /dev/null
+++ b/packages/project/test/lib/build/helpers/BuildContext.js
@@ -0,0 +1,309 @@
+import test from "ava";
+import sinon from "sinon";
+import OutputStyleEnum from "../../../../lib/build/helpers/ProjectBuilderOutputStyle.js";
+
+test.afterEach.always((t) => {
+ sinon.restore();
+});
+
+import BuildContext from "../../../../lib/build/helpers/BuildContext.js";
+
+test("Missing parameters", (t) => {
+ const error1 = t.throws(() => {
+ new BuildContext();
+ });
+
+ t.is(error1.message, `Missing parameter 'graph'`, "Threw with expected error message");
+
+ const error2 = t.throws(() => {
+ new BuildContext("graph");
+ });
+
+ t.is(error2.message, `Missing parameter 'taskRepository'`, "Threw with expected error message");
+});
+
+test("getRootProject", (t) => {
+ const rootProjectStub = sinon.stub()
+ .onFirstCall().returns({getType: () => "library"})
+ .returns("pony");
+ const graph = {getRoot: rootProjectStub};
+ const buildContext = new BuildContext(graph, "taskRepository");
+
+ t.is(buildContext.getRootProject(), "pony", "Returned correct value");
+});
+
+test("getGraph", (t) => {
+ const graph = {
+ getRoot: () => ({getType: () => "library"}),
+ };
+ const buildContext = new BuildContext(graph, "taskRepository");
+
+ t.deepEqual(buildContext.getGraph(), graph, "Returned correct value");
+});
+
+test("getTaskRepository", (t) => {
+ const graph = {
+ getRoot: () => ({getType: () => "library"}),
+ };
+ const buildContext = new BuildContext(graph, "taskRepository");
+
+ t.is(buildContext.getTaskRepository(), "taskRepository", "Returned correct value");
+});
+
+test("getBuildConfig: Default values", (t) => {
+ const graph = {
+ getRoot: () => ({getType: () => "library"}),
+ };
+ const buildContext = new BuildContext(graph, "taskRepository");
+
+ t.deepEqual(buildContext.getBuildConfig(), {
+ selfContained: false,
+ outputStyle: OutputStyleEnum.Default,
+ cssVariables: false,
+ jsdoc: false,
+ createBuildManifest: false,
+ includedTasks: [],
+ excludedTasks: [],
+ }, "Returned correct value");
+});
+
+test("getBuildConfig: Custom values", (t) => {
+ const buildContext = new BuildContext({
+ getRoot: () => {
+ return {
+ getType: () => "library"
+ };
+ }
+ }, "taskRepository", {
+ selfContained: true,
+ outputStyle: OutputStyleEnum.Namespace,
+ cssVariables: true,
+ jsdoc: true,
+ createBuildManifest: false,
+ includedTasks: ["included tasks"],
+ excludedTasks: ["excluded tasks"],
+ });
+
+ t.deepEqual(buildContext.getBuildConfig(), {
+ selfContained: true,
+ outputStyle: OutputStyleEnum.Namespace,
+ cssVariables: true,
+ jsdoc: true,
+ createBuildManifest: false,
+ includedTasks: ["included tasks"],
+ excludedTasks: ["excluded tasks"],
+ }, "Returned correct value");
+});
+
+test("createBuildManifest not supported for type application", (t) => {
+ const err = t.throws(() => {
+ new BuildContext({
+ getRoot: () => {
+ return {
+ getType: () => "application"
+ };
+ }
+ }, "taskRepository", {
+ createBuildManifest: true
+ });
+ });
+ t.is(err.message,
+ "Build manifest creation is currently not supported for projects of type application",
+ "Threw with expected error message");
+});
+
+test("createBuildManifest not supported for type module", (t) => {
+ const err = t.throws(() => {
+ new BuildContext({
+ getRoot: () => {
+ return {
+ getType: () => "module"
+ };
+ }
+ }, "taskRepository", {
+ createBuildManifest: true
+ });
+ });
+ t.is(err.message,
+ "Build manifest creation is currently not supported for projects of type module",
+ "Threw with expected error message");
+});
+
+test("createBuildManifest not supported for self-contained build", (t) => {
+ const err = t.throws(() => {
+ new BuildContext({
+ getRoot: () => {
+ return {
+ getType: () => "library"
+ };
+ }
+ }, "taskRepository", {
+ createBuildManifest: true,
+ selfContained: true
+ });
+ });
+ t.is(err.message,
+ "Build manifest creation is currently not supported for self-contained builds",
+ "Threw with expected error message");
+});
+
+test("createBuildManifest supported for css-variables build", (t) => {
+ t.notThrows(() => {
+ new BuildContext({
+ getRoot: () => {
+ return {
+ getType: () => "library"
+ };
+ }
+ }, "taskRepository", {
+ createBuildManifest: true,
+ cssVariables: true
+ });
+ });
+});
+
+test("createBuildManifest supported for jsdoc build", (t) => {
+ t.notThrows(() => {
+ new BuildContext({
+ getRoot: () => {
+ return {
+ getType: () => "library"
+ };
+ }
+ }, "taskRepository", {
+ createBuildManifest: true,
+ jsdoc: true
+ });
+ });
+});
+
+test("outputStyle='Namespace' supported for type application", (t) => {
+ t.notThrows(() => {
+ new BuildContext({
+ getRoot: () => {
+ return {
+ getType: () => "application"
+ };
+ }
+ }, "taskRepository", {
+ outputStyle: OutputStyleEnum.Namespace
+ });
+ });
+});
+
+test("outputStyle='Flat' not supported for type theme-library", (t) => {
+ const err = t.throws(() => {
+ new BuildContext({
+ getRoot: () => {
+ return {
+ getType: () => "theme-library"
+ };
+ }
+ }, "taskRepository", {
+ outputStyle: OutputStyleEnum.Flat
+ });
+ });
+ t.is(err.message,
+ "Flat build output style is currently not supported for projects of typetheme-library since they" +
+ " commonly have more than one namespace. Currently only the Default output style is supported" +
+ " for this project type.");
+});
+
+test("outputStyle='Flat' not supported for type module", (t) => {
+ const err = t.throws(() => {
+ new BuildContext({
+ getRoot: () => {
+ return {
+ getType: () => "module"
+ };
+ }
+ }, "taskRepository", {
+ outputStyle: OutputStyleEnum.Flat
+ });
+ });
+ t.is(err.message,
+ "Flat build output style is currently not supported for projects of typemodule. " +
+ "Their path mappings configuration can't be mapped to any namespace.Currently only the " +
+ "Default output style is supported for this project type.");
+});
+
+test("outputStyle='Flat' not supported for createBuildManifest build", (t) => {
+ const err = t.throws(() => {
+ new BuildContext({
+ getRoot: () => {
+ return {
+ getType: () => "library"
+ };
+ }
+ }, "taskRepository", {
+ createBuildManifest: true,
+ outputStyle: OutputStyleEnum.Flat
+ });
+ });
+ t.is(err.message,
+ "Build manifest creation is not supported in conjunction with flat build output",
+ "Threw with expected error message");
+});
+
+test("getOption", (t) => {
+ const graph = {
+ getRoot: () => ({getType: () => "library"}),
+ };
+ const buildContext = new BuildContext(graph, "taskRepository", {
+ cssVariables: "value",
+ });
+
+ t.is(buildContext.getOption("cssVariables"), "value",
+ "Returned correct value for build configuration 'cssVariables'");
+ t.is(buildContext.getOption("selfContained"), undefined,
+ "Returned undefined for build configuration 'selfContained' " +
+ "(not exposed as build option)");
+});
+
+test("createProjectContext", async (t) => {
+ const graph = {
+ getRoot: () => ({getType: () => "library"}),
+ };
+ const buildContext = new BuildContext(graph, "taskRepository");
+ const projectBuildContext = await buildContext.createProjectContext({
+ project: {
+ getName: () => "project",
+ getType: () => "type",
+ },
+ });
+
+ t.deepEqual(buildContext._projectBuildContexts, [projectBuildContext],
+ "Project build context has been added to internal array");
+});
+
+test("executeCleanupTasks", async (t) => {
+ const graph = {
+ getRoot: () => ({getType: () => "library"}),
+ };
+ const buildContext = new BuildContext(graph, "taskRepository");
+
+ const executeCleanupTasks = sinon.stub().resolves();
+
+ buildContext._projectBuildContexts.push({
+ executeCleanupTasks
+ });
+ buildContext._projectBuildContexts.push({
+ executeCleanupTasks
+ });
+
+ await buildContext.executeCleanupTasks();
+
+ t.is(executeCleanupTasks.callCount, 2,
+ "Project context executeCleanupTasks got called twice");
+ t.is(executeCleanupTasks.getCall(0).firstArg, false,
+ "Project context executeCleanupTasks got called with expected arguments");
+
+
+ executeCleanupTasks.reset();
+ await buildContext.executeCleanupTasks(true);
+
+ t.is(executeCleanupTasks.callCount, 2,
+ "Project context executeCleanupTasks got called twice");
+ t.is(executeCleanupTasks.getCall(0).firstArg, true,
+ "Project context executeCleanupTasks got called with expected arguments");
+});
diff --git a/packages/project/test/lib/build/helpers/ProjectBuildContext.js b/packages/project/test/lib/build/helpers/ProjectBuildContext.js
new file mode 100644
index 00000000000..03f9a568325
--- /dev/null
+++ b/packages/project/test/lib/build/helpers/ProjectBuildContext.js
@@ -0,0 +1,463 @@
+import test from "ava";
+import sinon from "sinon";
+import esmock from "esmock";
+import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection";
+
+test.beforeEach((t) => {
+ t.context.resourceTagCollection = new ResourceTagCollection({
+ allowedTags: ["me:MyTag"]
+ });
+});
+test.afterEach.always((t) => {
+ sinon.restore();
+});
+
+import ProjectBuildContext from "../../../../lib/build/helpers/ProjectBuildContext.js";
+
+test("Missing parameters", (t) => {
+ t.throws(() => {
+ new ProjectBuildContext({
+ project: {
+ getName: () => "project",
+ getType: () => "type",
+ },
+ });
+ }, {
+ message: `Missing parameter 'buildContext'`
+ }, "Correct error message");
+
+ t.throws(() => {
+ new ProjectBuildContext({
+ buildContext: "buildContext",
+ });
+ }, {
+ message: `Missing parameter 'project'`
+ }, "Correct error message");
+});
+
+test("isRootProject: true", (t) => {
+ const rootProject = {
+ getName: () => "root project",
+ getType: () => "type",
+ };
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {
+ getRootProject: () => rootProject
+ },
+ project: rootProject
+ });
+
+ t.true(projectBuildContext.isRootProject(), "Correctly identified root project");
+});
+
+test("isRootProject: false", (t) => {
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {
+ getRootProject: () => "root project"
+ },
+ project: {
+ getName: () => "not the root project",
+ getType: () => "type",
+ }
+ });
+
+ t.false(projectBuildContext.isRootProject(), "Correctly identified non-root project");
+});
+
+test("getBuildOption", (t) => {
+ const getOptionStub = sinon.stub().returns("pony");
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {
+ getOption: getOptionStub
+ },
+ project: {
+ getName: () => "project",
+ getType: () => "type",
+ }
+ });
+
+ t.is(projectBuildContext.getOption("option"), "pony", "Returned value is correct");
+ t.is(getOptionStub.getCall(0).args[0], "option", "getOption called with correct argument");
+});
+
+test("registerCleanupTask", (t) => {
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {},
+ project: {
+ getName: () => "project",
+ getType: () => "type",
+ }
+ });
+ projectBuildContext.registerCleanupTask("my task 1");
+ projectBuildContext.registerCleanupTask("my task 2");
+
+ t.is(projectBuildContext._queues.cleanup[0], "my task 1", "Cleanup task registered");
+ t.is(projectBuildContext._queues.cleanup[1], "my task 2", "Cleanup task registered");
+});
+
+test("executeCleanupTasks", (t) => {
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {},
+ project: {
+ getName: () => "project",
+ getType: () => "type",
+ }
+ });
+ const task1 = sinon.stub().resolves();
+ const task2 = sinon.stub().resolves();
+ projectBuildContext.registerCleanupTask(task1);
+ projectBuildContext.registerCleanupTask(task2);
+
+ projectBuildContext.executeCleanupTasks();
+
+ t.is(task1.callCount, 1, "Cleanup task 1 got called");
+ t.is(task2.callCount, 1, "my task 2", "Cleanup task 2 got called");
+});
+
+test.serial("getResourceTagCollection", async (t) => {
+ const projectAcceptsTagStub = sinon.stub().returns(false);
+ projectAcceptsTagStub.withArgs("project-tag").returns(true);
+ const projectContextAcceptsTagStub = sinon.stub().returns(false);
+ projectContextAcceptsTagStub.withArgs("project-context-tag").returns(true);
+
+ class DummyResourceTagCollection {
+ constructor({allowedTags, allowedNamespaces}) {
+ t.deepEqual(allowedTags, [
+ "ui5:OmitFromBuildResult",
+ "ui5:IsBundle"
+ ],
+ "Correct allowedTags parameter supplied");
+
+ t.deepEqual(allowedNamespaces, [
+ "build"
+ ],
+ "Correct allowedNamespaces parameter supplied");
+ }
+ acceptsTag(tag) {
+ // Redirect to stub
+ return projectContextAcceptsTagStub(tag);
+ }
+ }
+
+ const ProjectBuildContext = await esmock("../../../../lib/build/helpers/ProjectBuildContext.js", {
+ "@ui5/fs/internal/ResourceTagCollection": DummyResourceTagCollection
+ });
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {},
+ project: {
+ getName: () => "project",
+ getType: () => "type",
+ }
+ });
+
+ const fakeProjectCollection = {
+ acceptsTag: projectAcceptsTagStub
+ };
+ const fakeResource = {
+ getProject: () => {
+ return {
+ getResourceTagCollection: () => fakeProjectCollection
+ };
+ },
+ getPath: () => "/resource/path",
+ hasProject: () => true
+ };
+ const collection1 = projectBuildContext.getResourceTagCollection(fakeResource, "project-tag");
+ t.is(collection1, fakeProjectCollection, "Returned tag collection of resource project");
+
+ const collection2 = projectBuildContext.getResourceTagCollection(fakeResource, "project-context-tag");
+ t.true(collection2 instanceof DummyResourceTagCollection,
+ "Returned tag collection of project build context");
+
+ t.throws(() => {
+ projectBuildContext.getResourceTagCollection(fakeResource, "not-accepted-tag");
+ }, {
+ message: `Could not find collection for resource /resource/path and tag not-accepted-tag`
+ });
+});
+
+test("getResourceTagCollection: Assigns project to resource if necessary", (t) => {
+ const fakeProject = {
+ getName: () => "project",
+ getType: () => "type",
+ };
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {},
+ project: fakeProject,
+ log: {
+ silly: () => {}
+ }
+ });
+
+ const setProjectStub = sinon.stub();
+ const fakeResource = {
+ getProject: () => {
+ return {
+ getResourceTagCollection: () => {
+ return {
+ acceptsTag: () => false
+ };
+ }
+ };
+ },
+ getPath: () => "/resource/path",
+ hasProject: () => false,
+ setProject: setProjectStub
+ };
+ projectBuildContext.getResourceTagCollection(fakeResource, "build:MyTag");
+ t.is(setProjectStub.callCount, 1, "setProject got called once");
+ t.is(setProjectStub.getCall(0).args[0], fakeProject, "setProject got called with correct argument");
+});
+
+test("getProject", (t) => {
+ const project = {
+ getName: () => "project",
+ getType: () => "type",
+ };
+ const getProjectStub = sinon.stub().returns("pony");
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {
+ getGraph: () => {
+ return {
+ getProject: getProjectStub
+ };
+ }
+ },
+ project
+ });
+
+ t.is(projectBuildContext.getProject("pony project"), "pony", "Returned correct value");
+ t.is(getProjectStub.callCount, 1, "ProjectGraph#getProject got called once");
+ t.is(getProjectStub.getCall(0).args[0], "pony project", "ProjectGraph#getProject got called with correct argument");
+
+ t.is(projectBuildContext.getProject(), project);
+ t.is(getProjectStub.callCount, 1, "ProjectGraph#getProject is not called when requesting current project");
+});
+
+test("getProject: No name provided", (t) => {
+ const project = {
+ getName: () => "project",
+ getType: () => "type",
+ };
+ const getProjectStub = sinon.stub().returns("pony");
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {
+ getGraph: () => {
+ return {
+ getProject: getProjectStub
+ };
+ }
+ },
+ project
+ });
+
+ t.is(projectBuildContext.getProject(), project, "Returned correct value");
+ t.is(getProjectStub.callCount, 0, "ProjectGraph#getProject has not been called");
+});
+
+test("getDependencies", (t) => {
+ const project = {
+ getName: () => "project",
+ getType: () => "type",
+ };
+ const getDependenciesStub = sinon.stub().returns(["dep a", "dep b"]);
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {
+ getGraph: () => {
+ return {
+ getDependencies: getDependenciesStub
+ };
+ }
+ },
+ project
+ });
+
+ t.deepEqual(projectBuildContext.getDependencies("pony project"), ["dep a", "dep b"], "Returned correct value");
+ t.is(getDependenciesStub.callCount, 1, "ProjectGraph#getDependencies got called once");
+ t.is(getDependenciesStub.getCall(0).args[0], "pony project",
+ "ProjectGraph#getDependencies got called with correct arguments");
+});
+
+test("getDependencies: No name provided", (t) => {
+ const project = {
+ getName: () => "project",
+ getType: () => "type",
+ };
+ const getDependenciesStub = sinon.stub().returns(["dep a", "dep b"]);
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {
+ getGraph: () => {
+ return {
+ getDependencies: getDependenciesStub
+ };
+ }
+ },
+ project
+ });
+
+ t.deepEqual(projectBuildContext.getDependencies(), ["dep a", "dep b"], "Returned correct value");
+ t.is(getDependenciesStub.callCount, 1, "ProjectGraph#getDependencies got called once");
+ t.is(getDependenciesStub.getCall(0).args[0], "project",
+ "ProjectGraph#getDependencies got called with correct arguments");
+});
+
+test("getTaskUtil", (t) => {
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {},
+ project: {
+ getName: () => "project",
+ getType: () => "type",
+ }
+ });
+
+ t.truthy(projectBuildContext.getTaskUtil(), "Returned a TaskUtil instance");
+ t.is(projectBuildContext.getTaskUtil(), projectBuildContext.getTaskUtil(), "Caches TaskUtil instance");
+});
+
+test.serial("getTaskRunner", async (t) => {
+ t.plan(3);
+ const project = {
+ getName: () => "project",
+ getType: () => "type",
+ };
+ const {default: ProjectBuildLogger} = await import("@ui5/logger/internal/loggers/ProjectBuild");
+ class TaskRunnerMock {
+ constructor(params) {
+ t.true(params.log instanceof ProjectBuildLogger, "TaskRunner receives an instance of ProjectBuildLogger");
+ params.log = "log"; // replace log instance with string for deep comparison
+ t.deepEqual(params, {
+ graph: "graph",
+ project: project,
+ log: "log",
+ taskUtil: "taskUtil",
+ taskRepository: "taskRepository",
+ buildConfig: "buildConfig"
+ }, "TaskRunner created with expected constructor arguments");
+ }
+ }
+ const ProjectBuildContext = await esmock("../../../../lib/build/helpers/ProjectBuildContext.js", {
+ "../../../../lib/build/TaskRunner.js": TaskRunnerMock
+ });
+
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {
+ getGraph: () => "graph",
+ getTaskRepository: () => "taskRepository",
+ getBuildConfig: () => "buildConfig",
+ },
+ project
+ });
+
+ projectBuildContext.getTaskUtil = () => "taskUtil";
+
+ const taskRunner = projectBuildContext.getTaskRunner();
+ t.is(projectBuildContext.getTaskRunner(), taskRunner, "Returns cached TaskRunner instance");
+});
+
+
+test.serial("createProjectContext", async (t) => {
+ t.plan(4);
+
+ const project = {
+ getName: sinon.stub().returns("foo"),
+ getType: sinon.stub().returns("bar"),
+ };
+ const taskRunner = {"task": "runner"};
+ class ProjectContextMock {
+ constructor({buildContext, project}) {
+ t.is(buildContext, testBuildContext, "Correct buildContext parameter");
+ t.is(project, project, "Correct project parameter");
+ }
+ getTaskUtil() {
+ return "taskUtil";
+ }
+ setTaskRunner(_taskRunner) {
+ t.is(_taskRunner, taskRunner);
+ }
+ }
+ const BuildContext = await esmock("../../../../lib/build/helpers/BuildContext.js", {
+ "../../../../lib/build/helpers/ProjectBuildContext.js": ProjectContextMock,
+ "../../../../lib/build/TaskRunner.js": {
+ create: sinon.stub().resolves(taskRunner)
+ }
+ });
+ const graph = {
+ getRoot: () => ({getType: () => "library"}),
+ };
+ const testBuildContext = new BuildContext(graph, "taskRepository");
+
+ const projectContext = await testBuildContext.createProjectContext({
+ project
+ });
+
+ t.true(projectContext instanceof ProjectContextMock,
+ "Project context is an instance of ProjectContextMock");
+ t.is(testBuildContext._projectBuildContexts[0], projectContext,
+ "BuildContext stored correct ProjectBuildContext");
+});
+
+test("requiresBuild: has no build-manifest", (t) => {
+ const project = {
+ getName: sinon.stub().returns("foo"),
+ getType: sinon.stub().returns("bar"),
+ getBuildManifest: () => null
+ };
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {},
+ project
+ });
+ t.true(projectBuildContext.requiresBuild(), "Project without build-manifest requires to be build");
+});
+
+test("requiresBuild: has build-manifest", (t) => {
+ const project = {
+ getName: sinon.stub().returns("foo"),
+ getType: sinon.stub().returns("bar"),
+ getBuildManifest: () => {
+ return {
+ timestamp: "2022-07-28T12:00:00.000Z"
+ };
+ }
+ };
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {},
+ project
+ });
+ t.false(projectBuildContext.requiresBuild(), "Project with build-manifest does not require to be build");
+});
+
+test.serial("getBuildMetadata", (t) => {
+ const project = {
+ getName: sinon.stub().returns("foo"),
+ getType: sinon.stub().returns("bar"),
+ getBuildManifest: () => {
+ return {
+ timestamp: "2022-07-28T12:00:00.000Z"
+ };
+ }
+ };
+ const getTimeStub = sinon.stub(Date.prototype, "getTime").callThrough().onFirstCall().returns(1659016800000);
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {},
+ project
+ });
+
+ t.deepEqual(projectBuildContext.getBuildMetadata(), {
+ timestamp: "2022-07-28T12:00:00.000Z",
+ age: "7200 seconds"
+ }, "Project with build-manifest does not require to be build");
+ getTimeStub.restore();
+});
+
+test("getBuildMetadata: has no build-manifest", (t) => {
+ const project = {
+ getName: sinon.stub().returns("foo"),
+ getType: sinon.stub().returns("bar"),
+ getBuildManifest: () => null
+ };
+ const projectBuildContext = new ProjectBuildContext({
+ buildContext: {},
+ project
+ });
+ t.is(projectBuildContext.getBuildMetadata(), null, "Project has no build manifest");
+});
diff --git a/packages/project/test/lib/build/helpers/TaskUtil.js b/packages/project/test/lib/build/helpers/TaskUtil.js
new file mode 100644
index 00000000000..694cb84ed43
--- /dev/null
+++ b/packages/project/test/lib/build/helpers/TaskUtil.js
@@ -0,0 +1,509 @@
+import test from "ava";
+import sinon from "sinon";
+import TaskUtil from "../../../../lib/build/helpers/TaskUtil.js";
+import SpecificationVersion from "../../../../lib/specifications/SpecificationVersion.js";
+
+test.afterEach.always((t) => {
+ sinon.restore();
+});
+
+function getSpecificationVersion(specVersion) {
+ return new SpecificationVersion(specVersion);
+}
+
+const STANDARD_TAGS = Object.freeze({
+ IsDebugVariant: "ui5:IsDebugVariant",
+ HasDebugVariant: "ui5:HasDebugVariant",
+ OmitFromBuildResult: "ui5:OmitFromBuildResult",
+ IsBundle: "ui5:IsBundle"
+});
+
+test("Instantiation", (t) => {
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {}
+ });
+
+ t.deepEqual(taskUtil.STANDARD_TAGS, STANDARD_TAGS, "Correct standard tags exposed");
+});
+
+test("setTag", (t) => {
+ const setTagStub = sinon.stub();
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ getResourceTagCollection: () => {
+ return {
+ setTag: setTagStub
+ };
+ }
+ }
+ });
+
+ const dummyResource = {};
+ taskUtil.setTag(dummyResource, "my tag", "my value");
+
+ t.is(setTagStub.callCount, 1, "ResourceTagCollection#setTag got called once");
+ t.deepEqual(setTagStub.getCall(0).args[0], dummyResource, "Correct resource parameter supplied");
+ t.is(setTagStub.getCall(0).args[1], "my tag", "Correct tag parameter supplied");
+ t.is(setTagStub.getCall(0).args[2], "my value", "Correct value parameter supplied");
+});
+
+test("getTag", (t) => {
+ const getTagStub = sinon.stub().returns(42);
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ getResourceTagCollection: () => {
+ return {
+ getTag: getTagStub
+ };
+ }
+ }
+ });
+
+ const dummyResource = {};
+ const res = taskUtil.getTag(dummyResource, "my tag", "my value");
+
+ t.is(getTagStub.callCount, 1, "ResourceTagCollection#getTag got called once");
+ t.deepEqual(getTagStub.getCall(0).args[0], dummyResource, "Correct resource parameter supplied");
+ t.is(getTagStub.getCall(0).args[1], "my tag", "Correct tag parameter supplied");
+ t.is(res, 42, "Correct result");
+});
+
+test("clearTag", (t) => {
+ const clearTagStub = sinon.stub();
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ getResourceTagCollection: () => {
+ return {
+ clearTag: clearTagStub
+ };
+ }
+ }
+ });
+
+ const dummyResource = {};
+ taskUtil.clearTag(dummyResource, "my tag", "my value");
+
+ t.is(clearTagStub.callCount, 1, "ResourceTagCollection#clearTag got called once");
+ t.deepEqual(clearTagStub.getCall(0).args[0], dummyResource, "Correct resource parameter supplied");
+ t.is(clearTagStub.getCall(0).args[1], "my tag", "Correct tag parameter supplied");
+});
+
+test("setTag with resource path is not supported anymore", (t) => {
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {}
+ });
+
+ const err = t.throws(() => {
+ taskUtil.setTag("my resource", "my tag", "my value");
+ });
+ t.is(err.message,
+ "Deprecated parameter: Since UI5 CLI 3.0, #setTag " +
+ "requires a resource instance. Strings are no longer accepted",
+ "Threw with expected error message");
+});
+
+test("getTag with resource path is not supported anymore", (t) => {
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {}
+ });
+
+ const err = t.throws(() => {
+ taskUtil.getTag("my resource", "my tag", "my value");
+ });
+ t.is(err.message,
+ "Deprecated parameter: Since UI5 CLI 3.0, #getTag " +
+ "requires a resource instance. Strings are no longer accepted",
+ "Threw with expected error message");
+});
+
+test("clearTag with resource path is not supported anymore", (t) => {
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {}
+ });
+
+ const err = t.throws(() => {
+ taskUtil.clearTag("my resource", "my tag", "my value");
+ });
+ t.is(err.message,
+ "Deprecated parameter: Since UI5 CLI 3.0, #clearTag " +
+ "requires a resource instance. Strings are no longer accepted",
+ "Threw with expected error message");
+});
+
+test("isRootProject", (t) => {
+ const isRootProjectStub = sinon.stub().returns(true);
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ isRootProject: isRootProjectStub
+ }
+ });
+
+ const res = taskUtil.isRootProject();
+
+ t.is(isRootProjectStub.callCount, 1, "ProjectBuildContext#isRootProject got called once");
+ t.is(res, true, "Correct result");
+});
+
+test("getBuildOption", (t) => {
+ const getOptionStub = sinon.stub().returns("Pony");
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ getOption: getOptionStub
+ }
+ });
+
+ const res = taskUtil.getBuildOption("friend");
+
+ t.is(getOptionStub.callCount, 1, "ProjectBuildContext#getBuildOption got called once");
+ t.is(res, "Pony", "Correct result");
+});
+
+test("getProject", (t) => {
+ const getProjectStub = sinon.stub().returns("Pony farm!");
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ getProject: getProjectStub
+ }
+ });
+
+ const res = taskUtil.getProject("pony farm");
+
+ t.is(getProjectStub.callCount, 1, "ProjectBuildContext#getProject got called once");
+ t.is(getProjectStub.getCall(0).args[0], "pony farm",
+ "ProjectBuildContext#getProject got called with expected arguments");
+ t.is(res, "Pony farm!", "Correct result");
+});
+
+test("getProject: Default name", (t) => {
+ const getProjectStub = sinon.stub().returns("Pony farm!");
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ getProject: getProjectStub
+ }
+ });
+
+ const res = taskUtil.getProject();
+
+ t.is(getProjectStub.callCount, 1, "ProjectBuildContext#getProject got called once");
+ t.is(getProjectStub.getCall(0).args[0], undefined,
+ "ProjectBuildContext#getProject got called with no arguments");
+ t.is(res, "Pony farm!", "Correct result");
+});
+
+test("getProject: Resource", (t) => {
+ const getProjectStub = sinon.stub();
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ getProject: getProjectStub
+ }
+ });
+
+ const mockResource = {
+ getProject: sinon.stub().returns("Pig farm!")
+ };
+ const res = taskUtil.getProject(mockResource);
+
+ t.is(getProjectStub.callCount, 0, "ProjectBuildContext#getProject has not been called");
+ t.is(mockResource.getProject.callCount, 1, "Resource#getProject has been called once");
+ t.is(res, "Pig farm!", "Correct result");
+});
+
+test("getDependencies", (t) => {
+ const getDependenciesStub = sinon.stub().returns("Pony farm!");
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ getDependencies: getDependenciesStub
+ }
+ });
+
+ const res = taskUtil.getDependencies("pony farm");
+
+ t.is(getDependenciesStub.callCount, 1, "ProjectBuildContext#getDependencies got called once");
+ t.is(getDependenciesStub.getCall(0).args[0], "pony farm",
+ "ProjectBuildContext#getDependencies got called with expected arguments");
+ t.is(res, "Pony farm!", "Correct result");
+});
+
+test("getDependencies: Default name", (t) => {
+ const getDependenciesStub = sinon.stub().returns("Pony farm!");
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ getDependencies: getDependenciesStub
+ }
+ });
+
+ const res = taskUtil.getDependencies();
+
+ t.is(getDependenciesStub.callCount, 1, "ProjectBuildContext#getDependencies got called once");
+ t.is(getDependenciesStub.getCall(0).args[0], undefined,
+ "ProjectBuildContext#getDependencies got called with no arguments");
+ t.is(res, "Pony farm!", "Correct result");
+});
+
+test("resourceFactory", (t) => {
+ const {resourceFactory} = new TaskUtil({
+ projectBuildContext: {}
+ });
+ t.is(typeof resourceFactory.createResource, "function",
+ "resourceFactory function createResource is available");
+ t.is(typeof resourceFactory.createReaderCollection, "function",
+ "resourceFactory function createReaderCollection is available");
+ t.is(typeof resourceFactory.createReaderCollectionPrioritized, "function",
+ "resourceFactory function createReaderCollectionPrioritized is available");
+ t.is(typeof resourceFactory.createFilterReader, "function",
+ "resourceFactory function createFilterReader is available");
+ t.is(typeof resourceFactory.createLinkReader, "function",
+ "resourceFactory function createLinkReader is available");
+ t.is(typeof resourceFactory.createFlatReader, "function",
+ "resourceFactory function createFlatReader is available");
+});
+
+test("registerCleanupTask", (t) => {
+ const registerCleanupTaskStub = sinon.stub();
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ registerCleanupTask: registerCleanupTaskStub
+ }
+ });
+
+ taskUtil.registerCleanupTask("my callback");
+
+ t.is(registerCleanupTaskStub.callCount, 1, "ProjectBuildContext#registerCleanupTask got called once");
+ t.is(registerCleanupTaskStub.getCall(0).args[0], "my callback", "Correct callback parameter supplied");
+});
+
+test("getInterface: specVersion 1.0", (t) => {
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {}
+ });
+
+ const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("1.0"));
+
+ t.is(interfacedTaskUtil, undefined, "no interface provided");
+});
+
+test("getInterface: specVersion 2.2", (t) => {
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {}
+ });
+
+ const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("2.2"));
+
+ t.deepEqual(Object.keys(interfacedTaskUtil), [
+ "STANDARD_TAGS",
+ "setTag",
+ "clearTag",
+ "getTag",
+ "isRootProject",
+ "registerCleanupTask"
+ ], "Correct methods are provided");
+
+ t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided");
+ t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided");
+ t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided");
+ t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided");
+ t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided");
+ t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided");
+});
+
+test("getInterface: specVersion 2.3", (t) => {
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {}
+ });
+
+ const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("2.3"));
+
+ t.deepEqual(Object.keys(interfacedTaskUtil), [
+ "STANDARD_TAGS",
+ "setTag",
+ "clearTag",
+ "getTag",
+ "isRootProject",
+ "registerCleanupTask"
+ ], "Correct methods are provided");
+
+ t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided");
+ t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided");
+ t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided");
+ t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided");
+ t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided");
+ t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided");
+});
+
+test("getInterface: specVersion 2.4", (t) => {
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {}
+ });
+
+ const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("2.4"));
+
+ t.deepEqual(Object.keys(interfacedTaskUtil), [
+ "STANDARD_TAGS",
+ "setTag",
+ "clearTag",
+ "getTag",
+ "isRootProject",
+ "registerCleanupTask"
+ ], "Correct methods are provided");
+
+ t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided");
+ t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided");
+ t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided");
+ t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided");
+ t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided");
+ t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided");
+});
+
+test("getInterface: specVersion 2.5", (t) => {
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {}
+ });
+
+ const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("2.5"));
+
+ t.deepEqual(Object.keys(interfacedTaskUtil), [
+ "STANDARD_TAGS",
+ "setTag",
+ "clearTag",
+ "getTag",
+ "isRootProject",
+ "registerCleanupTask"
+ ], "Correct methods are provided");
+
+ t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided");
+ t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided");
+ t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided");
+ t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided");
+ t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided");
+ t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided");
+});
+
+test("getInterface: specVersion 2.6", (t) => {
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {}
+ });
+
+ const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("2.6"));
+
+ t.deepEqual(Object.keys(interfacedTaskUtil), [
+ "STANDARD_TAGS",
+ "setTag",
+ "clearTag",
+ "getTag",
+ "isRootProject",
+ "registerCleanupTask"
+ ], "Correct methods are provided");
+
+ t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided");
+ t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided");
+ t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided");
+ t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided");
+ t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided");
+ t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided");
+});
+
+test("getInterface: specVersion 3.0", (t) => {
+ const getProjectStub = sinon.stub().returns({
+ getSpecVersion: () => "specVersion",
+ getType: () => "type",
+ getName: () => "name",
+ getVersion: () => "version",
+ getNamespace: () => "namespace",
+ getRootReader: () => "rootReader",
+ getReader: () => "reader",
+ getRootPath: () => "rootPath",
+ getSourcePath: () => "sourcePath",
+ getCustomConfiguration: () => "customConfiguration",
+ isFrameworkProject: () => "isFrameworkProject",
+ getFrameworkVersion: () => "frameworkVersion",
+ getFrameworkName: () => "frameworkName",
+ getFrameworkDependencies: () => ["frameworkDependencies"],
+ hasBuildManifest: () => "hasBuildManifest", // Should not be exposed
+ });
+ const getDependenciesStub = sinon.stub().returns(["dep a", "dep b"]);
+
+ const taskUtil = new TaskUtil({
+ projectBuildContext: {
+ getProject: getProjectStub,
+ getDependencies: getDependenciesStub
+ }
+ });
+
+ const interfacedTaskUtil = taskUtil.getInterface(getSpecificationVersion("3.0"));
+
+ t.deepEqual(Object.keys(interfacedTaskUtil), [
+ "STANDARD_TAGS",
+ "setTag",
+ "clearTag",
+ "getTag",
+ "isRootProject",
+ "registerCleanupTask",
+ "getProject",
+ "getDependencies",
+ "resourceFactory",
+ ], "Correct methods are provided");
+
+ t.deepEqual(interfacedTaskUtil.STANDARD_TAGS, STANDARD_TAGS, "attribute STANDARD_TAGS is provided");
+ t.is(typeof interfacedTaskUtil.setTag, "function", "function setTag is provided");
+ t.is(typeof interfacedTaskUtil.clearTag, "function", "function clearTag is provided");
+ t.is(typeof interfacedTaskUtil.getTag, "function", "function getTag is provided");
+ t.is(typeof interfacedTaskUtil.isRootProject, "function", "function isRootProject is provided");
+ t.is(typeof interfacedTaskUtil.registerCleanupTask, "function", "function registerCleanupTask is provided");
+ t.is(typeof interfacedTaskUtil.getProject, "function", "function registerCleanupTask is provided");
+
+ // getProject
+ const interfacedProject = interfacedTaskUtil.getProject("pony");
+ t.deepEqual(Object.keys(interfacedProject), [
+ "getType",
+ "getName",
+ "getVersion",
+ "getNamespace",
+ "getRootReader",
+ "getReader",
+ "getRootPath",
+ "getSourcePath",
+ "getCustomConfiguration",
+ "isFrameworkProject",
+ "getFrameworkName",
+ "getFrameworkVersion",
+ "getFrameworkDependencies",
+ ], "Correct methods are provided");
+
+ t.is(interfacedProject.getType(), "type", "getType function is bound correctly");
+ t.is(interfacedProject.getName(), "name", "getName function is bound correctly");
+ t.is(interfacedProject.getVersion(), "version", "getVersion function is bound correctly");
+ t.is(interfacedProject.getNamespace(), "namespace", "getNamespace function is bound correctly");
+ t.is(interfacedProject.getRootPath(), "rootPath", "getRootPath function is bound correctly");
+ t.is(interfacedProject.getRootReader(), "rootReader", "getRootReader function is bound correctly");
+ t.is(interfacedProject.getSourcePath(), "sourcePath", "getSourcePath function is bound correctly");
+ t.is(interfacedProject.getReader(), "reader", "getReader function is bound correctly");
+ t.is(interfacedProject.getCustomConfiguration(), "customConfiguration",
+ "getCustomConfiguration function is bound correctly");
+ t.is(interfacedProject.isFrameworkProject(), "isFrameworkProject",
+ "isFrameworkProject function is bound correctly");
+ t.is(interfacedProject.getFrameworkVersion(), "frameworkVersion",
+ "getFrameworkVersion function is bound correctly");
+ t.is(interfacedProject.getFrameworkName(), "frameworkName",
+ "getFrameworkName function is bound correctly");
+ t.deepEqual(interfacedProject.getFrameworkDependencies(), ["frameworkDependencies"],
+ "getFrameworkDependencies function is bound correctly");
+
+ // getDependencies
+ t.deepEqual(interfacedTaskUtil.getDependencies("pony"), ["dep a", "dep b"],
+ "getDependencies function is available and bound correctly");
+
+ // resourceFactory
+ const resourceFactory = interfacedTaskUtil.resourceFactory;
+ t.is(typeof resourceFactory.createResource, "function",
+ "resourceFactory function createResource is available");
+ t.is(typeof resourceFactory.createReaderCollection, "function",
+ "resourceFactory function createReaderCollection is available");
+ t.is(typeof resourceFactory.createReaderCollectionPrioritized, "function",
+ "resourceFactory function createReaderCollectionPrioritized is available");
+ t.is(typeof resourceFactory.createFilterReader, "function",
+ "resourceFactory function createFilterReader is available");
+ t.is(typeof resourceFactory.createLinkReader, "function",
+ "resourceFactory function createLinkReader is available");
+ t.is(typeof resourceFactory.createFlatReader, "function",
+ "resourceFactory function createFlatReader is available");
+});
diff --git a/packages/project/test/lib/build/helpers/composeProjectList.js b/packages/project/test/lib/build/helpers/composeProjectList.js
new file mode 100644
index 00000000000..f8f58185f38
--- /dev/null
+++ b/packages/project/test/lib/build/helpers/composeProjectList.js
@@ -0,0 +1,326 @@
+import test from "ava";
+import sinon from "sinon";
+import esmock from "esmock";
+import path from "node:path";
+import {graphFromObject} from "../../../../lib/graph/graph.js";
+
+const __dirname = import.meta.dirname;
+
+const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a");
+const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e");
+const libraryFPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.f");
+const libraryGPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.g");
+const libraryDDependerPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d-depender");
+
+test.beforeEach(async (t) => {
+ t.context.log = {
+ warn: sinon.stub()
+ };
+ t.context.composeProjectList = await esmock("../../../../lib/build/helpers/composeProjectList", {
+ "@ui5/logger": {
+ getLogger: sinon.stub().withArgs("build:helpers:composeProjectList").returns(t.context.log)
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ sinon.restore();
+});
+
+test.serial("_getFlattenedDependencyTree", async (t) => {
+ const {_getFlattenedDependencyTree} = t.context.composeProjectList;
+ const tree = { // Does not reflect actual dependencies in fixtures
+ id: "application.a.id",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: [{
+ id: "library.e.id",
+ version: "1.0.0",
+ path: libraryEPath,
+ dependencies: [{
+ id: "library.d.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "library.d"),
+ dependencies: [{
+ id: "library.a.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.a"),
+ dependencies: [{
+ id: "library.b.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.b"),
+ dependencies: []
+ }, {
+ id: "library.c.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.c"),
+ dependencies: []
+ }]
+ }]
+ }]
+ }, {
+ id: "library.f.id",
+ version: "1.0.0",
+ path: libraryFPath,
+ dependencies: [{
+ id: "library.a.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.a"),
+ dependencies: [{
+ id: "library.b.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.b"),
+ dependencies: []
+ }, {
+ id: "library.c.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.c"),
+ dependencies: []
+ }]
+ }]
+ }]
+ };
+ const graph = await graphFromObject({dependencyTree: tree});
+
+ t.deepEqual(await _getFlattenedDependencyTree(graph), {
+ "library.e": ["library.d", "library.a", "library.b", "library.c"],
+ "library.f": ["library.a", "library.b", "library.c"],
+ "library.d": ["library.a", "library.b", "library.c"],
+ "library.a": ["library.b", "library.c"],
+ "library.b": [],
+ "library.c": []
+ });
+});
+
+async function assertCreateDependencyLists(t, {
+ includeAllDependencies,
+ includeDependency, includeDependencyRegExp, includeDependencyTree,
+ excludeDependency, excludeDependencyRegExp, excludeDependencyTree,
+ defaultIncludeDependency, defaultIncludeDependencyRegExp, defaultIncludeDependencyTree,
+ expectedIncludedDependencies, expectedExcludedDependencies,
+ expectedLogWarnCallCount = 0
+}) {
+ const tree = { // Does not reflect actual dependencies in fixtures
+ id: "application.a.id",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: [{
+ id: "library.e.id",
+ version: "1.0.0",
+ path: libraryEPath,
+ dependencies: [{
+ id: "library.d.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "library.d"),
+ dependencies: []
+ }, {
+ id: "library.a.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.a"),
+ dependencies: [{
+ id: "library.b.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.b"),
+ dependencies: []
+ }]
+ }]
+ }, {
+ id: "library.f.id",
+ version: "1.0.0",
+ path: libraryFPath,
+ dependencies: [{
+ id: "library.d.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "library.d"),
+ dependencies: []
+ }, {
+ id: "library.a.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.a"),
+ dependencies: [{
+ id: "library.b.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.b"),
+ dependencies: []
+ }]
+ }, {
+ id: "library.c.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.c"),
+ dependencies: []
+ }]
+ }, {
+ id: "library.g.id",
+ version: "1.0.0",
+ path: libraryGPath,
+ dependencies: [{
+ id: "library.d-depender.id",
+ version: "1.0.0",
+ path: libraryDDependerPath,
+ dependencies: []
+ }]
+ }]
+ };
+
+ const graph = await graphFromObject({dependencyTree: tree});
+
+ const {includedDependencies, excludedDependencies} = await t.context.composeProjectList(graph, {
+ includeAllDependencies,
+ includeDependency,
+ includeDependencyRegExp,
+ includeDependencyTree,
+ excludeDependency,
+ excludeDependencyRegExp,
+ excludeDependencyTree,
+ defaultIncludeDependency,
+ defaultIncludeDependencyRegExp,
+ defaultIncludeDependencyTree
+ });
+ t.deepEqual(includedDependencies, expectedIncludedDependencies, "Correct set of included dependencies");
+ t.deepEqual(excludedDependencies, expectedExcludedDependencies, "Correct set of excluded dependencies");
+
+ t.is(t.context.log.warn.callCount, expectedLogWarnCallCount);
+}
+
+test.serial("createDependencyLists: only includes", async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: false,
+ includeDependency: ["library.f", "library.c"],
+ includeDependencyRegExp: ["^library\\.d$"],
+ includeDependencyTree: ["library.g"],
+ expectedIncludedDependencies: ["library.f", "library.c", "library.d", "library.g", "library.d-depender"],
+ expectedExcludedDependencies: []
+ });
+});
+
+test.serial("createDependencyLists: only excludes", async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: false,
+ excludeDependency: ["library.f", "library.c"],
+ excludeDependencyRegExp: ["^library\\.d$"],
+ excludeDependencyTree: ["library.g"],
+ expectedIncludedDependencies: [],
+ expectedExcludedDependencies: ["library.f", "library.c", "library.d", "library.g", "library.d-depender"]
+ });
+});
+
+test.serial("createDependencyLists: include all + excludes", async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: true,
+ includeDependency: [],
+ excludeDependency: ["library.f", "library.c"],
+ excludeDependencyRegExp: ["^library\\.d$"],
+ excludeDependencyTree: ["library.g"],
+ expectedIncludedDependencies: ["library.b", "library.a", "library.e"],
+ expectedExcludedDependencies: ["library.f", "library.c", "library.d", "library.g", "library.d-depender"]
+ });
+});
+
+test.serial("createDependencyLists: include all", async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: true,
+ includeDependency: [],
+ excludeDependency: [],
+ excludeDependencyRegExp: [],
+ excludeDependencyTree: [],
+ expectedIncludedDependencies: [
+ "library.d", "library.b", "library.c",
+ "library.d-depender", "library.a", "library.g",
+ "library.e", "library.f"
+ ],
+ expectedExcludedDependencies: []
+ });
+});
+
+test.serial("createDependencyLists: includeDependencyTree has lower priority than excludes", async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: false,
+ includeDependencyTree: ["library.f"],
+ excludeDependency: ["library.f"],
+ excludeDependencyRegExp: ["^library\\.[acd]$"],
+ expectedIncludedDependencies: ["library.b"],
+ expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.a"]
+ });
+});
+
+test.serial("createDependencyLists: excludeDependencyTree has lower priority than includes", async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: false,
+ includeDependency: ["library.f"],
+ includeDependencyRegExp: ["^library\\.[acd]$"],
+ excludeDependencyTree: ["library.f"],
+ expectedIncludedDependencies: ["library.f", "library.d", "library.c", "library.a"],
+ expectedExcludedDependencies: ["library.b"]
+ });
+});
+
+test.serial("createDependencyLists: include all, exclude tree and include single", async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: true,
+ includeDependency: ["library.f"],
+ includeDependencyRegExp: ["^library\\.[acd]$"],
+ excludeDependencyTree: ["library.f"],
+ expectedIncludedDependencies: [
+ "library.f", "library.d", "library.c", "library.a", "library.d-depender",
+ "library.g", "library.e"
+ ],
+ expectedExcludedDependencies: ["library.b"]
+ });
+});
+
+test.serial("createDependencyLists: includeDependencyTree has higher priority than excludeDependencyTree",
+ async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: false,
+ includeDependencyTree: ["library.f"],
+ excludeDependencyTree: ["library.f"],
+ expectedIncludedDependencies: ["library.f", "library.d", "library.a", "library.b", "library.c"],
+ expectedExcludedDependencies: []
+ });
+ });
+
+test.serial("createDependencyLists: defaultIncludeDependency/RegExp has lower priority than excludes", async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: false,
+ defaultIncludeDependency: ["library.f", "library.c", "library.b"],
+ defaultIncludeDependencyRegExp: ["^library\\.d$"],
+ excludeDependency: ["library.f"],
+ excludeDependencyRegExp: ["^library\\.[acd](-depender)?$"],
+ expectedIncludedDependencies: ["library.b"],
+ expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.d-depender", "library.a"]
+ });
+});
+test.serial("createDependencyLists: include all and defaultIncludeDependency/RegExp", async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: true,
+ defaultIncludeDependency: ["library.f", "library.c", "library.b"],
+ defaultIncludeDependencyRegExp: ["^library\\.d$"],
+ excludeDependency: ["library.f"],
+ excludeDependencyRegExp: ["^library\\.[acd](-depender)?$"],
+ expectedIncludedDependencies: ["library.b", "library.g", "library.e"],
+ expectedExcludedDependencies: ["library.f", "library.d", "library.c", "library.d-depender", "library.a"]
+ });
+});
+
+test.serial("createDependencyLists: defaultIncludeDependencyTree has lower priority than excludes", async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: false,
+ defaultIncludeDependencyTree: ["library.f"],
+ excludeDependencyTree: ["library.a"],
+ expectedIncludedDependencies: ["library.f", "library.d", "library.c"],
+ expectedExcludedDependencies: ["library.a", "library.b"]
+ });
+});
+
+test.serial("createDependencyLists: Could not find dependency", async (t) => {
+ await assertCreateDependencyLists(t, {
+ includeAllDependencies: false,
+ includeDependency: ["not.in.dependency.tree"],
+ expectedIncludedDependencies: [],
+ expectedExcludedDependencies: [],
+ expectedLogWarnCallCount: 1
+ });
+ t.deepEqual(t.context.log.warn.getCall(0).args, [
+ `Could not find dependency "not.in.dependency.tree" for project application.a. Dependency filter is ignored`
+ ]);
+});
diff --git a/packages/project/test/lib/build/helpers/composeTaskList.js b/packages/project/test/lib/build/helpers/composeTaskList.js
new file mode 100644
index 00000000000..910994aabec
--- /dev/null
+++ b/packages/project/test/lib/build/helpers/composeTaskList.js
@@ -0,0 +1,243 @@
+import test from "ava";
+import sinon from "sinon";
+import esmock from "esmock";
+
+test.beforeEach(async (t) => {
+ t.context.log = {
+ warn: sinon.stub()
+ };
+ const getLoggerStub = sinon.stub().withArgs("build:helpers:composeTaskList").returns(t.context.log);
+
+ t.context.composeTaskList = await esmock("../../../../lib/build/helpers/composeTaskList.js", {
+ "@ui5/logger": {
+ getLogger: getLoggerStub
+ }
+ });
+});
+
+const allTasks = [
+ "replaceCopyright",
+ "replaceVersion",
+ "replaceBuildtime",
+ "escapeNonAsciiCharacters",
+ "executeJsdocSdkTransformation",
+ "generateApiIndex",
+ "generateJsdoc",
+ "minify",
+ "buildThemes",
+ "transformBootstrapHtml",
+ "generateLibraryManifest",
+ "generateVersionInfo",
+ "generateFlexChangesBundle",
+ "generateComponentPreload",
+ "generateResourcesJson",
+ "generateThemeDesignerResources",
+ "generateStandaloneAppBundle",
+ "generateBundle",
+ "generateLibraryPreload",
+ "generateCachebusterInfo",
+];
+
+
+[
+ [
+ "composeTaskList: archive=false / selfContained=false / jsdoc=false", {
+ archive: false,
+ selfContained: false,
+ jsdoc: false,
+ includedTasks: [],
+ excludedTasks: []
+ }, [
+ "replaceCopyright",
+ "replaceVersion",
+ "replaceBuildtime",
+ "escapeNonAsciiCharacters",
+ "minify",
+ "buildThemes",
+ "generateLibraryManifest",
+ "generateFlexChangesBundle",
+ "generateComponentPreload",
+ "generateBundle",
+ "generateLibraryPreload",
+ ]
+ ],
+ [
+ "composeTaskList: archive=true / selfContained=false / jsdoc=false", {
+ archive: true,
+ selfContained: false,
+ jsdoc: false,
+ includedTasks: [],
+ excludedTasks: []
+ }, [
+ "replaceCopyright",
+ "replaceVersion",
+ "replaceBuildtime",
+ "escapeNonAsciiCharacters",
+ "minify",
+ "buildThemes",
+ "generateLibraryManifest",
+ "generateFlexChangesBundle",
+ "generateComponentPreload",
+ "generateBundle",
+ "generateLibraryPreload",
+ ]
+ ],
+ [
+ "composeTaskList: archive=false / selfContained=true / jsdoc=false", {
+ archive: false,
+ selfContained: true,
+ jsdoc: false,
+ includedTasks: [],
+ excludedTasks: []
+ }, [
+ "replaceCopyright",
+ "replaceVersion",
+ "replaceBuildtime",
+ "escapeNonAsciiCharacters",
+ "minify",
+ "buildThemes",
+ "transformBootstrapHtml",
+ "generateLibraryManifest",
+ "generateFlexChangesBundle",
+ "generateStandaloneAppBundle",
+ "generateBundle"
+ ]
+ ],
+ [
+ "composeTaskList: archive=false / selfContained=false / jsdoc=true", {
+ archive: false,
+ selfContained: false,
+ jsdoc: true,
+ includedTasks: [],
+ excludedTasks: []
+ }, [
+ "escapeNonAsciiCharacters",
+ "executeJsdocSdkTransformation",
+ "generateApiIndex",
+ "generateJsdoc",
+ "buildThemes",
+ "generateVersionInfo",
+ "generateBundle",
+ ]
+ ],
+ [
+ "composeTaskList: includedTasks / excludedTasks", {
+ archive: false,
+ selfContained: false,
+ jsdoc: false,
+ includedTasks: ["generateResourcesJson", "replaceVersion"],
+ excludedTasks: ["replaceCopyright", "generateApiIndex"]
+ }, [
+ "replaceVersion",
+ "replaceBuildtime",
+ "escapeNonAsciiCharacters",
+ "minify",
+ "buildThemes",
+ "generateLibraryManifest",
+ "generateFlexChangesBundle",
+ "generateComponentPreload",
+ "generateResourcesJson",
+ "generateBundle",
+ "generateLibraryPreload",
+ ]
+ ],
+ [
+ "composeTaskList: includedTasks=*", {
+ archive: false,
+ selfContained: false,
+ jsdoc: false,
+ includedTasks: ["*"],
+ excludedTasks: []
+ }, [
+ "replaceCopyright",
+ "replaceVersion",
+ "replaceBuildtime",
+ "escapeNonAsciiCharacters",
+ "executeJsdocSdkTransformation",
+ "generateApiIndex",
+ "generateJsdoc",
+ "minify",
+ "buildThemes",
+ "transformBootstrapHtml",
+ "generateLibraryManifest",
+ "generateVersionInfo",
+ "generateFlexChangesBundle",
+ "generateComponentPreload",
+ "generateResourcesJson",
+ "generateThemeDesignerResources",
+ "generateStandaloneAppBundle",
+ "generateBundle",
+ "generateLibraryPreload",
+ "generateCachebusterInfo",
+ ]
+ ],
+ [
+ "composeTaskList: excludedTasks=*", {
+ archive: false,
+ selfContained: false,
+ jsdoc: false,
+ includedTasks: [],
+ excludedTasks: ["*"]
+ }, []
+ ],
+ [
+ "composeTaskList: includedTasks with unknown tasks", {
+ archive: false,
+ selfContained: false,
+ jsdoc: false,
+ includedTasks: ["foo", "bar"],
+ excludedTasks: []
+ }, [
+ "replaceCopyright",
+ "replaceVersion",
+ "replaceBuildtime",
+ "escapeNonAsciiCharacters",
+ "minify",
+ "buildThemes",
+ "generateLibraryManifest",
+ "generateFlexChangesBundle",
+ "generateComponentPreload",
+ "generateBundle",
+ "generateLibraryPreload",
+ ], (t) => {
+ const {log} = t.context;
+ t.is(log.warn.callCount, 0);
+ }
+ ],
+ [
+ "composeTaskList: excludedTasks with unknown tasks", {
+ archive: false,
+ selfContained: false,
+ jsdoc: false,
+ includedTasks: [],
+ excludedTasks: ["foo", "bar"],
+ }, [
+ "replaceCopyright",
+ "replaceVersion",
+ "replaceBuildtime",
+ "escapeNonAsciiCharacters",
+ "minify",
+ "buildThemes",
+ "generateLibraryManifest",
+ "generateFlexChangesBundle",
+ "generateComponentPreload",
+ "generateBundle",
+ "generateLibraryPreload",
+ ], (t) => {
+ const {log} = t.context;
+ t.is(log.warn.callCount, 0);
+ }
+ ],
+].forEach(([testTitle, args, expectedTaskList, assertCb]) => {
+ test.serial(testTitle, (t) => {
+ const {composeTaskList, log} = t.context;
+ const taskList = composeTaskList(allTasks, args);
+ t.deepEqual(taskList, expectedTaskList);
+ if (assertCb) {
+ assertCb(t);
+ } else {
+ // When no cb is defined, no logs are expected
+ t.is(log.warn.callCount, 0);
+ }
+ });
+});
diff --git a/packages/project/test/lib/build/helpers/createBuildManifest.integration.js b/packages/project/test/lib/build/helpers/createBuildManifest.integration.js
new file mode 100644
index 00000000000..015ca68bd1d
--- /dev/null
+++ b/packages/project/test/lib/build/helpers/createBuildManifest.integration.js
@@ -0,0 +1,108 @@
+import test from "ava";
+import path from "node:path";
+import createBuildManifest from "../../../../lib/build/helpers/createBuildManifest.js";
+import Module from "../../../../lib/graph/Module.js";
+import Specification from "../../../../lib/specifications/Specification.js";
+
+const __dirname = import.meta.dirname;
+
+const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a");
+const buildDescrApplicationAPath =
+ path.join(__dirname, "..", "..", "..", "fixtures", "build-manifest", "application.a");
+const applicationAConfig = {
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "application",
+ metadata: {name: "application.a"}
+ }
+};
+const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e");
+const buildDescrLibraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "build-manifest", "library.e");
+const libraryEConfig = {
+ id: "library.e.id",
+ version: "1.0.0",
+ modulePath: libraryEPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "library",
+ metadata: {name: "library.e"}
+ }
+};
+
+const buildConfig = {
+ selfContained: false,
+ jsdoc: false,
+ includedTasks: [],
+ excludedTasks: []
+};
+
+// Note: The actual build-manifest.json files in the fixtures are never used in these tests
+
+test("Create project from application project providing a build manifest", async (t) => {
+ const inputProject = await Specification.create(applicationAConfig);
+ inputProject.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant");
+
+ const taskRepository = {
+ getVersions: async () => ({a: "a", b: "b"})
+ };
+
+ const metadata = await createBuildManifest(inputProject, buildConfig, taskRepository);
+ const m = new Module({
+ id: "build-descr-application.a.id",
+ version: "2.0.0",
+ modulePath: buildDescrApplicationAPath,
+ configuration: metadata
+ });
+
+ const {project} = await m.getSpecifications();
+ t.truthy(project, "Module was able to create project from build manifest metadata");
+ t.is(project.getName(), project.getName(), "Archive project has correct name");
+ t.is(project.getNamespace(), project.getNamespace(), "Archive project has correct namespace");
+ t.is(project.getResourceTagCollection().getTag("/resources/id1/foo.js", "ui5:HasDebugVariant"), true,
+ "Archive project has correct tag");
+ t.is(project.getVersion(), "2.0.0", "Archive project has version from archive module");
+
+ const reader = project.getReader();
+ const resources = await reader.byGlob("**/test.js");
+ t.is(resources.length, 1,
+ "Found requested resource in archive project");
+ t.is(resources[0].getPath(), "/resources/id1/test.js",
+ "Resource has expected path");
+});
+
+test("Create project from library project providing a build manifest", async (t) => {
+ const inputProject = await Specification.create(libraryEConfig);
+ inputProject.getResourceTagCollection().setTag("/resources/library/e/file.js", "ui5:HasDebugVariant");
+
+ const taskRepository = {
+ getVersions: async () => ({a: "a", b: "b"})
+ };
+
+ const metadata = await createBuildManifest(inputProject, buildConfig, taskRepository);
+ const m = new Module({
+ id: "build-descr-library.e.id",
+ version: "2.0.0",
+ modulePath: buildDescrLibraryEPath,
+ configuration: metadata
+ });
+
+ const {project} = await m.getSpecifications();
+ t.truthy(project, "Module was able to create project from build manifest metadata");
+ t.is(project.getName(), project.getName(), "Archive project has correct name");
+ t.is(project.getNamespace(), project.getNamespace(), "Archive project has correct namespace");
+ t.is(project.getResourceTagCollection().getTag("/resources/library/e/file.js", "ui5:HasDebugVariant"), true,
+ "Archive project has correct tag");
+ t.is(project.getVersion(), "2.0.0", "Archive project has version from archive module");
+
+ const reader = project.getReader();
+ const resources = await reader.byGlob("**/some.js");
+ t.is(resources.length, 1,
+ "Found requested resource in archive project");
+ t.is(resources[0].getPath(), "/resources/library/e/some.js",
+ "Resource has expected path");
+});
diff --git a/packages/project/test/lib/build/helpers/createBuildManifest.js b/packages/project/test/lib/build/helpers/createBuildManifest.js
new file mode 100644
index 00000000000..7b2266b8897
--- /dev/null
+++ b/packages/project/test/lib/build/helpers/createBuildManifest.js
@@ -0,0 +1,176 @@
+import test from "ava";
+import path from "node:path";
+import semver from "semver";
+import createBuildManifest from "../../../../lib/build/helpers/createBuildManifest.js";
+import Specification from "../../../../lib/specifications/Specification.js";
+
+const __dirname = import.meta.dirname;
+
+const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a");
+const applicationProjectInput = {
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "application",
+ metadata: {name: "application.a"}
+ }
+};
+
+const libraryDPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d");
+const libraryProjectInput = {
+ id: "library.d.id",
+ version: "1.0.0",
+ modulePath: libraryDPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "library",
+ metadata: {
+ name: "library.d",
+ },
+ resources: {
+ configuration: {
+ paths: {
+ src: "main/src",
+ test: "main/test"
+ }
+ }
+ },
+ }
+};
+
+test("Missing parameter: project", async (t) => {
+ await t.throwsAsync(createBuildManifest(), {
+ message: "Missing parameter 'project'"
+ });
+});
+
+test("Missing parameter: buildConfig", async (t) => {
+ const project = await Specification.create(applicationProjectInput);
+
+ await t.throwsAsync(createBuildManifest(project), {
+ message: "Missing parameter 'buildConfig'"
+ });
+});
+
+test("Missing parameter: taskRepository", async (t) => {
+ const project = await Specification.create(applicationProjectInput);
+
+ await t.throwsAsync(createBuildManifest(project, "buildConfig"), {
+ message: "Missing parameter 'taskRepository'"
+ });
+});
+
+test("Create application from project with build manifest", async (t) => {
+ const project = await Specification.create(applicationProjectInput);
+ project.getResourceTagCollection().setTag("/resources/id1/foo.js", "ui5:HasDebugVariant");
+
+ const taskRepository = {
+ getVersions: async () => ({builderVersion: "", fsVersion: ""})
+ };
+
+ const metadata = await createBuildManifest(project, "buildConfig", taskRepository);
+
+ t.truthy(new Date(metadata.buildManifest.timestamp), "Timestamp is valid");
+ metadata.buildManifest.timestamp = "";
+
+ t.not(semver.valid(metadata.buildManifest.versions.fsVersion), null, "fs version should be filled");
+ metadata.buildManifest.versions.fsVersion = "";
+
+ t.not(semver.valid(metadata.buildManifest.versions.projectVersion), null, "project version should be filled");
+ metadata.buildManifest.versions.projectVersion = "";
+
+ t.deepEqual(metadata, {
+ project: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.a",
+ },
+ resources: {
+ configuration: {
+ paths: {
+ webapp: "resources/id1",
+ },
+ },
+ }
+ },
+ buildManifest: {
+ manifestVersion: "0.2",
+ buildConfig: "buildConfig",
+ namespace: "id1",
+ timestamp: "",
+ version: "1.0.0",
+ versions: {
+ builderVersion: "",
+ fsVersion: "",
+ projectVersion: "",
+ builderFsVersion: "",
+ },
+ tags: {
+ "/resources/id1/foo.js": {
+ "ui5:HasDebugVariant": true,
+ },
+ }
+ }
+ }, "Returned correct metadata");
+});
+
+test("Create library from project with build manifest", async (t) => {
+ const project = await Specification.create(libraryProjectInput);
+ project.getResourceTagCollection().setTag("/resources/library/d/foo.js", "ui5:HasDebugVariant");
+
+ const taskRepository = {
+ getVersions: async () => ({builderVersion: "", fsVersion: ""})
+ };
+
+ const metadata = await createBuildManifest(project, "buildConfig", taskRepository);
+
+ t.truthy(new Date(metadata.buildManifest.timestamp), "Timestamp is valid");
+ metadata.buildManifest.timestamp = "";
+
+ t.not(semver.valid(metadata.buildManifest.versions.fsVersion), null, "fs version should be filled");
+ metadata.buildManifest.versions.fsVersion = "";
+
+ t.not(semver.valid(metadata.buildManifest.versions.projectVersion), null, "project version should be filled");
+ metadata.buildManifest.versions.projectVersion = "";
+
+ t.deepEqual(metadata, {
+ project: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "library.d",
+ },
+ resources: {
+ configuration: {
+ paths: {
+ src: "resources",
+ test: "test-resources",
+ },
+ },
+ }
+ },
+ buildManifest: {
+ manifestVersion: "0.2",
+ buildConfig: "buildConfig",
+ namespace: "library/d",
+ timestamp: "",
+ version: "1.0.0",
+ versions: {
+ builderVersion: "",
+ fsVersion: "",
+ projectVersion: "",
+ builderFsVersion: "",
+ },
+ tags: {
+ "/resources/library/d/foo.js": {
+ "ui5:HasDebugVariant": true,
+ },
+ }
+ }
+ }, "Returned correct metadata");
+});
diff --git a/packages/project/test/lib/config/Configuration.js b/packages/project/test/lib/config/Configuration.js
new file mode 100644
index 00000000000..7acbadba201
--- /dev/null
+++ b/packages/project/test/lib/config/Configuration.js
@@ -0,0 +1,174 @@
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.homedirStub = sinon.stub().returns("~");
+ t.context.promisifyStub = sinon.stub();
+ t.context.resolveStub = sinon.stub().callsFake((path) => path);
+ t.context.joinStub = sinon.stub().callsFake((...args) => args.join("/"));
+ t.context.Configuration = await esmock.p("../../../lib/config/Configuration.js", {
+ "node:path": {
+ resolve: t.context.resolveStub,
+ join: t.context.joinStub
+ },
+ "node:util": {
+ "promisify": t.context.promisifyStub
+ },
+ "node:os": {
+ "homedir": t.context.homedirStub
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.Configuration);
+});
+
+test.serial("Configuration options", (t) => {
+ const {Configuration} = t.context;
+ t.deepEqual(Configuration.OPTIONS, [
+ "mavenSnapshotEndpointUrl",
+ "ui5DataDir"
+ ]);
+});
+
+test.serial("Build configuration with defaults", (t) => {
+ const {Configuration} = t.context;
+
+ const config = new Configuration({});
+
+ t.deepEqual(config.toJson(), {
+ mavenSnapshotEndpointUrl: undefined,
+ ui5DataDir: undefined,
+ });
+});
+
+test.serial("Overwrite defaults defaults", (t) => {
+ const {Configuration} = t.context;
+
+ const params = {
+ mavenSnapshotEndpointUrl: "https://snapshot.url",
+ ui5DataDir: "/custom/data/dir"
+ };
+
+ const config = new Configuration(params);
+
+ t.deepEqual(config.toJson(), params);
+});
+
+test.serial("Unknown configuration option", (t) => {
+ const {Configuration} = t.context;
+
+ const params = {
+ unknown: "foo"
+ };
+
+ t.throws(() => new Configuration(params), {
+ message: `Unknown configuration option 'unknown'`
+ });
+});
+
+test.serial("Check getters", (t) => {
+ const {Configuration} = t.context;
+
+ const params = {
+ mavenSnapshotEndpointUrl: "https://snapshot.url",
+ ui5DataDir: "/custom/data/dir"
+ };
+
+ const config = new Configuration(params);
+
+ t.is(config.getMavenSnapshotEndpointUrl(), params.mavenSnapshotEndpointUrl);
+ t.is(config.getUi5DataDir(), params.ui5DataDir);
+});
+
+
+test.serial("fromFile", async (t) => {
+ const fromFile = t.context.Configuration.fromFile;
+ const {promisifyStub, sinon} = t.context;
+
+ const ui5rcContents = {
+ mavenSnapshotEndpointUrl: "https://snapshot.url",
+ ui5DataDir: "/custom/data/dir"
+ };
+ const responseStub = sinon.stub().resolves(JSON.stringify(ui5rcContents));
+ promisifyStub.callsFake(() => responseStub);
+
+ const config = await fromFile("/custom/path/.ui5rc");
+
+ t.deepEqual(config.toJson(), ui5rcContents);
+});
+
+test.serial("fromFile: configuration file not found - fallback to default config", async (t) => {
+ const {promisifyStub, sinon, Configuration} = t.context;
+ const fromFile = Configuration.fromFile;
+
+ const responseStub = sinon.stub().throws({code: "ENOENT"});
+ promisifyStub.callsFake(() => responseStub);
+
+ const config = await fromFile("/non-existing/path/.ui5rc");
+
+ t.is(config instanceof Configuration, true, "Created a default configuration");
+ t.is(config.getMavenSnapshotEndpointUrl(), undefined, "Default settings");
+ t.is(config.getUi5DataDir(), undefined, "Default settings");
+});
+
+
+test.serial("fromFile: empty configuration file - fallback to default config", async (t) => {
+ const {promisifyStub, sinon, Configuration} = t.context;
+ const fromFile = Configuration.fromFile;
+
+ const responseStub = sinon.stub().resolves("");
+ promisifyStub.callsFake(() => responseStub);
+
+ const config = await fromFile("/non-existing/path/.ui5rc");
+
+ t.is(config instanceof Configuration, true, "Created a default configuration");
+ t.is(config.getMavenSnapshotEndpointUrl(), undefined, "Default settings");
+ t.is(config.getUi5DataDir(), undefined, "Default settings");
+});
+
+test.serial("fromFile: throws", async (t) => {
+ const fromFile = t.context.Configuration.fromFile;
+ const {promisifyStub, sinon} = t.context;
+
+ const responseStub = sinon.stub().throws(new Error("Error"));
+ promisifyStub.callsFake(() => responseStub);
+
+ await t.throwsAsync(fromFile(), {
+ message: `Failed to read UI5 CLI configuration from ~/.ui5rc: Error`
+ });
+});
+
+test.serial("toFile", async (t) => {
+ const {promisifyStub, sinon, Configuration} = t.context;
+ const toFile = Configuration.toFile;
+
+ const writeStub = sinon.stub().resolves();
+ promisifyStub.callsFake(() => writeStub);
+
+ const config = new Configuration({mavenSnapshotEndpointUrl: "https://registry.corp/vendor/build-snapshots/"});
+ await toFile(config, "/path/to/save/.ui5rc");
+
+ t.deepEqual(
+ writeStub.getCall(0).args,
+ ["/path/to/save/.ui5rc", JSON.stringify(config.toJson())],
+ "Write config to path"
+ );
+});
+
+test.serial("toFile: throws", async (t) => {
+ const {promisifyStub, sinon, Configuration} = t.context;
+ const toFile = Configuration.toFile;
+
+ const responseStub = sinon.stub().throws(new Error("Error"));
+ promisifyStub.callsFake(() => responseStub);
+
+ await t.throwsAsync(toFile(new Configuration({})), {
+ message: "Failed to write UI5 CLI configuration to ~/.ui5rc: Error"
+ });
+});
diff --git a/packages/project/test/lib/graph/Module.js b/packages/project/test/lib/graph/Module.js
new file mode 100644
index 00000000000..c87aac18542
--- /dev/null
+++ b/packages/project/test/lib/graph/Module.js
@@ -0,0 +1,509 @@
+import test from "ava";
+import sinon from "sinon";
+import path from "node:path";
+import Module from "../../../lib/graph/Module.js";
+
+const __dirname = import.meta.dirname;
+
+const fixturesPath = path.join(__dirname, "..", "..", "fixtures");
+const applicationAPath = path.join(fixturesPath, "application.a");
+const buildDescriptionApplicationAPath =
+ path.join(fixturesPath, "build-manifest", "application.a");
+const buildDescriptionLibraryAPath =
+ path.join(fixturesPath, "build-manifest", "library.e");
+const applicationHPath = path.join(fixturesPath, "application.h");
+const collectionPath = path.join(fixturesPath, "collection");
+const themeLibraryEPath = path.join(fixturesPath, "theme.library.e");
+
+const basicModuleInput = {
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath
+};
+const archiveAppProjectInput = {
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: buildDescriptionApplicationAPath
+};
+
+const archiveLibProjectInput = {
+ id: "library.e.id",
+ version: "1.0.0",
+ modulePath: buildDescriptionLibraryAPath
+};
+
+test("Instantiate a basic module", (t) => {
+ const ui5Module = new Module(basicModuleInput);
+ t.is(ui5Module.getId(), "application.a.id", "Should return correct ID");
+ t.is(ui5Module.getVersion(), "1.0.0", "Should return correct version");
+ t.is(ui5Module.getPath(), applicationAPath, "Should return correct module path");
+});
+
+test("Create module with missing id", (t) => {
+ t.throws(() => {
+ new Module({
+ version: "1.0.0",
+ modulePath: "/module/path"
+ });
+ }, {
+ message: "Could not create Module: Missing or empty parameter 'id'"
+ });
+});
+
+test("Create module with missing version", (t) => {
+ t.throws(() => {
+ new Module({
+ id: "application.a.id",
+ modulePath: "/module/path"
+ });
+ }, {
+ message: "Could not create Module: Missing or empty parameter 'version'"
+ });
+});
+
+test("Create module with missing modulePath", (t) => {
+ t.throws(() => {
+ new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ });
+ }, {
+ message: "Could not create Module: Missing or empty parameter 'modulePath'"
+ });
+});
+
+test("Create module with relative modulePath", (t) => {
+ t.throws(() => {
+ new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: "module/path"
+ });
+ }, {
+ message: "Could not create Module: Parameter 'modulePath' must contain an absolute path"
+ });
+});
+
+test("Access module root resources via reader", async (t) => {
+ const ui5Module = new Module(basicModuleInput);
+ const rootReader = ui5Module.getReader();
+ const packageJsonResource = await rootReader.byPath("/package.json");
+ t.is(packageJsonResource.getPath(), "/package.json", "Successfully retrieved root resource");
+});
+
+test("Get specifications from module", async (t) => {
+ const ui5Module = new Module(basicModuleInput);
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.is(project.getName(), "application.a", "Should return correct project");
+ t.is(extensions.length, 0, "Should return no extensions");
+});
+
+test("Get specifications from application project with build manifest", async (t) => {
+ const ui5Module = new Module(archiveAppProjectInput);
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.is(project.getName(), "application.a", "Should return correct project");
+ t.is(extensions.length, 0, "Should return no extensions");
+});
+
+test("Get specifications from library project with build manifest", async (t) => {
+ const ui5Module = new Module(archiveLibProjectInput);
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.is(project.getName(), "library.e", "Should return correct project");
+ t.is(extensions.length, 0, "Should return no extensions");
+});
+
+test("Use configuration from object", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: {
+ specVersion: "2.6",
+ type: "application",
+ metadata: {
+ name: "application.a-object"
+ },
+ customConfiguration: {
+ configurationTest: true
+ }
+ }
+ });
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.is(project.getName(), "application.a-object", "Used name from config object");
+ t.deepEqual(project.getCustomConfiguration(), {
+ configurationTest: true
+ }, "Provided configuration is available");
+ t.is(extensions.length, 0, "Should return no extensions");
+});
+
+test("Use configuration from array of objects", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: [{
+ specVersion: "2.6",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ customConfiguration: {
+ configurationTest: true
+ }
+ }, {
+ specVersion: "2.6",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "my-project-shim"
+ },
+ shims: {}
+ }]
+ });
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.deepEqual(project.getCustomConfiguration(), {
+ configurationTest: true
+ }, "Provided configuration is available");
+ t.is(extensions.length, 1, "Should return one extension");
+});
+
+test("Use configuration from configPath", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configPath: "ui5-test-configPath.yaml"
+ });
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.deepEqual(project.getCustomConfiguration(), {
+ configPathTest: true
+ }, "Provided configuration is available");
+ t.is(extensions.length, 0, "Should return no extensions");
+});
+
+test("Use configuration from absolute configPath", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configPath: path.join(applicationAPath, "ui5-test-configPath.yaml")
+ });
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.deepEqual(project.getCustomConfiguration(), {
+ configPathTest: true
+ }, "Provided configuration is available");
+ t.is(extensions.length, 0, "Should return no extensions");
+});
+
+test("configuration and configPath must not be provided together", (t) => {
+ // 'configuration' as object
+ t.throws(() => {
+ new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configPath: "test-ui5.yaml",
+ configuration: {
+ test: "configuration"
+ }
+ });
+ }, {
+ message: "Could not create Module: 'configPath' must not be provided in combination with 'configuration'"
+ });
+ // 'configuration' as array
+ t.throws(() => {
+ new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configPath: "test-ui5.yaml",
+ configuration: [{
+ test: "configuration"
+ }]
+ });
+ }, {
+ message: "Could not create Module: 'configPath' must not be provided in combination with 'configuration'"
+ });
+});
+
+test("Use configuration from project shim", async (t) => {
+ const getProjectConfigurationShimsStub = sinon.stub().returns([{
+ name: "shim-1",
+ shim: {
+ specVersion: "2.6",
+ type: "application",
+ metadata: {
+ name: "application.h"
+ },
+ customConfiguration: {
+ configurationTest: true
+ }
+ }
+ }]);
+
+ const ui5Module = new Module({
+ id: "application.h.id",
+ version: "1.0.0",
+ modulePath: applicationHPath,
+ configuration: [],
+ shimCollection: {
+ getProjectConfigurationShims: getProjectConfigurationShimsStub
+ }
+ });
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.is(getProjectConfigurationShimsStub.callCount, 1, "Should request configuration shims from collection");
+ t.is(getProjectConfigurationShimsStub.getCall(0).args[0], "application.h.id",
+ "Should request configuration shims for correct module ID");
+ t.truthy(project, "Should create a project form shim configuration");
+ t.deepEqual(project.getCustomConfiguration(), {
+ configurationTest: true
+ });
+ t.is(extensions.length, 0, "Should return no extension");
+});
+
+test("Extend configuration via shim", async (t) => {
+ const getProjectConfigurationShimsStub = sinon.stub().returns([{
+ name: "shim-1",
+ shim: {
+ customConfiguration: { // Overwrites whole object since merge is done with Object.assign
+ overwriteConfigurationTest: true
+ }
+ }
+ }]);
+
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: {
+ specVersion: "2.6",
+ type: "application",
+ metadata: {
+ name: "application.a-object"
+ },
+ customConfiguration: {
+ configurationTest: true
+ }
+ },
+ shimCollection: {
+ getProjectConfigurationShims: getProjectConfigurationShimsStub
+ }
+ });
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.deepEqual(project.getCustomConfiguration(), {
+ overwriteConfigurationTest: true
+ }, "Provided configuration is available");
+ t.is(extensions.length, 0, "Should return no extensions");
+});
+
+test("Module is a collection", async (t) => {
+ const ui5Module = new Module({
+ id: "collection.a",
+ version: "1.0.0",
+ modulePath: collectionPath,
+ configuration: [{
+ specVersion: "2.6",
+ type: "application",
+ metadata: {
+ name: "application.a-object"
+ },
+ customConfiguration: {
+ configurationTest: true
+ }
+ }, {
+ specVersion: "2.6",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "collection-shim"
+ },
+ shims: {
+ collections: {
+ "collection.a": {
+ modules: {
+ "library.a": "./library.a",
+ "library.b": "./library.b",
+ "library.c": "./library.c",
+ }
+ }
+ }
+ }
+ }]
+ });
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.falsy(project, "Should ignore the project since the shim defines the module itself as a collection");
+ t.is(extensions.length, 1, "Should return one extensions");
+ t.deepEqual(extensions[0].getCollectionShims(), {
+ "collection.a": {
+ modules: {
+ "library.a": "./library.a",
+ "library.b": "./library.b",
+ "library.c": "./library.c",
+ }
+ }
+ }, "Collection shim configured correctly");
+});
+
+test("Module can't define config shim for itself", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: [{
+ specVersion: "2.6",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "my-project-shim"
+ },
+ shims: {
+ configurations: {
+ "application.a.id": {
+ customConfiguration: {
+ overwriteConfigurationTest: true
+ }
+ }
+ }
+ }
+ }, {
+ specVersion: "2.6",
+ type: "application",
+ metadata: {
+ name: "application.a-object"
+ },
+ customConfiguration: {
+ configurationTest: true
+ }
+ }]
+ });
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.deepEqual(project.getCustomConfiguration(), {
+ configurationTest: true // Shim has not been applied
+ }, "Provided configuration is available");
+ t.is(extensions.length, 1, "Should return one extension");
+});
+
+test("Legacy patches are applied", async (t) => {
+ async function testLegacyLibrary(libraryName) {
+ const ui5Module = new Module({
+ id: "legacy-theme-library.e.id",
+ version: "1.0.0",
+ modulePath: themeLibraryEPath,
+ configuration: {
+ specVersion: "2.6", // should not matter
+ type: "library", // legacy config for theme-libraries
+ metadata: {
+ name: libraryName
+ }
+ }
+ });
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.is(project.getName(), libraryName, "Used name from config object");
+ t.is(project.getType(), "theme-library", "Project type got patched correctly");
+ t.is(extensions.length, 0, "Should return no extensions");
+ }
+
+ await Promise.all(
+ ["themelib_sap_fiori_3", "themelib_sap_bluecrystal", "themelib_sap_belize"]
+ .map(testLegacyLibrary));
+});
+
+test("Invalid configuration in file", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configPath: "ui5-test-error.yaml"
+ });
+ const err = await t.throwsAsync(ui5Module.getSpecifications());
+
+ t.true(err.message.includes("Invalid ui5.yaml configuration"), "Threw with validation error");
+ // Check that config file name is referenced. This validates that the error was not produced by
+ // the Specification instance but the Module
+ t.true(err.message.includes("ui5-test-error.yaml"), "Error message references file name");
+ t.truthy(err.yaml, "Error object contains yaml information");
+});
+
+test("Corrupt configuration in file", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configPath: "ui5-test-corrupt.yaml"
+ });
+ const err = await t.throwsAsync(ui5Module.getSpecifications());
+
+ t.regex(err.message,
+ new RegExp("^Failed to parse configuration for project application.a.id at 'ui5-test-corrupt.yaml'.*"),
+ "Threw with parsing error");
+});
+
+test("Empty configuration in file", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configPath: "ui5-test-empty.yaml"
+ });
+ const res = await ui5Module.getSpecifications();
+
+ t.deepEqual(res, {
+ project: null,
+ extensions: []
+ }, "Returned no project or extensions");
+});
+
+test("No configuration", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: fixturesPath, // does not contain a ui5.yaml
+ });
+ const res = await ui5Module.getSpecifications();
+
+ t.deepEqual(res, {
+ project: null,
+ extensions: []
+ }, "Returned no project or extensions");
+});
+
+test("Incorrect config path", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configPath: "ui5-does-not-exist.yaml"
+ });
+ const err = await t.throwsAsync(ui5Module.getSpecifications());
+
+ t.is(err.message,
+ "Failed to read configuration for module application.a.id: " +
+ "Could not find configuration file in module at path 'ui5-does-not-exist.yaml'",
+ "Threw with expected error message");
+});
+
+test("Incorrect absolute config path", async (t) => {
+ const configPath = path.join(applicationAPath, "ui5-does-not-exist.yaml");
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configPath
+ });
+ const err = await t.throwsAsync(ui5Module.getSpecifications());
+
+ t.true(err.message.startsWith(
+ `Failed to read configuration for module application.a.id at '${configPath}'. Error:`),
+ "Threw with expected error message");
+});
+
+test("Module without ui5.yaml is ignored", async (t) => {
+ const ui5Module = new Module({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationHPath
+ });
+ const {project, extensions} = await ui5Module.getSpecifications();
+ t.falsy(project, "Should return no project");
+ t.is(extensions.length, 0, "Should return no extensions");
+});
diff --git a/packages/project/test/lib/graph/ProjectGraph.js b/packages/project/test/lib/graph/ProjectGraph.js
new file mode 100644
index 00000000000..46295f96723
--- /dev/null
+++ b/packages/project/test/lib/graph/ProjectGraph.js
@@ -0,0 +1,1392 @@
+import path from "node:path";
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import Specification from "../../../lib/specifications/Specification.js";
+
+const __dirname = import.meta.dirname;
+
+const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a");
+
+async function createProject(name) {
+ return await Specification.create({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "application",
+ metadata: {name}
+ }
+ });
+}
+
+async function createExtension(name) {
+ return await Specification.create({
+ id: "extension.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "extension",
+ type: "task",
+ task: {},
+ metadata: {name}
+ }
+ });
+}
+
+function traverseBreadthFirst(...args) {
+ return _traverse(...args, true);
+}
+
+function traverseDepthFirst(...args) {
+ return _traverse(...args, false);
+}
+
+async function _traverse(t, graph, expectedOrder, bfs) {
+ if (bfs === undefined) {
+ throw new Error("Test error: Parameter 'bfs' must be specified");
+ }
+ const callbackStub = t.context.sinon.stub().resolves();
+ if (bfs) {
+ await graph.traverseBreadthFirst(callbackStub);
+ } else {
+ await graph.traverseDepthFirst(callbackStub);
+ }
+
+ t.is(callbackStub.callCount, expectedOrder.length, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order");
+}
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.log = {
+ warn: sinon.stub(),
+ verbose: sinon.stub(),
+ error: sinon.stub(),
+ info: sinon.stub(),
+ isLevelEnabled: () => true
+ };
+
+ t.context.ProjectGraph = await esmock.p("../../../lib/graph/ProjectGraph.js", {
+ "@ui5/logger": {
+ getLogger: sinon.stub().withArgs("graph:ProjectGraph").returns(t.context.log)
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.ProjectGraph);
+});
+
+test("Instantiate a basic project graph", (t) => {
+ const {ProjectGraph} = t.context;
+ t.notThrows(() => {
+ new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ }, "Should not throw");
+});
+
+test("Instantiate a basic project with missing parameter rootProjectName", (t) => {
+ const {ProjectGraph} = t.context;
+ const error = t.throws(() => {
+ new ProjectGraph({});
+ });
+ t.is(error.message, "Could not create ProjectGraph: Missing or empty parameter 'rootProjectName'",
+ "Should throw with expected error message");
+});
+
+test("getRoot", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "application.a"
+ });
+ const project = await createProject("application.a");
+ graph.addProject(project);
+ const res = graph.getRoot();
+ t.is(res, project, "Should return correct root project");
+});
+
+test("getRoot: Root not added to graph", (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "application.a"
+ });
+
+ const error = t.throws(() => {
+ graph.getRoot();
+ });
+ t.is(error.message,
+ "Unable to find root project with name application.a in project graph",
+ "Should throw with expected error message");
+});
+
+test("add-/getProject", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const project = await createProject("application.a");
+ graph.addProject(project);
+ const res = graph.getProject("application.a");
+ t.is(res, project, "Should return correct project");
+});
+
+test("addProject: Add duplicate", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const project1 = await createProject("application.a");
+ graph.addProject(project1);
+
+ const project2 = await createProject("application.a");
+ const error = t.throws(() => {
+ graph.addProject(project2);
+ });
+ t.is(error.message,
+ "Failed to add project application.a to graph: A project with that name has already been added. " +
+ "This might be caused by multiple modules containing projects with the same name",
+ "Should throw with expected error message");
+
+ const res = graph.getProject("application.a");
+ t.is(res, project1, "Should return correct project");
+});
+
+test("addProject: Add project with integer-like name", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const project = await createProject("1337");
+
+ const error = t.throws(() => {
+ graph.addProject(project);
+ });
+ t.is(error.message,
+ "Failed to add project 1337 to graph: Project name must not be integer-like",
+ "Should throw with expected error message");
+});
+
+test("getProject: Project is not in graph", (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const res = graph.getProject("application.a");
+ t.is(res, undefined, "Should return undefined");
+});
+
+test("getProjects", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const project1 = await createProject("application.a");
+ graph.addProject(project1);
+
+ const project2 = await createProject("application.b");
+ graph.addProject(project2);
+
+ const res = graph.getProjects();
+ t.deepEqual(Array.from(res), [
+ project1, project2
+ ], "Should return an iterable for all projects");
+});
+
+test("getProjectNames", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const project1 = await createProject("application.a");
+ graph.addProject(project1);
+
+ const project2 = await createProject("application.b");
+ graph.addProject(project2);
+
+ const res = graph.getProjectNames();
+ t.deepEqual(res, [
+ "application.a", "application.b"
+ ], "Should return all project names in a flat array");
+});
+
+test("getSize", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const project1 = await createProject("application.a");
+ graph.addProject(project1);
+
+ const project2 = await createProject("application.b");
+ graph.addProject(project2);
+
+ // Extensions should not influence graph size
+ const extension1 = await createExtension("extension.a");
+ graph.addExtension(extension1);
+
+ t.is(graph.getSize(), 2, "Should return correct project count");
+});
+
+test("add-/getExtension", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const extension = await createExtension("extension.a");
+ graph.addExtension(extension);
+ const res = graph.getExtension("extension.a");
+ t.is(res, extension, "Should return correct extension");
+});
+
+test("addExtension: Add duplicate", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const extension1 = await createExtension("extension.a");
+ graph.addExtension(extension1);
+
+ const extension2 = await createExtension("extension.a");
+ const error = t.throws(() => {
+ graph.addExtension(extension2);
+ });
+ t.is(error.message,
+ "Failed to add extension extension.a to graph: An extension with that name has already been added. " +
+ "This might be caused by multiple modules containing extensions with the same name",
+ "Should throw with expected error message");
+
+ const res = graph.getExtension("extension.a");
+ t.is(res, extension1, "Should return correct extension");
+});
+
+test("addExtension: Add extension with integer-like name", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const extension = await createExtension("1337");
+
+ const error = t.throws(() => {
+ graph.addExtension(extension);
+ });
+ t.is(error.message,
+ "Failed to add extension 1337 to graph: Extension name must not be integer-like",
+ "Should throw with expected error message");
+});
+
+test("getExtension: Project is not in graph", (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const res = graph.getExtension("extension.a");
+ t.is(res, undefined, "Should return undefined");
+});
+
+test("getExtensions", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ const extension1 = await createExtension("extension.a");
+ graph.addExtension(extension1);
+
+ const extension2 = await createExtension("extension.b");
+ graph.addExtension(extension2);
+ const res = graph.getExtensions();
+ t.deepEqual(Array.from(res), [
+ extension1, extension2
+ ], "Should return an iterable for all extensions");
+});
+
+test("declareDependency / getDependencies", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareDependency("library.a", "library.b");
+ t.deepEqual(graph.getDependencies("library.a"), [
+ "library.b"
+ ], "Should store and return correct dependencies for library.a");
+ t.deepEqual(graph.getDependencies("library.b"), [],
+ "Should store and return correct dependencies for library.b");
+
+ graph.declareDependency("library.b", "library.a");
+
+ t.deepEqual(graph.getDependencies("library.a"), [
+ "library.b"
+ ], "Should store and return correct dependencies for library.a");
+ t.deepEqual(graph.getDependencies("library.b"), [
+ "library.a"
+ ], "Should store and return correct dependencies for library.b");
+
+ t.is(graph.isOptionalDependency("library.a", "library.b"), false,
+ "Should declare dependency as non-optional");
+
+ t.is(graph.isOptionalDependency("library.b", "library.a"), false,
+ "Should declare dependency as non-optional");
+});
+
+test("getTransitiveDependencies", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+ graph.addProject(await createProject("library.d"));
+ graph.addProject(await createProject("library.e"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.b", "library.c");
+ graph.declareDependency("library.c", "library.d");
+ graph.declareDependency("library.a", "library.d");
+ graph.declareDependency("library.d", "library.e");
+
+ t.deepEqual(graph.getTransitiveDependencies("library.a"), [
+ "library.b",
+ "library.c",
+ "library.d",
+ "library.e",
+ ], "Should store and return correct transitive dependencies for library.a");
+});
+
+test("getTransitiveDependencies: Unknown project", (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+
+ const error = t.throws(() => {
+ graph.getTransitiveDependencies("library.x");
+ });
+ t.is(error.message,
+ "Failed to get transitive dependencies for project library.x: Unable to find project in project graph",
+ "Should throw with expected error message");
+});
+
+test("declareDependency: Unknown source", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ graph.addProject(await createProject("library.b"));
+
+ const error = t.throws(() => {
+ graph.declareDependency("library.a", "library.b");
+ });
+ t.is(error.message,
+ "Failed to declare dependency from project library.a to library.b: Unable " +
+ "to find depending project with name library.a in project graph",
+ "Should throw with expected error message");
+});
+
+test("declareDependency: Unknown target", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ graph.addProject(await createProject("library.a"));
+
+ const error = t.throws(() => {
+ graph.declareDependency("library.a", "library.b");
+ });
+ t.is(error.message,
+ "Failed to declare dependency from project library.a to library.b: Unable " +
+ "to find dependency project with name library.b in project graph",
+ "Should throw with expected error message");
+});
+
+test("declareDependency: Same target as source", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ const error = t.throws(() => {
+ graph.declareDependency("library.a", "library.a");
+ });
+ t.is(error.message,
+ "Failed to declare dependency from project library.a to library.a: " +
+ "A project can't depend on itself",
+ "Should throw with expected error message");
+});
+
+test("declareDependency: Already declared", async (t) => {
+ const {ProjectGraph, log} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.a", "library.b");
+
+ t.is(log.warn.callCount, 1, "log.warn should be called once");
+ t.is(log.warn.getCall(0).args[0],
+ `Dependency has already been declared: library.a depends on library.b`,
+ "log.warn should be called once with the expected argument");
+});
+
+test("declareDependency: Already declared as optional", async (t) => {
+ const {ProjectGraph, log} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareOptionalDependency("library.a", "library.b");
+ graph.declareOptionalDependency("library.a", "library.b");
+
+ t.is(log.warn.callCount, 1, "log.warn should be called once");
+ t.is(log.warn.getCall(0).args[0],
+ `Dependency has already been declared: library.a depends on library.b`,
+ "log.warn should be called once with the expected argument");
+
+ t.is(graph.isOptionalDependency("library.a", "library.b"), true,
+ "Should declare dependency as optional");
+});
+
+test("declareDependency: Already declared as non-optional", async (t) => {
+ const {ProjectGraph, log} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareDependency("library.a", "library.b");
+
+ graph.declareOptionalDependency("library.a", "library.b");
+
+ t.is(log.warn.callCount, 0, "log.warn should not be called");
+
+ t.is(graph.isOptionalDependency("library.a", "library.b"), false,
+ "Should declare dependency as non-optional");
+});
+
+test("declareDependency: Already declared as optional, now non-optional", async (t) => {
+ const {ProjectGraph, log} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareOptionalDependency("library.a", "library.b");
+ graph.declareDependency("library.a", "library.b");
+
+ t.is(log.warn.callCount, 0, "log.warn should not be called");
+
+ t.is(graph.isOptionalDependency("library.a", "library.b"), false,
+ "Should declare dependency as non-optional");
+});
+
+test("getDependencies: Project without dependencies", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+
+ graph.addProject(await createProject("library.a"));
+
+ t.deepEqual(graph.getDependencies("library.a"), [],
+ "Should return an empty array for project without dependencies");
+});
+
+test("getDependencies: Unknown project", (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "my root project"
+ });
+
+ const error = t.throws(() => {
+ graph.getDependencies("library.x");
+ });
+ t.is(error.message,
+ "Failed to get dependencies for project library.x: Unable to find project in project graph",
+ "Should throw with expected error message");
+});
+
+test("resolveOptionalDependencies", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+ graph.addProject(await createProject("library.d"));
+
+ graph.declareOptionalDependency("library.a", "library.b");
+ graph.declareOptionalDependency("library.a", "library.c");
+ graph.declareDependency("library.a", "library.d");
+ graph.declareDependency("library.d", "library.b");
+ graph.declareDependency("library.d", "library.c");
+
+ await graph.resolveOptionalDependencies();
+
+ t.is(graph.isOptionalDependency("library.a", "library.b"), false,
+ "library.a should have no optional dependency to library.b anymore");
+
+ t.is(graph.isOptionalDependency("library.a", "library.c"), false,
+ "library.a should have no optional dependency to library.c anymore");
+
+ t.false(graph._hasUnresolvedOptionalDependencies,
+ "Graph has no unresolved optional dependencies");
+
+ await traverseDepthFirst(t, graph, [
+ "library.b",
+ "library.c",
+ "library.d",
+ "library.a"
+ ]);
+});
+
+test("resolveOptionalDependencies: Optional dependency has not been resolved", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+ graph.addProject(await createProject("library.d"));
+
+ graph.declareOptionalDependency("library.a", "library.b");
+ graph.declareOptionalDependency("library.a", "library.c");
+ graph.declareDependency("library.a", "library.d");
+
+ await graph.resolveOptionalDependencies();
+
+ t.true(graph.isOptionalDependency("library.a", "library.b"),
+ "Dependency from library.a to library.b should still be optional");
+
+ t.true(graph.isOptionalDependency("library.a", "library.c"),
+ "Dependency from library.a to library.c should still be optional");
+
+ await traverseDepthFirst(t, graph, [
+ "library.d",
+ "library.a"
+ ]);
+
+ t.true(graph._hasUnresolvedOptionalDependencies,
+ "Graph still has unresolved optional dependencies");
+
+ // Make library.c resolvable through library.d
+ graph.declareDependency("library.d", "library.c");
+
+ await graph.resolveOptionalDependencies();
+
+ t.true(graph.isOptionalDependency("library.a", "library.b"),
+ "Dependency from library.a to library.b should still be optional");
+
+ t.false(graph.isOptionalDependency("library.a", "library.c"),
+ "Dependency from library.a to library.c should be resolved now");
+
+ t.true(graph._hasUnresolvedOptionalDependencies,
+ "Graph still has unresolved optional dependencies");
+
+ await traverseDepthFirst(t, graph, [
+ "library.c",
+ "library.d",
+ "library.a"
+ ]);
+});
+
+test("resolveOptionalDependencies: Dependency of optional dependency has not been resolved", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+ graph.addProject(await createProject("library.d"));
+
+ graph.declareOptionalDependency("library.a", "library.b");
+ graph.declareOptionalDependency("library.a", "library.c");
+ graph.declareDependency("library.b", "library.c");
+
+ await graph.resolveOptionalDependencies();
+
+ t.is(graph.isOptionalDependency("library.a", "library.b"), true,
+ "Dependency from library.a to library.b should still be optional");
+
+ t.is(graph.isOptionalDependency("library.a", "library.c"), true,
+ "Dependency from library.a to library.c should still be optional");
+
+ await traverseDepthFirst(t, graph, [
+ "library.a"
+ ]);
+});
+
+test("resolveOptionalDependencies: Cyclic optional dependency is not resolved", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.a", "library.c");
+ graph.declareDependency("library.c", "library.b");
+ graph.declareOptionalDependency("library.b", "library.c");
+
+ await graph.resolveOptionalDependencies();
+
+ t.is(graph.isOptionalDependency("library.b", "library.c"), true,
+ "Dependency from library.b to library.c should still be optional");
+
+ t.true(graph._hasUnresolvedOptionalDependencies,
+ "Graph still has unresolved optional dependencies");
+
+ await traverseDepthFirst(t, graph, [
+ "library.b",
+ "library.c",
+ "library.a"
+ ]);
+});
+
+test("resolveOptionalDependencies: Resolves transitive optional dependencies", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+ graph.addProject(await createProject("library.d"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareOptionalDependency("library.a", "library.c");
+ graph.declareDependency("library.b", "library.c");
+ graph.declareDependency("library.c", "library.d");
+ graph.declareOptionalDependency("library.a", "library.d");
+
+ await graph.resolveOptionalDependencies();
+
+ t.is(graph.isOptionalDependency("library.a", "library.c"), false,
+ "Dependency from library.a to library.c should not be optional anymore");
+
+ t.is(graph.isOptionalDependency("library.a", "library.d"), false,
+ "Dependency from library.a to library.d should not be optional anymore");
+
+ t.false(graph._hasUnresolvedOptionalDependencies,
+ "Graph has no unresolved optional dependencies");
+
+ await traverseDepthFirst(t, graph, [
+ "library.d",
+ "library.c",
+ "library.b",
+ "library.a"
+ ]);
+});
+
+test("traverseBreadthFirst: Async", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareDependency("library.a", "library.b");
+
+ const callbackStub = t.context.sinon.stub().resolves().onFirstCall().callsFake(() => {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ t.is(callbackStub.callCount, 1, "Callback still called only once while waiting for promise");
+ resolve();
+ }, 100);
+ });
+ });
+ await graph.traverseBreadthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 2, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "library.a",
+ "library.b",
+ ], "Traversed graph in correct order, starting with library.a");
+});
+
+test("traverseBreadthFirst: Sync", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareDependency("library.a", "library.b");
+
+ const callbackStub = t.context.sinon.stub().returns();
+ await graph.traverseBreadthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 2, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "library.a",
+ "library.b",
+ ], "Traversed graph in correct order, starting with library.a");
+});
+
+test("traverseBreadthFirst: No project visited twice", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.a", "library.c");
+ graph.declareDependency("library.b", "library.c");
+
+ await traverseBreadthFirst(t, graph, [
+ "library.a",
+ "library.b",
+ "library.c"
+ ]);
+});
+
+test("traverseBreadthFirst: Detect cycle", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.b", "library.a");
+
+ const error = await t.throwsAsync(graph.traverseBreadthFirst(() => {}));
+ t.is(error.message,
+ "Detected cyclic dependency chain: *library.a* -> library.b -> *library.a*",
+ "Should throw with expected error message");
+});
+
+test("traverseBreadthFirst: No cycle when visited breadth first", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.a", "library.c");
+ graph.declareDependency("library.b", "library.c");
+ graph.declareDependency("library.c", "library.b");
+
+ await traverseBreadthFirst(t, graph, [
+ "library.a",
+ "library.b",
+ "library.c"
+ ]);
+});
+
+test("traverseBreadthFirst: Can't find start node", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+
+ const error = await t.throwsAsync(graph.traverseBreadthFirst(() => {}));
+ t.is(error.message,
+ "Failed to start graph traversal: Could not find project library.a in project graph",
+ "Should throw with expected error message");
+});
+
+test("traverseBreadthFirst: Custom start node", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.b", "library.c");
+
+ const callbackStub = t.context.sinon.stub().resolves();
+ await graph.traverseBreadthFirst("library.b", callbackStub);
+
+ t.is(callbackStub.callCount, 2, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "library.b",
+ "library.c"
+ ], "Traversed graph in correct order, starting with library.b");
+});
+
+test("traverseBreadthFirst: dependencies parameter", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.a", "library.c");
+ graph.declareDependency("library.b", "library.c");
+
+ const callbackStub = t.context.sinon.stub().resolves();
+ await graph.traverseBreadthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 3, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+ const dependencies = callbackStub.getCalls().map((call) => {
+ return call.args[0].dependencies;
+ });
+
+ t.deepEqual(callbackCalls, [
+ "library.a",
+ "library.b",
+ "library.c"
+ ], "Traversed graph in correct order");
+
+ t.deepEqual(dependencies, [
+ ["library.b", "library.c"],
+ ["library.c"],
+ []
+ ], "Provided correct dependencies for each visited project");
+});
+
+test("traverseBreadthFirst: Dependency declaration order is followed", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph1 = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph1.addProject(await createProject("library.a"));
+ graph1.addProject(await createProject("library.b"));
+ graph1.addProject(await createProject("library.c"));
+ graph1.addProject(await createProject("library.d"));
+
+ graph1.declareDependency("library.a", "library.b");
+ graph1.declareDependency("library.a", "library.c");
+ graph1.declareDependency("library.a", "library.d");
+
+ await traverseBreadthFirst(t, graph1, [
+ "library.a",
+ "library.b",
+ "library.c",
+ "library.d"
+ ]);
+
+ const graph2 = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph2.addProject(await createProject("library.a"));
+ graph2.addProject(await createProject("library.b"));
+ graph2.addProject(await createProject("library.c"));
+ graph2.addProject(await createProject("library.d"));
+
+ graph2.declareDependency("library.a", "library.d");
+ graph2.declareDependency("library.a", "library.c");
+ graph2.declareDependency("library.a", "library.b");
+
+ await traverseBreadthFirst(t, graph2, [
+ "library.a",
+ "library.d",
+ "library.c",
+ "library.b"
+ ]);
+});
+
+test("traverseDepthFirst: Async", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareDependency("library.a", "library.b");
+
+ const callbackStub = t.context.sinon.stub().resolves().onFirstCall().callsFake(() => {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ t.is(callbackStub.callCount, 1, "Callback still called only once while waiting for promise");
+ resolve();
+ }, 100);
+ });
+ });
+ await graph.traverseDepthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 2, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "library.b",
+ "library.a",
+ ], "Traversed graph in correct order, starting with library.b");
+});
+
+test("traverseDepthFirst: Sync", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareDependency("library.a", "library.b");
+
+ const callbackStub = t.context.sinon.stub().returns();
+ await graph.traverseDepthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 2, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "library.b",
+ "library.a",
+ ], "Traversed graph in correct order, starting with library.b");
+});
+
+test("traverseDepthFirst: No project visited twice", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.a", "library.c");
+ graph.declareDependency("library.b", "library.c");
+
+ await traverseDepthFirst(t, graph, [
+ "library.c",
+ "library.b",
+ "library.a"
+ ]);
+});
+
+test("traverseDepthFirst: Detect cycle", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.b", "library.a");
+
+ const error = await t.throwsAsync(graph.traverseDepthFirst(() => {}));
+ t.is(error.message,
+ "Detected cyclic dependency chain: *library.a* -> library.b -> *library.a*",
+ "Should throw with expected error message");
+});
+
+test("traverseDepthFirst: Cycle which does not occur in BFS", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.a", "library.c");
+ graph.declareDependency("library.b", "library.c");
+ graph.declareDependency("library.c", "library.b");
+
+ const error = await t.throwsAsync(graph.traverseDepthFirst(() => {}));
+ t.is(error.message,
+ "Detected cyclic dependency chain: library.a -> *library.b* -> library.c -> *library.b*",
+ "Should throw with expected error message");
+});
+
+test("traverseDepthFirst: Can't find start node", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+
+ const error = await t.throwsAsync(graph.traverseDepthFirst(() => {}));
+ t.is(error.message,
+ "Failed to start graph traversal: Could not find project library.a in project graph",
+ "Should throw with expected error message");
+});
+
+test("traverseDepthFirst: Custom start node", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.b", "library.c");
+
+ const callbackStub = t.context.sinon.stub().resolves();
+ await graph.traverseDepthFirst("library.b", callbackStub);
+
+ t.is(callbackStub.callCount, 2, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "library.c",
+ "library.b"
+ ], "Traversed graph in correct order, starting with library.b");
+});
+
+test("traverseDepthFirst: dependencies parameter", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.a", "library.c");
+ graph.declareDependency("library.b", "library.c");
+
+ const callbackStub = t.context.sinon.stub().resolves();
+ await graph.traverseDepthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 3, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+ const dependencies = callbackStub.getCalls().map((call) => {
+ return call.args[0].dependencies;
+ });
+
+ t.deepEqual(callbackCalls, [
+ "library.c",
+ "library.b",
+ "library.a",
+ ], "Traversed graph in correct order");
+
+ t.deepEqual(dependencies, [
+ [],
+ ["library.c"],
+ ["library.b", "library.c"],
+ ], "Provided correct dependencies for each visited project");
+});
+
+test("traverseDepthFirst: Dependency declaration order is followed", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph1 = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph1.addProject(await createProject("library.a"));
+ graph1.addProject(await createProject("library.b"));
+ graph1.addProject(await createProject("library.c"));
+ graph1.addProject(await createProject("library.d"));
+
+ graph1.declareDependency("library.a", "library.b");
+ graph1.declareDependency("library.a", "library.c");
+ graph1.declareDependency("library.a", "library.d");
+
+ await traverseDepthFirst(t, graph1, [
+ "library.b",
+ "library.c",
+ "library.d",
+ "library.a",
+ ]);
+
+ const graph2 = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph2.addProject(await createProject("library.a"));
+ graph2.addProject(await createProject("library.b"));
+ graph2.addProject(await createProject("library.c"));
+ graph2.addProject(await createProject("library.d"));
+
+ graph2.declareDependency("library.a", "library.d");
+ graph2.declareDependency("library.a", "library.c");
+ graph2.declareDependency("library.a", "library.b");
+
+ await traverseDepthFirst(t, graph2, [
+ "library.d",
+ "library.c",
+ "library.b",
+ "library.a",
+ ]);
+});
+
+test("join", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph1 = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ const graph2 = new ProjectGraph({
+ rootProjectName: "theme.a"
+ });
+ graph1.addProject(await createProject("library.a"));
+ graph1.addProject(await createProject("library.b"));
+ graph1.addProject(await createProject("library.c"));
+ graph1.addProject(await createProject("library.d"));
+
+ graph1.declareDependency("library.a", "library.b");
+ graph1.declareDependency("library.a", "library.c");
+ graph1.declareDependency("library.a", "library.d");
+
+ const extensionA = await createExtension("extension.a");
+ graph1.addExtension(extensionA);
+
+ graph2.addProject(await createProject("theme.a"));
+ graph2.addProject(await createProject("theme.b"));
+ graph2.addProject(await createProject("theme.c"));
+ graph2.addProject(await createProject("theme.d"));
+ graph2.addProject(await createProject("theme.e"));
+
+ graph2.declareDependency("theme.a", "theme.d");
+ graph2.declareDependency("theme.a", "theme.c");
+ graph2.declareDependency("theme.b", "theme.a"); // This causes theme.b to not appear
+ graph2.declareOptionalDependency("theme.a", "theme.e");
+
+ const extensionB = await createExtension("extension.b");
+ graph2.addExtension(extensionB);
+ graph1.join(graph2);
+
+ t.true(graph1._hasUnresolvedOptionalDependencies,
+ "Graph has unresolved optional dependencies taken over from graph2");
+
+ graph1.declareDependency("library.d", "theme.a");
+ graph1.declareDependency("library.d", "theme.e");
+
+ graph1.resolveOptionalDependencies();
+
+ await traverseDepthFirst(t, graph1, [
+ "library.b",
+ "library.c",
+ "theme.d",
+ "theme.c",
+ "theme.e",
+ "theme.a",
+ "library.d",
+ "library.a",
+ ]);
+
+ t.is(graph1.getExtension("extension.a"), extensionA, "Should return correct extension");
+ t.is(graph1.getExtension("extension.b"), extensionB, "Should return correct joined extension");
+
+ // graph2 remained unmodified
+ await traverseDepthFirst(t, graph2, [
+ "theme.d",
+ "theme.c",
+ "theme.a",
+ ]);
+});
+
+test("join: Preserves hasUnresolvedOptionalDependencies flag", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph1 = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ const graph2 = new ProjectGraph({
+ rootProjectName: "theme.a"
+ });
+ graph1.addProject(await createProject("library.a"));
+ graph1.addProject(await createProject("library.b"));
+ graph1.declareOptionalDependency("library.a", "library.b");
+
+ graph1.join(graph2);
+
+ t.true(graph1._hasUnresolvedOptionalDependencies,
+ "graph1 still has unresolved optional dependencies");
+ t.false(graph2._hasUnresolvedOptionalDependencies,
+ "graph2 still does not have unresolved optional dependencies");
+});
+
+test("join: Seals incoming graph", (t) => {
+ const {ProjectGraph} = t.context;
+ const graph1 = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ const graph2 = new ProjectGraph({
+ rootProjectName: "theme.a"
+ });
+
+
+ const sealSpy = t.context.sinon.spy(graph2, "seal");
+ graph1.join(graph2);
+
+ t.is(sealSpy.callCount, 1, "Should call seal() on incoming graph once");
+});
+
+test("join: Incoming graph already sealed", (t) => {
+ const {ProjectGraph} = t.context;
+ const graph1 = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ const graph2 = new ProjectGraph({
+ rootProjectName: "theme.a"
+ });
+
+ graph2.seal();
+ const sealSpy = t.context.sinon.spy(graph2, "seal");
+ graph1.join(graph2);
+
+ t.is(sealSpy.callCount, 0, "Should not call seal() on incoming graph");
+});
+
+test("join: Unexpected project intersection", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph1 = new ProjectGraph({
+ rootProjectName: "😹"
+ });
+ const graph2 = new ProjectGraph({
+ rootProjectName: "😼"
+ });
+ graph1.addProject(await createProject("library.a"));
+ graph2.addProject(await createProject("library.a"));
+
+
+ const error = t.throws(() => {
+ graph1.join(graph2);
+ });
+ t.is(error.message,
+ `Failed to join project graph with root project 😼 into project graph with root ` +
+ `project 😹: Failed to merge map: Key 'library.a' already present in target set`,
+ "Should throw with expected error message");
+});
+
+test("join: Unexpected extension intersection", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph1 = new ProjectGraph({
+ rootProjectName: "😹"
+ });
+ const graph2 = new ProjectGraph({
+ rootProjectName: "😼"
+ });
+ graph1.addExtension(await createExtension("extension.a"));
+ graph2.addExtension(await createExtension("extension.a"));
+
+
+ const error = t.throws(() => {
+ graph1.join(graph2);
+ });
+ t.is(error.message,
+ `Failed to join project graph with root project 😼 into project graph with root ` +
+ `project 😹: Failed to merge map: Key 'extension.a' already present in target set`,
+ "Should throw with expected error message");
+});
+
+
+test("Seal/isSealed", async (t) => {
+ const {ProjectGraph} = t.context;
+ const graph = new ProjectGraph({
+ rootProjectName: "library.a"
+ });
+ graph.addProject(await createProject("library.a"));
+ graph.addProject(await createProject("library.b"));
+ graph.addProject(await createProject("library.c"));
+
+ graph.declareDependency("library.a", "library.b");
+ graph.declareDependency("library.a", "library.c");
+ graph.declareDependency("library.b", "library.c");
+ graph.declareOptionalDependency("library.c", "library.a");
+
+ graph.addExtension(await createExtension("extension.a"));
+
+ t.is(graph.isSealed(), false, "Graph should not be sealed");
+ // Seal it
+ graph.seal();
+ t.is(graph.isSealed(), true, "Graph should be sealed");
+
+ const expectedSealMsg = "Project graph with root node library.a has been sealed and is read-only";
+
+ const libX = await createProject("library.x");
+ t.throws(() => {
+ graph.addProject(libX);
+ }, {
+ message: expectedSealMsg
+ });
+ t.throws(() => {
+ graph.declareDependency("library.c", "library.b");
+ }, {
+ message: expectedSealMsg
+ });
+ t.throws(() => {
+ graph.declareOptionalDependency("library.b", "library.a");
+ }, {
+ message: expectedSealMsg
+ });
+ const extB = await createExtension("extension.b");
+ t.throws(() => {
+ graph.addExtension(extB);
+ }, {
+ message: expectedSealMsg
+ });
+ await t.throwsAsync(graph.resolveOptionalDependencies(), {
+ message: expectedSealMsg
+ });
+
+
+ const graph2 = new ProjectGraph({
+ rootProjectName: "library.x"
+ });
+ t.throws(() => {
+ graph.join(graph2);
+ }, {
+ message:
+ `Failed to join project graph with root project library.x into project graph ` +
+ `with root project library.a: ${expectedSealMsg}`
+ });
+ await traverseBreadthFirst(t, graph, [
+ "library.a",
+ "library.b",
+ "library.c"
+ ]);
+
+ await traverseDepthFirst(t, graph, [
+ "library.c",
+ "library.b",
+ "library.a",
+ ]);
+
+ const project = graph.getProject("library.x");
+ t.is(project, undefined, "library.x should not be added");
+
+ const extension = graph.getExtension("extension.b");
+ t.is(extension, undefined, "extension.b should not be added");
+});
diff --git a/packages/project/test/lib/graph/ShimCollection.js b/packages/project/test/lib/graph/ShimCollection.js
new file mode 100644
index 00000000000..05e6b881d32
--- /dev/null
+++ b/packages/project/test/lib/graph/ShimCollection.js
@@ -0,0 +1,81 @@
+import test from "ava";
+import ShimCollection from "../../../lib/graph/ShimCollection.js";
+
+test("Add shims", (t) => {
+ const collection = new ShimCollection();
+ collection.addProjectShim({
+ getName: () => "shim-1",
+ getConfigurationShims: () => {
+ return {
+ "module-1": "configuration shim 1-1",
+ "module-2": "configuration shim 2-1"
+ };
+ },
+ getDependencyShims: () => {
+ return {
+ "module-1": ["dependency shim 1-1"],
+ "module-2": ["dependency shim 2-1"]
+ };
+ },
+ getCollectionShims: () => {
+ return {
+ "module-1": "collection shim 1-1",
+ "module-2": "collection shim 2-1"
+ };
+ },
+ });
+ collection.addProjectShim({
+ getName: () => "shim-2",
+ getConfigurationShims: () => {
+ return {
+ "module-1": "configuration shim 1-2",
+ "module-2": "configuration shim 2-2"
+ };
+ },
+ getDependencyShims: () => {
+ return {
+ "module-1": ["dependency shim 1-2"],
+ "module-2": ["dependency shim 2-2"]
+ };
+ },
+ getCollectionShims: () => {
+ return {
+ "module-1": "collection shim 1-2",
+ "module-2": "collection shim 2-2"
+ };
+ },
+ });
+
+ t.deepEqual(collection.getProjectConfigurationShims("module-1"), [{
+ name: "shim-1",
+ shim: "configuration shim 1-1",
+ }, {
+ name: "shim-2",
+ shim: "configuration shim 1-2",
+ }], "Returns correct project configuration shims for module-1");
+
+ t.deepEqual(collection.getCollectionShims("module-2"), [{
+ name: "shim-1",
+ shim: "collection shim 2-1",
+ }, {
+ name: "shim-2",
+ shim: "collection shim 2-2",
+ }], "Returns correct collection shims for module-2");
+
+ t.deepEqual(collection.getAllDependencyShims(), {
+ "module-1": [{
+ name: "shim-1",
+ shim: ["dependency shim 1-1"],
+ }, {
+ name: "shim-2",
+ shim: ["dependency shim 1-2"],
+ }],
+ "module-2": [{
+ name: "shim-1",
+ shim: ["dependency shim 2-1"],
+ }, {
+ name: "shim-2",
+ shim: ["dependency shim 2-2"],
+ }]
+ }, "Returns correct dependency shims");
+});
diff --git a/packages/project/test/lib/graph/Workspace.js b/packages/project/test/lib/graph/Workspace.js
new file mode 100644
index 00000000000..6e5b586e138
--- /dev/null
+++ b/packages/project/test/lib/graph/Workspace.js
@@ -0,0 +1,475 @@
+import path from "node:path";
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import Module from "../../../lib/graph/Module.js";
+
+const __dirname = import.meta.dirname;
+const libraryD = path.join(__dirname, "..", "..", "fixtures", "library.d");
+const libraryE = path.join(__dirname, "..", "..", "fixtures", "library.e");
+const collectionLibraryA = path.join(__dirname, "..", "..", "fixtures", "collection", "library.a");
+const collectionLibraryB = path.join(__dirname, "..", "..", "fixtures", "collection", "library.b");
+const collectionLibraryC = path.join(__dirname, "..", "..", "fixtures", "collection", "library.c");
+const collectionBLibraryA = path.join(__dirname, "..", "..", "fixtures", "collection.b", "library.a");
+const collectionBLibraryB = path.join(__dirname, "..", "..", "fixtures", "collection.b", "library.b");
+const collectionBLibraryC = path.join(__dirname, "..", "..", "fixtures", "collection.b", "library.c");
+
+function createWorkspaceConfig({dependencyManagement}) {
+ return {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "workspace-name"
+ },
+ dependencyManagement
+ };
+}
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.log = {
+ warn: sinon.stub(),
+ verbose: sinon.stub(),
+ error: sinon.stub(),
+ info: sinon.stub(),
+ isLevelEnabled: () => true
+ };
+
+ t.context.Workspace = await esmock("../../../lib/graph/Workspace.js", {
+ "@ui5/logger": {
+ getLogger: sinon.stub().withArgs("graph:Workspace").returns(t.context.log)
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.ProjectGraph);
+});
+
+test("Basic resolution", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/library.d"
+ }, {
+ path: "../../fixtures/library.e"
+ }]
+ }
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.d", "library.e"], "Correct project name keys");
+
+ const libE = projectNameMap.get("library.e");
+ t.true(libE instanceof Module, "library.e value is instance of Module");
+ t.is(libE.getVersion(), "1.0.0", "Correct version for library.e");
+ t.is(libE.getPath(), libraryE, "Correct path for library.e");
+
+ const libD = projectNameMap.get("library.d");
+ t.true(libD instanceof Module, "library.d value is instance of Module");
+ t.is(libD.getVersion(), "1.0.0", "Correct version for library.d");
+ t.is(libD.getPath(), libraryD, "Correct path for library.d");
+
+ t.is(await workspace.getModuleByProjectName("library.d"), libD,
+ "getModuleByProjectName returns correct module for library.d");
+ t.is(await workspace.getModuleByNodeId("library.d"), libD,
+ "getModuleByNodeId returns correct module for library.d");
+
+ const modules = await workspace.getModules();
+ t.deepEqual(modules, [libD, libE], "getModules returns modules sorted by module ID");
+
+ t.deepEqual(Array.from(moduleIdMap.keys()).sort(), ["library.d", "library.e"], "Correct module ID keys");
+ moduleIdMap.forEach((value, key) => {
+ t.is(value, projectNameMap.get(key), `Same instance of module ${key} in both maps`);
+ });
+});
+
+test("Basic resolution: package.json is missing name field", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/library.d"
+ }, {
+ path: "../../fixtures/library.e"
+ }]
+ }
+ })
+ });
+
+ t.context.sinon.stub(workspace, "_readPackageJson")
+ .resolves({
+ version: "1.0.0",
+ });
+
+ const err = await t.throwsAsync(workspace._getResolvedModules());
+ t.is(err.message,
+ `Failed to resolve workspace dependency resolution path ` +
+ `../../fixtures/library.d to ${libraryD}: package.json must contain fields 'name' and 'version'`,
+ "Threw with expected error message");
+});
+
+test("Basic resolution: package.json is missing version field", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/library.d"
+ }, {
+ path: "../../fixtures/library.e"
+ }]
+ }
+ })
+ });
+
+ t.context.sinon.stub(workspace, "_readPackageJson")
+ .resolves({
+ name: "Package",
+ });
+
+ const err = await t.throwsAsync(workspace._getResolvedModules());
+ t.is(err.message,
+ `Failed to resolve workspace dependency resolution path ` +
+ `../../fixtures/library.d to ${libraryD}: package.json must contain fields 'name' and 'version'`,
+ "Threw with expected error message");
+});
+
+test("Package workspace resolution: Static patterns", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/collection"
+ }]
+ }
+ })
+ });
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.a", "library.b", "library.c"],
+ "Correct project name keys");
+
+ const libA = projectNameMap.get("library.a");
+ t.true(libA instanceof Module, "library.a value is instance of Module");
+ t.is(libA.getVersion(), "1.0.0", "Correct version for library.a");
+ t.is(libA.getPath(), collectionLibraryA, "Correct path for library.a");
+
+ const libB = projectNameMap.get("library.b");
+ t.true(libB instanceof Module, "library.b value is instance of Module");
+ t.is(libB.getVersion(), "1.0.0", "Correct version for library.b");
+ t.is(libB.getPath(), collectionLibraryB, "Correct path for library.b");
+
+ const libC = projectNameMap.get("library.c");
+ t.true(libC instanceof Module, "library.c value is instance of Module");
+ t.is(libC.getVersion(), "1.0.0", "Correct version for library.c");
+ t.is(libC.getPath(), collectionLibraryC, "Correct path for library.c");
+
+ t.deepEqual(Array.from(moduleIdMap.keys()).sort(), ["library.a", "library.b", "library.c"],
+ "Correct module ID keys");
+ moduleIdMap.forEach((value, key) => {
+ t.is(value, projectNameMap.get(key), `Same instance of module ${key} in both maps`);
+ });
+});
+
+test("Package workspace resolution: Dynamic patterns", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/collection.b"
+ }]
+ }
+ })
+ });
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.deepEqual(Array.from(projectNameMap.keys()).sort(), ["library.a", "library.b", "library.c", "library.d"],
+ "Correct project name keys");
+
+ const libA = projectNameMap.get("library.a");
+ t.true(libA instanceof Module, "library.a value is instance of Module");
+ t.is(libA.getVersion(), "1.0.0", "Correct version for library.a");
+ t.is(libA.getPath(), collectionBLibraryA, "Correct path for library.a");
+
+ const libB = projectNameMap.get("library.b");
+ t.true(libB instanceof Module, "library.b value is instance of Module");
+ t.is(libB.getVersion(), "1.0.0", "Correct version for library.b");
+ t.is(libB.getPath(), collectionBLibraryB, "Correct path for library.b");
+
+ const libC = projectNameMap.get("library.c");
+ t.true(libC instanceof Module, "library.c value is instance of Module");
+ t.is(libC.getVersion(), "1.0.0", "Correct version for library.c");
+ t.is(libC.getPath(), collectionBLibraryC, "Correct path for library.c");
+
+ const libD = projectNameMap.get("library.d");
+ t.true(libD instanceof Module, "library.d value is instance of Module");
+ t.is(libD.getVersion(), "1.0.0", "Correct version for library.d");
+ t.is(libD.getPath(), libraryD, "Correct path for library.d");
+
+ t.deepEqual(Array.from(moduleIdMap.keys()).sort(), ["library.a", "library.b", "library.c", "library.d"],
+ "Correct module ID keys");
+ moduleIdMap.forEach((value, key) => {
+ t.is(value, projectNameMap.get(key), `Same instance of module ${key} in both maps`);
+ });
+});
+
+test("Package workspace resolution: Nested workspaces", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/library.xyz"
+ }]
+ }
+ })
+ });
+
+ const readPackageJsonStub = t.context.sinon.stub(workspace, "_readPackageJson")
+ .rejects(new Error("Test does not provide for more package mocks"))
+ .onCall(0).resolves({
+ name: "First Package",
+ version: "1.0.0",
+ ui5: {
+ workspaces: [
+ "workspace-a",
+ "workspace-b"
+ ]
+ }
+ }).onCall(1).resolves({
+ name: "Second Package",
+ version: "1.0.0",
+ workspaces: [
+ "workspace-c",
+ "workspace-d"
+ ]
+ }).onCall(2).resolves({
+ name: "Third Package",
+ version: "1.0.0"
+ }).onCall(3).resolves({
+ name: "Fourth Package",
+ version: "1.0.0",
+ }).onCall(4).resolves({
+ name: "Fifth Package",
+ version: "1.0.0",
+ });
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ // All workspaces. Should not resolve to any module
+ t.is(readPackageJsonStub.callCount, 5, "readPackageJson got called five times");
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+});
+
+test("Package workspace resolution: Recursive workspaces", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/library.xyz"
+ }]
+ }
+ })
+ });
+
+ const basePath = path.join(__dirname, "../../fixtures/library.xyz");
+ const workspaceAPath = path.join(basePath, "workspace-a");
+
+ const readPackageJsonStub = t.context.sinon.stub(workspace, "_readPackageJson");
+ readPackageJsonStub.withArgs(basePath).resolves({
+ name: "Base Package",
+ version: "1.0.0",
+ workspaces: [
+ "workspace-a"
+ ]
+ });
+ readPackageJsonStub.withArgs(workspaceAPath).resolves({
+ name: "Workspace A Package",
+ version: "1.0.0",
+ workspaces: [
+ ".."
+ ]
+ });
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ // All workspaces. Should not resolve to any module
+ // Recursive workspace definition should not lead to another readPackageJson call
+ t.is(readPackageJsonStub.callCount, 2, "readPackageJson got called two times");
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+});
+
+test("No resolutions configuration", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {}
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+
+ t.falsy(await workspace.getModuleByProjectName("library.e"),
+ "getModuleByProjectName yields no result for library.e");
+ t.falsy(await workspace.getModuleByNodeId("library.e"),
+ "getModuleByNodeId yields no result for library.e");
+});
+
+test("Empty dependencyManagement configuration", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {}
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+});
+
+test("Empty resolutions configuration", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: []
+ }
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+});
+
+test("Missing path in resolution", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{}]
+ }
+ })
+ });
+
+ await t.throwsAsync(workspace._getResolvedModules(), {
+ message: "Missing property 'path' in dependency resolution configuration of workspace workspace-name"
+ }, "Threw with expected error message");
+});
+
+test("Invalid specVersion", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: {
+ specVersion: "project/1.0",
+ metadata: {
+ name: "workspace-name"
+ }
+ }
+ });
+
+ const err = await t.throwsAsync(workspace._getResolvedModules());
+ t.true(
+ err.message.includes(`Unsupported "specVersion"`),
+ "Threw with expected error message");
+});
+
+test("Invalid resolutions configuration", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/does-not-exist"
+ }]
+ }
+ })
+ });
+
+ const absPath = path.join(__dirname, "../../fixtures/does-not-exist");
+
+ const err = await t.throwsAsync(workspace._getResolvedModules());
+ t.true(
+ err.message.startsWith(`Failed to resolve workspace dependency resolution path ` +
+ `../../fixtures/does-not-exist to ${absPath}: ENOENT:`),
+ "Threw with expected error message");
+});
+
+test("Resolves extension only", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ path: "../../fixtures/extension.a"
+ }]
+ }
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 1, "Added one entry to Module ID to module map");
+ t.deepEqual(Array.from(moduleIdMap.keys()), ["extension.a"],
+ "Expected entry in Module ID to module map");
+});
+
+test("Resolution does not lead to a project", async (t) => {
+ const workspace = new t.context.Workspace({
+ cwd: __dirname,
+ configuration: createWorkspaceConfig({
+ dependencyManagement: {
+ resolutions: [{
+ // Using a directory with a package.json but no ui5.yaml
+ path: "../../fixtures/init-library"
+ }]
+ }
+ })
+ });
+
+ t.is(workspace.getName(), "workspace-name");
+
+ const {projectNameMap, moduleIdMap} = await workspace._getResolvedModules();
+ t.is(projectNameMap.size, 0, "Project name to module map is empty");
+ t.is(moduleIdMap.size, 0, "Module ID to module map is empty");
+});
+
+test("Missing parameters", (t) => {
+ t.throws(() => {
+ new t.context.Workspace({
+ configuration: {metadata: {name: "config-a"}}
+ });
+ }, {
+ message: "Could not create Workspace: Missing or empty parameter 'cwd'"
+ }, "Threw with expected error message");
+
+ t.throws(() => {
+ new t.context.Workspace({
+ cwd: "cwd"
+ });
+ }, {
+ message: "Could not create Workspace: Missing or empty parameter 'configuration'"
+ }, "Threw with expected error message");
+});
diff --git a/packages/project/test/lib/graph/graph.integration.js b/packages/project/test/lib/graph/graph.integration.js
new file mode 100644
index 00000000000..9b459a0f823
--- /dev/null
+++ b/packages/project/test/lib/graph/graph.integration.js
@@ -0,0 +1,283 @@
+import test from "ava";
+import path from "node:path";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import Workspace from "../../../lib/graph/Workspace.js";
+import CacheMode from "../../../lib/ui5Framework/maven/CacheMode.js";
+const __dirname = import.meta.dirname;
+
+const fixturesPath = path.join(__dirname, "..", "..", "fixtures");
+const libraryHPath = path.join(fixturesPath, "library.h");
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.npmProviderConstructorStub = sinon.stub();
+ class MockNpmProvider {
+ constructor(params) {
+ t.context.npmProviderConstructorStub(params);
+ }
+ }
+
+ t.context.MockNpmProvider = MockNpmProvider;
+
+ t.context.projectGraphBuilderStub = sinon.stub().resolves("graph");
+ t.context.enrichProjectGraphStub = sinon.stub();
+ t.context.graph = await esmock.p("../../../lib/graph/graph.js", {
+ "../../../lib/graph/providers/NodePackageDependencies.js": t.context.MockNpmProvider,
+ "../../../lib/graph/projectGraphBuilder.js": t.context.projectGraphBuilderStub,
+ "../../../lib/graph/helpers/ui5Framework.js": {
+ enrichProjectGraph: t.context.enrichProjectGraphStub
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.graph);
+});
+
+test.serial("graphFromPackageDependencies with workspace object", async (t) => {
+ const {
+ npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceConfiguration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ });
+
+ t.is(res, "graph");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace,
+ "projectGraphBuilder got called with correct workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride",
+ "enrichProjectGraph got called with correct versionOverride parameter");
+ t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace,
+ "enrichProjectGraph got called with correct workspace parameter");
+});
+
+test.serial("graphFromPackageDependencies with workspace object and workspace name", async (t) => {
+ const {
+ npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "dolphin",
+ workspaceConfiguration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "dolphin"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ });
+
+ t.is(res, "graph");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace,
+ "projectGraphBuilder got called with correct workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride",
+ "enrichProjectGraph got called with correct versionOverride parameter");
+ t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace,
+ "enrichProjectGraph got called with correct workspace parameter");
+});
+
+test.serial("graphFromPackageDependencies with workspace object not matching workspaceName", async (t) => {
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ await t.throwsAsync(graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "other",
+ workspaceConfiguration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ }), {
+ message: "The provided workspace name 'other' does not match the provided workspace configuration 'default'"
+ }, "Threw with expected error message");
+});
+
+test.serial("graphFromPackageDependencies with workspace file", async (t) => {
+ const {
+ npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: libraryHPath,
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "default",
+ });
+
+ t.is(res, "graph");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: libraryHPath,
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath"
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace,
+ "projectGraphBuilder got called with correct workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride",
+ "enrichProjectGraph got called with correct versionOverride parameter");
+ t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace,
+ "enrichProjectGraph got called with correct workspace parameter");
+});
+
+test.serial("graphFromPackageDependencies with workspace file at custom path", async (t) => {
+ const {
+ npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "default",
+ workspaceConfigPath: path.join(libraryHPath, "ui5-workspace.yaml")
+ });
+
+ t.is(res, "graph");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath"
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.true(projectGraphBuilderStub.getCall(0).args[1] instanceof Workspace,
+ "projectGraphBuilder got called with correct workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.is(enrichProjectGraphStub.getCall(0).args[1].versionOverride, "versionOverride",
+ "enrichProjectGraph got called with correct versionOverride parameter");
+ t.true(enrichProjectGraphStub.getCall(0).args[1].workspace instanceof Workspace,
+ "enrichProjectGraph got called with correct workspace parameter");
+});
+
+test.serial("graphFromPackageDependencies with inactive workspace file at custom path", async (t) => {
+ const {
+ npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "default",
+ workspaceConfigPath: path.join(libraryHPath, "custom-ui5-workspace.yaml"),
+ cacheMode: CacheMode.Force
+ });
+
+ t.is(res, "graph");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath"
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.is(projectGraphBuilderStub.getCall(0).args[1], null,
+ "projectGraphBuilder got called with no workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], {
+ versionOverride: "versionOverride",
+ workspace: null,
+ cacheMode: "Force"
+ }, "enrichProjectGraph got called with correct options");
+});
diff --git a/packages/project/test/lib/graph/graph.js b/packages/project/test/lib/graph/graph.js
new file mode 100644
index 00000000000..4cc4c1386c0
--- /dev/null
+++ b/packages/project/test/lib/graph/graph.js
@@ -0,0 +1,440 @@
+import test from "ava";
+import path from "node:path";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import CacheMode from "../../../lib/ui5Framework/maven/CacheMode.js";
+
+const __dirname = import.meta.dirname;
+const fixturesPath = path.join(__dirname, "..", "..", "fixtures");
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.npmProviderConstructorStub = sinon.stub();
+ class MockNpmProvider {
+ constructor(params) {
+ t.context.npmProviderConstructorStub(params);
+ }
+ }
+ t.context.createWorkspaceStub = sinon.stub().returns("workspace");
+
+ t.context.MockNpmProvider = MockNpmProvider;
+
+ t.context.dependencyTreeProviderStub = sinon.stub();
+ class DummyDependencyTreeProvider {
+ constructor(params) {
+ t.context.dependencyTreeProviderStub(params);
+ }
+ }
+ t.context.DummyDependencyTreeProvider = DummyDependencyTreeProvider;
+
+ t.context.projectGraphBuilderStub = sinon.stub().resolves("graph");
+ t.context.enrichProjectGraphStub = sinon.stub();
+ t.context.graph = await esmock.p("../../../lib/graph/graph.js", {
+ "../../../lib/graph/providers/NodePackageDependencies.js": t.context.MockNpmProvider,
+ "../../../lib/graph/providers/DependencyTree.js": t.context.DummyDependencyTreeProvider,
+ "../../../lib/graph/helpers/createWorkspace.js": t.context.createWorkspaceStub,
+ "../../../lib/graph/projectGraphBuilder.js": t.context.projectGraphBuilderStub,
+ "../../../lib/graph/helpers/ui5Framework.js": {
+ "enrichProjectGraph": t.context.enrichProjectGraphStub
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.graph);
+});
+
+test.serial("graphFromPackageDependencies", async (t) => {
+ const {
+ createWorkspaceStub, npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ cacheMode: CacheMode.Off,
+ workspaceName: null
+ });
+
+ t.is(res, "graph");
+
+ t.is(createWorkspaceStub.callCount, 0, "createWorkspace did not get called");
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath"
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.is(projectGraphBuilderStub.getCall(0).args[1], undefined,
+ "projectGraphBuilder got called with an empty workspace");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], {
+ versionOverride: "versionOverride",
+ workspace: undefined,
+ cacheMode: "Off"
+ }, "enrichProjectGraph got called with correct options");
+});
+
+test.serial("graphFromPackageDependencies with workspace name", async (t) => {
+ const {
+ createWorkspaceStub, npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "dolphin",
+ cacheMode: CacheMode.Off
+ });
+
+ t.is(res, "graph");
+
+ t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once");
+ t.deepEqual(createWorkspaceStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ name: "dolphin",
+ configPath: "ui5-workspace.yaml",
+ configObject: undefined,
+ }, "createWorkspace called with correct parameters");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.is(projectGraphBuilderStub.getCall(0).args[1], "workspace",
+ "projectGraphBuilder got called with correct workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], {
+ versionOverride: "versionOverride",
+ workspace: "workspace",
+ cacheMode: "Off"
+ }, "enrichProjectGraph got called with correct options");
+});
+
+test.serial("graphFromPackageDependencies with workspace object", async (t) => {
+ const {
+ createWorkspaceStub
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceConfiguration: "workspaceConfiguration",
+ workspaceName: null
+ });
+
+ t.is(res, "graph");
+
+ t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once");
+ t.deepEqual(createWorkspaceStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ configPath: "ui5-workspace.yaml",
+ name: null,
+ configObject: "workspaceConfiguration"
+ }, "createWorkspace called with correct parameters");
+});
+
+test.serial("graphFromPackageDependencies with workspace object and workspace name", async (t) => {
+ const {
+ createWorkspaceStub
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "dolphin",
+ workspaceConfiguration: "workspaceConfiguration"
+ });
+
+ t.is(res, "graph");
+
+ t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once");
+ t.deepEqual(createWorkspaceStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ name: "dolphin",
+ configPath: "ui5-workspace.yaml",
+ configObject: "workspaceConfiguration"
+ }, "createWorkspace called with correct parameters");
+});
+
+test.serial("graphFromPackageDependencies with workspace path and workspace name", async (t) => {
+ const {
+ createWorkspaceStub
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "dolphin",
+ workspaceConfigPath: "workspaceConfigurationPath"
+ });
+
+ t.is(res, "graph");
+
+ t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once");
+ t.deepEqual(createWorkspaceStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ name: "dolphin",
+ configPath: "workspaceConfigurationPath",
+ configObject: undefined
+ }, "createWorkspace called with correct parameters");
+});
+
+test.serial("graphFromPackageDependencies with empty workspace", async (t) => {
+ const {
+ createWorkspaceStub, npmProviderConstructorStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, MockNpmProvider
+ } = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ // Simulate no workspace config found
+ createWorkspaceStub.resolves(null);
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ workspaceName: "dolphin",
+ cacheMode: CacheMode.Off
+ });
+
+ t.is(res, "graph");
+
+ t.is(createWorkspaceStub.callCount, 1, "createWorkspace got called once");
+ t.deepEqual(createWorkspaceStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ name: "dolphin",
+ configPath: "ui5-workspace.yaml",
+ configObject: undefined,
+ }, "createWorkspace called with correct parameters");
+
+ t.is(npmProviderConstructorStub.callCount, 1, "NPM provider constructor got called once");
+ t.deepEqual(npmProviderConstructorStub.getCall(0).args[0], {
+ cwd: path.join(__dirname, "..", "..", "..", "cwd"),
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof MockNpmProvider,
+ "projectGraphBuilder got called with correct provider instance");
+ t.is(projectGraphBuilderStub.getCall(0).args[1], null,
+ "projectGraphBuilder got called with correct workspace instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], {
+ versionOverride: "versionOverride",
+ workspace: null,
+ cacheMode: "Off"
+ }, "enrichProjectGraph got called with correct options");
+});
+
+test.serial("graphFromPackageDependencies: Do not resolve framework dependencies", async (t) => {
+ const {enrichProjectGraphStub} = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ resolveFrameworkDependencies: false
+ });
+
+ t.is(res, "graph");
+ t.is(enrichProjectGraphStub.callCount, 0, "enrichProjectGraph did not get called");
+});
+
+test.serial("graphFromPackageDependencies: Default workspace name", async (t) => {
+ const {createWorkspaceStub} = t.context;
+ const {graphFromPackageDependencies} = t.context.graph;
+
+ const res = await graphFromPackageDependencies({
+ cwd: "cwd",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ resolveFrameworkDependencies: false
+ });
+
+ t.is(res, "graph");
+ t.true(createWorkspaceStub.calledOnce, "createWorkspace is called");
+ t.is(createWorkspaceStub.getCall(0).args[0].name, "default",
+ "createWorkspace is called with 'default' workspace");
+});
+
+test.serial("graphFromStaticFile", async (t) => {
+ const {
+ dependencyTreeProviderStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, DummyDependencyTreeProvider
+ } = t.context;
+ const {graphFromStaticFile} = t.context.graph;
+
+ const readDependencyConfigFileStub = t.context.sinon.stub(graphFromStaticFile._utils, "readDependencyConfigFile")
+ .resolves("dependencyTree");
+
+ const res = await graphFromStaticFile({
+ cwd: "cwd",
+ filePath: "file/path",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ cacheMode: CacheMode.Off
+ });
+
+ t.is(res, "graph");
+
+ t.is(readDependencyConfigFileStub.callCount, 1, "_readDependencyConfigFile got called once");
+ t.is(readDependencyConfigFileStub.getCall(0).args[0], path.join(__dirname, "..", "..", "..", "cwd"),
+ "_readDependencyConfigFile got called with correct directory");
+ t.is(readDependencyConfigFileStub.getCall(0).args[1], "file/path",
+ "_readDependencyConfigFile got called with correct file path");
+
+ t.is(dependencyTreeProviderStub.callCount, 1, "DependencyTree provider constructor got called once");
+ t.deepEqual(dependencyTreeProviderStub.getCall(0).args[0], {
+ dependencyTree: "dependencyTree",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof DummyDependencyTreeProvider,
+ "projectGraphBuilder got called with correct provider instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], {
+ versionOverride: "versionOverride",
+ cacheMode: "Off"
+ }, "enrichProjectGraph got called with correct options");
+});
+
+test.serial("graphFromStaticFile: Do not resolve framework dependencies", async (t) => {
+ const {enrichProjectGraphStub} = t.context;
+ const {graphFromStaticFile} = t.context.graph;
+
+ t.context.sinon.stub(graphFromStaticFile._utils, "readDependencyConfigFile")
+ .resolves("dependencyTree");
+
+ const res = await graphFromStaticFile({
+ cwd: "cwd",
+ filePath: "filePath",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ resolveFrameworkDependencies: false
+ });
+
+ t.is(res, "graph");
+ t.is(enrichProjectGraphStub.callCount, 0, "enrichProjectGraph did not get called");
+});
+
+test.serial("usingObject", async (t) => {
+ const {
+ dependencyTreeProviderStub,
+ projectGraphBuilderStub, enrichProjectGraphStub, DummyDependencyTreeProvider
+ } = t.context;
+ const {graphFromObject} = t.context.graph;
+
+ const res = await graphFromObject({
+ dependencyTree: "dependencyTree",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ cacheMode: "Off"
+ });
+
+ t.is(res, "graph");
+
+ t.is(dependencyTreeProviderStub.callCount, 1, "DependencyTree provider constructor got called once");
+ t.deepEqual(dependencyTreeProviderStub.getCall(0).args[0], {
+ dependencyTree: "dependencyTree",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ }, "Created NodePackageDependencies provider instance with correct parameters");
+
+ t.is(projectGraphBuilderStub.callCount, 1, "projectGraphBuilder got called once");
+ t.true(projectGraphBuilderStub.getCall(0).args[0] instanceof DummyDependencyTreeProvider,
+ "projectGraphBuilder got called with correct provider instance");
+
+ t.is(enrichProjectGraphStub.callCount, 1, "enrichProjectGraph got called once");
+ t.is(enrichProjectGraphStub.getCall(0).args[0], "graph",
+ "enrichProjectGraph got called with graph");
+ t.deepEqual(enrichProjectGraphStub.getCall(0).args[1], {
+ versionOverride: "versionOverride",
+ cacheMode: "Off"
+ }, "enrichProjectGraph got called with correct options");
+});
+
+test.serial("usingObject: Do not resolve framework dependencies", async (t) => {
+ const {enrichProjectGraphStub} = t.context;
+ const {graphFromObject} = t.context.graph;
+ const res = await graphFromObject({
+ cwd: "cwd",
+ filePath: "filePath",
+ rootConfiguration: "rootConfiguration",
+ rootConfigPath: "/rootConfigPath",
+ versionOverride: "versionOverride",
+ resolveFrameworkDependencies: false
+ });
+
+ t.is(res, "graph");
+ t.is(enrichProjectGraphStub.callCount, 0, "enrichProjectGraph did not get called");
+});
+
+test.serial("utils: readDependencyConfigFile", async (t) => {
+ const {graphFromStaticFile} = t.context.graph;
+ const res = await graphFromStaticFile._utils.readDependencyConfigFile(
+ path.join(fixturesPath, "application.h"), "projectDependencies.yaml");
+
+ t.deepEqual(res, {
+ id: "static-application.a",
+ path: path.join(fixturesPath, "application.a"),
+ version: "0.0.1",
+ dependencies: [{
+ id: "static-library.e",
+ path: path.join(fixturesPath, "library.e"),
+ version: "0.0.1",
+ }],
+ }, "Returned correct file content");
+});
+
diff --git a/packages/project/test/lib/graph/graphFromObject.js b/packages/project/test/lib/graph/graphFromObject.js
new file mode 100644
index 00000000000..e331fdb4033
--- /dev/null
+++ b/packages/project/test/lib/graph/graphFromObject.js
@@ -0,0 +1,1664 @@
+import test from "ava";
+import path from "node:path";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import ValidationError from "../../../lib/validation/ValidationError.js";
+
+const __dirname = import.meta.dirname;
+
+const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a");
+const applicationBPath = path.join(__dirname, "..", "..", "fixtures", "application.b");
+const applicationCPath = path.join(__dirname, "..", "..", "fixtures", "application.c");
+const libraryAPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.a");
+const libraryBPath = path.join(__dirname, "..", "..", "fixtures", "collection", "library.b");
+const libraryDPath = path.join(__dirname, "..", "..", "fixtures", "library.d");
+const cycleDepsBasePath = path.join(__dirname, "..", "..", "fixtures", "cyclic-deps", "node_modules");
+const pathToInvalidModule = path.join(__dirname, "..", "..", "fixtures", "invalidModule");
+
+const legacyLibraryAPath = path.join(__dirname, "..", "..", "fixtures", "legacy.library.a");
+const legacyLibraryBPath = path.join(__dirname, "..", "..", "fixtures", "legacy.library.b");
+const legacyCollectionAPath = path.join(__dirname, "..", "..", "fixtures", "legacy.collection.a");
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.log = {
+ error: sinon.stub(),
+ warn: sinon.stub(),
+ info: sinon.stub(),
+ verbose: sinon.stub(),
+ silly: sinon.stub(),
+ isLevelEnabled: () => true
+ };
+
+ t.context.graph = await esmock.p("../../../lib/graph/graph.js", {
+ "../../../lib/graph/projectGraphBuilder": await esmock("../../../lib/graph/projectGraphBuilder.js", {
+ "@ui5/logger": {
+ getLogger: sinon.stub().withArgs("graph:projectGraphBuilder").returns(t.context.log)
+ }
+ })
+ });
+ t.context.graphFromObject = t.context.graph.graphFromObject;
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.graph);
+});
+
+test("Application A", async (t) => {
+ const {graphFromObject} = t.context;
+ const projectGraph = await graphFromObject({dependencyTree: getApplicationATree()});
+ const rootProject = projectGraph.getRoot();
+ t.is(rootProject.getName(), "application.a", "Returned correct root project");
+});
+
+test("Application A: Traverse project graph breadth first", async (t) => {
+ const {graphFromObject} = t.context;
+ const projectGraph = await graphFromObject({dependencyTree: getApplicationATree()});
+ const callbackStub = t.context.sinon.stub().resolves();
+ await projectGraph.traverseBreadthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 5, "Five projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "application.a",
+ "library.d",
+ "library.a",
+ "library.b",
+ "library.c"
+ ], "Traversed graph in correct order");
+});
+
+test("Application Cycle A: Traverse project graph breadth first with cycles", async (t) => {
+ const {graphFromObject, sinon} = t.context;
+ const projectGraph = await graphFromObject({dependencyTree: applicationCycleATreeIncDeduped});
+ const callbackStub = sinon.stub().resolves();
+ const error = await t.throwsAsync(projectGraph.traverseBreadthFirst(callbackStub));
+
+ t.is(callbackStub.callCount, 4, "Four projects have been visited");
+
+ t.is(error.message,
+ "Detected cyclic dependency chain: *application.cycle.a* -> component.cycle.a " +
+ "-> *application.cycle.a*",
+ "Threw with expected error message");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+ t.deepEqual(callbackCalls, [
+ "application.cycle.a",
+ "component.cycle.a",
+ "library.cycle.a",
+ "library.cycle.b",
+ ], "Traversed graph in correct order");
+});
+
+test("Application Cycle B: Traverse project graph breadth first with cycles", async (t) => {
+ const {graphFromObject, sinon} = t.context;
+ const projectGraph = await graphFromObject({dependencyTree: applicationCycleBTreeIncDeduped});
+ const callbackStub = sinon.stub().resolves();
+ await projectGraph.traverseBreadthFirst(callbackStub);
+
+ // TODO: Confirm this behavior with FW. BFS works fine since all modules have already been visited
+ // before a cycle is entered. DFS fails because it dives into the cycle first.
+
+ t.is(callbackStub.callCount, 3, "Four projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+ t.deepEqual(callbackCalls, [
+ "application.cycle.b",
+ "module.d",
+ "module.e"
+ ], "Traversed graph in correct order");
+});
+
+test("Application A: Traverse project graph depth first", async (t) => {
+ const {graphFromObject, sinon} = t.context;
+ const projectGraph = await graphFromObject({dependencyTree: getApplicationATree()});
+ const callbackStub = sinon.stub().resolves();
+ await projectGraph.traverseDepthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 5, "Five projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "library.a",
+ "library.b",
+ "library.c",
+ "library.d",
+ "application.a",
+
+ ], "Traversed graph in correct order");
+});
+
+
+test("Application Cycle A: Traverse project graph depth first with cycles", async (t) => {
+ const {graphFromObject, sinon} = t.context;
+ const projectGraph = await graphFromObject({dependencyTree: applicationCycleATreeIncDeduped});
+ const callbackStub = sinon.stub().resolves();
+ const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub));
+
+ t.is(callbackStub.callCount, 0, "Zero projects have been visited");
+
+ t.is(error.message,
+ "Detected cyclic dependency chain: *application.cycle.a* -> component.cycle.a " +
+ "-> *application.cycle.a*",
+ "Threw with expected error message");
+});
+
+test("Application Cycle B: Traverse project graph depth first with cycles", async (t) => {
+ const {graphFromObject, sinon} = t.context;
+ const projectGraph = await graphFromObject({dependencyTree: applicationCycleBTreeIncDeduped});
+ const callbackStub = sinon.stub().resolves();
+ const error = await t.throwsAsync(projectGraph.traverseDepthFirst(callbackStub));
+
+ t.is(callbackStub.callCount, 0, "Zero projects have been visited");
+
+ t.is(error.message,
+ "Detected cyclic dependency chain: application.cycle.b -> *module.d* " +
+ "-> module.e -> *module.d*",
+ "Threw with expected error message");
+});
+
+
+/* ================================================================================================= */
+/* ======= The following tests have been derived from the existing projectPreprocessor tests ======= */
+
+function testBasicGraphCreationBfs(...args) {
+ return _testBasicGraphCreation(...args, true);
+}
+
+function testBasicGraphCreationDfs(...args) {
+ return _testBasicGraphCreation(...args, false);
+}
+
+async function _testBasicGraphCreation(t, tree, expectedOrder, bfs) {
+ if (bfs === undefined) {
+ throw new Error("Test error: Parameter 'bfs' must be specified");
+ }
+ const {graphFromObject, sinon} = t.context;
+ const projectGraph = await graphFromObject({dependencyTree: tree});
+ const callbackStub = sinon.stub().resolves();
+ if (bfs) {
+ await projectGraph.traverseBreadthFirst(callbackStub);
+ } else {
+ await projectGraph.traverseDepthFirst(callbackStub);
+ }
+
+ t.is(callbackStub.callCount, expectedOrder.length, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order");
+ return projectGraph;
+}
+
+test("Project with inline configuration", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ dependencies: [],
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "xy"
+ }
+ }
+ };
+
+ await testBasicGraphCreationDfs(t, tree, [
+ "xy"
+ ]);
+});
+
+
+test("Project with inline configuration as array", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ dependencies: [],
+ version: "1.0.0",
+ configuration: [{
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "xy"
+ }
+ }]
+ };
+
+ await testBasicGraphCreationDfs(t, tree, [
+ "xy"
+ ]);
+});
+
+test("Project with inline configuration for two projects", async (t) => {
+ const {graphFromObject} = t.context;
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ dependencies: [],
+ version: "1.0.0",
+ configuration: [{
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "xy"
+ }
+ }, {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "yz"
+ }
+ }]
+ };
+
+ await t.throwsAsync(graphFromObject({dependencyTree: tree}),
+ {
+ message:
+ `Found 2 configurations of kind 'project' for module application.a.id. ` +
+ `There must be only one project per module.`
+ },
+ "Rejected with error");
+});
+
+test("Project with configPath", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ configPath: path.join(applicationBPath, "ui5.yaml"), // B, not A - just to have something different
+ dependencies: [],
+ version: "1.0.0"
+ };
+
+ await testBasicGraphCreationDfs(t, tree, [
+ "application.b"
+ ]);
+});
+
+test("Project with ui5.yaml at default location", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: []
+ };
+
+ await testBasicGraphCreationDfs(t, tree, [
+ "application.a"
+ ]);
+});
+
+test("Project with ui5.yaml at default location and some configuration", async (t) => {
+ const tree = {
+ id: "application.c",
+ version: "1.0.0",
+ path: applicationCPath,
+ dependencies: []
+ };
+
+ await testBasicGraphCreationDfs(t, tree, [
+ "application.c"
+ ]);
+});
+
+test("Missing configuration file for root project", async (t) => {
+ const {graphFromObject} = t.context;
+ const tree = {
+ id: "application.a.id",
+ version: "1.0.0",
+ path: "/non-existent",
+ dependencies: []
+ };
+ await t.throwsAsync(graphFromObject({dependencyTree: tree}),
+ {
+ message:
+ "Failed to create a UI5 project from module application.a.id at /non-existent. " +
+ "Make sure the path is correct and a project configuration is present or supplied."
+ },
+ "Rejected with error");
+});
+
+test("Missing id for root project", async (t) => {
+ const {graphFromObject} = t.context;
+ const tree = {
+ path: path.join(__dirname, "fixtures/application.a"),
+ dependencies: []
+ };
+ await t.throwsAsync(graphFromObject({dependencyTree: tree}),
+ {message: "Could not create Module: Missing or empty parameter 'id'"}, "Rejected with error");
+});
+
+test("No type configured for root project", async (t) => {
+ const {graphFromObject} = t.context;
+ const tree = {
+ id: "application.a.id",
+ version: "1.0.0",
+ path: path.join(__dirname, "fixtures/application.a"),
+ dependencies: [],
+ configuration: {
+ specVersion: "2.1",
+ metadata: {
+ name: "application.a",
+ namespace: "id1"
+ }
+ }
+ };
+ const error = await t.throwsAsync(graphFromObject({dependencyTree: tree}));
+
+ t.is(error.message, `Unable to create Specification instance: Unknown specification type 'undefined'`);
+});
+
+test("Missing dependencies", async (t) => {
+ const {graphFromObject} = t.context;
+ const tree = ({
+ id: "application.a.id",
+ version: "1.0.0",
+ path: applicationAPath
+ });
+ await t.notThrowsAsync(graphFromObject({dependencyTree: tree}),
+ "Gracefully accepted project with no dependencies attribute");
+});
+
+test("Missing second-level dependencies", async (t) => {
+ const {graphFromObject} = t.context;
+ const tree = ({
+ id: "application.a.id",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: [{
+ id: "library.d.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "library.d")
+ }]
+ });
+ await t.notThrowsAsync(graphFromObject({dependencyTree: tree}),
+ "Gracefully accepted project with no dependencies attribute");
+});
+
+test("Single non-root application-project", async (t) => {
+ const tree = ({
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ dependencies: [{
+ id: "application.a.id",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: []
+ }]
+ });
+
+ await testBasicGraphCreationDfs(t, tree, [
+ "application.a",
+ "library.a"
+ ]);
+});
+
+test("Multiple non-root application-projects on same level", async (t) => {
+ const {log} = t.context;
+ const tree = ({
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ dependencies: [{
+ id: "application.a",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: []
+ }, {
+ id: "application.b",
+ version: "1.0.0",
+ path: applicationBPath,
+ dependencies: []
+ }]
+ });
+
+ await testBasicGraphCreationDfs(t, tree, [
+ "application.a",
+ "library.a"
+ ]);
+
+ t.is(log.info.callCount, 1, "log.info should be called once");
+ t.is(log.info.getCall(0).args[0],
+ `Excluding additional application project application.b from graph. `+
+ `The project graph can only feature a single project of type application. ` +
+ `Project application.a has already qualified for that role.`,
+ "log.info should be called once with the expected argument");
+});
+
+test("Multiple non-root application-projects on different levels", async (t) => {
+ const {log} = t.context;
+ const tree = ({
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ dependencies: [{
+ id: "application.a",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: []
+ }, {
+ id: "library.b",
+ version: "1.0.0",
+ path: libraryBPath,
+ dependencies: [{
+ id: "application.b",
+ version: "1.0.0",
+ path: applicationBPath,
+ dependencies: []
+ }]
+ }]
+ });
+
+ await testBasicGraphCreationDfs(t, tree, [
+ "application.a",
+ "library.b",
+ "library.a"
+ ]);
+
+ t.is(log.info.callCount, 1, "log.info should be called once");
+ t.is(log.info.getCall(0).args[0],
+ `Excluding additional application project application.b from graph. `+
+ `The project graph can only feature a single project of type application. ` +
+ `Project application.a has already qualified for that role.`,
+ "log.info should be called once with the expected argument");
+});
+
+test("Root- and non-root application-projects", async (t) => {
+ const {log} = t.context;
+ const tree = ({
+ id: "application.a",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: [{
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ dependencies: [{
+ id: "application.b",
+ version: "1.0.0",
+ path: applicationBPath,
+ dependencies: []
+ }]
+ }]
+ });
+ await testBasicGraphCreationDfs(t, tree, [
+ "library.a",
+ "application.a",
+ ]);
+
+ t.is(log.info.callCount, 1, "log.info should be called once");
+ t.is(log.info.getCall(0).args[0],
+ `Excluding additional application project application.b from graph. `+
+ `The project graph can only feature a single project of type application. ` +
+ `Project application.a has already qualified for that role.`,
+ "log.info should be called once with the expected argument");
+});
+
+test("Ignores additional application-projects", async (t) => {
+ const {log} = t.context;
+ const tree = ({
+ id: "application.a",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: [{
+ id: "application.b",
+ version: "1.0.0",
+ path: applicationBPath,
+ dependencies: []
+ }]
+ });
+ await testBasicGraphCreationDfs(t, tree, [
+ "application.a",
+ ]);
+
+ t.is(log.info.callCount, 1, "log.info should be called once");
+ t.is(log.info.getCall(0).args[0],
+ `Excluding additional application project application.b from graph. `+
+ `The project graph can only feature a single project of type application. ` +
+ `Project application.a has already qualified for that role.`,
+ "log.info should be called once with the expected argument");
+});
+
+test("Inconsistent dependencies with same ID", async (t) => {
+ // The one closer to the root should win
+ const tree = {
+ id: "application.a",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: [
+ {
+ id: "library.d",
+ version: "1.0.0",
+ path: libraryDPath,
+ resources: {
+ configuration: {
+ propertiesFileSourceEncoding: "UTF-8",
+ paths: {
+ src: "main/src",
+ test: "main/test"
+ }
+ }
+ },
+ dependencies: [
+ {
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryBPath, // B, not A - inconsistency!
+ configuration: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "library.XY",
+ }
+ },
+ dependencies: []
+ }
+ ]
+ },
+ {
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ dependencies: []
+ }
+ ]
+ };
+ await testBasicGraphCreationDfs(t, tree, [
+ // "library.XY" is ignored since the ID has already been processed and resolved to library A
+ "library.a",
+ "library.d",
+ "application.a"
+ ]);
+});
+
+test("Project tree A with inline configs depth first", async (t) => {
+ await testBasicGraphCreationDfs(t, applicationATreeWithInlineConfigs, [
+ "library.a",
+ "library.d",
+ "application.a"
+ ]);
+});
+
+test("Project tree A with configPaths depth first", async (t) => {
+ await testBasicGraphCreationDfs(t, applicationATreeWithConfigPaths, [
+ "library.a",
+ "library.d",
+ "application.a"
+
+ ]);
+});
+
+test("Project tree A with default YAMLs depth first", async (t) => {
+ await testBasicGraphCreationDfs(t, applicationATreeWithDefaultYamls, [
+ "library.a",
+ "library.d",
+ "application.a"
+ ]);
+});
+
+test("Project tree A with inline configs breadth first", async (t) => {
+ await testBasicGraphCreationBfs(t, applicationATreeWithInlineConfigs, [
+ "application.a",
+ "library.d",
+ "library.a",
+ ]);
+});
+
+test("Project tree A with configPaths breadth first", async (t) => {
+ await testBasicGraphCreationBfs(t, applicationATreeWithConfigPaths, [
+ "application.a",
+ "library.d",
+ "library.a"
+
+ ]);
+});
+
+test("Project tree A with default YAMLs breadth first", async (t) => {
+ await testBasicGraphCreationBfs(t, applicationATreeWithDefaultYamls, [
+ "application.a",
+ "library.d",
+ "library.a"
+ ]);
+});
+
+test("Project tree B with inline configs", async (t) => {
+ // Tree B depends on Library B which has a dependency to Library D
+ await testBasicGraphCreationDfs(t, applicationBTreeWithInlineConfigs, [
+ "library.a",
+ "library.d",
+ "library.b",
+ "application.b"
+ ]);
+});
+
+test("Project with nested invalid dependencies", async (t) => {
+ await testBasicGraphCreationDfs(t, treeWithInvalidModules, [
+ "library.a",
+ "library.b",
+ "application.a"
+ ]);
+});
+
+/* ========================= */
+/* ======= Test data ======= */
+
+function getApplicationATree() {
+ return {
+ id: "application.a.id",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: [
+ {
+ id: "library.d.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "library.d"),
+ dependencies: [
+ {
+ id: "library.a.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.a"),
+ dependencies: []
+ },
+ {
+ id: "library.b.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.b"),
+ dependencies: []
+ },
+ {
+ id: "library.c.id",
+ version: "1.0.0",
+ path: path.join(applicationAPath, "node_modules", "collection", "library.c"),
+ dependencies: []
+ }
+ ]
+ }
+ ]
+ };
+}
+
+
+const applicationCycleATreeIncDeduped = {
+ id: "@ui5-internal/application.cycle.a",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "application.cycle.a"),
+ dependencies: [
+ {
+ id: "@ui5-internal/component.cycle.a",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "component.cycle.a"),
+ dependencies: [
+ {
+ id: "@ui5-internal/library.cycle.a",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "library.cycle.a"),
+ dependencies: [
+ {
+ id: "@ui5-internal/component.cycle.a",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "component.cycle.a"),
+ dependencies: [],
+ deduped: true
+ }
+ ]
+ },
+ {
+ id: "@ui5-internal/library.cycle.b",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "library.cycle.b"),
+ dependencies: [
+ {
+ id: "@ui5-internal/component.cycle.a",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "component.cycle.a"),
+ dependencies: [],
+ deduped: true
+ }
+ ]
+ },
+ {
+ id: "@ui5-internal/application.cycle.a",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "application.cycle.a"),
+ dependencies: [],
+ deduped: true
+ }
+ ]
+ }
+ ]
+};
+
+const applicationCycleBTreeIncDeduped = {
+ id: "@ui5-internal/application.cycle.b",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "application.cycle.b"),
+ dependencies: [
+ {
+ id: "@ui5-internal/module.d",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "module.d"),
+ dependencies: [
+ {
+ id: "@ui5-internal/module.e",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "module.e"),
+ dependencies: [
+ {
+ id: "@ui5-internal/module.d",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "module.d"),
+ dependencies: [],
+ deduped: true
+ }
+ ]
+ }
+ ]
+ },
+ {
+ id: "@ui5-internal/module.e",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "module.e"),
+ dependencies: [
+ {
+ id: "@ui5-internal/module.d",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "module.d"),
+ dependencies: [
+ {
+ id: "@ui5-internal/module.e",
+ version: "1.0.0",
+ path: path.join(cycleDepsBasePath, "@ui5-internal", "module.e"),
+ dependencies: [],
+ deduped: true
+ }
+ ]
+ }
+ ]
+ }
+ ]
+};
+
+
+/* === Tree A === */
+const applicationATreeWithInlineConfigs = {
+ id: "application.a",
+ version: "1.0.0",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.a",
+ },
+ },
+ dependencies: [
+ {
+ id: "library.d",
+ version: "1.0.0",
+ path: libraryDPath,
+ configuration: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "library.d",
+ },
+ resources: {
+ configuration: {
+ propertiesFileSourceEncoding: "UTF-8",
+ paths: {
+ src: "main/src",
+ test: "main/test"
+ }
+ }
+ }
+ },
+ dependencies: [
+ {
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ configuration: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "library.a",
+ },
+ },
+ dependencies: []
+ }
+ ]
+ },
+ {
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ configuration: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "library.a"
+ },
+ },
+ dependencies: []
+ }
+ ]
+};
+
+const applicationATreeWithConfigPaths = {
+ id: "application.a",
+ version: "1.0.0",
+ path: applicationAPath,
+ configPath: path.join(applicationAPath, "ui5.yaml"),
+ dependencies: [
+ {
+ id: "library.d",
+ version: "1.0.0",
+ path: libraryDPath,
+ configPath: path.join(libraryDPath, "ui5.yaml"),
+ dependencies: [
+ {
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ configPath: path.join(libraryAPath, "ui5.yaml"),
+ dependencies: []
+ }
+ ]
+ },
+ {
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ configPath: path.join(libraryAPath, "ui5.yaml"),
+ dependencies: []
+ }
+ ]
+};
+
+const applicationATreeWithDefaultYamls = {
+ id: "application.a",
+ version: "1.0.0",
+ path: applicationAPath,
+ dependencies: [
+ {
+ id: "library.d",
+ version: "1.0.0",
+ path: libraryDPath,
+ dependencies: [
+ {
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ dependencies: []
+ }
+ ]
+ },
+ {
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ dependencies: []
+ }
+ ]
+};
+
+/* === Tree B === */
+const applicationBTreeWithInlineConfigs = {
+ id: "application.b",
+ version: "1.0.0",
+ path: applicationBPath,
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.b"
+ }
+ },
+ dependencies: [
+ {
+ id: "library.b",
+ version: "1.0.0",
+ path: libraryBPath,
+ configuration: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "library.b",
+ }
+ },
+ dependencies: [
+ {
+ id: "library.d",
+ version: "1.0.0",
+ path: libraryDPath,
+ configuration: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "library.d",
+ },
+ resources: {
+ configuration: {
+ propertiesFileSourceEncoding: "UTF-8",
+ paths: {
+ src: "main/src",
+ test: "main/test"
+ }
+ }
+ }
+ },
+ dependencies: [
+ {
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ configuration: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "library.a"
+ }
+ },
+ dependencies: []
+ }
+ ]
+ }
+ ]
+ },
+ {
+ id: "library.d",
+ version: "1.0.0",
+ path: libraryDPath,
+ configuration: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "library.d",
+ },
+ resources: {
+ configuration: {
+ propertiesFileSourceEncoding: "UTF-8",
+ paths: {
+ src: "main/src",
+ test: "main/test"
+ }
+ }
+ }
+ },
+ dependencies: [
+ {
+ id: "library.a",
+ version: "1.0.0",
+ path: libraryAPath,
+ configuration: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "library.a"
+ }
+ },
+ dependencies: []
+ }
+ ]
+ }
+ ]
+};
+
+/* === Invalid Modules */
+const treeWithInvalidModules = {
+ id: "application.a",
+ path: applicationAPath,
+ dependencies: [
+ // A
+ {
+ id: "library.a",
+ path: libraryAPath,
+ dependencies: [
+ {
+ // C - invalid - should be missing in preprocessed tree
+ id: "module.c",
+ dependencies: [],
+ path: pathToInvalidModule,
+ version: "1.0.0"
+ },
+ {
+ // D - invalid - should be missing in preprocessed tree
+ id: "module.d",
+ dependencies: [],
+ path: pathToInvalidModule,
+ version: "1.0.0"
+ }
+ ],
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {name: "library.a"}
+ }
+ },
+ // B
+ {
+ id: "library.b",
+ path: libraryBPath,
+ dependencies: [
+ {
+ // C - invalid - should be missing in preprocessed tree
+ id: "module.c",
+ dependencies: [],
+ path: pathToInvalidModule,
+ version: "1.0.0"
+ },
+ {
+ // D - invalid - should be missing in preprocessed tree
+ id: "module.d",
+ dependencies: [],
+ path: pathToInvalidModule,
+ version: "1.0.0"
+ }
+ ],
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {name: "library.b"}
+ }
+ }
+ ],
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ }
+};
+
+/* ======================================================================================= */
+/* ======= The following tests have been derived from the existing extension tests ======= */
+
+/* The following scenario is supported by the projectPreprocessor but not by projectGraphFromTree
+ * A shim extension located in a project's dependencies can't influence other dependencies of that project anymore
+ * TODO: Check whether the above is fine for us
+
+test("Legacy: Project with project-shim extension with dependency configuration", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ },
+ dependencies: [{
+ id: "extension.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ dependencies: [],
+ configuration: {
+ specVersion: "2.3",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "shim.a"
+ },
+ shims: {
+ configurations: {
+ "legacy.library.a.id": {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "legacy.library.a",
+ }
+ }
+ }
+ }
+ }
+ }, {
+ id: "legacy.library.a.id",
+ version: "1.0.0",
+ path: legacyLibraryAPath,
+ dependencies: []
+ }]
+ };
+ await testBasicGraphCreationDfs(t, tree, [
+ "legacy.library.a",
+ "application.a",
+ ]);
+});*/
+
+test("Project with project-shim extension with dependency configuration", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: [{
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ }, {
+ specVersion: "2.3",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "shim.a"
+ },
+ shims: {
+ configurations: {
+ "legacy.library.a.id": {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "legacy.library.a",
+ }
+ }
+ }
+ }
+ }],
+ dependencies: [{
+ id: "legacy.library.a.id",
+ version: "1.0.0",
+ path: legacyLibraryAPath,
+ dependencies: []
+ }]
+ };
+ await testBasicGraphCreationDfs(t, tree, [
+ "legacy.library.a",
+ "application.a",
+ ]);
+});
+
+test("Project with project-shim extension dependency with dependency configuration", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ },
+ dependencies: [{
+ id: "extension.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "shim.a"
+ },
+ shims: {
+ configurations: {
+ "legacy.library.a.id": {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "legacy.library.a",
+ }
+ }
+ }
+ }
+ },
+ dependencies: [{
+ id: "legacy.library.a.id",
+ version: "1.0.0",
+ path: legacyLibraryAPath,
+ dependencies: []
+ }],
+ }]
+ };
+ await testBasicGraphCreationDfs(t, tree, [
+ "legacy.library.a",
+ "application.a",
+ ]);
+
+ const {log} = t.context;
+ t.is(log.warn.callCount, 0, "log.warn should not have been called");
+ t.is(log.info.callCount, 0, "log.info should not have been called");
+});
+
+test("Project with project-shim extension with invalid dependency configuration", async (t) => {
+ const {graphFromObject} = t.context;
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: [{
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "xy"
+ }
+ }, {
+ specVersion: "2.3",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "shims.a"
+ },
+ shims: {
+ configurations: {
+ "legacy.library.a.id": {
+ specVersion: "2.3",
+ type: "library"
+ }
+ }
+ }
+ }],
+ dependencies: [{
+ id: "legacy.library.a.id",
+ version: "1.0.0",
+ path: legacyLibraryAPath,
+ dependencies: []
+ }]
+ };
+ const validationError = await t.throwsAsync(graphFromObject({dependencyTree: tree}), {
+ instanceOf: ValidationError
+ });
+ t.true(validationError.message.includes("Configuration must have required property 'metadata'"),
+ "ValidationError should contain error about missing metadata configuration");
+});
+
+test("Project with project-shim extension with dependency declaration and configuration", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ },
+ dependencies: [{
+ id: "extension.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "shims.a"
+ },
+ shims: {
+ configurations: {
+ "legacy.library.a.id": {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "legacy.library.a",
+ }
+ },
+ "legacy.library.b.id": {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "legacy.library.b",
+ }
+ }
+ },
+ dependencies: {
+ "legacy.library.a.id": [
+ "legacy.library.b.id"
+ ]
+ }
+ }
+ },
+ dependencies: [{
+ id: "legacy.library.a.id",
+ version: "1.0.0",
+ path: legacyLibraryAPath,
+ dependencies: []
+ }, {
+ id: "legacy.library.b.id",
+ version: "1.0.0",
+ path: legacyLibraryBPath,
+ dependencies: []
+ }],
+ }]
+ };
+ // application.a and legacy.library.a will both have a dependency to legacy.library.b
+ // (one because it's the actual dependency and one because it's a shimmed dependency)
+ const graph = await testBasicGraphCreationDfs(t, tree, [
+ "legacy.library.b",
+ "legacy.library.a",
+ "application.a",
+ ]);
+ t.deepEqual(graph.getDependencies("legacy.library.a"), [
+ "legacy.library.b"
+ ], "Shimmed dependencies should be applied");
+
+ const {log} = t.context;
+ t.is(log.warn.callCount, 0, "log.warn should not have been called");
+ t.is(log.info.callCount, 0, "log.info should not have been called");
+});
+
+test("Project with project-shim extension with collection", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ },
+ dependencies: [{
+ id: "extension.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "shims.a"
+ },
+ shims: {
+ configurations: {
+ "legacy.library.x.id": {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "legacy.library.x",
+ }
+ },
+ "legacy.library.y.id": {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "legacy.library.y",
+ }
+ }
+ },
+ dependencies: {
+ "application.a.id": [
+ "legacy.library.x.id",
+ "legacy.library.y.id"
+ ],
+ "legacy.library.x.id": [
+ "legacy.library.y.id"
+ ]
+ },
+ collections: {
+ "legacy.collection.a": {
+ modules: {
+ "legacy.library.x.id": "src/legacy.library.x",
+ "legacy.library.y.id": "src/legacy.library.y"
+ }
+ }
+ }
+ }
+ },
+ dependencies: [{
+ id: "legacy.collection.a",
+ version: "1.0.0",
+ path: legacyCollectionAPath,
+ dependencies: []
+ }]
+ }]
+ };
+
+ const graph = await testBasicGraphCreationDfs(t, tree, [
+ "legacy.library.y",
+ "legacy.library.x",
+ "application.a",
+ ]);
+ t.deepEqual(graph.getDependencies("application.a"), [
+ "legacy.library.x",
+ "legacy.library.y"
+ ], "Shimmed dependencies should be applied");
+
+ const {log} = t.context;
+ t.is(log.warn.callCount, 0, "log.warn should not have been called");
+ t.is(log.info.callCount, 0, "log.info should not have been called");
+});
+
+// TODO: Fixme
+// eslint-disable-next-line ava/no-skip-test
+test.skip("Project with project-shim extension with self-containing collection shim", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ },
+ dependencies: [{
+ id: "legacy.collection.a",
+ path: legacyCollectionAPath,
+ version: "1.0.0",
+ configuration: [{
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "my.fe"
+ },
+ framework: {
+ name: "OpenUI5"
+ }
+ }, {
+ specVersion: "2.3",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "shims.a"
+ },
+ shims: {
+ configurations: {
+ "legacy.library.x.id": {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "legacy.library.x",
+ }
+ },
+ "legacy.library.y.id": {
+ specVersion: "2.3",
+ type: "library",
+ metadata: {
+ name: "legacy.library.y",
+ }
+ }
+ },
+ dependencies: {
+ "legacy.library.x.id": [
+ "legacy.library.y.id"
+ ]
+ },
+ collections: {
+ "legacy.collection.a": {
+ modules: {
+ "legacy.library.x.id": "src/legacy.library.x",
+ "legacy.library.y.id": "src/legacy.library.y"
+ }
+ }
+ }
+ }
+ }],
+ dependencies: []
+ }]
+ };
+
+ const graph = await testBasicGraphCreationDfs(t, tree, [
+ "legacy.library.y",
+ "legacy.library.x",
+ "application.a",
+ ]);
+ t.deepEqual(graph.getDependencies("application.a"), [
+ "legacy.library.x",
+ "legacy.library.y"
+ ], "Shimmed dependencies should be applied");
+
+ const {log} = t.context;
+ t.is(log.warn.callCount, 0, "log.warn should not have been called");
+ t.is(log.info.callCount, 0, "log.info should not have been called");
+
+ const libraryY = graph.getProject("legacy.library.y");
+ t.deepEqual(libraryY.getFrameworkName(), {
+ name: "OpenUI5"
+ }, "Configuration from collection project should be taken over into shimmed project");
+});
+
+test("Project with unknown extension dependency inline configuration", async (t) => {
+ const {graphFromObject} = t.context;
+ const tree = {
+ id: "application.a",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "xy"
+ }
+ },
+ dependencies: [{
+ id: "extension.a",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ kind: "extension",
+ type: "phony-pony",
+ metadata: {
+ name: "pinky.pie"
+ }
+ },
+ dependencies: [],
+ }],
+ };
+ const validationError = await t.throwsAsync(graphFromObject({dependencyTree: tree}));
+ t.is(validationError.message,
+ `Unable to create Specification instance: Unknown specification type 'phony-pony'`,
+ "Should throw with expected error message");
+});
+
+test("Project with task extension dependency", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ },
+ dependencies: [{
+ id: "ext.task.a",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task.a"
+ },
+ task: {
+ path: "task.a.js"
+ }
+ },
+ dependencies: [],
+ }]
+ };
+ const graph = await testBasicGraphCreationDfs(t, tree, [
+ "application.a"
+ ]);
+ t.truthy(graph.getExtension("task.a"), "Extension should be added to the graph");
+});
+
+test("Project with middleware extension dependency", async (t) => {
+ const tree = {
+ id: "application.a.id",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ },
+ dependencies: [{
+ id: "ext.middleware.a",
+ path: applicationAPath,
+ version: "1.0.0",
+ configuration: {
+ specVersion: "2.3",
+ kind: "extension",
+ type: "server-middleware",
+ metadata: {
+ name: "middleware.a"
+ },
+ middleware: {
+ path: "middleware.a.js"
+ }
+ },
+ dependencies: [],
+ }],
+ };
+ const graph = await testBasicGraphCreationDfs(t, tree, [
+ "application.a"
+ ]);
+ t.truthy(graph.getExtension("middleware.a"), "Extension should be added to the graph");
+});
+
+test("rootConfiguration", async (t) => {
+ const {graphFromObject} = t.context;
+ const projectGraph = await graphFromObject({
+ dependencyTree: getApplicationATree(),
+ rootConfiguration: {
+ specVersion: "2.6",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ customConfiguration: {
+ rootConfigurationTest: true
+ }
+ }
+ });
+
+ t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), {
+ rootConfigurationTest: true
+ });
+});
+
+test("rootConfig", async (t) => {
+ const {graphFromObject} = t.context;
+ const projectGraph = await graphFromObject({
+ dependencyTree: getApplicationATree(),
+ cwd: applicationAPath,
+ rootConfigPath: "ui5-test-configPath.yaml",
+ });
+ t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), {
+ configPathTest: true
+ });
+});
diff --git a/packages/project/test/lib/graph/graphFromPackageDependencies.js b/packages/project/test/lib/graph/graphFromPackageDependencies.js
new file mode 100644
index 00000000000..491cc442ed5
--- /dev/null
+++ b/packages/project/test/lib/graph/graphFromPackageDependencies.js
@@ -0,0 +1,41 @@
+import test from "ava";
+import path from "node:path";
+import sinonGlobal from "sinon";
+import {graphFromPackageDependencies} from "../../../lib/graph/graph.js";
+
+const __dirname = import.meta.dirname;
+
+const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a");
+test.beforeEach((t) => {
+ t.context.sinon = sinonGlobal.createSandbox();
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test("Application A", async (t) => {
+ const projectGraph = await graphFromPackageDependencies({cwd: applicationAPath});
+ const rootProject = projectGraph.getRoot();
+ t.is(rootProject.getName(), "application.a", "Returned correct root project");
+});
+
+test("Application A: Traverse project graph breadth first", async (t) => {
+ const projectGraph = await graphFromPackageDependencies({cwd: applicationAPath});
+ const callbackStub = t.context.sinon.stub().resolves();
+ await projectGraph.traverseBreadthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 5, "Five projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "application.a",
+ "library.d",
+ "library.a",
+ "library.b",
+ "library.c"
+ ], "Traversed graph in correct order");
+});
+
+// More integration tests for package.json dependencies in graph/providers/NodePackageDependencies.integration.js
diff --git a/packages/project/test/lib/graph/graphFromStaticFile.js b/packages/project/test/lib/graph/graphFromStaticFile.js
new file mode 100644
index 00000000000..acff5761ab3
--- /dev/null
+++ b/packages/project/test/lib/graph/graphFromStaticFile.js
@@ -0,0 +1,112 @@
+import test from "ava";
+import path from "node:path";
+import sinonGlobal from "sinon";
+
+import {graphFromStaticFile} from "../../../lib/graph/graph.js";
+
+const __dirname = import.meta.dirname;
+
+const applicationHPath = path.join(__dirname, "..", "..", "fixtures", "application.h");
+const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a");
+const notExistingPath = path.join(__dirname, "..", "..", "fixtures", "does_not_exist");
+
+test.beforeEach((t) => {
+ t.context.sinon = sinonGlobal.createSandbox();
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test("Application H: Traverse project graph breadth first", async (t) => {
+ const projectGraph = await graphFromStaticFile({
+ cwd: applicationHPath
+ });
+ const callbackStub = t.context.sinon.stub().resolves();
+ await projectGraph.traverseBreadthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 2, "Two projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, [
+ "application.a",
+ "library.e",
+ ], "Traversed graph in correct order");
+});
+
+test("Throws error if file not found", async (t) => {
+ const err = await t.throwsAsync(graphFromStaticFile({
+ cwd: notExistingPath
+ }));
+ t.is(err.message,
+ `Failed to load dependency tree configuration from path ` +
+ `${path.join(notExistingPath, "projectDependencies.yaml")}: ` +
+ `ENOENT: no such file or directory, open '${path.join(notExistingPath, "projectDependencies.yaml")}'`,
+ "Correct error message");
+});
+
+test("Throws for missing id", async (t) => {
+ const err = await t.throwsAsync(graphFromStaticFile({
+ cwd: applicationHPath,
+ filePath: "projectDependencies-missing-id.yaml"
+ }));
+ t.is(err.message,
+ `Failed to load dependency tree configuration from path ` +
+ `${path.join(applicationHPath, "projectDependencies-missing-id.yaml")}: ` +
+ `Missing or empty attribute 'id' for project with path ${applicationAPath}`,
+ "Correct error message");
+});
+
+test("Throws for missing version", async (t) => {
+ const err = await t.throwsAsync(graphFromStaticFile({
+ cwd: applicationHPath,
+ filePath: "projectDependencies-missing-version.yaml"
+ }));
+ t.is(err.message,
+ `Failed to load dependency tree configuration from path ` +
+ `${path.join(applicationHPath, "projectDependencies-missing-version.yaml")}: ` +
+ `Missing or empty attribute 'version' for project static-application.a`,
+ "Correct error message");
+});
+
+test("Throws for missing path", async (t) => {
+ const err = await t.throwsAsync(graphFromStaticFile({
+ cwd: applicationHPath,
+ filePath: "projectDependencies-missing-path.yaml"
+ }));
+ t.is(err.message,
+ `Failed to load dependency tree configuration from path ` +
+ `${path.join(applicationHPath, "projectDependencies-missing-path.yaml")}: ` +
+ `Missing or empty attribute 'path' for project static-library.e`,
+ "Correct error message");
+});
+
+test("rootConfiguration", async (t) => {
+ const projectGraph = await graphFromStaticFile({
+ cwd: applicationHPath,
+ rootConfiguration: {
+ specVersion: "2.6",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ customConfiguration: {
+ rootConfigurationTest: true
+ }
+ }
+ });
+ t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), {
+ rootConfigurationTest: true
+ });
+});
+
+test("rootConfig", async (t) => {
+ const projectGraph = await graphFromStaticFile({
+ cwd: applicationHPath,
+ rootConfigPath: "../application.a/ui5-test-configPath.yaml"
+ });
+ t.deepEqual(projectGraph.getRoot().getCustomConfiguration(), {
+ configPathTest: true
+ });
+});
diff --git a/packages/project/test/lib/graph/helpers/createWorkspace.js b/packages/project/test/lib/graph/helpers/createWorkspace.js
new file mode 100644
index 00000000000..b73a7c4a395
--- /dev/null
+++ b/packages/project/test/lib/graph/helpers/createWorkspace.js
@@ -0,0 +1,332 @@
+import test from "ava";
+import path from "node:path";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+
+const __dirname = import.meta.dirname;
+const fixturesPath = path.join(__dirname, "..", "..", "..", "fixtures");
+const libraryHPath = path.join(fixturesPath, "library.h");
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.workspaceConstructorStub = sinon.stub();
+ class MockWorkspace {
+ constructor(params) {
+ t.context.workspaceConstructorStub(params);
+ }
+ }
+ t.context.MockWorkspace = MockWorkspace;
+
+ t.context.createWorkspace = await esmock("../../../../lib/graph/helpers/createWorkspace", {
+ "../../../../lib/graph/Workspace.js": t.context.MockWorkspace
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test("createWorkspace: Missing parameter 'configObject' or 'configPath'", async (t) => {
+ const {createWorkspace} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: "cwd",
+ }));
+ t.is(err.message, "createWorkspace: Missing parameter 'cwd', 'configObject' or 'configPath'",
+ "Threw with expected error message");
+});
+
+test("createWorkspace: Missing parameter 'cwd'", async (t) => {
+ const {createWorkspace} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ configPath: path.join(libraryHPath, "invalid-ui5-workspace.yaml")
+ }));
+ t.is(err.message, "createWorkspace: Missing parameter 'cwd', 'configObject' or 'configPath'",
+ "Threw with expected error message");
+});
+
+test("createWorkspace: Missing parameter 'name' if 'configPath' is set", async (t) => {
+ const {createWorkspace} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: "cwd",
+ configPath: path.join(libraryHPath, "invalid-ui5-workspace.yaml")
+ }));
+ t.is(err.message, "createWorkspace: Parameter 'configPath' implies parameter 'name', but it's empty",
+ "Threw with expected error message");
+});
+
+test("createWorkspace: Using object", async (t) => {
+ const {
+ workspaceConstructorStub,
+ MockWorkspace,
+ createWorkspace
+ } = t.context;
+
+ const res = await createWorkspace({
+ cwd: "cwd",
+ configObject: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ });
+
+ t.true(res instanceof MockWorkspace, "Returned instance of Workspace");
+
+ t.is(workspaceConstructorStub.callCount, 1, "Workspace constructor got called once");
+ t.deepEqual(workspaceConstructorStub.getCall(0).args[0], {
+ cwd: "cwd",
+ configuration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ }, "Created Workspace instance with correct parameters");
+});
+
+test("createWorkspace: Using invalid object", async (t) => {
+ const {createWorkspace} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: "cwd",
+ configObject: {
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ }));
+ t.is(err.message, "Invalid workspace configuration: Missing or empty property 'metadata.name'",
+ "Threw with validation error");
+});
+
+test("createWorkspace: Using name and object with different workspace name", async (t) => {
+ const {createWorkspace} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: "cwd",
+ name: "my-workspace",
+ configObject: {
+ metadata: {
+ name: "my-other-workspace"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "resolution/path"
+ }]
+ }
+ }
+ }));
+ t.is(err.message,
+ `The provided workspace name 'my-workspace' does not match ` +
+ `the provided workspace configuration 'my-other-workspace'`,
+ "Threw with validation error");
+});
+
+test("createWorkspace: Using file", async (t) => {
+ const {createWorkspace, MockWorkspace, workspaceConstructorStub} = t.context;
+
+ const res = await createWorkspace({
+ cwd: "cwd",
+ name: "default",
+ configPath: path.join(libraryHPath, "ui5-workspace.yaml")
+ });
+
+ t.true(res instanceof MockWorkspace, "Returned instance of Workspace");
+
+ t.is(workspaceConstructorStub.callCount, 1, "Workspace constructor got called once");
+ t.deepEqual(workspaceConstructorStub.getCall(0).args[0], {
+ cwd: libraryHPath,
+ configuration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "../library.d"
+ }]
+ }
+ }
+ }, "Created Workspace instance with correct parameters");
+});
+
+test("createWorkspace: Using invalid file", async (t) => {
+ const {createWorkspace} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: "cwd",
+ name: "default",
+ configPath: path.join(libraryHPath, "invalid-ui5-workspace.yaml")
+ }));
+
+ t.true(err.message.includes("Invalid workspace configuration"), "Threw with validation error");
+});
+
+test("createWorkspace: Using missing file", async (t) => {
+ const {createWorkspace, workspaceConstructorStub} = t.context;
+
+ const res = await createWorkspace({
+ cwd: path.join(fixturesPath, "library.d"),
+ name: "default",
+ configPath: "ui5-workspace.yaml"
+ });
+
+ t.is(res, null, "Returned no workspace");
+
+ t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor did not get called");
+});
+
+test("createWorkspace: Missing default workspace in file", async (t) => {
+ const {createWorkspace, workspaceConstructorStub} = t.context;
+
+ const res = await createWorkspace({
+ cwd: path.join(fixturesPath, "library.h"),
+ name: "default",
+ configPath: path.join(fixturesPath, "library.h", "custom-ui5-workspace.yaml")
+ });
+
+ t.is(res, null, "Returned no workspace");
+
+ t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor did not get called");
+});
+
+test("createWorkspace: Using missing file and non-default name", async (t) => {
+ const {createWorkspace, workspaceConstructorStub} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: path.join(fixturesPath, "library.d"),
+ name: "special",
+ configPath: "ui5-workspace.yaml"
+ }));
+
+ const filePath = path.join(fixturesPath, "library.d", "ui5-workspace.yaml");
+ t.true(err.message.startsWith(
+ `Failed to load workspace configuration from path ${filePath}: `), "Threw with expected error message");
+
+ t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor did not get called");
+});
+
+test("createWorkspace: Using non-default file and non-default name", async (t) => {
+ const {createWorkspace, workspaceConstructorStub, MockWorkspace} = t.context;
+
+ const res = await createWorkspace({
+ cwd: path.join(fixturesPath, "library.h"),
+ name: "library-d",
+ configPath: "custom-ui5-workspace.yaml"
+ });
+
+ t.true(res instanceof MockWorkspace, "Returned instance of Workspace");
+
+ t.is(workspaceConstructorStub.callCount, 1, "Workspace constructor got called once");
+ t.deepEqual(workspaceConstructorStub.getCall(0).args[0], {
+ cwd: libraryHPath,
+ configuration: {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "library-d"
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "../library.d"
+ }]
+ }
+ }
+ }, "Created Workspace instance with correct parameters");
+});
+
+test("createWorkspace: Using non-default file and non-default name which is not in file", async (t) => {
+ const {createWorkspace, workspaceConstructorStub} = t.context;
+
+ const err = await t.throwsAsync(createWorkspace({
+ cwd: path.join(fixturesPath, "library.h"),
+ name: "special",
+ configPath: "custom-ui5-workspace.yaml"
+ }));
+
+ t.is(err.message, `Could not find a workspace named 'special' in custom-ui5-workspace.yaml`,
+ "Threw with expected error message");
+
+ t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor did not get called");
+});
+
+test("readWorkspaceConfigFile", async (t) => {
+ const {createWorkspace} = t.context;
+ const res = await createWorkspace._readWorkspaceConfigFile(
+ path.join(libraryHPath, "ui5-workspace.yaml"), false);
+ t.deepEqual(res,
+ [{
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "default",
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "../library.d",
+ }]
+ },
+ }, {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "all-libraries",
+ },
+ dependencyManagement: {
+ resolutions: [{
+ path: "../library.d",
+ }, {
+ path: "../library.e",
+ }, {
+ path: "../library.f",
+ }],
+ },
+ }], "Read workspace configuration file correctly");
+});
+
+test("readWorkspaceConfigFile: Throws for missing file", async (t) => {
+ const {createWorkspace} = t.context;
+ const filePath = path.join(fixturesPath, "library.d", "other-ui5-workspace.yaml");
+ const err =
+ await t.throwsAsync(createWorkspace._readWorkspaceConfigFile(filePath));
+ t.true(err.message.startsWith(
+ `Failed to load workspace configuration from path ${filePath}: `), "Threw with expected error message");
+});
+
+test("readWorkspaceConfigFile: Validation errors", async (t) => {
+ const {createWorkspace} = t.context;
+ const filePath = path.join(libraryHPath, "invalid-ui5-workspace.yaml");
+ const err =
+ await t.throwsAsync(createWorkspace._readWorkspaceConfigFile(filePath, true));
+ t.true(err.message.includes("Invalid workspace configuration"), "Threw with validation error");
+});
+
+test("readWorkspaceConfigFile: Not a YAML", async (t) => {
+ const {createWorkspace} = t.context;
+ const filePath = path.join(libraryHPath, "corrupt-ui5-workspace.yaml");
+ const err =
+ await t.throwsAsync(createWorkspace._readWorkspaceConfigFile(filePath, true));
+ t.true(err.message.includes(`Failed to parse workspace configuration at ${filePath}`),
+ "Threw with parsing error");
+});
+
+test("readWorkspaceConfigFile: Empty file", async (t) => {
+ const {createWorkspace} = t.context;
+ const filePath = path.join(libraryHPath, "empty-ui5-workspace.yaml");
+ const res = await createWorkspace._readWorkspaceConfigFile(filePath, true);
+ t.deepEqual(res, [], "No workspace configuration returned");
+});
diff --git a/packages/project/test/lib/graph/helpers/ui5Framework.integration.js b/packages/project/test/lib/graph/helpers/ui5Framework.integration.js
new file mode 100644
index 00000000000..93096d50109
--- /dev/null
+++ b/packages/project/test/lib/graph/helpers/ui5Framework.integration.js
@@ -0,0 +1,925 @@
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import path from "node:path";
+import DependencyTreeProvider from "../../../../lib/graph/providers/DependencyTree.js";
+
+const __dirname = import.meta.dirname;
+
+// Use path within project as mocking base directory to reduce chance of side effects
+// in case mocks/stubs do not work and real fs is used
+const fakeBaseDir = path.join(__dirname, "fake-tmp");
+const ui5FrameworkBaseDir = path.join(fakeBaseDir, "homedir", ".ui5", "framework");
+const ui5PackagesBaseDir = path.join(ui5FrameworkBaseDir, "packages");
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.logStub = {
+ info: sinon.stub(),
+ verbose: sinon.stub(),
+ silly: sinon.stub(),
+ isLevelEnabled: sinon.stub().returns(false),
+ _getLogger: sinon.stub()
+ };
+ const ui5Logger = {
+ getLogger: sinon.stub().returns(t.context.logStub)
+ };
+
+ t.context.pacote = {
+ extract: sinon.stub(),
+ manifest: sinon.stub()
+ };
+
+ class Config {
+ static get typeDefs() {
+ return {};
+ }
+
+ async load() {}
+
+ get flat() {
+ return {};
+ }
+ }
+ sinon.stub(Config.prototype, "flat").value({
+ registry: "https://registry.fake",
+ proxy: ""
+ });
+
+ t.context.Registry = await esmock.p("../../../../lib/ui5Framework/npm/Registry.js", {
+ "@ui5/logger": ui5Logger,
+ "pacote": t.context.pacote,
+ "@npmcli/config": {
+ "default": Config
+ }
+ });
+
+ const AbstractInstaller = await esmock.p("../../../../lib/ui5Framework/AbstractInstaller.js", {
+ "@ui5/logger": ui5Logger,
+ "../../../../lib/utils/fs.js": {
+ mkdirp: sinon.stub().resolves()
+ },
+ "lockfile": {
+ lock: sinon.stub().yieldsAsync(),
+ unlock: sinon.stub().yieldsAsync()
+ }
+ });
+
+ t.context.Installer = await esmock.p("../../../../lib/ui5Framework/npm/Installer.js", {
+ "@ui5/logger": ui5Logger,
+ "graceful-fs": {
+ rename: sinon.stub().yieldsAsync(),
+ },
+ "../../../../lib/utils/fs.js": {
+ mkdirp: sinon.stub().resolves()
+ },
+ "../../../../lib/ui5Framework/npm/Registry.js": t.context.Registry,
+ "../../../../lib/ui5Framework/AbstractInstaller.js": AbstractInstaller
+ });
+
+ t.context.AbstractResolver = await esmock.p("../../../../lib/ui5Framework/AbstractResolver.js", {
+ "@ui5/logger": ui5Logger,
+ "node:os": {
+ homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir"))
+ },
+ });
+
+ t.context.Openui5Resolver = await esmock.p("../../../../lib/ui5Framework/Openui5Resolver.js", {
+ "@ui5/logger": ui5Logger,
+ "node:os": {
+ homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir"))
+ },
+ "../../../../lib/ui5Framework/AbstractResolver.js": t.context.AbstractResolver,
+ "../../../../lib/ui5Framework/npm/Installer.js": t.context.Installer
+ });
+
+ t.context.Sapui5Resolver = await esmock.p("../../../../lib/ui5Framework/Sapui5Resolver.js", {
+ "@ui5/logger": ui5Logger,
+ "node:os": {
+ homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir"))
+ },
+ "../../../../lib/ui5Framework/AbstractResolver.js": t.context.AbstractResolver,
+ "../../../../lib/ui5Framework/npm/Installer.js": t.context.Installer
+ });
+
+ t.context.Application = await esmock.p("../../../../lib/specifications/types/Application.js");
+ t.context.Library = await esmock.p("../../../../lib/specifications/types/Library.js");
+
+ // Stub specification internal checks since none of the projects actually exist on disk
+ sinon.stub(t.context.Application.prototype, "_configureAndValidatePaths").resolves();
+ sinon.stub(t.context.Library.prototype, "_configureAndValidatePaths").resolves();
+ sinon.stub(t.context.Application.prototype, "_parseConfiguration").resolves();
+ sinon.stub(t.context.Library.prototype, "_parseConfiguration").resolves();
+
+ t.context.Specification = await esmock.p("../../../../lib/specifications/Specification.js", {
+ "@ui5/logger": ui5Logger,
+ "../../../../lib/specifications/types/Application.js": t.context.Application,
+ "../../../../lib/specifications/types/Library.js": t.context.Library
+ });
+
+ t.context.Module = await esmock.p("../../../../lib/graph/Module.js", {
+ "@ui5/logger": ui5Logger,
+ "../../../../lib/specifications/Specification.js": t.context.Specification
+ });
+
+ // Stub os homedir to prevent the actual ~/.ui5rc from being used in tests
+ t.context.Configuration = await esmock.p("../../../../lib/config/Configuration.js", {
+ "node:os": {
+ homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir"))
+ }
+ });
+
+ t.context.ui5Framework = await esmock.p("../../../../lib/graph/helpers/ui5Framework.js", {
+ "@ui5/logger": ui5Logger,
+ "../../../../lib/graph/Module.js": t.context.Module,
+ "../../../../lib/ui5Framework/Openui5Resolver.js": t.context.Openui5Resolver,
+ "../../../../lib/ui5Framework/Sapui5Resolver.js": t.context.Sapui5Resolver,
+ "../../../../lib/config/Configuration.js": t.context.Configuration
+ });
+
+ t.context.projectGraphBuilder = await esmock.p("../../../../lib/graph/projectGraphBuilder.js", {
+ "@ui5/logger": ui5Logger,
+ "../../../../lib/graph/Module.js": t.context.Module
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.Registry);
+ esmock.purge(t.context.Installer);
+ esmock.purge(t.context.AbstractResolver);
+ esmock.purge(t.context.Openui5Resolver);
+ esmock.purge(t.context.Sapui5Resolver);
+ esmock.purge(t.context.Application);
+ esmock.purge(t.context.Library);
+ esmock.purge(t.context.Specification);
+ esmock.purge(t.context.Module);
+ esmock.purge(t.context.ui5Framework);
+ esmock.purge(t.context.projectGraphBuilder);
+});
+
+function defineTest(testName, {
+ frameworkName,
+ verbose = false,
+ librariesInWorkspace = null
+}) {
+ const npmScope = frameworkName === "SAPUI5" ? "@sapui5" : "@openui5";
+
+ const distributionMetadata = {
+ libraries: {
+ "sap.ui.lib1": {
+ npmPackageName: "@sapui5/sap.ui.lib1",
+ version: "1.75.1",
+ dependencies: [],
+ optionalDependencies: []
+ },
+ "sap.ui.lib2": {
+ npmPackageName: "@sapui5/sap.ui.lib2",
+ version: "1.75.2",
+ dependencies: [
+ "sap.ui.lib3"
+ ],
+ optionalDependencies: []
+ },
+ "sap.ui.lib3": {
+ npmPackageName: "@sapui5/sap.ui.lib3",
+ version: "1.75.3",
+ dependencies: [],
+ optionalDependencies: [
+ "sap.ui.lib4"
+ ]
+ },
+ "sap.ui.lib4": {
+ npmPackageName: "@openui5/sap.ui.lib4",
+ version: "1.75.4",
+ dependencies: [
+ "sap.ui.lib1"
+ ],
+ optionalDependencies: []
+ },
+ "sap.ui.lib8": {
+ npmPackageName: "@sapui5/sap.ui.lib8",
+ version: "1.75.8",
+ dependencies: [],
+ optionalDependencies: []
+ }
+ }
+ };
+
+ test.serial(`${frameworkName}: ${verbose ? "(verbose) " : ""}${testName}`, async (t) => {
+ const {sinon, ui5Framework, Installer, projectGraphBuilder, Module, pacote, logStub} = t.context;
+
+ // Enable verbose logging
+ if (verbose) {
+ logStub.isLevelEnabled.withArgs("verbose").returns(true);
+ }
+
+ const testDependency = {
+ id: "test-dependency-id",
+ version: "4.5.6",
+ path: path.join(fakeBaseDir, "project-test-dependency"),
+ dependencies: [],
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "test-dependency"
+ },
+ framework: {
+ version: "1.99.0",
+ name: frameworkName,
+ libraries: [
+ {
+ name: "sap.ui.lib1"
+ },
+ {
+ name: "sap.ui.lib2"
+ },
+ {
+ name: "sap.ui.lib5",
+ optional: true
+ },
+ {
+ name: "sap.ui.lib6",
+ development: true
+ },
+ {
+ name: "sap.ui.lib8",
+ // optional dependency gets resolved by dev-dependency of root project
+ optional: true
+ }
+ ]
+ }
+ }
+ };
+ const dependencyTree = {
+ id: "test-application-id",
+ version: "1.2.3",
+ path: path.join(fakeBaseDir, "project-test-application"),
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "test-application"
+ },
+ framework: {
+ name: frameworkName,
+ version: "1.75.0",
+ libraries: [
+ {
+ name: "sap.ui.lib1"
+ },
+ {
+ name: "sap.ui.lib4",
+ optional: true
+ },
+ {
+ name: "sap.ui.lib8",
+ development: true
+ }
+ ]
+ }
+ },
+ dependencies: [
+ testDependency,
+ {
+ id: "test-dependency-no-framework-id",
+ version: "7.8.9",
+ path: path.join(fakeBaseDir, "project-test-dependency-no-framework"),
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "test-dependency-no-framework"
+ }
+ },
+ dependencies: [
+ testDependency
+ ]
+ }
+ ]
+ };
+
+ sinon.stub(Module.prototype, "_readConfigFile")
+ .callsFake(async function() {
+ // eslint-disable-next-line no-invalid-this
+ switch (this.getPath()) {
+ case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib1",
+ frameworkName === "SAPUI5" ? "1.75.1" : "1.75.0"):
+ return [{
+ specVersion: "1.0",
+ type: "library",
+ metadata: {
+ name: "sap.ui.lib1"
+ },
+ framework: {
+ name: frameworkName,
+ libraries: []
+ }
+ }];
+ case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib2",
+ frameworkName === "SAPUI5" ? "1.75.2" : "1.75.0"):
+ return [{
+ specVersion: "1.0",
+ type: "library",
+ metadata: {
+ name: "sap.ui.lib2"
+ },
+ framework: {
+ name: frameworkName,
+ libraries: []
+ }
+ }];
+ case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib3",
+ frameworkName === "SAPUI5" ? "1.75.3" : "1.75.0"):
+ return [{
+ specVersion: "1.0",
+ type: "library",
+ metadata: {
+ name: "sap.ui.lib3"
+ },
+ framework: {
+ name: frameworkName,
+ libraries: []
+ }
+ }];
+ case path.join(ui5PackagesBaseDir, "@openui5", "sap.ui.lib4",
+ frameworkName === "SAPUI5" ? "1.75.4" : "1.75.0"):
+ return [{
+ specVersion: "1.0",
+ type: "library",
+ metadata: {
+ name: "sap.ui.lib4"
+ },
+ framework: {
+ name: frameworkName,
+ libraries: []
+ }
+ }];
+ case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib8",
+ frameworkName === "SAPUI5" ? "1.75.8" : "1.75.0"):
+ return [{
+ specVersion: "1.0",
+ type: "library",
+ metadata: {
+ name: "sap.ui.lib8"
+ },
+ framework: {
+ name: frameworkName,
+ libraries: []
+ }
+ }];
+ default:
+ throw new Error(
+ "Module#_readConfigFile stub called with unknown project: " +
+ // eslint-disable-next-line no-invalid-this
+ (this.getId())
+ );
+ }
+ });
+
+ pacote.extract.resolves();
+
+ if (frameworkName === "OpenUI5") {
+ pacote.manifest
+ .callsFake(async (spec) => {
+ throw new Error("pacote.manifest stub called with unknown spec: " + spec);
+ })
+ .withArgs("@openui5/sap.ui.lib1@1.75.0")
+ .resolves({
+ name: "@openui5/sap.ui.lib1",
+ version: "1.75.0",
+ dependencies: {}
+ })
+ .withArgs("@openui5/sap.ui.lib2@1.75.0")
+ .resolves({
+ name: "@openui5/sap.ui.lib2",
+ version: "1.75.0",
+ dependencies: {
+ "@openui5/sap.ui.lib3": "1.75.0"
+ }
+ })
+ .withArgs("@openui5/sap.ui.lib3@1.75.0")
+ .resolves({
+ name: "@openui5/sap.ui.lib3",
+ version: "1.75.0",
+ devDependencies: {
+ "@openui5/sap.ui.lib4": "1.75.0"
+ }
+ })
+ .withArgs("@openui5/sap.ui.lib4@1.75.0")
+ .resolves({
+ name: "@openui5/sap.ui.lib4",
+ version: "1.75.0",
+ dependencies: {
+ "@openui5/sap.ui.lib1": "1.75.0"
+ }
+ })
+ .withArgs("@openui5/sap.ui.lib8@1.75.0")
+ .resolves({
+ name: "@openui5/sap.ui.lib8",
+ version: "1.75.0",
+ dependencies: {}
+ });
+ } else if (frameworkName === "SAPUI5") {
+ sinon.stub(Installer.prototype, "readJson")
+ .callsFake(async (path) => {
+ throw new Error("Installer#readJson stub called with unknown path: " + path);
+ })
+ .withArgs(path.join(fakeBaseDir,
+ "homedir", ".ui5", "framework", "packages",
+ "@sapui5", "distribution-metadata", "1.75.0",
+ "metadata.json"))
+ .resolves(distributionMetadata);
+ }
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ if (librariesInWorkspace) {
+ const projectNameMap = new Map();
+ const moduleIdMap = new Map();
+ librariesInWorkspace.forEach((libName) => {
+ const libraryDistMetadata = distributionMetadata.libraries[libName];
+ const module = {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ getName: sinon.stub().returns(libName),
+ getVersion: sinon.stub().returns("1.76.0-SNAPSHOT"),
+ getRootPath: sinon.stub().returns(path.join(fakeBaseDir, "workspace", libName)),
+ isFrameworkProject: sinon.stub().returns(true),
+ getId: sinon.stub().returns(libraryDistMetadata.npmPackageName),
+ getRootReader: sinon.stub().returns({
+ byPath: sinon.stub().resolves({
+ getString: sinon.stub().resolves(JSON.stringify({dependencies: {}}))
+ })
+ }),
+ getFrameworkDependencies: sinon.stub().callsFake(() => {
+ const deps = [];
+ libraryDistMetadata.dependencies.forEach((dep) => {
+ deps.push({name: dep});
+ });
+ libraryDistMetadata.optionalDependencies.forEach((optDep) => {
+ deps.push({name: optDep, optional: true});
+ });
+ return deps;
+ }),
+ isDeprecated: sinon.stub().returns(false),
+ isSapInternal: sinon.stub().returns(false),
+ getAllowSapInternal: sinon.stub().returns(false),
+ }
+ }),
+ getVersion: sinon.stub().returns("1.76.0-SNAPSHOT"),
+ getPath: sinon.stub().returns(path.join(fakeBaseDir, "workspace", libName)),
+ };
+ projectNameMap.set(libName, module);
+ moduleIdMap.set(libraryDistMetadata.npmPackageName, module);
+ });
+
+ const getModuleByProjectName = sinon.stub().callsFake(
+ async (projectName) => projectNameMap.get(projectName)
+ );
+ const getModules = sinon.stub().callsFake(
+ async () => {
+ const sortedMap = new Map([...moduleIdMap].sort((a, b) => String(a[0]).localeCompare(b[0])));
+ return Array.from(sortedMap.values());
+ }
+ );
+
+ const workspace = {
+ getName: sinon.stub().returns("test"),
+ getModules,
+ getModuleByProjectName
+ };
+
+ await ui5Framework.enrichProjectGraph(projectGraph, {workspace});
+ } else {
+ await ui5Framework.enrichProjectGraph(projectGraph);
+ }
+
+ const callbackStub = sinon.stub().resolves();
+ await projectGraph.traverseDepthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 8, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+ t.deepEqual(callbackCalls, [
+ "sap.ui.lib1",
+ "sap.ui.lib8",
+ "sap.ui.lib4",
+ "sap.ui.lib3",
+ "sap.ui.lib2",
+ "test-dependency",
+ "test-dependency-no-framework",
+ "test-application",
+ ], "Traversed graph in correct order");
+
+ t.deepEqual(projectGraph.getDependencies("test-application"), [
+ "test-dependency",
+ "test-dependency-no-framework",
+ "sap.ui.lib1",
+ "sap.ui.lib4",
+ "sap.ui.lib8",
+ ], `Non-framework dependency has correct dependencies`);
+
+ t.deepEqual(projectGraph.getDependencies("test-dependency"), [
+ "sap.ui.lib1",
+ "sap.ui.lib2",
+ "sap.ui.lib8",
+ ], `Non-framework dependency has correct dependencies`);
+
+ const frameworkLibAlreadyAddedInfoLogged = (logStub.info.getCalls()
+ .map(($) => $.firstArg)
+ .findIndex(($) => $.includes("defines a dependency to the UI5 framework library")) !== -1);
+ t.false(frameworkLibAlreadyAddedInfoLogged, "No info regarding already added UI5 framework libraries logged");
+ });
+}
+
+defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", {
+ frameworkName: "SAPUI5"
+});
+defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", {
+ frameworkName: "SAPUI5",
+ verbose: true
+});
+defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", {
+ frameworkName: "OpenUI5"
+});
+defineTest("ui5Framework helper should enhance project graph with UI5 framework libraries", {
+ frameworkName: "OpenUI5",
+ verbose: true
+});
+
+defineTest("ui5Framework helper should not cause install of libraries within workspace", {
+ frameworkName: "SAPUI5",
+ librariesInWorkspace: ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib8"]
+});
+defineTest("ui5Framework helper should not cause install of libraries within workspace", {
+ frameworkName: "OpenUI5",
+ librariesInWorkspace: ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib8"]
+});
+
+function defineErrorTest(testName, {
+ frameworkName,
+ failExtract = false,
+ failMetadata = false,
+ expectedErrorMessage
+}) {
+ test.serial(testName, async (t) => {
+ const {sinon, ui5Framework, Installer, projectGraphBuilder, pacote} = t.context;
+
+ const dependencyTree = {
+ id: "test-id",
+ version: "1.2.3",
+ path: path.join(fakeBaseDir, "application-project"),
+ dependencies: [],
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "test-project"
+ },
+ framework: {
+ name: frameworkName,
+ version: "1.75.0",
+ libraries: [
+ {
+ name: "sap.ui.lib1"
+ },
+ {
+ name: "sap.ui.lib4",
+ optional: true
+ }
+ ]
+ }
+ }
+ };
+
+ pacote.extract.callsFake(async (spec) => {
+ throw new Error("pacote.extract stub called with unknown spec: " + spec);
+ });
+
+ pacote.manifest.callsFake(async (spec) => {
+ throw new Error("pacote.manifest stub called with unknown spec: " + spec);
+ });
+
+ if (frameworkName === "SAPUI5") {
+ if (failExtract) {
+ pacote.extract
+ .withArgs("@sapui5/sap.ui.lib1@1.75.1")
+ .rejects(new Error("404 - @sapui5/sap.ui.lib1"))
+ .withArgs("@openui5/sap.ui.lib4@1.75.4")
+ .rejects(new Error("404 - @openui5/sap.ui.lib4"));
+ } else {
+ pacote.extract
+ .withArgs("@sapui5/sap.ui.lib1@1.75.1").resolves()
+ .withArgs("@openui5/sap.ui.lib4@1.75.4").resolves();
+ }
+ if (failMetadata) {
+ pacote.extract
+ .withArgs("@sapui5/distribution-metadata@1.75.0")
+ .rejects(new Error("404 - @sapui5/distribution-metadata"));
+ } else {
+ pacote.extract
+ .withArgs("@sapui5/distribution-metadata@1.75.0")
+ .resolves();
+ sinon.stub(Installer.prototype, "readJson")
+ .callThrough()
+ .withArgs(path.join(fakeBaseDir,
+ "homedir", ".ui5", "framework", "packages",
+ "@sapui5", "distribution-metadata", "1.75.0",
+ "metadata.json"))
+ .resolves({
+ libraries: {
+ "sap.ui.lib1": {
+ npmPackageName: "@sapui5/sap.ui.lib1",
+ version: "1.75.1",
+ dependencies: [],
+ optionalDependencies: []
+ },
+ "sap.ui.lib2": {
+ npmPackageName: "@sapui5/sap.ui.lib2",
+ version: "1.75.2",
+ dependencies: [
+ "sap.ui.lib3"
+ ],
+ optionalDependencies: []
+ },
+ "sap.ui.lib3": {
+ npmPackageName: "@sapui5/sap.ui.lib3",
+ version: "1.75.3",
+ dependencies: [],
+ optionalDependencies: [
+ "sap.ui.lib4"
+ ]
+ },
+ "sap.ui.lib4": {
+ npmPackageName: "@openui5/sap.ui.lib4",
+ version: "1.75.4",
+ dependencies: [
+ "sap.ui.lib1"
+ ],
+ optionalDependencies: []
+ }
+ }
+ });
+ }
+ } else if (frameworkName === "OpenUI5") {
+ if (failExtract) {
+ pacote.extract
+ .withArgs("@openui5/sap.ui.lib1@1.75.0")
+ .rejects(new Error("404 - @openui5/sap.ui.lib1"))
+ .withArgs("@openui5/sap.ui.lib4@1.75.0")
+ .rejects(new Error("404 - @openui5/sap.ui.lib4"));
+ } else {
+ pacote.extract
+ .withArgs("@openui5/sap.ui.lib1@1.75.0")
+ .resolves()
+ .withArgs("@openui5/sap.ui.lib4@1.75.0")
+ .resolves();
+ }
+ if (failMetadata) {
+ pacote.manifest
+ .withArgs("@openui5/sap.ui.lib1@1.75.0")
+ .rejects(new Error("Failed to read manifest of @openui5/sap.ui.lib1@1.75.0"))
+ .withArgs("@openui5/sap.ui.lib4@1.75.0")
+ .rejects(new Error("Failed to read manifest of @openui5/sap.ui.lib4@1.75.0"));
+ } else {
+ pacote.manifest
+ .withArgs("@openui5/sap.ui.lib1@1.75.0")
+ .resolves({
+ name: "@openui5/sap.ui.lib1",
+ version: "1.75.0",
+ dependencies: {}
+ })
+ .withArgs("@openui5/sap.ui.lib4@1.75.0")
+ .resolves({
+ name: "@openui5/sap.ui.lib4",
+ version: "1.75.0"
+ });
+ }
+ }
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+ await t.throwsAsync(async () => {
+ await ui5Framework.enrichProjectGraph(projectGraph);
+ }, {message: expectedErrorMessage});
+ });
+}
+
+defineErrorTest("SAPUI5: ui5Framework helper should throw a proper error when metadata request fails", {
+ frameworkName: "SAPUI5",
+ failMetadata: true,
+ expectedErrorMessage: `Resolution of framework libraries failed with errors:
+ 1. Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` +
+`404 - @sapui5/distribution-metadata
+ 2. Failed to resolve library sap.ui.lib4: Error already logged`
+});
+defineErrorTest("SAPUI5: ui5Framework helper should throw a proper error when package extraction fails", {
+ frameworkName: "SAPUI5",
+ failExtract: true,
+ expectedErrorMessage: `Resolution of framework libraries failed with errors:
+ 1. Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/sap.ui.lib1@1.75.1: ` +
+`404 - @sapui5/sap.ui.lib1
+ 2. Failed to resolve library sap.ui.lib4: Failed to extract package @openui5/sap.ui.lib4@1.75.4: ` +
+`404 - @openui5/sap.ui.lib4`
+});
+defineErrorTest(
+ "SAPUI5: ui5Framework helper should throw a proper error when metadata request and package extraction fails", {
+ frameworkName: "SAPUI5",
+ failMetadata: true,
+ failExtract: true,
+ expectedErrorMessage: `Resolution of framework libraries failed with errors:
+ 1. Failed to resolve library sap.ui.lib1: Failed to extract package @sapui5/distribution-metadata@1.75.0: ` +
+`404 - @sapui5/distribution-metadata
+ 2. Failed to resolve library sap.ui.lib4: Error already logged`
+ });
+
+
+defineErrorTest("OpenUI5: ui5Framework helper should throw a proper error when metadata request fails", {
+ frameworkName: "OpenUI5",
+ failMetadata: true,
+ expectedErrorMessage: `Resolution of framework libraries failed with errors:
+ 1. Failed to resolve library sap.ui.lib1: Failed to read manifest of @openui5/sap.ui.lib1@1.75.0
+ 2. Failed to resolve library sap.ui.lib4: Failed to read manifest of @openui5/sap.ui.lib4@1.75.0`
+});
+defineErrorTest("OpenUI5: ui5Framework helper should throw a proper error when package extraction fails", {
+ frameworkName: "OpenUI5",
+ failExtract: true,
+ expectedErrorMessage: `Resolution of framework libraries failed with errors:
+ 1. Failed to resolve library sap.ui.lib1: Failed to extract package @openui5/sap.ui.lib1@1.75.0: ` +
+`404 - @openui5/sap.ui.lib1
+ 2. Failed to resolve library sap.ui.lib4: Failed to extract package @openui5/sap.ui.lib4@1.75.0: ` +
+`404 - @openui5/sap.ui.lib4`
+});
+defineErrorTest(
+ "OpenUI5: ui5Framework helper should throw a proper error when metadata request and package extraction fails", {
+ frameworkName: "OpenUI5",
+ failMetadata: true,
+ failExtract: true,
+ expectedErrorMessage: `Resolution of framework libraries failed with errors:
+ 1. Failed to resolve library sap.ui.lib1: Failed to read manifest of @openui5/sap.ui.lib1@1.75.0
+ 2. Failed to resolve library sap.ui.lib4: Failed to read manifest of @openui5/sap.ui.lib4@1.75.0`
+ });
+
+test.serial("ui5Framework helper should not fail when no framework configuration is given", async (t) => {
+ const {ui5Framework, projectGraphBuilder} = t.context;
+
+ const dependencyTree = {
+ id: "test-id",
+ version: "1.2.3",
+ path: path.join(fakeBaseDir, "application-project"),
+ dependencies: [],
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "test-project"
+ }
+ }
+ };
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+ await ui5Framework.enrichProjectGraph(projectGraph);
+
+ t.is(projectGraph, projectGraph, "Returned same graph without error");
+});
+
+test.serial("ui5Framework translator should not try to install anything when no library is referenced", async (t) => {
+ const {ui5Framework, projectGraphBuilder, pacote} = t.context;
+
+ const dependencyTree = {
+ id: "test-id",
+ version: "1.2.3",
+ path: path.join(fakeBaseDir, "application-project"),
+ dependencies: [],
+ configuration: {
+ specVersion: "2.1",
+ type: "application",
+ metadata: {
+ name: "test-project"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.75.0"
+ }
+ }
+ };
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+
+ t.is(pacote.extract.callCount, 0, "No package should be extracted");
+ t.is(pacote.manifest.callCount, 0, "No manifest should be requested");
+});
+
+test.serial("ui5Framework helper shouldn't throw when framework version and libraries are not provided", async (t) => {
+ const {ui5Framework, projectGraphBuilder, logStub} = t.context;
+
+ const dependencyTree = {
+ id: "test-id",
+ version: "1.2.3",
+ path: path.join(fakeBaseDir, "application-project"),
+ dependencies: [],
+ configuration: {
+ specVersion: "2.1",
+ type: "application",
+ metadata: {
+ name: "test-project"
+ },
+ framework: {
+ name: "SAPUI5"
+ }
+ }
+ };
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+
+ t.is(logStub.verbose.callCount, 5);
+ t.deepEqual(logStub.verbose.getCall(0).args, [
+ "Configuration for module test-id has been supplied directly"
+ ]);
+ t.deepEqual(logStub.verbose.getCall(1).args, [
+ "Module test-id contains project test-project"
+ ]);
+ t.deepEqual(logStub.verbose.getCall(2).args, [
+ "Root project test-project qualified as application project for project graph"
+ ]);
+ t.deepEqual(logStub.verbose.getCall(3).args, [
+ "Project test-project has no framework dependencies"
+ ]);
+ t.deepEqual(logStub.verbose.getCall(4).args, [
+ "No SAPUI5 libraries referenced in project test-project or in any of its dependencies"
+ ]);
+});
+
+test.serial(
+ "SAPUI5: ui5Framework helper should throw error when using a library that is not part of the dist metadata",
+ async (t) => {
+ const {sinon, ui5Framework, Installer, projectGraphBuilder} = t.context;
+
+ const dependencyTree = {
+ id: "test-id",
+ version: "1.2.3",
+ path: path.join(fakeBaseDir, "application-project"),
+ dependencies: [],
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "test-project"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.75.0",
+ libraries: [
+ {name: "sap.ui.lib1"},
+ {name: "does.not.exist"},
+ {name: "sap.ui.lib4"},
+ ]
+ }
+ }
+ };
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ sinon.stub(Installer.prototype, "readJson")
+ .callThrough()
+ .withArgs(path.join(fakeBaseDir,
+ "homedir", ".ui5", "framework", "packages",
+ "@sapui5", "distribution-metadata", "1.75.0",
+ "metadata.json"))
+ .resolves({
+ libraries: {
+ "sap.ui.lib1": {
+ npmPackageName: "@sapui5/sap.ui.lib1",
+ version: "1.75.1",
+ dependencies: [],
+ optionalDependencies: []
+ },
+ "sap.ui.lib4": {
+ npmPackageName: "@openui5/sap.ui.lib4",
+ version: "1.75.4",
+ dependencies: [
+ "sap.ui.lib1"
+ ],
+ optionalDependencies: []
+ }
+ }
+ });
+
+ await t.throwsAsync(async () => {
+ await ui5Framework.enrichProjectGraph(projectGraph);
+ }, {
+ message: `Failed to resolve library does.not.exist: Could not find library "does.not.exist"`});
+ });
+
+// TODO test: Should not download packages again in case they are already installed
+
+// TODO test: Should ignore framework libraries in dependencies
diff --git a/packages/project/test/lib/graph/helpers/ui5Framework.js b/packages/project/test/lib/graph/helpers/ui5Framework.js
new file mode 100644
index 00000000000..ceae8c52e54
--- /dev/null
+++ b/packages/project/test/lib/graph/helpers/ui5Framework.js
@@ -0,0 +1,2517 @@
+import path from "node:path";
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import DependencyTreeProvider from "../../../../lib/graph/providers/DependencyTree.js";
+import projectGraphBuilder from "../../../../lib/graph/projectGraphBuilder.js";
+import Specification from "../../../../lib/specifications/Specification.js";
+import CacheMode from "../../../../lib/ui5Framework/maven/CacheMode.js";
+
+const __dirname = import.meta.dirname;
+
+const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a");
+const libraryDPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d");
+const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e");
+const libraryFPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.f");
+
+test.beforeEach(async (t) => {
+ // Tests either rely on not having UI5_DATA_DIR defined, or explicitly define it
+ t.context.originalUi5DataDirEnv = process.env.UI5_DATA_DIR;
+ delete process.env.UI5_DATA_DIR;
+
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.log = {
+ info: sinon.stub(),
+ warn: sinon.stub(),
+ verbose: sinon.stub(),
+ isLevelEnabled: sinon.stub().returns(false),
+ _getLogger: sinon.stub()
+ };
+ const ui5Logger = {
+ getLogger: sinon.stub().returns(t.context.log)
+ };
+
+ t.context.Openui5ResolverStub = sinon.stub();
+
+ t.context.Sapui5ResolverStub = sinon.stub();
+ t.context.Sapui5ResolverInstallStub = sinon.stub();
+ t.context.Sapui5ResolverStub.callsFake(() => {
+ return {
+ install: t.context.Sapui5ResolverInstallStub
+ };
+ });
+ t.context.Sapui5ResolverResolveVersionStub = sinon.stub();
+ t.context.Sapui5ResolverStub.resolveVersion = t.context.Sapui5ResolverResolveVersionStub;
+
+ t.context.Sapui5MavenSnapshotResolverInstallStub = sinon.stub();
+ t.context.Sapui5MavenSnapshotResolverStub = sinon.stub()
+ .callsFake(() => {
+ return {
+ install: t.context.Sapui5MavenSnapshotResolverInstallStub
+ };
+ });
+ t.context.Sapui5MavenSnapshotResolverResolveVersionStub = sinon.stub();
+ t.context.Sapui5MavenSnapshotResolverStub.resolveVersion = t.context.Sapui5MavenSnapshotResolverResolveVersionStub;
+
+ t.context.getUi5DataDirStub = sinon.stub().returns(undefined);
+
+ t.context.ConfigurationStub = {
+ fromFile: sinon.stub().resolves({
+ getUi5DataDir: t.context.getUi5DataDirStub
+ })
+ };
+
+ t.context.ui5Framework = await esmock.p("../../../../lib/graph/helpers/ui5Framework.js", {
+ "@ui5/logger": ui5Logger,
+ "../../../../lib/ui5Framework/Sapui5Resolver.js": t.context.Sapui5ResolverStub,
+ "../../../../lib/ui5Framework/Sapui5MavenSnapshotResolver.js": t.context.Sapui5MavenSnapshotResolverStub,
+ "../../../../lib/config/Configuration.js": t.context.ConfigurationStub,
+ });
+ t.context.utils = t.context.ui5Framework._utils;
+});
+
+test.afterEach.always((t) => {
+ // Reset UI5_DATA_DIR env
+ if (typeof t.context.originalUi5DataDirEnv === "undefined") {
+ delete process.env.UI5_DATA_DIR;
+ } else {
+ process.env.UI5_DATA_DIR = t.context.originalUi5DataDirEnv;
+ }
+ t.context.sinon.restore();
+ esmock.purge(t.context.ui5Framework);
+});
+
+test.serial("enrichProjectGraph", async (t) => {
+ const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub} = t.context;
+
+ const dependencyTree = {
+ id: "test1",
+ version: "1.0.0",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.75.0"
+ }
+ }
+ };
+
+ const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"];
+ const libraryMetadata = {fake: "metadata"};
+
+ const getFrameworkLibrariesFromGraphStub = sinon.stub(utils, "getFrameworkLibrariesFromGraph")
+ .resolves(referencedLibraries);
+
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+
+
+ const addProjectToGraphStub = sinon.stub();
+ const ProjectProcessorStub = sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+
+ t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once");
+
+ t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
+ t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{
+ cacheMode: undefined,
+ cwd: dependencyTree.path,
+ version: dependencyTree.configuration.framework.version,
+ ui5DataDir: undefined,
+ providedLibraryMetadata: undefined
+ }], "Sapui5Resolver#constructor should be called with expected args");
+
+ t.is(t.context.Sapui5ResolverInstallStub.callCount, 1, "Sapui5Resolver#install should be called once");
+ t.deepEqual(t.context.Sapui5ResolverInstallStub.getCall(0).args, [
+ referencedLibraries
+ ], "Sapui5Resolver#install should be called with expected args");
+
+ t.is(ProjectProcessorStub.callCount, 1, "ProjectProcessor#constructor should be called once");
+ const projectProcessorConstructorArgs = ProjectProcessorStub.getCall(0).args[0];
+ t.deepEqual(projectProcessorConstructorArgs.libraryMetadata, libraryMetadata,
+ "Correct libraryMetadata provided to ProjectProcessor");
+ t.is(projectProcessorConstructorArgs.graph._rootProjectName,
+ "fake-root-of-application.a-framework-dependency-graph",
+ "Correct graph provided to ProjectProcessor");
+ t.falsy(projectProcessorConstructorArgs.workspace,
+ "No workspace provided to ProjectProcessor");
+
+ t.is(addProjectToGraphStub.callCount, 3, "ProjectProcessor#getProject should be called 3 times");
+ t.deepEqual(addProjectToGraphStub.getCall(0).args[0], referencedLibraries[0],
+ "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 1)");
+ t.deepEqual(addProjectToGraphStub.getCall(1).args[0], referencedLibraries[1],
+ "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 2)");
+ t.deepEqual(addProjectToGraphStub.getCall(2).args[0], referencedLibraries[2],
+ "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 3)");
+
+
+ const callbackStub = sinon.stub().resolves();
+ await projectGraph.traverseDepthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 1, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+ t.deepEqual(callbackCalls, [
+ "application.a"
+ ], "Traversed graph in correct order");
+});
+
+test.serial("enrichProjectGraph: without framework configuration", async (t) => {
+ const {ui5Framework, log} = t.context;
+ const dependencyTree = {
+ id: "application.a",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ }
+ };
+
+ t.is(log.verbose.callCount, 0);
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+ t.is(projectGraph.getSize(), 1, "Project graph should remain unchanged");
+ t.is(log.verbose.callCount, 1);
+ t.deepEqual(log.verbose.getCall(0).args, [
+ "Root project application.a has no framework configuration. Nothing to do here"
+ ]);
+});
+
+test.serial("enrichProjectGraph SNAPSHOT", async (t) => {
+ const {sinon, ui5Framework, utils, Sapui5MavenSnapshotResolverInstallStub} = t.context;
+
+ const dependencyTree = {
+ id: "test1",
+ version: "1.0.0",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.75.0-SNAPSHOT"
+ }
+ }
+ };
+
+ const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"];
+ const libraryMetadata = {libraryMetadata: {fake: "metadata"}};
+
+ const getFrameworkLibrariesFromGraphStub = sinon.stub(utils, "getFrameworkLibrariesFromGraph")
+ .resolves(referencedLibraries);
+
+ Sapui5MavenSnapshotResolverInstallStub.resolves(libraryMetadata);
+
+ const addProjectToGraphStub = sinon.stub();
+ const ProjectProcessorStub = sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph, {
+ cacheMode: CacheMode.Force
+ });
+
+ t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once");
+
+ t.is(t.context.Sapui5MavenSnapshotResolverInstallStub.callCount,
+ 1, "Sapui5MavenSnapshotResolverInstallStub#constructor should be called once");
+ t.deepEqual(
+ t.context.Sapui5MavenSnapshotResolverInstallStub.getCall(0).args,
+ [["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"]],
+ "Sapui5MavenSnapshotResolverInstallStub#constructor should be called with expected args"
+ );
+
+ t.is(t.context.Sapui5MavenSnapshotResolverInstallStub.callCount, 1,
+ "Sapui5MavenSnapshotResolverInstallStub#install should be called once");
+ t.deepEqual(t.context.Sapui5MavenSnapshotResolverInstallStub.getCall(0).args, [
+ referencedLibraries
+ ], "Sapui5MavenSnapshotResolverInstallStub#install should be called with expected args");
+
+ t.is(ProjectProcessorStub.callCount, 1, "ProjectProcessor#constructor should be called once");
+ const projectProcessorConstructorArgs = ProjectProcessorStub.getCall(0).args[0];
+ t.deepEqual(projectProcessorConstructorArgs.libraryMetadata, libraryMetadata.libraryMetadata,
+ "Correct libraryMetadata provided to ProjectProcessor");
+ t.is(projectProcessorConstructorArgs.graph._rootProjectName,
+ "fake-root-of-application.a-framework-dependency-graph",
+ "Correct graph provided to ProjectProcessor");
+ t.falsy(projectProcessorConstructorArgs.workspace,
+ "No workspace provided to ProjectProcessor");
+
+ t.is(addProjectToGraphStub.callCount, 3, "ProjectProcessor#getProject should be called 3 times");
+ t.deepEqual(addProjectToGraphStub.getCall(0).args[0], referencedLibraries[0],
+ "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 1)");
+ t.deepEqual(addProjectToGraphStub.getCall(1).args[0], referencedLibraries[1],
+ "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 2)");
+ t.deepEqual(addProjectToGraphStub.getCall(2).args[0], referencedLibraries[2],
+ "ProjectProcessor#addProjectToGraph should be called with expected first arg (call 3)");
+
+
+ const callbackStub = sinon.stub().resolves();
+ await projectGraph.traverseDepthFirst(callbackStub);
+
+ t.is(callbackStub.callCount, 1, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+ t.deepEqual(callbackCalls, [
+ "application.a"
+ ], "Traversed graph in correct order");
+});
+
+test.serial("enrichProjectGraph: With versionOverride", async (t) => {
+ const {
+ sinon, ui5Framework, utils,
+ Sapui5ResolverStub, Sapui5ResolverResolveVersionStub, Sapui5ResolverInstallStub
+ } = t.context;
+
+ const dependencyTree = {
+ id: "test1",
+ version: "1.0.0",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.75.0"
+ }
+ }
+ };
+
+ const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"];
+ const libraryMetadata = {fake: "metadata"};
+
+ sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries);
+
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+
+ Sapui5ResolverResolveVersionStub.resolves("1.99.9");
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride: "1.99"});
+
+ t.is(Sapui5ResolverResolveVersionStub.callCount, 1);
+ t.deepEqual(Sapui5ResolverResolveVersionStub.getCall(0).args, ["1.99", {
+ cwd: dependencyTree.path,
+ ui5DataDir: undefined,
+ }]);
+
+ t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
+ t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
+ cacheMode: undefined,
+ cwd: dependencyTree.path,
+ version: "1.99.9",
+ ui5DataDir: undefined,
+ providedLibraryMetadata: undefined
+ }], "Sapui5Resolver#constructor should be called with expected args");
+});
+
+test.serial("enrichProjectGraph: With versionOverride containing snapshot version", async (t) => {
+ const {
+ sinon, ui5Framework, utils,
+ Sapui5MavenSnapshotResolverStub, Sapui5MavenSnapshotResolverResolveVersionStub,
+ Sapui5MavenSnapshotResolverInstallStub
+ } = t.context;
+
+ const dependencyTree = {
+ id: "test1",
+ version: "1.0.0",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.75.0"
+ }
+ }
+ };
+
+ const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"];
+ const libraryMetadata = {fake: "metadata"};
+
+ sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries);
+
+ Sapui5MavenSnapshotResolverInstallStub.resolves({libraryMetadata});
+
+ Sapui5MavenSnapshotResolverResolveVersionStub.resolves("1.99.9-SNAPSHOT");
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride: "1.99-SNAPSHOT"});
+
+ t.is(Sapui5MavenSnapshotResolverResolveVersionStub.callCount, 1);
+ t.deepEqual(Sapui5MavenSnapshotResolverResolveVersionStub.getCall(0).args, ["1.99-SNAPSHOT", {
+ cwd: dependencyTree.path,
+ ui5DataDir: undefined,
+ }]);
+
+ t.is(Sapui5MavenSnapshotResolverStub.callCount, 1,
+ "Sapui5MavenSnapshotResolverStub#constructor should be called once");
+ t.deepEqual(Sapui5MavenSnapshotResolverStub.getCall(0).args, [{
+ cacheMode: undefined,
+ cwd: dependencyTree.path,
+ version: "1.99.9-SNAPSHOT",
+ ui5DataDir: undefined,
+ providedLibraryMetadata: undefined
+ }], "Sapui5Resolver#constructor should be called with expected args");
+});
+
+test.serial("enrichProjectGraph: With versionOverride containing latest-snapshot", async (t) => {
+ const {
+ sinon, ui5Framework, utils,
+ Sapui5MavenSnapshotResolverStub, Sapui5MavenSnapshotResolverResolveVersionStub,
+ Sapui5MavenSnapshotResolverInstallStub
+ } = t.context;
+
+ const dependencyTree = {
+ id: "test1",
+ version: "1.0.0",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.75.0"
+ }
+ }
+ };
+
+ const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"];
+ const libraryMetadata = {fake: "metadata"};
+
+ sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries);
+
+ Sapui5MavenSnapshotResolverInstallStub.resolves({libraryMetadata});
+
+ Sapui5MavenSnapshotResolverResolveVersionStub.resolves("1.99.9-SNAPSHOT");
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride: "latest-snapshot"});
+
+ t.is(Sapui5MavenSnapshotResolverResolveVersionStub.callCount, 1);
+ t.deepEqual(Sapui5MavenSnapshotResolverResolveVersionStub.getCall(0).args, ["latest-snapshot", {
+ cwd: dependencyTree.path,
+ ui5DataDir: undefined,
+ }]);
+
+ t.is(Sapui5MavenSnapshotResolverStub.callCount, 1,
+ "Sapui5MavenSnapshotResolverStub#constructor should be called once");
+ t.deepEqual(Sapui5MavenSnapshotResolverStub.getCall(0).args, [{
+ cacheMode: undefined,
+ cwd: dependencyTree.path,
+ version: "1.99.9-SNAPSHOT",
+ ui5DataDir: undefined,
+ providedLibraryMetadata: undefined
+ }], "Sapui5Resolver#constructor should be called with expected args");
+});
+
+test.serial("enrichProjectGraph shouldn't throw when no framework version and no libraries are provided", async (t) => {
+ const {ui5Framework, log, Sapui5ResolverResolveVersionStub} = t.context;
+ const dependencyTree = {
+ id: "test-id",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5"
+ }
+ }
+ };
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ // Framework override is fine, even if no framework version is configured
+ await ui5Framework.enrichProjectGraph(projectGraph, {
+ versionOverride: "1.75.0"
+ });
+
+ t.is(Sapui5ResolverResolveVersionStub.callCount, 0,
+ "resolveVersion should not be called when no libraries are provided");
+
+ t.is(log.verbose.callCount, 2);
+ t.deepEqual(log.verbose.getCall(0).args, [
+ "Project application.a has no framework dependencies"
+ ]);
+ t.deepEqual(log.verbose.getCall(1).args, [
+ "No SAPUI5 libraries referenced in project application.a or in any of its dependencies"
+ ]);
+});
+
+test.serial("enrichProjectGraph should skip framework project without version", async (t) => {
+ const {ui5Framework} = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5"
+ }
+ }
+ };
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+ t.is(projectGraph.getSize(), 1, "Project graph should remain unchanged");
+});
+
+test.serial("enrichProjectGraph should resolve framework project with version and framework config", async (t) => {
+ // Framework projects should not specify framework versions, but they might do so in dedicated configuration files
+ // In this case the graph is generated the usual way for the root-project. However, framework projects on
+ // other levels of the graph are ignored
+ const {
+ sinon, ui5Framework, utils,
+ Sapui5ResolverStub, Sapui5ResolverInstallStub
+ } = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.2.3",
+ libraries: [
+ {
+ name: "lib1",
+ optional: true
+ }
+ ]
+ }
+ },
+ dependencies: [{
+ id: "@openui5/test1", // Will not be scanned
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "library.d"
+ },
+ framework: {
+ name: "OpenUI5",
+ libraries: [{
+ name: "lib2"
+ }]
+ }
+ }
+ }, {
+ id: "@openui5/lib1",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib1"
+ },
+ framework: {
+ name: "OpenUI5",
+ libraries: [{
+ name: "lib3"
+ }]
+ }
+ }
+ }]
+ };
+ const referencedLibraries = ["lib1"];
+ const libraryMetadata = {fake: "metadata"};
+
+ const getFrameworkLibrariesFromGraphStub =
+ sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries);
+
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+ t.is(projectGraph.getSize(), 3, "Project graph should remain unchanged");
+
+ t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGrap should be called once");
+ t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
+ t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
+ cacheMode: undefined,
+ cwd: dependencyTree.path,
+ version: "1.2.3",
+ ui5DataDir: undefined,
+ providedLibraryMetadata: undefined
+ }], "Sapui5Resolver#constructor should be called with expected args");
+});
+
+test.serial("enrichProjectGraph should resolve framework project " +
+ "with framework config and version override", async (t) => {
+ // Framework projects should not specify framework versions, but they might do so in dedicated configuration files
+ // In this case the graph is generated the usual way for the root-project. However, framework projects on
+ // other levels of the graph are ignored
+ const {
+ sinon, ui5Framework, utils,
+ Sapui5ResolverStub, Sapui5ResolverResolveVersionStub, Sapui5ResolverInstallStub
+ } = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ libraries: [
+ {
+ name: "lib1",
+ optional: true
+ }
+ ]
+ }
+ },
+ dependencies: [{
+ id: "@openui5/test1", // Will not be scanned
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "library.d"
+ },
+ framework: {
+ name: "OpenUI5",
+ libraries: [{
+ name: "lib2"
+ }]
+ }
+ }
+ }, {
+ id: "@openui5/lib1",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib1"
+ },
+ framework: {
+ name: "OpenUI5",
+ libraries: [{
+ name: "lib3"
+ }]
+ }
+ }
+ }]
+ };
+ const referencedLibraries = ["lib1"];
+ const libraryMetadata = {fake: "metadata"};
+
+ const getFrameworkLibrariesFromGraphStub =
+ sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries);
+
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+ Sapui5ResolverResolveVersionStub.resolves("1.99.9");
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph, {versionOverride: "3.4.5"});
+ t.is(projectGraph.getSize(), 3, "Project graph should remain unchanged");
+
+ t.is(Sapui5ResolverResolveVersionStub.callCount, 1);
+ t.deepEqual(Sapui5ResolverResolveVersionStub.getCall(0).args, ["3.4.5", {
+ cwd: dependencyTree.path,
+ ui5DataDir: undefined,
+ }]);
+
+ t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
+ t.is(getFrameworkLibrariesFromGraphStub.callCount, 1, "getFrameworkLibrariesFromGraph should be called once");
+ t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
+ cacheMode: undefined,
+ cwd: dependencyTree.path,
+ version: "1.99.9",
+ ui5DataDir: undefined,
+ providedLibraryMetadata: undefined
+ }], "Sapui5Resolver#constructor should be called with expected args");
+});
+
+test.serial("enrichProjectGraph should skip framework project when all dependencies are in graph", async (t) => {
+ const {ui5Framework} = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ libraries: [
+ {name: "lib1"}
+ ]
+ }
+ },
+ dependencies: [{
+ id: "@openui5/lib1",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib1"
+ }
+ }
+ }]
+ };
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+ t.is(projectGraph.getSize(), 2, "Project graph should remain unchanged");
+});
+
+test.serial("enrichProjectGraph should throw for framework project with dependency missing in graph", async (t) => {
+ const {ui5Framework, Sapui5ResolverInstallStub} = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ libraries: [
+ {
+ name: "lib1"
+ }
+ ]
+ }
+ }
+ };
+
+ const installError = new Error("Resolution of framework libraries failed with errors: TEST ERROR");
+
+ Sapui5ResolverInstallStub.rejects(installError);
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ const err = await t.throwsAsync(ui5Framework.enrichProjectGraph(projectGraph));
+ t.is(err.message, installError.message);
+});
+
+test.serial("enrichProjectGraph should throw for incorrect framework name", async (t) => {
+ const {ui5Framework, sinon} = t.context;
+ const dependencyTree = {
+ id: "project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.2.3",
+ libraries: [
+ {
+ name: "lib1",
+ optional: true
+ }
+ ]
+ }
+ }
+ };
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ sinon.stub(projectGraph.getRoot(), "getFrameworkName").returns("Pony5");
+ const err = await t.throwsAsync(ui5Framework.enrichProjectGraph(projectGraph));
+ t.is(err.message, `Unknown framework.name "Pony5" for project application.a. Must be "OpenUI5" or "SAPUI5"`,
+ "Threw with expected error message");
+});
+
+test.serial("enrichProjectGraph should ignore root project without framework configuration", async (t) => {
+ const {ui5Framework} = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ }
+ }
+ };
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+ t.is(projectGraph.getSize(), 1, "Project graph should remain unchanged");
+});
+
+test.serial("enrichProjectGraph should throw error when projectGraph contains a framework library project " +
+"that is also defined in framework configuration", async (t) => {
+ const {
+ sinon, ui5Framework, utils,
+ Sapui5ResolverResolveVersionStub, Sapui5ResolverInstallStub
+ } = t.context;
+ const dependencyTree = {
+ id: "application.a",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.100.0",
+ libraries: [{
+ name: "sap.ui.core"
+ }]
+ }
+ },
+ dependencies: [{
+ id: "@openui5/sap.ui.core",
+ version: "1.99.0",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "sap.ui.core"
+ }
+ }
+ }]
+ };
+
+ const referencedLibraries = ["sap.ui.core"];
+ const libraryMetadata = {fake: "metadata"};
+
+ sinon.stub(utils, "getFrameworkLibrariesFromGraph").resolves(referencedLibraries);
+
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+ Sapui5ResolverResolveVersionStub.resolves("1.100.0");
+
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(({graph}) => {
+ return {
+ async addProjectToGraph() {
+ const fakeCoreProject = await Specification.create({
+ id: "@openui5/sap.ui.core",
+ version: "1.100.0",
+ modulePath: libraryEPath,
+ configuration: {
+ specVersion: "3.1",
+ kind: "project",
+ type: "library",
+ metadata: {
+ name: "sap.ui.core"
+ }
+ }
+ });
+ graph.addProject(fakeCoreProject);
+ }
+ };
+ });
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ await t.throwsAsync(ui5Framework.enrichProjectGraph(projectGraph), {
+ message: `Duplicate framework dependency definition(s) found for project application.a: sap.ui.core.\n` +
+ `Framework libraries should only be referenced via ui5.yaml configuration. Neither the root project, ` +
+ `nor any of its dependencies should include them as direct dependencies (e.g. via package.json).`
+ });
+});
+
+test.serial("enrichProjectGraph should use framework library metadata from workspace", async (t) => {
+ const {ui5Framework, utils, Sapui5ResolverStub, Sapui5ResolverInstallStub, sinon} = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.111.1",
+ libraries: [
+ {name: "lib1"},
+ {name: "lib2"}
+ ]
+ }
+ }
+ };
+
+ const workspace = {
+ getName: sinon.stub().resolves("default")
+ };
+
+ const workspaceFrameworkLibraryMetadata = {};
+ const libraryMetadata = {};
+
+ sinon.stub(utils, "getWorkspaceFrameworkLibraryMetadata").resolves(workspaceFrameworkLibraryMetadata);
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ sinon.stub(utils, "declareFrameworkDependenciesInGraph").resolves();
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider, {workspace});
+
+ await ui5Framework.enrichProjectGraph(projectGraph, {workspace});
+
+ t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
+ t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
+ cacheMode: undefined,
+ cwd: dependencyTree.path,
+ version: "1.111.1",
+ ui5DataDir: undefined,
+ providedLibraryMetadata: workspaceFrameworkLibraryMetadata
+ }], "Sapui5Resolver#constructor should be called with expected args");
+ t.is(Sapui5ResolverStub.getCall(0).args[0].providedLibraryMetadata, workspaceFrameworkLibraryMetadata);
+});
+
+test.serial("enrichProjectGraph should allow omitting framework version in case " +
+ "all framework libraries come from the workspace", async (t) => {
+ const {ui5Framework, utils, Sapui5ResolverStub, Sapui5ResolverInstallStub, sinon} = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ libraries: [
+ {name: "lib1"},
+ {name: "lib2"}
+ ]
+ }
+ }
+ };
+
+ const workspace = {
+ getName: sinon.stub().resolves("default")
+ };
+
+ const workspaceFrameworkLibraryMetadata = {};
+ const libraryMetadata = {};
+
+ sinon.stub(utils, "getWorkspaceFrameworkLibraryMetadata").resolves(workspaceFrameworkLibraryMetadata);
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ sinon.stub(utils, "declareFrameworkDependenciesInGraph").resolves();
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider, {workspace});
+
+ await ui5Framework.enrichProjectGraph(projectGraph, {workspace});
+
+ t.is(Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
+ t.deepEqual(Sapui5ResolverStub.getCall(0).args, [{
+ cacheMode: undefined,
+ cwd: dependencyTree.path,
+ ui5DataDir: undefined,
+ version: undefined,
+ providedLibraryMetadata: workspaceFrameworkLibraryMetadata
+ }], "Sapui5Resolver#constructor should be called with expected args");
+ t.is(Sapui5ResolverStub.getCall(0).args[0].providedLibraryMetadata, workspaceFrameworkLibraryMetadata);
+});
+
+test.serial("enrichProjectGraph should use UI5 data dir from env var", async (t) => {
+ const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub} = t.context;
+
+ const dependencyTree = {
+ id: "test1",
+ version: "1.0.0",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.75.0"
+ }
+ }
+ };
+
+ const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"];
+ const libraryMetadata = {fake: "metadata"};
+
+ sinon.stub(utils, "getFrameworkLibrariesFromGraph")
+ .resolves(referencedLibraries);
+
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ process.env.UI5_DATA_DIR = "./ui5-data-dir-from-env-var";
+
+ const expectedUi5DataDir = path.resolve(dependencyTree.path, "./ui5-data-dir-from-env-var");
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+
+ t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
+ t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{
+ cacheMode: undefined,
+ cwd: dependencyTree.path,
+ version: dependencyTree.configuration.framework.version,
+ ui5DataDir: expectedUi5DataDir,
+ providedLibraryMetadata: undefined
+ }], "Sapui5Resolver#constructor should be called with expected args");
+});
+
+test.serial("enrichProjectGraph should use UI5 data dir from configuration", async (t) => {
+ const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub, getUi5DataDirStub} = t.context;
+
+ const dependencyTree = {
+ id: "test1",
+ version: "1.0.0",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.75.0"
+ }
+ }
+ };
+
+ const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"];
+ const libraryMetadata = {fake: "metadata"};
+
+ sinon.stub(utils, "getFrameworkLibrariesFromGraph")
+ .resolves(referencedLibraries);
+
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ getUi5DataDirStub.returns("./ui5-data-dir-from-config");
+
+ const expectedUi5DataDir = path.resolve(dependencyTree.path, "./ui5-data-dir-from-config");
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+
+ t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
+ t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{
+ cacheMode: undefined,
+ cwd: dependencyTree.path,
+ version: dependencyTree.configuration.framework.version,
+ ui5DataDir: expectedUi5DataDir,
+ providedLibraryMetadata: undefined
+ }], "Sapui5Resolver#constructor should be called with expected args");
+});
+
+test.serial("enrichProjectGraph should use absolute UI5 data dir from configuration", async (t) => {
+ const {sinon, ui5Framework, utils, Sapui5ResolverInstallStub, getUi5DataDirStub} = t.context;
+
+ const dependencyTree = {
+ id: "test1",
+ version: "1.0.0",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.75.0"
+ }
+ }
+ };
+
+ const referencedLibraries = ["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib3"];
+ const libraryMetadata = {fake: "metadata"};
+
+ sinon.stub(utils, "getFrameworkLibrariesFromGraph")
+ .resolves(referencedLibraries);
+
+ Sapui5ResolverInstallStub.resolves({libraryMetadata});
+
+
+ const addProjectToGraphStub = sinon.stub();
+ sinon.stub(utils, "ProjectProcessor")
+ .callsFake(() => {
+ return {
+ addProjectToGraph: addProjectToGraphStub
+ };
+ });
+
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ getUi5DataDirStub.returns("/absolute-ui5-data-dir-from-config");
+
+ const expectedUi5DataDir = path.resolve("/absolute-ui5-data-dir-from-config");
+
+ await ui5Framework.enrichProjectGraph(projectGraph);
+
+ t.is(t.context.Sapui5ResolverStub.callCount, 1, "Sapui5Resolver#constructor should be called once");
+ t.deepEqual(t.context.Sapui5ResolverStub.getCall(0).args, [{
+ cacheMode: undefined,
+ cwd: dependencyTree.path,
+ version: dependencyTree.configuration.framework.version,
+ ui5DataDir: expectedUi5DataDir,
+ providedLibraryMetadata: undefined
+ }], "Sapui5Resolver#constructor should be called with expected args");
+});
+
+test.serial("utils.shouldIncludeDependency", (t) => {
+ const {utils} = t.context;
+ // root project dependency should always be included
+ t.true(utils.shouldIncludeDependency({}, true));
+ t.true(utils.shouldIncludeDependency({optional: true}, true));
+ t.true(utils.shouldIncludeDependency({optional: false}, true));
+ t.true(utils.shouldIncludeDependency({optional: null}, true));
+ t.true(utils.shouldIncludeDependency({optional: "abc"}, true));
+ t.true(utils.shouldIncludeDependency({development: true}, true));
+ t.true(utils.shouldIncludeDependency({development: false}, true));
+ t.true(utils.shouldIncludeDependency({development: null}, true));
+ t.true(utils.shouldIncludeDependency({development: "abc"}, true));
+ t.true(utils.shouldIncludeDependency({foo: true}, true));
+
+ t.true(utils.shouldIncludeDependency({}, false));
+ t.false(utils.shouldIncludeDependency({optional: true}, false));
+ t.true(utils.shouldIncludeDependency({optional: false}, false));
+ t.true(utils.shouldIncludeDependency({optional: null}, false));
+ t.true(utils.shouldIncludeDependency({optional: "abc"}, false));
+ t.false(utils.shouldIncludeDependency({development: true}, false));
+ t.true(utils.shouldIncludeDependency({development: false}, false));
+ t.true(utils.shouldIncludeDependency({development: null}, false));
+ t.true(utils.shouldIncludeDependency({development: "abc"}, false));
+ t.true(utils.shouldIncludeDependency({foo: true}, false));
+
+ // Having both optional and development should not be the case, but that should be validated beforehand
+ t.true(utils.shouldIncludeDependency({optional: true, development: true}, true));
+ t.false(utils.shouldIncludeDependency({optional: true, development: true}, false));
+});
+
+test.serial("utils.getFrameworkLibrariesFromTree: Project without dependencies", async (t) => {
+ const {utils} = t.context;
+ const dependencyTree = {
+ id: "test-id",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.100.0",
+ libraries: []
+ }
+ }
+ };
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ const ui5Dependencies = await utils.getFrameworkLibrariesFromGraph(projectGraph);
+ t.deepEqual(ui5Dependencies, []);
+});
+
+test.serial("utils.getFrameworkLibrariesFromTree: Framework project with framework dependency", async (t) => {
+ // Only root-level framework projects are scanned
+ const {utils} = t.context;
+ const dependencyTree = {
+ id: "@sapui5/project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.100.0",
+ libraries: [
+ {
+ name: "lib1"
+ }
+ ]
+ }
+ },
+ dependencies: [{
+ id: "@openui5/test1", // Will not be scanned
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "library.d"
+ },
+ framework: {
+ name: "OpenUI5",
+ libraries: [{
+ name: "lib2"
+ }]
+ }
+ }
+ }]
+ };
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ const ui5Dependencies = await utils.getFrameworkLibrariesFromGraph(projectGraph);
+ t.deepEqual(ui5Dependencies, ["lib1"]);
+});
+
+test.serial("utils.getFrameworkLibrariesFromTree: Project with libraries and dependency with libraries", async (t) => {
+ const {utils} = t.context;
+ const dependencyTree = {
+ id: "test-project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.100.0",
+ libraries: [{
+ name: "lib1"
+ }, {
+ name: "lib2",
+ optional: true
+ }, {
+ name: "lib6",
+ development: true
+ }]
+ }
+ },
+ dependencies: [{
+ id: "test2",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "test2"
+ },
+ framework: {
+ name: "OpenUI5",
+ libraries: [{
+ name: "lib3"
+ }, {
+ name: "lib4",
+ optional: true
+ }]
+ }
+ },
+ dependencies: [{
+ id: "test3",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "test3"
+ },
+ framework: {
+ name: "OpenUI5",
+ libraries: [{
+ name: "lib5"
+ }, {
+ name: "lib7",
+ optional: true
+ }]
+ }
+ }
+ }]
+ }, {
+ id: "@openui5/lib8",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib8"
+ },
+ framework: {
+ name: "OpenUI5",
+ libraries: [{
+ name: "should.be.ignored"
+ }]
+ }
+ }
+ }, {
+ id: "@openui5/lib9",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib9"
+ }
+ }
+ }]
+ };
+ const provider = new DependencyTreeProvider({dependencyTree});
+ const projectGraph = await projectGraphBuilder(provider);
+
+ const ui5Dependencies = await utils.getFrameworkLibrariesFromGraph(projectGraph);
+ t.deepEqual(ui5Dependencies, ["lib1", "lib2", "lib6", "lib3", "lib5"]);
+});
+
+test.serial("utils.declareFrameworkDependenciesInGraph", async (t) => {
+ const {utils, sinon, log} = t.context;
+ const projectTree = {
+ id: "test-project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "application.a"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.100.0",
+ libraries: [{
+ name: "lib1"
+ }, {
+ name: "lib2",
+ optional: true
+ }, {
+ name: "lib3",
+ development: true
+ }, {
+ name: "lib5",
+ optional: true
+ }]
+ }
+ },
+ dependencies: [{
+ id: "library.a",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "library.a"
+ },
+ framework: {
+ name: "OpenUI5",
+ libraries: [{
+ name: "lib2"
+ }, {
+ name: "lib3",
+ optional: true
+ }, {
+ name: "lib4",
+ development: true
+ }, {
+ name: "lib5",
+ optional: true
+ }, {
+ name: "lib6",
+ optional: true
+ }]
+ }
+ },
+ dependencies: []
+ }]
+ };
+ const frameworkTree = {
+ id: "dummy-framework-tree-root",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "dummy-framework-tree-root"
+ }
+ },
+ dependencies: [{
+ id: "@openui5/lib1",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib1",
+ deprecated: true
+ },
+ framework: {
+ name: "OpenUI5",
+ libraries: [{
+ name: "should.be.ignored"
+ }]
+ }
+ }
+ }, {
+ id: "@openui5/lib2",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib2",
+ sapInternal: true
+ }
+ }
+ }, {
+ id: "@openui5/lib3",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib3",
+ deprecated: true
+ }
+ }
+ }, {
+ id: "@openui5/lib5",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib5"
+ }
+ }
+ }]
+ };
+ const projectGraph = await projectGraphBuilder(new DependencyTreeProvider({
+ dependencyTree: projectTree
+ }));
+ const frameworkGraph = await projectGraphBuilder(new DependencyTreeProvider({
+ dependencyTree: frameworkTree
+ }));
+ projectGraph.join(frameworkGraph);
+
+ const declareDependencySpy = sinon.spy(projectGraph, "declareDependency");
+ const declareOptionalDependencySpy = sinon.spy(projectGraph, "declareOptionalDependency");
+ const resolveOptionalDependenciesSpy = sinon.spy(projectGraph, "resolveOptionalDependencies");
+ await utils.declareFrameworkDependenciesInGraph(projectGraph);
+
+ t.is(declareDependencySpy.callCount, 7, "declareDependency got called seven times");
+ t.deepEqual(declareDependencySpy.getCall(0).args, ["application.a", "lib1"],
+ "declareDependency got called with correct arguments on first call");
+ t.deepEqual(declareDependencySpy.getCall(1).args, ["application.a", "lib2"],
+ "declareDependency got called with correct arguments on second call");
+ t.deepEqual(declareDependencySpy.getCall(2).args, ["application.a", "lib3"],
+ "declareDependency got called with correct arguments on third call");
+ t.deepEqual(declareDependencySpy.getCall(3).args, ["application.a", "lib5"],
+ "declareDependency got called with correct arguments on fourth call");
+ t.deepEqual(declareDependencySpy.getCall(4).args, ["library.a", "lib2"],
+ "declareDependency got called with correct arguments on fifth call");
+ t.deepEqual(declareDependencySpy.getCall(5).args, ["library.a", "lib3"],
+ "declareDependency got called with correct arguments on sixth call");
+ t.deepEqual(declareDependencySpy.getCall(6).args, ["library.a", "lib5"],
+ "declareDependency got called with correct arguments on seventh call");
+ t.is(declareOptionalDependencySpy.callCount, 2, "declareOptionalDependency got called ");
+ t.deepEqual(declareOptionalDependencySpy.getCall(0).args, ["library.a", "lib3"],
+ "declareOptionalDependency got called with correct arguments on first call");
+ t.deepEqual(declareOptionalDependencySpy.getCall(1).args, ["library.a", "lib5"],
+ "declareOptionalDependency got called with correct arguments on second call");
+ t.is(resolveOptionalDependenciesSpy.callCount, 1,
+ "resolveOptionalDependenciesSpy got called once");
+
+ t.is(log.warn.callCount, 3,
+ "Three warnings got logged");
+ t.is(log.warn.getCall(0).args[0], "Dependency lib1 is deprecated and should not be used for new projects!",
+ "Expected first warning logged");
+ t.is(log.warn.getCall(1).args[0],
+ `Dependency lib2 is restricted for use by SAP internal projects only! If the project application.a is ` +
+ `an SAP internal project, add the attribute "allowSapInternal: true" to its metadata configuration`,
+ "Expected first warning logged");
+ t.is(log.warn.getCall(2).args[0], "Dependency lib3 is deprecated and should not be used for new projects!",
+ "Expected first warning logged");
+
+ t.deepEqual(projectGraph.getDependencies("application.a"), [
+ "library.a",
+ "lib1",
+ "lib2",
+ "lib3",
+ "lib5"
+ ], `Root project has correct dependencies`);
+
+ t.deepEqual(projectGraph.getDependencies("library.a"), [
+ "lib2",
+ "lib3",
+ "lib5"
+ ], `Non-framework dependency has correct dependencies`);
+});
+
+test.serial("utils.declareFrameworkDependenciesInGraph: No deprecation warnings for testsuite projects", async (t) => {
+ const {utils, log} = t.context;
+
+ const projectTree = {
+ id: "test-project",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "testsuite"
+ },
+ framework: {
+ name: "SAPUI5",
+ version: "1.100.0",
+ libraries: [{
+ name: "lib1"
+ }, {
+ name: "lib2",
+ optional: true
+ }, {
+ name: "lib3",
+ development: true
+ }]
+ }
+ },
+ dependencies: []
+ };
+ const frameworkTree = {
+ id: "dummy-framework-tree-root",
+ version: "1.2.3",
+ path: applicationAPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "application",
+ metadata: {
+ name: "dummy-framework-tree-root"
+ }
+ },
+ dependencies: [{
+ id: "@openui5/lib1",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib1",
+ deprecated: true
+ },
+ framework: {
+ name: "OpenUI5",
+ libraries: [{
+ name: "should.be.ignored"
+ }]
+ }
+ }
+ }, {
+ id: "@openui5/lib2",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib2",
+ sapInternal: true
+ }
+ }
+ }, {
+ id: "@openui5/lib3",
+ version: "1.2.3",
+ path: libraryEPath,
+ configuration: {
+ specVersion: "2.0",
+ type: "library",
+ metadata: {
+ name: "lib3",
+ deprecated: true
+ }
+ }
+ }]
+ };
+ const projectGraph = await projectGraphBuilder(new DependencyTreeProvider({
+ dependencyTree: projectTree
+ }));
+ const frameworkGraph = await projectGraphBuilder(new DependencyTreeProvider({
+ dependencyTree: frameworkTree
+ }));
+ projectGraph.join(frameworkGraph);
+
+ await utils.declareFrameworkDependenciesInGraph(projectGraph);
+
+ t.is(log.warn.callCount, 1,
+ "One warning got logged");
+
+ t.is(log.warn.getCall(0).args[0],
+ `Dependency lib2 is restricted for use by SAP internal projects only! If the project testsuite is ` +
+ `an SAP internal project, add the attribute "allowSapInternal: true" to its metadata configuration`,
+ "Expected first warning logged");
+
+ t.deepEqual(projectGraph.getDependencies("testsuite"), [
+ "lib1",
+ "lib2",
+ "lib3"
+ ], `Root project has correct dependencies`);
+});
+
+test("utils.checkForDuplicateFrameworkProjects: No duplicates", (t) => {
+ const {utils, sinon} = t.context;
+
+ const projectGraph = {
+ getRoot: sinon.stub().returns({
+ getName: sinon.stub().returns("root-project")
+ }),
+ getProjectNames: sinon.stub().returns(["lib1", "lib2", "lib3"])
+ };
+ const frameworkGraph = {
+ getProjectNames: sinon.stub().returns(["sap.ui.core"])
+ };
+
+ t.notThrows(() => utils.checkForDuplicateFrameworkProjects(projectGraph, frameworkGraph));
+});
+
+test("utils.checkForDuplicateFrameworkProjects: One duplicate", (t) => {
+ const {utils, sinon} = t.context;
+
+ const projectGraph = {
+ getRoot: sinon.stub().returns({
+ getName: sinon.stub().returns("root-project")
+ }),
+ getProjectNames: sinon.stub().returns(["lib1", "sap.ui.core", "lib2", "lib3"])
+ };
+ const frameworkGraph = {
+ getProjectNames: sinon.stub().returns(["sap.ui.core"])
+ };
+
+ t.throws(() => utils.checkForDuplicateFrameworkProjects(projectGraph, frameworkGraph), {
+ message: "Duplicate framework dependency definition(s) found for project root-project: " +
+ "sap.ui.core.\n" +
+ "Framework libraries should only be referenced via ui5.yaml configuration. Neither the root project, " +
+ "nor any of its dependencies should include them as direct dependencies (e.g. via package.json)."
+ });
+});
+
+test("utils.checkForDuplicateFrameworkProjects: Two duplicates", (t) => {
+ const {utils, sinon} = t.context;
+
+ const projectGraph = {
+ getRoot: sinon.stub().returns({
+ getName: sinon.stub().returns("root-project")
+ }),
+ getProjectNames: sinon.stub().returns(["lib1", "sap.ui.core", "lib2", "sap.ui.layout", "lib3"])
+ };
+ const frameworkGraph = {
+ getProjectNames: sinon.stub().returns(["sap.ui.core", "sap.ui.layout", "sap.m"])
+ };
+
+ t.throws(() => utils.checkForDuplicateFrameworkProjects(projectGraph, frameworkGraph), {
+ message: "Duplicate framework dependency definition(s) found for project root-project: " +
+ "sap.ui.core, sap.ui.layout.\n" +
+ "Framework libraries should only be referenced via ui5.yaml configuration. Neither the root project, " +
+ "nor any of its dependencies should include them as direct dependencies (e.g. via package.json)."
+ });
+});
+
+test("utils.getFrameworkLibraryDependencies: OpenUI5 library", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const project = {
+ getId: sinon.stub().returns("@openui5/sap.ui.lib1"),
+ getRootReader: sinon.stub().returns({
+ byPath: sinon.stub().withArgs("/package.json").resolves({
+ getString: sinon.stub().resolves(JSON.stringify({
+ dependencies: {
+ "@openui5/sap.ui.lib2": "*"
+ },
+ devDependencies: {
+ "@openui5/themelib_fancy": "*"
+ }
+ }))
+ })
+ }),
+ };
+
+ const result = await utils.getFrameworkLibraryDependencies(project);
+ t.deepEqual(result, {
+ dependencies: ["sap.ui.lib2"],
+ optionalDependencies: ["themelib_fancy"]
+ });
+});
+
+test("utils.getFrameworkLibraryDependencies: SAPUI5 library", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const project = {
+ getId: sinon.stub().returns("@sapui5/sap.ui.lib1"),
+ getFrameworkDependencies: sinon.stub().returns([
+ {
+ name: "sap.ui.lib2"
+ },
+ {
+ name: "themelib_fancy",
+ optional: true
+ },
+ {
+ name: "sap.ui.lib3",
+ development: true
+ }
+ ])
+ };
+
+ const result = await utils.getFrameworkLibraryDependencies(project);
+ t.deepEqual(result, {
+ dependencies: ["sap.ui.lib2"],
+ optionalDependencies: ["themelib_fancy"]
+ });
+});
+
+test("utils.getFrameworkLibraryDependencies: OpenUI5 library - no dependencies", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const project = {
+ getId: sinon.stub().returns("@openui5/sap.ui.lib1"),
+ getRootReader: sinon.stub().returns({
+ byPath: sinon.stub().withArgs("/package.json").resolves({
+ getString: sinon.stub().resolves(JSON.stringify({}))
+ })
+ }),
+ };
+
+ const result = await utils.getFrameworkLibraryDependencies(project);
+ t.deepEqual(result, {
+ dependencies: [],
+ optionalDependencies: []
+ });
+});
+
+test("utils.getFrameworkLibraryDependencies: No framework library", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const project = {
+ getId: sinon.stub().returns("foo")
+ };
+
+ const result = await utils.getFrameworkLibraryDependencies(project);
+ t.deepEqual(result, {
+ dependencies: [],
+ optionalDependencies: []
+ });
+});
+
+test("utils.getWorkspaceFrameworkLibraryMetadata: No workspace modules", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const workspace = {
+ getModules: sinon.stub().resolves([])
+ };
+
+ const libraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({workspace, projectGraph: {}});
+
+ t.deepEqual(libraryMetadata, {});
+});
+
+test("utils.getWorkspaceFrameworkLibraryMetadata: With workspace modules", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const workspace = {
+ getModules: sinon.stub().resolves([
+ {
+ // Extensions don't have projects, should be ignored
+ getSpecifications: sinon.stub().resolves({
+ project: null
+ })
+ },
+ {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ // some types don't have a "isFrameworkProject" method
+ }
+ })
+ },
+ {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ isFrameworkProject: sinon.stub().returns(false)
+ }
+ })
+ },
+ {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ isFrameworkProject: sinon.stub().returns(true),
+ getName: sinon.stub().returns("sap.ui.lib1"),
+ getId: sinon.stub().returns("@openui5/sap.ui.lib1"),
+ getRootPath: sinon.stub().returns("/rootPath"),
+ getRootReader: sinon.stub().returns({
+ byPath: sinon.stub().withArgs("/package.json").resolves({
+ getString: sinon.stub().resolves(JSON.stringify({
+ dependencies: {
+ "@openui5/sap.ui.lib2": "*"
+ },
+ devDependencies: {
+ "@openui5/themelib_fancy": "*"
+ }
+ }))
+ })
+ }),
+ getVersion: sinon.stub().returns("1.0.0"),
+ }
+ })
+ },
+ {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ isFrameworkProject: sinon.stub().returns(true),
+ getName: sinon.stub().returns("sap.ui.lib3"),
+ getId: sinon.stub().returns("@sapui5/sap.ui.lib3"),
+ getRootPath: sinon.stub().returns("/rootPath"),
+ getVersion: sinon.stub().returns("1.0.0"),
+ getFrameworkDependencies: sinon.stub().returns([
+ {
+ name: "sap.ui.lib4"
+ },
+ {
+ name: "sap.ui.lib5",
+ optional: true
+ },
+ {
+ name: "sap.ui.lib6",
+ development: true
+ }
+ ])
+ }
+ })
+ }
+ ])
+ };
+
+ const getProject = sinon.stub();
+ getProject.withArgs("sap.ui.lib1").returns(undefined);
+ const projectGraph = {
+ getProject
+ };
+
+ const libraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({workspace, projectGraph});
+
+ t.deepEqual(libraryMetadata, {
+ "sap.ui.lib1": {
+ dependencies: [
+ "sap.ui.lib2"
+ ],
+ id: "@openui5/sap.ui.lib1",
+ optionalDependencies: [
+ "themelib_fancy"
+ ],
+ path: "/rootPath",
+ version: "1.0.0"
+ },
+ "sap.ui.lib3": {
+ dependencies: [
+ "sap.ui.lib4"
+ ],
+ id: "@sapui5/sap.ui.lib3",
+ optionalDependencies: [
+ "sap.ui.lib5"
+ ],
+ path: "/rootPath",
+ version: "1.0.0"
+ },
+ });
+});
+
+test("utils.getWorkspaceFrameworkLibraryMetadata: With workspace module within projectGraph", async (t) => {
+ const {utils, sinon} = t.context;
+
+ const workspace = {
+ getModules: sinon.stub().resolves([
+ {
+ getSpecifications: sinon.stub().resolves({
+ project: {
+ isFrameworkProject: sinon.stub().returns(true),
+ getName: sinon.stub().returns("sap.ui.lib1")
+ }
+ })
+ }
+ ])
+ };
+
+ const getProject = sinon.stub();
+ getProject.withArgs("sap.ui.lib1").returns({});
+ const projectGraph = {
+ getProject
+ };
+
+ const libraryMetadata = await utils.getWorkspaceFrameworkLibraryMetadata({workspace, projectGraph});
+
+ t.deepEqual(libraryMetadata, {});
+});
+
+test.serial("ProjectProcessor: Add project to graph", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub()
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 1, "graph#addProject got called once");
+ t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e",
+ "graph#addProject got called with the correct project");
+});
+
+test.serial("ProjectProcessor: Add same project twice", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub()
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 1, "graph#addProject got called once");
+ t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e",
+ "graph#addProject got called with the correct project");
+});
+
+test.serial("ProjectProcessor: Project already in graph", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns("project"),
+ addProject: sinon.stub()
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 1, "graph#getProject got called once");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 0, "graph#addProject never got called");
+});
+
+test.serial("ProjectProcessor: Add project with dependencies to graph", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: ["library.d"],
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 2, "graph#addProject got called twice");
+ t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.d",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.e",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once");
+ t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"],
+ "graph#declareDependency got called with the correct arguments");
+});
+
+test.serial("ProjectProcessor: Resolve project via workspace", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const libraryEProjectMock = {
+ getName: () => "library.e",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.d"
+ }])
+ };
+ const libraryDProjectMock = {
+ getName: () => "library.d",
+ getFrameworkDependencies: sinon.stub().returns([])
+ };
+ const moduleMock = {
+ getVersion: () => "1.0.0",
+ getPath: () => path.join("module", "path"),
+ getSpecifications: sinon.stub()
+ .onFirstCall().resolves({
+ project: libraryDProjectMock
+ })
+ .onSecondCall().resolves({
+ project: libraryEProjectMock
+ })
+ };
+ const workspaceMock = {
+ getName: sinon.stub().returns("workspace name"),
+ getModuleByProjectName: sinon.stub().resolves(moduleMock),
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: ["library.d"],
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock,
+ workspace: workspaceMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 2, "graph#addProject got called once");
+ t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.d",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.e",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once");
+ t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"],
+ "graph#declareDependency got called with the correct arguments");
+});
+
+test.serial("ProjectProcessor: Resolve project via workspace with additional dependency", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const libraryEProjectMock = {
+ getName: () => "library.e",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.d"
+ }])
+ };
+ const libraryDProjectMock = {
+ getName: () => "library.d",
+ getFrameworkDependencies: sinon.stub().returns([])
+ };
+ const moduleMock = {
+ getVersion: () => "1.0.0",
+ getPath: () => path.join("module", "path"),
+ getSpecifications: sinon.stub()
+ .onFirstCall().resolves({
+ project: libraryEProjectMock
+ })
+ .onSecondCall().resolves({
+ project: libraryDProjectMock
+ })
+ };
+ const workspaceMock = {
+ getName: sinon.stub().returns("workspace name"),
+ getModuleByProjectName: sinon.stub().resolves(moduleMock),
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: [], // Dependency to library.d is only declared in workspace-resolved library.e
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock,
+ workspace: workspaceMock
+ });
+
+ await projectProcessor.addProjectToGraph("library.e");
+ t.is(graphMock.getProject.callCount, 2, "graph#getProject got called twice");
+ t.is(graphMock.getProject.getCall(0).args[0], "library.e", "graph#getProject got called with the correct argument");
+ t.is(graphMock.getProject.getCall(1).args[0], "library.d", "graph#getProject got called with the correct argument");
+ t.is(graphMock.addProject.callCount, 2, "graph#addProject got called once");
+ t.is(graphMock.addProject.getCall(0).args[0].getName(), "library.e",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.addProject.getCall(1).args[0].getName(), "library.d",
+ "graph#addProject got called with the correct project");
+ t.is(graphMock.declareDependency.callCount, 1, "graph#declareDependency got called once");
+ t.deepEqual(graphMock.declareDependency.getCall(0).args, ["library.e", "library.d"],
+ "graph#declareDependency got called with the correct arguments");
+});
+
+test.serial("ProjectProcessor: Resolve project via workspace with additional, unknown dependency", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const libraryEProjectMock = {
+ getName: () => "library.e",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.xyz"
+ }])
+ };
+ const libraryDProjectMock = {
+ getName: () => "library.d",
+ getFrameworkDependencies: sinon.stub().returns([])
+ };
+ const moduleMock = {
+ getVersion: () => "1.0.0",
+ getPath: () => path.join("module", "path"),
+ getSpecifications: sinon.stub()
+ .onFirstCall().resolves({
+ project: libraryEProjectMock
+ })
+ .onSecondCall().resolves({
+ project: libraryDProjectMock
+ })
+ };
+ const workspaceMock = {
+ getName: sinon.stub().returns("workspace name"),
+ getModuleByProjectName: sinon.stub().resolves(moduleMock),
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: ["library.d"],
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock,
+ workspace: workspaceMock
+ });
+
+ await t.throwsAsync(projectProcessor.addProjectToGraph("library.e"), {
+ message:
+ "Unable to find dependency library.xyz, required by project library.e " +
+ "(resolved via workspace name workspace) " +
+ "in current set of libraries. Try adding it temporarily to the root project's dependencies"
+ }, "Threw with expected error message");
+});
+
+test.serial("ProjectProcessor: Resolve project via workspace with cyclic dependency", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const libraryEProjectMock = {
+ getName: () => "library.e",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.d"
+ }])
+ };
+ const libraryDProjectMock = {
+ getName: () => "library.d",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.e" // Cyclic dependency in workspace project
+ }])
+ };
+ const moduleMock = {
+ getVersion: () => "1.0.0",
+ getPath: () => path.join("module", "path"),
+ getSpecifications: sinon.stub()
+ .onFirstCall().resolves({
+ project: libraryEProjectMock
+ })
+ .onSecondCall().resolves({
+ project: libraryDProjectMock
+ })
+ };
+ const workspaceMock = {
+ getName: sinon.stub().returns("workspace name"),
+ getModuleByProjectName: sinon.stub().resolves(moduleMock),
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: ["library.d"],
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: [],
+ optionalDependencies: []
+ }
+ },
+ graph: graphMock,
+ workspace: workspaceMock
+ });
+
+ await t.throwsAsync(projectProcessor.addProjectToGraph("library.e"), {
+ message:
+ "ui5Framework:ProjectPreprocessor: Detected cyclic dependency chain: " +
+ "library.e -> *library.d* -> *library.d*"
+ }, "Threw with expected error message");
+});
+
+test.serial("ProjectProcessor: Resolve project via workspace with distant cyclic dependency", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub(),
+ declareDependency: sinon.stub()
+ };
+ const libraryEProjectMock = {
+ getName: () => "library.e",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.d"
+ }])
+ };
+ const libraryDProjectMock = {
+ getName: () => "library.d",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.f"
+ }])
+ };
+ const libraryFProjectMock = {
+ getName: () => "library.f",
+ getFrameworkDependencies: sinon.stub().returns([{
+ name: "library.e" // Cyclic dependency in workspace project
+ }])
+ };
+ const moduleMock = {
+ getVersion: () => "1.0.0",
+ getPath: () => path.join("module", "path"),
+ getSpecifications: sinon.stub()
+ .onFirstCall().resolves({
+ project: libraryEProjectMock
+ })
+ .onSecondCall().resolves({
+ project: libraryDProjectMock
+ })
+ .onThirdCall().resolves({
+ project: libraryFProjectMock
+ })
+ };
+ const workspaceMock = {
+ getName: sinon.stub().returns("workspace name"),
+ getModuleByProjectName: sinon.stub().resolves(moduleMock),
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "library.e": {
+ id: "lib.e.id",
+ version: "1000.0.0",
+ path: libraryEPath,
+ dependencies: ["library.d"],
+ optionalDependencies: []
+ },
+ "library.d": {
+ id: "lib.d.id",
+ version: "120000.0.0",
+ path: libraryDPath,
+ dependencies: ["library.f"],
+ optionalDependencies: []
+ },
+ "library.f": {
+ id: "lib.f.id",
+ version: "1.0.0",
+ path: libraryFPath,
+ dependencies: [],
+ optionalDependencies: []
+ },
+ },
+ graph: graphMock,
+ workspace: workspaceMock
+ });
+
+ await t.throwsAsync(projectProcessor.addProjectToGraph("library.e"), {
+ message:
+ "ui5Framework:ProjectPreprocessor: Detected cyclic dependency chain: " +
+ "library.e -> *library.d* -> library.f -> *library.d*"
+ }, "Threw with expected error message");
+});
+
+test.serial("ProjectProcessor: Project missing in metadata", async (t) => {
+ const {sinon} = t.context;
+ const {ProjectProcessor} = t.context.utils;
+ const graphMock = {
+ getProject: sinon.stub().returns(),
+ addProject: sinon.stub()
+ };
+ const projectProcessor = new ProjectProcessor({
+ libraryMetadata: {
+ "lib.x": {}
+ },
+ graph: graphMock
+ });
+
+ await t.throwsAsync(projectProcessor.addProjectToGraph("lib.a"), {
+ message: "Failed to find library lib.a in dist packages metadata.json"
+ }, "Threw with expected error message");
+});
diff --git a/packages/project/test/lib/graph/projectGraphBuilder.js b/packages/project/test/lib/graph/projectGraphBuilder.js
new file mode 100644
index 00000000000..d6bfdd392a0
--- /dev/null
+++ b/packages/project/test/lib/graph/projectGraphBuilder.js
@@ -0,0 +1,967 @@
+import test from "ava";
+import path from "node:path";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import projectGraphBuilder from "../../../lib/graph/projectGraphBuilder.js";
+
+const __dirname = import.meta.dirname;
+
+const libraryEPath = path.join(__dirname, "..", "..", "fixtures", "library.e");
+const libraryFPath = path.join(__dirname, "..", "..", "fixtures", "library.f");
+const libraryGPath = path.join(__dirname, "..", "..", "fixtures", "library.g");
+const collectionPath = path.join(__dirname, "..", "..", "fixtures", "collection");
+const nonExistingPath = path.join(__dirname, "..", "..", "fixtures", "does-not-exist");
+
+function createNode({id, name, version = "1.0.0", modulePath, optional, configuration}) {
+ if (!Array.isArray(configuration)) {
+ configuration = Object.assign({
+ specVersion: "2.6",
+ type: "library",
+ metadata: {
+ name: name || id
+ }
+ }, configuration);
+ }
+ return {
+ id,
+ version,
+ path: modulePath || libraryEPath,
+ optional,
+ configuration
+ };
+}
+
+function traverseBreadthFirst(...args) {
+ return _traverse(...args, true);
+}
+
+async function _traverse(t, graph, expectedOrder, bfs) {
+ if (bfs === undefined) {
+ throw new Error("Test error: Parameter 'bfs' must be specified");
+ }
+ const callbackStub = t.context.sinon.stub().resolves();
+ if (bfs) {
+ await graph.traverseBreadthFirst(callbackStub);
+ } else {
+ await graph.traverseDepthFirst(callbackStub);
+ }
+
+ t.is(callbackStub.callCount, expectedOrder.length, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order");
+}
+
+test.beforeEach((t) => {
+ t.context.sinon = sinonGlobal.createSandbox();
+ t.context.getRootNode = t.context.sinon.stub();
+ t.context.getDependencies = t.context.sinon.stub().resolves([]);
+
+ t.context.provider = {
+ getRootNode: t.context.getRootNode,
+ getDependencies: t.context.getDependencies,
+ };
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test("Basic graph creation", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1"
+ }));
+ const graph = await projectGraphBuilder(t.context.provider);
+
+ await traverseBreadthFirst(t, graph, [
+ "id1"
+ ]);
+
+ const p = graph.getProject("id1");
+ t.is(p.getRootPath(), libraryEPath, "Project returned correct path");
+
+ t.is(t.context.getRootNode.callCount, 1, "NodeProvider#getRoodNode got called once");
+ t.is(t.context.getDependencies.callCount, 1, "NodeProvider#getDependencies got called once");
+});
+
+test("Basic graph with dependencies", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([createNode({
+ id: "id2",
+ name: "project-2"
+ })]);
+ t.context.getDependencies.onSecondCall().resolves([createNode({
+ id: "id3",
+ name: "project-3"
+ })]);
+ const graph = await projectGraphBuilder(t.context.provider);
+
+ await traverseBreadthFirst(t, graph, [
+ "project-1",
+ "project-2",
+ "project-3"
+ ]);
+
+ const p = graph.getProject("project-1");
+ t.is(p.getRootPath(), libraryEPath, "Project returned correct path");
+
+ t.is(t.context.getRootNode.callCount, 1, "NodeProvider#getRoodNode got called once");
+ t.is(t.context.getDependencies.callCount, 3, "NodeProvider#getDependencies got called once");
+});
+
+test.serial("Correct warnings logged", async (t) => {
+ const {sinon, getRootNode, getDependencies, provider} = t.context;
+ const logWarnStub = sinon.stub();
+
+ const projectGraphBuilder = await esmock("../../../lib/graph/projectGraphBuilder.js", {
+ "@ui5/logger": {
+ getLogger: sinon.stub()
+ .withArgs("graph:projectGraphBuilder").returns({
+ warn: logWarnStub,
+ verbose: () => "",
+ silly: () => "",
+ })
+ }
+ });
+
+ getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ const node2 = createNode({
+ id: "id2",
+ name: "project-2"
+ });
+ node2.configuration.metadata.deprecated = true;
+ node2.configuration.metadata.sapInternal = true;
+ getDependencies.onFirstCall().resolves([node2]);
+ const node3 = createNode({
+ id: "id3",
+ name: "project-3"
+ });
+ node3.configuration.metadata.deprecated = true;
+ node3.configuration.metadata.sapInternal = true;
+ getDependencies.onSecondCall().resolves([node3]);
+ const graph = await projectGraphBuilder(provider);
+
+ await traverseBreadthFirst(t, graph, [
+ "project-1",
+ "project-2",
+ "project-3"
+ ]);
+
+ t.is(logWarnStub.callCount, 2, "Two warnings logged");
+ t.is(logWarnStub.getCall(0).args[0], "Dependency project-2 is deprecated and should not be used for new projects!",
+ "Correct deprecation warning logged");
+ t.is(logWarnStub.getCall(1).args[0],
+ `Dependency project-2 is restricted for use by SAP internal projects only! If the project project-1 is an ` +
+ `SAP internal project, add the attribute "allowSapInternal: true" to its metadata configuration`,
+ "Correct SAP-internal project warning logged");
+});
+
+test.serial("No warnings logged", async (t) => {
+ const {sinon, getRootNode, getDependencies} = t.context;
+ const logWarnStub = sinon.stub();
+
+ const projectGraphBuilder = await esmock("../../../lib/graph/projectGraphBuilder.js", {
+ "@ui5/logger": {
+ getLogger: sinon.stub()
+ .withArgs("graph:projectGraphBuilder").returns({
+ warn: logWarnStub,
+ verbose: () => "",
+ silly: () => "",
+ })
+ }
+ });
+
+ const node1 = createNode({
+ id: "id1",
+ name: "testsuite" // "testsuite" name should suppress deprecation warnings
+ });
+ node1.configuration.metadata.allowSapInternal = true;
+ getRootNode.resolves(node1);
+ const node2 = createNode({
+ id: "id2",
+ name: "project-2"
+ });
+ node2.configuration.metadata.deprecated = true;
+ node2.configuration.metadata.sapInternal = true;
+ getDependencies.onFirstCall().resolves([node2]);
+ const node3 = createNode({
+ id: "id3",
+ name: "project-3"
+ });
+ node3.configuration.metadata.deprecated = true;
+ node3.configuration.metadata.sapInternal = true;
+ getDependencies.onSecondCall().resolves([node3]);
+ const graph = await projectGraphBuilder(t.context.provider);
+
+ await traverseBreadthFirst(t, graph, [
+ "testsuite",
+ "project-2",
+ "project-3"
+ ]);
+
+ t.is(logWarnStub.callCount, 0, "No warnings logged");
+});
+
+test("Legacy node with specVersion attribute as root", async (t) => {
+ const node = createNode({
+ id: "id1"
+ });
+ node.specVersion = "1.0";
+ t.context.getRootNode.resolves(node);
+ const err = await t.throwsAsync(projectGraphBuilder(t.context.provider));
+
+ t.is(err.message,
+ "Provided node with ID id1 contains a top-level 'specVersion' property. With UI5 CLI 3.0, " +
+ "project configuration needs to be provided in a dedicated 'configuration' object",
+ "Threw with expected error message");
+});
+
+test("Legacy node with metadata attribute in dependencies", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1"
+ }));
+ const node = createNode({
+ id: "id2"
+ });
+ node.metadata = {name: "id2"};
+ t.context.getDependencies.resolves([node]);
+ const err = await t.throwsAsync(projectGraphBuilder(t.context.provider));
+
+ t.is(err.message,
+ "Provided node with ID id2 contains a top-level 'metadata' property. With UI5 CLI 3.0, " +
+ "project configuration needs to be provided in a dedicated 'configuration' object",
+ "Threw with expected error message");
+});
+
+test("Node depends on itself", async (t) => {
+ const node = createNode({
+ id: "id1",
+ name: "project-1"
+ });
+ t.context.getRootNode.resolves(node);
+ t.context.getDependencies.resolves([node]);
+ const err = await t.throwsAsync(projectGraphBuilder(t.context.provider));
+
+ t.is(err.message,
+ "Failed to declare dependency from project project-1 to project-1: A project can't depend on itself",
+ "Threw with expected error message");
+});
+
+test("Cyclic dependencies", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies
+ .onFirstCall().resolves([
+ createNode({
+ id: "id2",
+ name: "project-2"
+ }),
+ ])
+ .onSecondCall().resolves([
+ createNode({
+ id: "id1",
+ name: "project-1"
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ t.deepEqual(graph.getDependencies("project-1"), ["project-2"], "Cyclic dependency has been added");
+ t.deepEqual(graph.getDependencies("project-2"), ["project-1"], "Cyclic dependency has been added");
+});
+
+test("Nested node with same id is processed correctly", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([
+ createNode({
+ id: "id2",
+ name: "project-2"
+ }),
+ ]);
+ t.context.getDependencies.onSecondCall().resolves([
+ createNode({
+ id: "id1",
+ name: "project-3" // name will be ignored, since the first "id1" node is being used
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ const p = graph.getProject("project-1");
+ t.is(p.getRootPath(), libraryEPath, "Project returned correct path");
+ t.falsy(graph.getProject("project-3"), "Configuration of project with same ID has been ignored");
+ t.deepEqual(graph.getDependencies("project-2"), ["project-1"], "Cyclic dependency has been added");
+});
+
+test("Nested node with different id but same project is processed correctly", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([
+ createNode({
+ id: "id2",
+ name: "project-2",
+ modulePath: libraryFPath
+ }),
+ ]);
+ t.context.getDependencies.onSecondCall().resolves([
+ createNode({
+ id: "id3",
+ name: "project-1", // Project is already in the graph and won't be added again
+ modulePath: libraryGPath
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ const p = graph.getProject("project-1");
+ t.is(p.getRootPath(), libraryEPath, "Project returned correct path");
+ t.deepEqual(graph.getDependencies("project-2"), ["project-1"], "Cyclic dependency has been added");
+});
+
+test("Unresolved optional dependency", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([
+ // Deps of id1
+ createNode({
+ id: "id2",
+ name: "project-2",
+ optional: true
+ }),
+ createNode({
+ id: "id3",
+ name: "project-3"
+ }),
+ ]);
+ t.context.getDependencies.onSecondCall().resolves([
+ // Deps of id2
+ createNode({
+ id: "id4",
+ name: "project-4"
+ }),
+ ]);
+ t.context.getDependencies.onThirdCall().resolves([
+ // Deps of id3
+ createNode({
+ id: "id2",
+ name: "project-2",
+ optional: true
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ const p = graph.getProject("project-1");
+ t.is(p.getRootPath(), libraryEPath, "Project returned correct path");
+ t.deepEqual(graph.getDependencies("project-1"), ["project-3"], "Correct dependencies for project-1");
+ t.deepEqual(graph.getDependencies("project-2"), ["project-4"], "Correct dependencies for project-2");
+ t.deepEqual(graph.getDependencies("project-3"), [], "Correct dependencies for project-3");
+});
+
+test("Nested node with same project resolves optional dependency", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([
+ // Deps of id1
+ createNode({
+ id: "id2",
+ name: "project-2",
+ optional: true
+ }),
+ createNode({
+ id: "id3",
+ name: "project-3"
+ }),
+ ]);
+ t.context.getDependencies.onSecondCall().resolves([
+ // Deps of id2
+ createNode({
+ id: "id4",
+ name: "project-4"
+ }),
+ ]);
+ t.context.getDependencies.onThirdCall().resolves([
+ // Deps of id3
+ createNode({
+ // non-optional dependency to id2/project-2
+ id: "id2",
+ name: "project-2",
+ modulePath: libraryGPath // Different path but same module id should be ignored (first module is reused)
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ const p = graph.getProject("project-1");
+ t.is(p.getRootPath(), libraryEPath, "Project returned correct path");
+ t.deepEqual(graph.getDependencies("project-1"), ["project-3", "project-2"], "Correct dependencies for project-1");
+ t.deepEqual(graph.getDependencies("project-2"), ["project-4"], "Correct dependencies for project-2");
+ t.deepEqual(graph.getDependencies("project-3"), ["project-2"], "Correct dependencies for project-3");
+});
+
+test("Nested node with different id but same project resolves optional dependency", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([
+ // Deps of id1
+ createNode({
+ id: "id2",
+ name: "project-2",
+ optional: true
+ }),
+ createNode({
+ id: "id3",
+ name: "project-3"
+ }),
+ ]);
+ t.context.getDependencies.onSecondCall().resolves([
+ // Deps of id2
+ createNode({
+ id: "id4",
+ name: "project-4"
+ }),
+ ]);
+ t.context.getDependencies.onThirdCall().resolves([
+ // Deps of id3
+ createNode({
+ // non-optional dependency to project-2
+ id: "id5", // Different module but same project should still resolve the optional dependency
+ name: "project-2",
+ modulePath: libraryGPath
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ const p = graph.getProject("project-1");
+ t.is(p.getRootPath(), libraryEPath, "Project returned correct path");
+ t.deepEqual(graph.getDependencies("project-1"), ["project-3", "project-2"], "Correct dependencies for project-1");
+ t.deepEqual(graph.getDependencies("project-2"), ["project-4"], "Correct dependencies for project-2");
+ t.deepEqual(graph.getDependencies("project-3"), ["project-2"], "Correct dependencies for project-3");
+});
+
+test("Root node must provide a project", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1",
+ modulePath: collectionPath,
+ configuration: {
+ kind: "extension",
+ type: "project-shim",
+ shims: {
+ collections: {
+ "id1": {
+ modules: {
+ "library.a": "./library.a",
+ "library.b": "./library.b",
+ "library.c": "./library.c",
+ }
+ }
+ }
+ }
+ }
+ }));
+ const err = await t.throwsAsync(projectGraphBuilder(t.context.provider));
+ t.is(err.message,
+ `Failed to create a UI5 project from module id1 at ${collectionPath}. ` +
+ `Make sure the path is correct and a project configuration is present or supplied.`,
+ "Threw with expected error message");
+});
+
+test("Dependency is a collection", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([
+ createNode({
+ id: "id2",
+ name: "shim-1",
+ modulePath: collectionPath,
+ configuration: {
+ kind: "extension",
+ type: "project-shim",
+ shims: {
+ collections: {
+ "id2": {
+ modules: {
+ "lib.a": "./library.a",
+ "lib.b": "./library.b",
+ "lib.c": "./library.c",
+ }
+ }
+ },
+ dependencies: {
+ "lib.a": ["lib.b"],
+ }
+ }
+ }
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ await traverseBreadthFirst(t, graph, [
+ "project-1",
+ "library.a",
+ "library.b",
+ "library.c"
+ ]);
+ const p = graph.getProject("project-1");
+ t.is(p.getRootPath(), libraryEPath, "Project returned correct path");
+ t.deepEqual(graph.getDependencies("project-1"), [
+ "library.a", "library.b", "library.c"
+ ], "Correct dependencies for root node maintained");
+ t.deepEqual(graph.getDependencies("library.a"), [
+ "library.b"
+ ], "Correct dependencies for library.a maintained");
+});
+
+test("Shim in root defines collection", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ configuration: [{
+ specVersion: "2.6",
+ type: "library",
+ metadata: {
+ name: "project-1"
+ }
+ }, {
+ specVersion: "2.6",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "shim"
+ },
+ shims: {
+ collections: {
+ "id2": {
+ modules: {
+ "lib.a": "./library.a",
+ "lib.b": "./library.b",
+ "lib.c": "./library.c",
+ }
+ }
+ },
+ dependencies: {
+ "lib.a": ["lib.b"],
+ },
+ configurations: {
+ "lib.a": {
+ customConfiguration: {
+ someConfig: true
+ }
+ }
+ }
+ }
+ }]
+ }));
+ t.context.getDependencies.onFirstCall().resolves([
+ createNode({
+ id: "id2",
+ name: "shim-1",
+ modulePath: collectionPath,
+ configuration: []
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ await traverseBreadthFirst(t, graph, [
+ "project-1",
+ "library.a",
+ "library.b",
+ "library.c"
+ ]);
+ const p = graph.getProject("library.a");
+ t.deepEqual(p.getCustomConfiguration(), {
+ someConfig: true
+ }, "Custom configuration from shim has been applied");
+});
+
+test("Project defining a collection shim for itself should be ignored", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1",
+ }));
+ t.context.getDependencies.onFirstCall().resolves([
+ createNode({
+ id: "id2",
+ name: "shim-1",
+ modulePath: collectionPath,
+ configuration: [{
+ specVersion: "2.6",
+ type: "library",
+ metadata: {
+ name: "collection-library" // will be ignored
+ },
+ customConfiguration: {
+ someConfig: true
+ }
+ }, {
+ specVersion: "2.6",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "shim"
+ },
+ shims: {
+ collections: {
+ "id2": {
+ modules: {
+ "lib.a": "./library.a",
+ "lib.b": "./library.b",
+ "lib.c": "./library.c",
+ }
+ }
+ },
+ dependencies: {
+ "lib.a": ["lib.b"],
+ }
+ }
+ }]
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ await traverseBreadthFirst(t, graph, [
+ "project-1",
+ "library.a",
+ "library.b",
+ "library.c"
+ ]);
+ const p = graph.getProject("library.a");
+ t.is(p.getCustomConfiguration(), undefined,
+ "No configuration from collection project has been applied");
+});
+
+test("Dependencies defined through shim", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([
+ createNode({
+ id: "ext1",
+ configuration: {
+ kind: "extension",
+ type: "project-shim",
+ shims: {
+ dependencies: {
+ "id3": ["id2"],
+ }
+ }
+ }
+ }),
+ ]);
+ t.context.getDependencies.onSecondCall().resolves([
+ createNode({
+ id: "id2",
+ name: "project-2"
+ }),
+ ]);
+ t.context.getDependencies.onThirdCall().resolves([
+ createNode({
+ id: "id3",
+ name: "project-3"
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ t.deepEqual(graph.getDependencies("project-3"), ["project-2"], "Shimmed dependency has been defined");
+});
+
+test("Define external dependency as shims in sub-module", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "app",
+ version: "1.0.0",
+ path: "/app"
+ }));
+
+ t.context.getDependencies.onCall(0).resolves([
+ createNode({
+ id: "lib",
+ version: "1.0.0",
+ path: "/lib"
+ }),
+ {
+ id: "external-thirdparty",
+ version: "1.0.0",
+ path: "/app/node_modules/external-thirdparty"
+ },
+ createNode({
+ id: "external-thirdparty-shim",
+ configuration: {
+ kind: "extension",
+ type: "project-shim",
+ shims: {
+ configurations: {
+ "external-thirdparty": {
+ specVersion: "3.1",
+ type: "module",
+ metadata: {name: "external-thirdparty"},
+ resources: {
+ configuration: {
+ paths: {"/resources/": ""},
+ },
+ },
+ },
+ },
+ },
+ }
+ })
+ ]);
+
+ t.context.getDependencies.onCall(1).resolves([
+ createNode({
+ id: "external-thirdparty",
+ version: "1.0.0",
+ path: "/app/node_modules/external-thirdparty",
+ optional: false
+ })
+ ]);
+
+ const graph = await projectGraphBuilder(t.context.provider);
+
+ t.deepEqual(graph.getDependencies("app"), ["lib"], "'app' depends on 'lib'");
+ t.deepEqual(graph.getDependencies("lib"), ["external-thirdparty"], "'lib' depends on 'external-thirdparty'");
+});
+
+test("Extension in dependencies", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([
+ createNode({
+ id: "id2",
+ modulePath: libraryEPath,
+ configuration: {
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task-a"
+ },
+ task: {
+ path: "task-a.js"
+ }
+ }
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ t.truthy(graph.getExtension("task-a"), "Extension has been added to the graph");
+});
+
+test("Extension is an optional dependency of the root project", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([
+ createNode({
+ id: "id2",
+ modulePath: libraryEPath,
+ optional: true,
+ configuration: {
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task-a"
+ },
+ task: {
+ path: "task-a.js"
+ }
+ }
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ t.truthy(graph.getExtension("task-a"), "Extension has been added to the graph");
+});
+
+test("Extension is an optional dependency of a non-root project", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([createNode({
+ id: "id2",
+ name: "project-2"
+ })]);
+ t.context.getDependencies.onSecondCall().resolves([
+ createNode({
+ id: "id3",
+ modulePath: libraryEPath,
+ optional: true,
+ configuration: {
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task-a"
+ },
+ task: {
+ path: "task-a.js"
+ }
+ }
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ t.falsy(graph.getExtension("task-a"), "Extension has not been added to the graph");
+});
+
+test("Extension is an optional dependency of a non-root project and is not available", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([createNode({
+ id: "id2",
+ name: "project-2"
+ })]);
+ t.context.getDependencies.onSecondCall().resolves([
+ createNode({
+ id: "id3",
+ modulePath: nonExistingPath, // Module is not installed (transitive devDependency)
+ optional: true,
+ configuration: []
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ t.falsy(graph.getExtension("task-a"), "Extension has not been added to the graph");
+});
+
+test("Extension is a partially optional dependency of a non-root project", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([createNode({
+ id: "id2",
+ name: "project-2"
+ }), createNode({
+ id: "id3",
+ name: "project-3"
+ })]);
+ t.context.getDependencies.onSecondCall().resolves([
+ // Deps of id2
+ createNode({
+ id: "id4",
+ modulePath: libraryEPath,
+ optional: true,
+ configuration: {
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task-a"
+ },
+ task: {
+ path: "task-a.js"
+ }
+ }
+ }),
+ ]);
+ t.context.getDependencies.onThirdCall().resolves([
+ // Deps of id3
+ createNode({
+ id: "id4", // Will reuse the already visited id4 module
+ optional: false, // Will cause the extension to be added
+ modulePath: libraryEPath
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ t.truthy(graph.getExtension("task-a"), "Extension has been added to the graph");
+});
+
+test("Multiple dependencies to same module containing an extension", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([createNode({
+ id: "id2",
+ name: "project-2"
+ }), createNode({
+ id: "id3",
+ name: "project-3"
+ })]);
+ t.context.getDependencies.onSecondCall().resolves([
+ // Deps of id2
+ createNode({
+ id: "id4",
+ modulePath: libraryEPath,
+ configuration: {
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task-a"
+ },
+ task: {
+ path: "task-a.js"
+ }
+ }
+ }),
+ ]);
+ t.context.getDependencies.onThirdCall().resolves([
+ // Deps of id3
+ createNode({
+ id: "id4", // Will reuse the already visited id4 module
+ modulePath: libraryEPath
+ }),
+ ]);
+ const graph = await projectGraphBuilder(t.context.provider);
+ t.truthy(graph.getExtension("task-a"), "Extension has been added to the graph");
+});
+
+test("Multiple dependencies to different module containing the same extension", async (t) => {
+ t.context.getRootNode.resolves(createNode({
+ id: "id1",
+ name: "project-1"
+ }));
+ t.context.getDependencies.onFirstCall().resolves([createNode({
+ id: "id2",
+ name: "project-2"
+ }), createNode({
+ id: "id3",
+ name: "project-3"
+ })]);
+ t.context.getDependencies.onSecondCall().resolves([
+ // Deps of id2
+ createNode({
+ id: "id4",
+ modulePath: libraryEPath,
+ configuration: {
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task-a"
+ },
+ task: {
+ path: "task-a.js"
+ }
+ }
+ }),
+ ]);
+ t.context.getDependencies.onThirdCall().resolves([
+ // Deps of id3
+ createNode({
+ id: "id5",
+ modulePath: libraryEPath,
+ configuration: {
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task-a"
+ },
+ task: {
+ path: "task-a.js"
+ }
+ }
+ }),
+ ]);
+ await t.throwsAsync(projectGraphBuilder(t.context.provider), {
+ message:
+ "Failed to add extension task-a to graph: An extension with that name has already been added. " +
+ "This might be caused by multiple modules containing extensions with the same name"
+ });
+});
diff --git a/packages/project/test/lib/graph/providers/NodePackageDependencies.integration.js b/packages/project/test/lib/graph/providers/NodePackageDependencies.integration.js
new file mode 100644
index 00000000000..104b6c70402
--- /dev/null
+++ b/packages/project/test/lib/graph/providers/NodePackageDependencies.integration.js
@@ -0,0 +1,272 @@
+import test from "ava";
+import path from "node:path";
+import sinonGlobal from "sinon";
+
+const __dirname = import.meta.dirname;
+
+const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a");
+const applicationAAliasesPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a.aliases");
+const applicationCPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.c");
+const applicationC2Path = path.join(__dirname, "..", "..", "..", "fixtures", "application.c2");
+const applicationC3Path = path.join(__dirname, "..", "..", "..", "fixtures", "application.c3");
+const applicationDPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.d");
+const applicationFPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.f");
+const applicationGPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.g");
+const errApplicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "err.application.a");
+const cycleDepsBasePath = path.join(__dirname, "..", "..", "..", "fixtures", "cyclic-deps", "node_modules");
+const libraryDOverridePath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d-adtl-deps");
+
+import projectGraphBuilder from "../../../../lib/graph/projectGraphBuilder.js";
+import NodePackageDependenciesProvider from "../../../../lib/graph/providers/NodePackageDependencies.js";
+
+test.beforeEach((t) => {
+ t.context.sinon = sinonGlobal.createSandbox();
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+function testGraphCreationBfs(...args) {
+ return _testGraphCreation(true, ...args);
+}
+
+function testGraphCreationDfs(...args) {
+ return _testGraphCreation(false, ...args);
+}
+
+async function _testGraphCreation(bfs, t, npmProvider, expectedOrder, workspace) {
+ if (bfs === undefined) {
+ throw new Error("Test error: Parameter 'bfs' must be specified");
+ }
+ const projectGraph = await projectGraphBuilder(npmProvider, workspace);
+ const callbackStub = t.context.sinon.stub().resolves();
+ if (bfs) {
+ await projectGraph.traverseBreadthFirst(callbackStub);
+ } else {
+ await projectGraph.traverseDepthFirst(callbackStub);
+ }
+
+ t.is(callbackStub.callCount, expectedOrder.length, "Correct number of projects have been visited");
+
+ const callbackCalls = callbackStub.getCalls().map((call) => call.args[0].project.getName());
+
+ t.deepEqual(callbackCalls, expectedOrder, "Traversed graph in correct order");
+ return projectGraph;
+}
+
+test("AppA: project with collection dependency", async (t) => {
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationAPath
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "library.d",
+ "library.a",
+ "library.b",
+ "library.c",
+ "application.a",
+ ]);
+});
+
+test("AppA: project with an alias dependency", async (t) => {
+ const workspace = {
+ getName: () => "workspace name",
+ getModuleByNodeId: t.context.sinon.stub().resolves(undefined).onFirstCall().resolves({
+ getPath: () => path.join(applicationAAliasesPath, "node_modules", "extension.a.esm.alias"),
+ getVersion: () => "1.0.0",
+ })
+ };
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationAAliasesPath
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "extension.a.esm.alias",
+ "application.a.aliases",
+ ], workspace);
+});
+
+test("AppA: project with workspace overrides", async (t) => {
+ const workspace = {
+ getName: () => "workspace name",
+ getModuleByNodeId: t.context.sinon.stub().resolves(undefined).onFirstCall().resolves({
+ // This version of library.d has an additional dependency to library.f,
+ // which in turn has a dependency to library.g
+ getPath: () => libraryDOverridePath,
+ getVersion: () => "1.0.0",
+ })
+ };
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationAPath
+ });
+ const graph = await testGraphCreationDfs(t, npmProvider, [
+ "library.g", // Added through workspace override of library.d
+ "library.a",
+ "library.b",
+ "library.c",
+ "library.f", // Added through workspace override of library.d
+ "library.d",
+ "application.a",
+ ], workspace);
+
+ t.is(workspace.getModuleByNodeId.callCount, 2, "Workspace#getModuleByNodeId got called twice");
+ t.is(workspace.getModuleByNodeId.getCall(0).args[0], "library.d",
+ "Workspace#getModuleByNodeId got called with correct argument on first call");
+ t.is(workspace.getModuleByNodeId.getCall(1).args[0], "collection",
+ "Workspace#getModuleByNodeId got called with correct argument on second call");
+ t.is(graph.getProject("library.d").getVersion(), "2.0.0", "Version from override is used");
+});
+
+test("AppC: project with dependency with optional dependency resolved through root project", async (t) => {
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationCPath
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "library.d",
+ "library.e",
+ "application.c",
+ ]);
+});
+
+test("AppC2: project with dependency with optional dependency resolved through other project", async (t) => {
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationC2Path
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "library.d",
+ "library.e",
+ "library.d-depender",
+ "application.c2"
+ ]);
+});
+
+test("AppC3: project with dependency with optional dependency resolved " +
+ "through other project (but got hoisted)", async (t) => {
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationC3Path
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "library.d",
+ "library.e",
+ "library.d-depender",
+ "application.c3"
+ ]);
+});
+
+test("AppD: project with dependency with unresolved optional dependency", async (t) => {
+ // application.d`s dependency "library.e" has an optional dependency to "library.d"
+ // which is already present in the node_modules directory of library.e
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationDPath
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "library.e",
+ "application.d"
+ ]);
+});
+
+test("AppF: UI5-dependencies in package.json are ignored", async (t) => {
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationFPath
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "library.d",
+ "library.e",
+ "application.f"
+ ]);
+});
+
+test("AppG: project with npm 'optionalDependencies' should not fail if optional dependency cannot be resolved",
+ async (t) => {
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationGPath
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "library.d",
+ "application.g"
+ ]);
+ });
+
+test("AppCycleA: cyclic dev deps", async (t) => {
+ const applicationCycleAPath = path.join(cycleDepsBasePath, "@ui5-internal/application.cycle.a");
+
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationCycleAPath
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "library.cycle.a",
+ "library.cycle.b",
+ "component.cycle.a",
+ "application.cycle.a"
+ ]);
+});
+
+test("AppCycleB: cyclic npm deps - Cycle via devDependency on second level", async (t) => {
+ const applicationCycleBPath = path.join(cycleDepsBasePath, "@ui5-internal/application.cycle.b");
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationCycleBPath
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "module.e",
+ "module.d",
+ "application.cycle.b"
+ ]);
+});
+
+test("AppCycleC: cyclic npm deps - Cycle on third level (one indirection)", async (t) => {
+ const applicationCycleCPath = path.join(cycleDepsBasePath, "@ui5-internal/application.cycle.c");
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationCycleCPath
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "module.f",
+ "module.g",
+ "application.cycle.c"
+ ]);
+ await testGraphCreationBfs(t, npmProvider, [
+ "application.cycle.c",
+ "module.f",
+ "module.g",
+ ]);
+});
+
+test("AppCycleD: cyclic npm deps - Cycles everywhere", async (t) => {
+ const applicationCycleDPath = path.join(cycleDepsBasePath, "@ui5-internal/application.cycle.d");
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationCycleDPath
+ });
+
+ const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, []));
+ t.is(error.message,
+ `Detected cyclic dependency chain: application.cycle.d -> *module.h* -> module.i -> module.k -> *module.h*`);
+});
+
+test("AppCycleE: cyclic npm deps - Cycle via devDependency", async (t) => {
+ const applicationCycleEPath = path.join(cycleDepsBasePath, "@ui5-internal/application.cycle.e");
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: applicationCycleEPath
+ });
+ await testGraphCreationDfs(t, npmProvider, [
+ "module.l",
+ "module.m",
+ "application.cycle.e"
+ ]);
+});
+
+test("Error: missing package.json", async (t) => {
+ const dir = path.parse(__dirname).root;
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: dir
+ });
+ const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, []));
+ t.is(error.message, `Failed to locate package.json for directory ${dir}`);
+});
+
+test("Error: missing dependency", async (t) => {
+ const npmProvider = new NodePackageDependenciesProvider({
+ cwd: errApplicationAPath
+ });
+ const error = await t.throwsAsync(testGraphCreationDfs(t, npmProvider, []));
+ t.is(error.message,
+ `Unable to locate module library.xx via resolve logic: Cannot find module 'library.xx/package.json' from ` +
+ `'${errApplicationAPath}'`);
+});
diff --git a/packages/project/test/lib/graph/providers/NodePackageDependencies.js b/packages/project/test/lib/graph/providers/NodePackageDependencies.js
new file mode 100644
index 00000000000..5a7b2e6554d
--- /dev/null
+++ b/packages/project/test/lib/graph/providers/NodePackageDependencies.js
@@ -0,0 +1,54 @@
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.readPackageUp = sinon.stub();
+
+ t.context.NodePackageDependencies = await esmock("../../../../lib/graph/providers/NodePackageDependencies.js", {
+ "read-package-up": {
+ readPackageUp: t.context.readPackageUp
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test("getRootNode should reject with error when 'name' is empty/missing in package.json", async (t) => {
+ const {NodePackageDependencies, readPackageUp} = t.context;
+
+ const resolver = new NodePackageDependencies({cwd: "cwd"});
+
+ readPackageUp.resolves({
+ path: "/path/to/root/package.json",
+ packageJson: {
+ name: ""
+ }
+ });
+
+ await t.throwsAsync(() => resolver.getRootNode(), {
+ message: "Missing or empty 'name' attribute in package.json at /path/to/root"
+ });
+});
+
+test("getRootNode should reject with error when 'version' is empty/missing in package.json", async (t) => {
+ const {NodePackageDependencies, readPackageUp} = t.context;
+
+ const resolver = new NodePackageDependencies({cwd: "cwd"});
+
+ readPackageUp.resolves({
+ path: "/path/to/root/package.json",
+ packageJson: {
+ name: "test-package-name",
+ version: ""
+ }
+ });
+
+ await t.throwsAsync(() => resolver.getRootNode(), {
+ message: "Missing or empty 'version' attribute in package.json at /path/to/root"
+ });
+});
diff --git a/packages/project/test/lib/package-exports.js b/packages/project/test/lib/package-exports.js
new file mode 100644
index 00000000000..305cdbb04b1
--- /dev/null
+++ b/packages/project/test/lib/package-exports.js
@@ -0,0 +1,50 @@
+import test from "ava";
+import {createRequire} from "node:module";
+
+// Using CommonsJS require since JSON module imports are still experimental
+const require = createRequire(import.meta.url);
+
+// package.json should be exported to allow reading version (e.g. from @ui5/cli)
+test("export of package.json", (t) => {
+ const packageJson = require("@ui5/project/package.json");
+ t.truthy(packageJson.version);
+});
+
+// Check number of definied exports
+test("check number of exports", (t) => {
+ const packageJson = require("@ui5/project/package.json");
+ t.is(Object.keys(packageJson.exports).length, 13);
+});
+
+// Public API contract (exported modules)
+[
+ "config/Configuration",
+ "specifications/Specification",
+ "specifications/SpecificationVersion",
+ "ui5Framework/Openui5Resolver",
+ "ui5Framework/Sapui5Resolver",
+ "ui5Framework/Sapui5MavenSnapshotResolver",
+ "ui5Framework/maven/CacheMode",
+ "validation/validator",
+ "validation/ValidationError",
+ "graph/ProjectGraph",
+ "graph/projectGraphBuilder",
+ {exportedSpecifier: "graph", mappedModule: "../../lib/graph/graph.js"},
+].forEach((v) => {
+ let exportedSpecifier; let mappedModule;
+ if (typeof v === "string") {
+ exportedSpecifier = v;
+ } else {
+ exportedSpecifier = v.exportedSpecifier;
+ mappedModule = v.mappedModule;
+ }
+ if (!mappedModule) {
+ mappedModule = `../../lib/${exportedSpecifier}.js`;
+ }
+ const spec = `@ui5/project/${exportedSpecifier}`;
+ test(`${spec}`, async (t) => {
+ const actual = await import(spec);
+ const expected = await import(mappedModule);
+ t.is(actual, expected, "Correct module exported");
+ });
+});
diff --git a/packages/project/test/lib/specifications/ComponentProject.js b/packages/project/test/lib/specifications/ComponentProject.js
new file mode 100644
index 00000000000..1c662e3b4b5
--- /dev/null
+++ b/packages/project/test/lib/specifications/ComponentProject.js
@@ -0,0 +1,233 @@
+import test from "ava";
+import path from "node:path";
+import sinon from "sinon";
+import Specification from "../../../lib/specifications/Specification.js";
+
+function clone(o) {
+ return JSON.parse(JSON.stringify(o));
+}
+
+const __dirname = import.meta.dirname;
+
+const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a");
+const basicProjectInput = {
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: {
+ specVersion: "2.6",
+ kind: "project",
+ type: "application",
+ metadata: {name: "application.a"}
+ }
+};
+
+test.afterEach.always((t) => {
+ sinon.restore();
+});
+
+test("Default getters", async (t) => {
+ const project = await Specification.create(basicProjectInput);
+ t.is(project.getPropertiesFileSourceEncoding(), "UTF-8",
+ "Returned correct default propertiesFileSourceEncoding configuration");
+ t.is(project.getCopyright(), undefined,
+ "Returned correct default copyright configuration");
+ t.deepEqual(project.getComponentPreloadPaths(), [],
+ "Returned correct default componentPreloadPaths configuration");
+ t.deepEqual(project.getComponentPreloadNamespaces(), [],
+ "Returned correct default componentPreloadNamespaces configuration");
+ t.deepEqual(project.getComponentPreloadExcludes(), [],
+ "Returned correct default componentPreloadExcludes configuration");
+ t.deepEqual(project.getMinificationExcludes(), [],
+ "Returned correct default minificationExcludes configuration");
+ t.deepEqual(project.getBundles(), [],
+ "Returned correct default bundles configuration");
+});
+
+test("getPropertiesFileSourceEncoding", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.resources = {
+ configuration: {
+ propertiesFileSourceEncoding: "ISO-8859-1"
+ }
+ };
+ const project = await Specification.create(customProjectInput);
+ t.is(project.getPropertiesFileSourceEncoding(), "ISO-8859-1",
+ "Returned correct propertiesFileSourceEncoding configuration");
+});
+
+test("getCopyright", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.metadata.copyright = "copyright";
+ const project = await Specification.create(customProjectInput);
+ t.is(project.getCopyright(), "copyright",
+ "Returned correct copyright configuration");
+});
+
+test("getComponentPreloadPaths", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.builder = {
+ componentPreload: {
+ paths: ["paths"]
+ }
+ };
+ const project = await Specification.create(customProjectInput);
+ t.deepEqual(project.getComponentPreloadPaths(), ["paths"],
+ "Returned correct componentPreloadPaths configuration");
+});
+
+test("getComponentPreloadNamespaces", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.builder = {
+ componentPreload: {
+ namespaces: ["namespaces"]
+ }
+ };
+ const project = await Specification.create(customProjectInput);
+ t.deepEqual(project.getComponentPreloadNamespaces(), ["namespaces"],
+ "Returned correct componentPreloadNamespaces configuration");
+});
+
+test("getComponentPreloadExcludes", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.builder = {
+ componentPreload: {
+ excludes: ["excludes"]
+ }
+ };
+ const project = await Specification.create(customProjectInput);
+ t.deepEqual(project.getComponentPreloadExcludes(), ["excludes"],
+ "Returned correct componentPreloadExcludes configuration");
+});
+
+test("getMinificationExcludes", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.builder = {
+ minification: {
+ excludes: ["excludes"]
+ }
+ };
+ const project = await Specification.create(customProjectInput);
+ t.deepEqual(project.getMinificationExcludes(), ["excludes"],
+ "Returned correct minificationExcludes configuration");
+});
+
+test("getBundles", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.builder = {
+ bundles: [{bundleDefinition: {name: "bundle"}}]
+ };
+ const project = await Specification.create(customProjectInput);
+ t.deepEqual(project.getBundles(), [{bundleDefinition: {name: "bundle"}}],
+ "Returned correct bundles configuration");
+});
+
+test("hasMavenPlaceholder: has maven placeholder", async (t) => {
+ const project = await Specification.create(basicProjectInput);
+ const res = project._hasMavenPlaceholder("${mvn-pony}");
+ t.true(res, "String has maven placeholder");
+});
+
+test("hasMavenPlaceholder: has no maven placeholder", async (t) => {
+ const project = await Specification.create(basicProjectInput);
+
+ const res = project._hasMavenPlaceholder("$mvn-pony}");
+ t.false(res, "String has no maven placeholder");
+});
+
+test("_resolveMavenPlaceholder: resolves maven placeholder from first POM level", async (t) => {
+ const project = await Specification.create(basicProjectInput);
+ sinon.stub(project, "_getPom").resolves({
+ project: {
+ properties: {
+ "mvn-pony": "unicorn"
+ }
+ }
+ });
+
+ const res = await project._resolveMavenPlaceholder("${mvn-pony}");
+ t.is(res, "unicorn", "Resolved placeholder correctly");
+});
+
+test("_resolveMavenPlaceholder: resolves maven placeholder from deeper POM level", async (t) => {
+ const project = await Specification.create(basicProjectInput);
+ sinon.stub(project, "_getPom").resolves({
+ "mvn-pony": {
+ some: {
+ id: "unicorn"
+ }
+ }
+ });
+
+ const res = await project._resolveMavenPlaceholder("${mvn-pony.some.id}");
+ t.is(res, "unicorn", "Resolved placeholder correctly");
+});
+
+test("_resolveMavenPlaceholder: can't resolve from POM", async (t) => {
+ const project = await Specification.create(basicProjectInput);
+ sinon.stub(project, "_getPom").resolves({});
+
+ const err = await t.throwsAsync(project._resolveMavenPlaceholder("${mvn-pony}"));
+ t.deepEqual(err.message,
+ `"\${mvn-pony}" couldn't be resolved from maven property "mvn-pony" ` +
+ `of pom.xml of project application.a`,
+ "Rejected with correct error message");
+});
+
+test("_resolveMavenPlaceholder: provided value is no placeholder", async (t) => {
+ const project = await Specification.create(basicProjectInput);
+
+ const err = await t.throwsAsync(project._resolveMavenPlaceholder("My ${mvn-pony}"));
+ t.is(err.message,
+ `"My \${mvn-pony}" is not a maven placeholder`,
+ "Rejected with correct error message");
+});
+
+test("_getPom: reads correctly", async (t) => {
+ const projectInput = clone(basicProjectInput);
+ // Application H contains a pom.xml
+ const applicationHPath = path.join(__dirname, "..", "..", "fixtures", "application.h");
+ projectInput.modulePath = applicationHPath;
+ projectInput.configuration.metadata.name = "application.h";
+ const project = await Specification.create(projectInput);
+
+ const res = await project._getPom();
+ t.is(res.project.modelVersion, "4.0.0", "pom.xml content has been read");
+});
+
+test.serial("_getPom: fs read error", async (t) => {
+ const project = await Specification.create(basicProjectInput);
+ project.getRootReader = () => {
+ return {
+ byPath: async () => {
+ throw new Error("EPON: Pony Error");
+ }
+ };
+ };
+ const error = await t.throwsAsync(project._getPom());
+ t.deepEqual(error.message,
+ "Failed to read pom.xml for project application.a: " +
+ "EPON: Pony Error",
+ "Rejected with correct error message");
+});
+
+test.serial("_getPom: result is cached", async (t) => {
+ const project = await Specification.create(basicProjectInput);
+
+ const byPathStub = sinon.stub().resolves({
+ getString: async () => `no unicorn `
+ });
+
+ project.getRootReader = () => {
+ return {
+ byPath: byPathStub
+ };
+ };
+
+ let res = await project._getPom();
+ t.deepEqual(res, {pony: "no unicorn"}, "Correct result on first call");
+ res = await project._getPom();
+ t.deepEqual(res, {pony: "no unicorn"}, "Correct result on second call");
+
+ t.is(byPathStub.callCount, 1, "getRootReader().byPath got called exactly once (and then cached)");
+});
diff --git a/packages/project/test/lib/specifications/Project.js b/packages/project/test/lib/specifications/Project.js
new file mode 100644
index 00000000000..bb5aec17529
--- /dev/null
+++ b/packages/project/test/lib/specifications/Project.js
@@ -0,0 +1,187 @@
+import test from "ava";
+import path from "node:path";
+import chalk from "chalk";
+import Specification from "../../../lib/specifications/Specification.js";
+
+function clone(obj) {
+ return JSON.parse(JSON.stringify(obj));
+}
+
+const __dirname = import.meta.dirname;
+
+const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a");
+const basicProjectInput = {
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: {
+ specVersion: "2.6",
+ kind: "project",
+ type: "application",
+ metadata: {name: "application.a"}
+ }
+};
+
+test("Invalid configuration", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.resources = {
+ configuration: {
+ propertiesFileSourceEncoding: "Ponycode"
+ }
+ };
+ const error = await t.throwsAsync(Specification.create(customProjectInput));
+ t.is(error.message, `${chalk.red("Invalid ui5.yaml configuration for project application.a.id")}
+
+Configuration \
+${chalk.underline(chalk.red("resources/configuration/propertiesFileSourceEncoding"))} \
+must be equal to one of the allowed values
+Allowed values: UTF-8, ISO-8859-1`, "Threw with validation error");
+});
+
+test("getCustomTasks", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.builder = {
+ customTasks: [{
+ name: "myTask",
+ beforeTask: "minify",
+ configuration: {
+ color: "orange"
+ }
+ }]
+ };
+ const project = await Specification.create(customProjectInput);
+ t.deepEqual(project.getCustomTasks(), [{
+ name: "myTask",
+ beforeTask: "minify",
+ configuration: {
+ color: "orange"
+ }
+ }], "Returned correct custom task configuration");
+});
+
+test("getCustomMiddleware", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.server = {
+ customMiddleware: [{
+ name: "myMiddleware",
+ mountPath: "/app",
+ afterMiddleware: "compression",
+ configuration: {
+ color: "orange"
+ }
+ }]
+ };
+ const project = await Specification.create(customProjectInput);
+ t.deepEqual(project.getCustomMiddleware(), [{
+ name: "myMiddleware",
+ mountPath: "/app",
+ afterMiddleware: "compression",
+ configuration: {
+ color: "orange"
+ }
+ }], "Returned correct custom middleware configuration");
+});
+
+test("getCustomTasks/getCustomMiddleware defaults", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ const project = await Specification.create(customProjectInput);
+ t.deepEqual(project.getCustomTasks(), [],
+ "Returned correct default value for custom task configuration");
+ t.deepEqual(project.getCustomMiddleware(), [],
+ "Returned correct default value for custom middleware configuration");
+});
+
+test("getFramework*: Defaults", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ const project = await Specification.create(customProjectInput);
+ t.is(project.getFrameworkName(), undefined, "Returned correct framework name");
+ t.is(project.getFrameworkVersion(), undefined, "Returned correct framework version");
+ t.deepEqual(project.getFrameworkDependencies(), [], "Returned correct framework dependencies");
+ t.false(project.isFrameworkProject(), "Is not a framework project");
+});
+
+test("getFramework* configurations", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.framework = {
+ name: "OpenUI5",
+ version: "1.111.1",
+ libraries: [
+ {name: "lib-1"},
+ {name: "lib-2"},
+ ]
+ };
+ customProjectInput.id = "@openui5/" + customProjectInput.id;
+ const project = await Specification.create(customProjectInput);
+ t.is(project.getFrameworkName(), "OpenUI5", "Returned correct framework name");
+ t.is(project.getFrameworkVersion(), "1.111.1", "Returned correct framework version");
+ t.deepEqual(project.getFrameworkDependencies(), [
+ {name: "lib-1"},
+ {name: "lib-2"}
+ ], "Returned correct framework dependencies");
+ t.true(project.isFrameworkProject(), "Is a framework project");
+});
+
+test("isFrameworkProject: sapui5", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.id = "@sapui5/" + customProjectInput.id;
+ const project = await Specification.create(customProjectInput);
+ t.true(project.isFrameworkProject(), "Is a framework project");
+});
+
+test("isDeprecated/isSapInternal: Defaults", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+
+ const project = await Specification.create(customProjectInput);
+ t.false(project.isDeprecated(), "Is not deprecated");
+ t.false(project.isSapInternal(), "Is not SAP-internal");
+ t.false(project.getAllowSapInternal(), "Does not allow SAP-internal");
+});
+
+test("isDeprecated/isSapInternal: True", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.metadata.deprecated = true;
+ customProjectInput.configuration.metadata.sapInternal = true;
+ customProjectInput.configuration.metadata.allowSapInternal = true;
+ const project = await Specification.create(customProjectInput);
+ t.true(project.isDeprecated(), "Is deprecated");
+ t.true(project.isSapInternal(), "Is SAP-internal");
+ t.true(project.getAllowSapInternal(), "Does allow SAP-internal");
+});
+
+test("getServerSettings", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.server = {
+ settings: {
+ httpPort: 1337
+ }
+ };
+ const project = await Specification.create(customProjectInput);
+ t.deepEqual(project.getServerSettings(), {
+ httpPort: 1337
+ }, "Returned correct server settings");
+});
+
+test("getBuilderSettings", async (t) => {
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.configuration.builder = {
+ settings: {
+ includeDependency: ["my-lib"]
+ }
+ };
+ const project = await Specification.create(customProjectInput);
+ t.deepEqual(project.getBuilderSettings(), {
+ includeDependency: ["my-lib"]
+ }, "Returned correct build settings");
+});
+
+test("getBuildManifest", async (t) => {
+ const projectWithoutBuildManifest = await Specification.create(clone(basicProjectInput));
+ t.is(projectWithoutBuildManifest.getBuildManifest(), null, "Project has a no build manifest");
+
+ const customProjectInput = clone(basicProjectInput);
+ customProjectInput.buildManifest = "buildManifest";
+ const project = await Specification.create(customProjectInput);
+ t.is(project.getBuildManifest(), "buildManifest", "Returned correct build manifest");
+});
+
+// == Most functionality is tested in the specific types
diff --git a/packages/project/test/lib/specifications/Specification.js b/packages/project/test/lib/specifications/Specification.js
new file mode 100644
index 00000000000..ab449809182
--- /dev/null
+++ b/packages/project/test/lib/specifications/Specification.js
@@ -0,0 +1,428 @@
+import test from "ava";
+import esmock from "esmock";
+import path from "node:path";
+import sinon from "sinon";
+import Specification from "../../../lib/specifications/Specification.js";
+import Application from "../../../lib/specifications/types/Application.js";
+import Library from "../../../lib/specifications/types/Library.js";
+import ThemeLibrary from "../../../lib/specifications/types/ThemeLibrary.js";
+import Module from "../../../lib/specifications/types/Module.js";
+import Task from "../../../lib/specifications/extensions/Task.js";
+import ProjectShim from "../../../lib/specifications/extensions/ProjectShim.js";
+import ServerMiddleware from "../../../lib/specifications/extensions/ServerMiddleware.js";
+
+const __dirname = import.meta.dirname;
+
+const applicationAPath = path.join(__dirname, "..", "..", "fixtures", "application.a");
+const libraryHPath = path.join(__dirname, "..", "..", "fixtures", "library.h");
+const themeLibraryEPath = path.join(__dirname, "..", "..", "fixtures", "theme.library.e");
+const genericExtensionPath = path.join(__dirname, "..", "..", "fixtures", "extension.a");
+const moduleAPath = path.join(__dirname, "..", "..", "fixtures", "module.a");
+
+function createSubclass(Specification) {
+ class MockSpecification extends Specification {
+ getRootPath() {
+ return "path";
+ }
+ getType() {
+ return "type";
+ }
+ getKind() {
+ return "kind";
+ }
+ getName() {
+ return "name";
+ }
+ }
+ return MockSpecification;
+}
+
+test.beforeEach((t) => {
+ t.context.basicProjectInput = {
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "application",
+ metadata: {name: "application.a"}
+ }
+ };
+});
+
+test.afterEach.always((t) => {
+ sinon.restore();
+});
+
+test("Specification can't be instantiated", (t) => {
+ t.throws(() => {
+ new Specification();
+ }, {
+ message: "Class 'Specification' is abstract. Please use one of the 'types' subclasses"
+ });
+});
+
+test("Instantiate a basic project", async (t) => {
+ const project = await Specification.create(t.context.basicProjectInput);
+ t.is(project.getId(), "application.a.id", "Returned correct ID");
+ t.is(project.getName(), "application.a", "Returned correct name");
+ t.is(project.getVersion(), "1.0.0", "Returned correct version");
+ t.is(project.getRootPath(), applicationAPath, "Returned correct project path");
+});
+
+test("init: Missing id", async (t) => {
+ delete t.context.basicProjectInput.id;
+ await t.throwsAsync(Specification.create(t.context.basicProjectInput), {
+ message: "Could not create Specification: Missing or empty parameter 'id'"
+ }, "Threw with expected error message");
+});
+
+test("init: Missing version", async (t) => {
+ delete t.context.basicProjectInput.version;
+ await t.throwsAsync(Specification.create(t.context.basicProjectInput), {
+ message: "Could not create Specification: Missing or empty parameter 'version'"
+ }, "Threw with expected error message");
+});
+
+test("init: Missing modulePath", async (t) => {
+ delete t.context.basicProjectInput.modulePath;
+ await t.throwsAsync(Specification.create(t.context.basicProjectInput), {
+ message: "Could not create Specification: Missing or empty parameter 'modulePath'"
+ }, "Threw with expected error message");
+});
+
+test("init: Missing configuration", async (t) => {
+ delete t.context.basicProjectInput.configuration;
+ const project = new Application();
+
+ await t.throwsAsync(project.init(t.context.basicProjectInput), {
+ message: "Could not create Specification: Missing or empty parameter 'configuration'"
+ }, "Threw with expected error message");
+});
+
+test("init: Invalid constructor name", async (t) => {
+ const MockSpecification = createSubclass(Specification);
+ const project = new MockSpecification();
+
+ await t.throwsAsync(project.init(t.context.basicProjectInput), {
+ message: "Configuration mismatch: Supplied configuration of type 'application' " +
+ "does not match with specification class MockSpecification"
+ }, "Threw with expected error message");
+});
+
+test("Configurations", async (t) => {
+ const project = await Specification.create(t.context.basicProjectInput);
+ t.is(project.getKind(), "project", "Returned correct kind configuration");
+ t.is(project.getType(), "application", "Returned correct type configuration");
+ t.is(project.getSpecVersion().toString(), "2.3", "Returned correct specification version");
+ t.is(project.getSpecVersion().major(), 2,
+ "SpecVersionComparator returned correct major version");
+});
+
+test("Access project root resources via reader", async (t) => {
+ const project = await Specification.create(t.context.basicProjectInput);
+ const rootReader = await project.getRootReader();
+ const packageJsonResource = await rootReader.byPath("/package.json");
+ t.is(packageJsonResource.getPath(), "/package.json", "Successfully retrieved root resource");
+});
+
+test("_dirExists: Directory exists", async (t) => {
+ const project = await Specification.create(t.context.basicProjectInput);
+ const bExists = await project._dirExists("/webapp");
+ t.true(bExists, "directory exists");
+});
+
+test("_dirExists: Missing leading slash", async (t) => {
+ const project = await Specification.create(t.context.basicProjectInput);
+
+ await t.throwsAsync(project._dirExists("webapp"), {
+ message: "Failed to resolve virtual path 'webapp': Path must be absolute"
+ });
+});
+
+test("_dirExists: Trailing slash is ok", async (t) => {
+ const project = await Specification.create(t.context.basicProjectInput);
+ const bExists = await project._dirExists("/webapp/");
+ t.true(bExists, "directory exists");
+});
+
+test("_dirExists: Directory is a file", async (t) => {
+ const project = await Specification.create(t.context.basicProjectInput);
+
+ await t.throwsAsync(project._dirExists("webapp/index.html"), {
+ message: "Failed to resolve virtual path 'webapp/index.html': Path must be absolute"
+ });
+});
+
+test("_dirExists: Directory does not exist", async (t) => {
+ const project = await Specification.create(t.context.basicProjectInput);
+
+ const bExists = await project._dirExists("/w");
+ t.false(bExists, "directory does not exist");
+});
+
+test("Project with incorrect name", async (t) => {
+ const project = await Specification.create({
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "application",
+ metadata: {name: "application a"}
+ }
+ });
+ t.is(project.getName(), "application a", "Returned correct name");
+ t.is(project.getVersion(), "1.0.0", "Returned correct version");
+ t.is(project.getRootPath(), applicationAPath, "Returned correct project path");
+});
+
+test("Migrate legacy project", async (t) => {
+ t.context.basicProjectInput.configuration.specVersion = "1.0";
+ const project = await Specification.create(t.context.basicProjectInput);
+
+ t.is(project.getSpecVersion().toString(), "2.6", "Project got migrated to latest specVersion");
+});
+
+test("Migrate legacy project unexpected configuration", async (t) => {
+ t.context.basicProjectInput.configuration.specVersion = "1.0";
+ t.context.basicProjectInput.configuration.someCustomSetting = "Pineapple";
+ const err = await t.throwsAsync(Specification.create(t.context.basicProjectInput));
+
+ t.is(err.message,
+ "project application.a defines unsupported Specification Version 1.0. Please manually upgrade to 3.0 or " +
+ "higher. For details see https://ui5.github.io/cli/pages/Configuration/#specification-versions - " +
+ "An attempted migration to a supported specification version failed, likely due to unrecognized " +
+ "configuration. Check verbose log for details.",
+ "Threw with expected error message");
+});
+
+test("Migrate legacy module: specVersion 1.0", async (t) => {
+ const project = await Specification.create({
+ id: "my.task",
+ version: "3.4.7-beta",
+ modulePath: genericExtensionPath,
+ configuration: {
+ specVersion: "1.0",
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task-a"
+ },
+ task: {
+ path: "lib/extensionModule.js"
+ }
+ }
+ });
+
+ t.is(project.getSpecVersion().toString(), "2.6", "Project got migrated to latest specVersion");
+});
+
+test("Migrate legacy module: specVersion 0.1", async (t) => {
+ const project = await Specification.create({
+ id: "my.task",
+ version: "3.4.7-beta",
+ modulePath: genericExtensionPath,
+ configuration: {
+ specVersion: "0.1",
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task-a"
+ },
+ task: {
+ path: "lib/extensionModule.js"
+ }
+ }
+ });
+
+ t.is(project.getSpecVersion().toString(), "2.6", "Project got migrated to latest specVersion");
+});
+
+test("Migrate legacy extension", async (t) => {
+ const project = await Specification.create({
+ id: "module.a.id",
+ version: "1.0.0",
+ modulePath: moduleAPath,
+ configuration: {
+ specVersion: "1.1",
+ kind: "project",
+ type: "module",
+ metadata: {
+ name: "module.a",
+ copyright: "Some fancy copyright" // allowed but ignored
+ },
+ resources: {
+ configuration: {
+ paths: {
+ "/": "dist",
+ "/dev/": "dev"
+ }
+ }
+ }
+ }
+ });
+
+ t.is(project.getSpecVersion().toString(), "2.6", "Project got migrated to latest specVersion");
+});
+
+[{
+ kind: "project",
+ type: "application",
+ modulePath: applicationAPath,
+ SpecificationClass: Application
+}, {
+ kind: "project",
+ type: "library",
+ modulePath: libraryHPath,
+ SpecificationClass: Library
+}, {
+ kind: "project",
+ type: "theme-library",
+ modulePath: themeLibraryEPath,
+ SpecificationClass: ThemeLibrary
+}, {
+ kind: "project",
+ type: "module",
+ modulePath: moduleAPath,
+ SpecificationClass: Module
+}, {
+ kind: "extension",
+ type: "task",
+ modulePath: genericExtensionPath,
+ SpecificationClass: Task
+}, {
+ kind: "extension",
+ type: "project-shim",
+ modulePath: genericExtensionPath,
+ SpecificationClass: ProjectShim
+}, {
+ kind: "extension",
+ type: "server-middleware",
+ modulePath: genericExtensionPath,
+ SpecificationClass: ServerMiddleware
+}].forEach(({kind, type, modulePath, SpecificationClass}) => {
+ test(`create: kind '${kind}', type '${type}'`, async (t) => {
+ const additionalConfiguration = {};
+ if (type === "task") {
+ additionalConfiguration.task = {path: "lib/middleware.js"};
+ } else if (type === "server-middleware") {
+ additionalConfiguration.middleware = {path: "lib/middleware.js"};
+ } else if (type === "project-shim") {
+ additionalConfiguration.shims = {};
+ }
+ const project = await Specification.create({
+ id: `${type}.a.id`,
+ version: "1.0.0",
+ modulePath,
+ configuration: {
+ specVersion: "2.6",
+ kind,
+ type,
+ metadata: {
+ name: `${type}.a`
+ },
+ ...additionalConfiguration
+ }
+ });
+ t.true(project instanceof SpecificationClass);
+ });
+});
+
+test("create: Missing configuration", async (t) => {
+ await t.throwsAsync(Specification.create({
+ id: "application.a.id",
+ version: "1.0.0",
+ }), {
+ message: "Unable to create Specification instance: Missing configuration parameter"
+ });
+});
+
+test("create: Unknown kind", async (t) => {
+ await t.throwsAsync(Specification.create({
+ configuration: {
+ kind: "foo",
+ }
+ }), {
+ message: "Unable to create Specification instance: Unknown kind 'foo'"
+ });
+});
+
+test("create: Unknown type", async (t) => {
+ await t.throwsAsync(Specification.create({
+ configuration: {
+ kind: "project",
+ type: "foo"
+ }
+ }), {
+ message: "Unable to create Specification instance: Unknown specification type 'foo'"
+ });
+});
+
+test("Invalid specVersion", async (t) => {
+ t.context.basicProjectInput.configuration.specVersion = "0.5";
+ await t.throwsAsync(Specification.create(t.context.basicProjectInput), {
+ message:
+ "Unsupported Specification Version 0.5 defined. Your UI5 CLI installation might be outdated. " +
+ "For details, see https://ui5.github.io/cli/pages/Configuration/#specification-versions"
+ }, "Threw with expected error message");
+});
+
+test("getRootReader: Default parameters", async (t) => {
+ // Since Specification#create instantiates a far-away subclass, it would be a mess to mock
+ // every class up to "Specification.js" just to stub the resourceFactory's createReader method
+ // Therefore we just come up with our own subclass that can be instantiated right away:
+
+ const createReaderStub = sinon.stub();
+ const Specification = await esmock("../../../lib/specifications/Specification.js", {
+ "@ui5/fs/resourceFactory": {
+ createReader: createReaderStub
+ }
+ });
+
+ const MockSpecification = createSubclass(Specification);
+ const spec = new MockSpecification();
+ await spec.getRootReader();
+
+ t.is(createReaderStub.callCount, 1, "createReader got called once");
+ t.deepEqual(createReaderStub.getCall(0).args[0], {
+ fsBasePath: "path",
+ name: "Root reader for type kind name",
+ useGitignore: true,
+ virBasePath: "/",
+ }, "createReader got called with expected arguments");
+});
+
+test("getRootReader: Custom parameters", async (t) => {
+ const createReaderStub = sinon.stub();
+ const Specification = await esmock("../../../lib/specifications/Specification.js", {
+ "@ui5/fs/resourceFactory": {
+ createReader: createReaderStub
+ }
+ });
+
+ const MockSpecification = createSubclass(Specification);
+ const spec = new MockSpecification();
+ await spec.getRootReader({});
+ await spec.getRootReader({
+ useGitignore: false
+ });
+
+
+ t.is(createReaderStub.callCount, 2, "createReader got called twice");
+ t.deepEqual(createReaderStub.getCall(0).args[0], {
+ fsBasePath: "path",
+ name: "Root reader for type kind name",
+ useGitignore: true,
+ virBasePath: "/",
+ }, "createReader got called with expected arguments on first call");
+
+ t.deepEqual(createReaderStub.getCall(1).args[0], {
+ fsBasePath: "path",
+ name: "Root reader for type kind name",
+ useGitignore: false,
+ virBasePath: "/",
+ }, "createReader got called with expected arguments on second call");
+});
diff --git a/packages/project/test/lib/specifications/SpecificationVersion.js b/packages/project/test/lib/specifications/SpecificationVersion.js
new file mode 100644
index 00000000000..45d90a39fe1
--- /dev/null
+++ b/packages/project/test/lib/specifications/SpecificationVersion.js
@@ -0,0 +1,294 @@
+import test from "ava";
+import sinonGlobal from "sinon";
+import SpecificationVersion from "../../../lib/specifications/SpecificationVersion.js";
+import {__localFunctions__} from "../../../lib/specifications/SpecificationVersion.js";
+
+const unsupportedSpecVersionText = (specVersion) =>
+ `Unsupported Specification Version ${specVersion} defined. Your UI5 CLI installation might be outdated. ` +
+ `For details, see https://ui5.github.io/cli/pages/Configuration/#specification-versions`;
+
+test.beforeEach((t) => {
+ t.context.sinon = sinonGlobal.createSandbox();
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test.serial("Invalid specVersion", (t) => {
+ const {sinon} = t.context;
+ const isSupportedSpecVersionStub =
+ sinon.stub(SpecificationVersion, "isSupportedSpecVersion").returns(false);
+
+ t.throws(() => {
+ new SpecificationVersion("2.5");
+ }, {
+ message: unsupportedSpecVersionText("2.5")
+ }, "Threw with expected error message");
+
+ t.is(isSupportedSpecVersionStub.callCount, 1, "Static isSupportedSpecVersionStub has been called once");
+ t.deepEqual(isSupportedSpecVersionStub.getCall(0).args, ["2.5"],
+ "Static isSupportedSpecVersionStub has been called with expected arguments");
+});
+
+test("(instance) toString", (t) => {
+ t.is(new SpecificationVersion("0.1").toString(), "0.1");
+ t.is(new SpecificationVersion("1.1").toString(), "1.1");
+});
+
+test("(instance) major", (t) => {
+ t.is(new SpecificationVersion("0.1").major(), 0);
+ t.is(new SpecificationVersion("1.1").major(), 1);
+ t.is(new SpecificationVersion("2.1").major(), 2);
+
+ t.is(t.throws(() => {
+ new SpecificationVersion("0.2").major();
+ }).message, unsupportedSpecVersionText("0.2"));
+});
+
+test("(instance) minor", (t) => {
+ t.is(new SpecificationVersion("2.1").minor(), 1);
+ t.is(new SpecificationVersion("2.2").minor(), 2);
+ t.is(new SpecificationVersion("2.3").minor(), 3);
+
+ t.is(t.throws(() => {
+ new SpecificationVersion("1.2").minor();
+ }).message, unsupportedSpecVersionText("1.2"));
+});
+
+test("(instance) satisfies", (t) => {
+ // range: 1.x
+ t.is(new SpecificationVersion("1.0").satisfies("1.x"), true);
+ t.is(new SpecificationVersion("1.1").satisfies("1.x"), true);
+ t.is(new SpecificationVersion("2.0").satisfies("1.x"), false);
+
+ // range: ^2.2
+ t.is(new SpecificationVersion("2.1").satisfies("^2.2"), false);
+ t.is(new SpecificationVersion("2.2").satisfies("^2.2"), true);
+ t.is(new SpecificationVersion("2.3").satisfies("^2.2"), true);
+
+ // range: >=2.2
+ t.is(new SpecificationVersion("2.1").satisfies(">=2.2"), false);
+ t.is(new SpecificationVersion("2.2").satisfies(">=2.2"), true);
+ t.is(new SpecificationVersion("2.3").satisfies(">=2.2"), true);
+ t.is(new SpecificationVersion("3.1").satisfies(">=2.2"), true);
+ t.is(new SpecificationVersion("4.0").satisfies(">=2.2"), true);
+
+ // range: > 1.0
+ t.is(new SpecificationVersion("1.0").satisfies("> 1.0"), false);
+ t.is(new SpecificationVersion("1.1").satisfies("> 1.0"), true);
+ t.is(new SpecificationVersion("2.2").satisfies("> 1.0"), true);
+
+ // range: 2.2 - 2.4
+ t.is(new SpecificationVersion("2.1").satisfies("2.2 - 2.4"), false);
+ t.is(new SpecificationVersion("2.2").satisfies("2.2 - 2.4"), true);
+ t.is(new SpecificationVersion("2.3").satisfies("2.2 - 2.4"), true);
+ t.is(new SpecificationVersion("2.4").satisfies("2.2 - 2.4"), true);
+ t.is(new SpecificationVersion("2.5").satisfies("2.2 - 2.4"), false);
+
+ // range: 0.1 || 1.0 - 1.1 || ^2.5
+ t.is(new SpecificationVersion("0.1").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), true);
+ t.is(new SpecificationVersion("1.0").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), true);
+ t.is(new SpecificationVersion("1.1").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), true);
+ t.is(new SpecificationVersion("2.4").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), false);
+ t.is(new SpecificationVersion("2.5").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), true);
+ t.is(new SpecificationVersion("2.6").satisfies("0.1 || 1.0 - 1.1 || ^2.5"), true);
+
+ // unsupported spec version
+ t.is(t.throws(() => {
+ new SpecificationVersion("0.2").satisfies("1.x");
+ }).message, unsupportedSpecVersionText("0.2"));
+});
+
+test("(instance) low level comparator", (t) => {
+ t.is(new SpecificationVersion("2.1").gt("2.2"), false);
+ t.is(new SpecificationVersion("2.2").gt("2.2"), false);
+ t.is(new SpecificationVersion("2.3").gt("2.2"), true);
+
+ t.is(new SpecificationVersion("2.1").gte("2.2"), false);
+ t.is(new SpecificationVersion("2.2").gte("2.2"), true);
+ t.is(new SpecificationVersion("2.3").gte("2.2"), true);
+
+ t.is(new SpecificationVersion("2.1").lt("2.2"), true);
+ t.is(new SpecificationVersion("2.2").lt("2.2"), false);
+ t.is(new SpecificationVersion("2.3").lt("2.2"), false);
+
+ t.is(new SpecificationVersion("2.1").lte("2.2"), true);
+ t.is(new SpecificationVersion("2.2").lte("2.2"), true);
+ t.is(new SpecificationVersion("2.3").lte("2.2"), false);
+
+ t.is(new SpecificationVersion("2.0").eq("2.2"), false);
+ t.is(new SpecificationVersion("2.2").eq("2.2"), true);
+
+ t.is(new SpecificationVersion("2.0").neq("2.2"), true);
+ t.is(new SpecificationVersion("2.2").neq("2.2"), false);
+});
+
+test("(static) isSupportedSpecVersion", (t) => {
+ t.is(SpecificationVersion.isSupportedSpecVersion("0.1"), true);
+ t.is(SpecificationVersion.isSupportedSpecVersion("1.0"), true);
+ t.is(SpecificationVersion.isSupportedSpecVersion("1.1"), true);
+ t.is(SpecificationVersion.isSupportedSpecVersion("2.0"), true);
+ t.is(SpecificationVersion.isSupportedSpecVersion("2.4"), true);
+ t.is(SpecificationVersion.isSupportedSpecVersion("0.2"), false);
+ t.is(SpecificationVersion.isSupportedSpecVersion("1.2"), false);
+ t.is(SpecificationVersion.isSupportedSpecVersion(1.1), false);
+ t.is(SpecificationVersion.isSupportedSpecVersion("foo"), false);
+ t.is(SpecificationVersion.isSupportedSpecVersion(""), false);
+ t.is(SpecificationVersion.isSupportedSpecVersion(), false);
+});
+
+test("(static) major", (t) => {
+ t.is(SpecificationVersion.major("0.1"), 0);
+ t.is(SpecificationVersion.major("1.1"), 1);
+ t.is(SpecificationVersion.major("2.1"), 2);
+
+ t.is(t.throws(() => {
+ SpecificationVersion.major("0.2");
+ }).message, unsupportedSpecVersionText("0.2"));
+});
+
+test("(static) minor", (t) => {
+ t.is(SpecificationVersion.minor("2.1"), 1);
+ t.is(SpecificationVersion.minor("2.2"), 2);
+ t.is(SpecificationVersion.minor("2.3"), 3);
+
+ t.is(t.throws(() => {
+ SpecificationVersion.minor("1.2");
+ }).message, unsupportedSpecVersionText("1.2"));
+});
+
+test("(static) satisfies", (t) => {
+ // range: 1.x
+ t.is(SpecificationVersion.satisfies("1.0", "1.x"), true);
+ t.is(SpecificationVersion.satisfies("1.1", "1.x"), true);
+ t.is(SpecificationVersion.satisfies("2.0", "1.x"), false);
+
+ // range: ^2.2
+ t.is(SpecificationVersion.satisfies("2.1", "^2.2"), false);
+ t.is(SpecificationVersion.satisfies("2.2", "^2.2"), true);
+ t.is(SpecificationVersion.satisfies("2.3", "^2.2"), true);
+
+ // range: >=2.2
+ t.is(SpecificationVersion.satisfies("2.1", ">=2.2"), false);
+ t.is(SpecificationVersion.satisfies("2.2", ">=2.2"), true);
+ t.is(SpecificationVersion.satisfies("2.3", ">=2.2"), true);
+ t.is(SpecificationVersion.satisfies("3.1", ">=2.2"), true);
+
+ // range: > 1.0
+ t.is(SpecificationVersion.satisfies("1.0", "> 1.0"), false);
+ t.is(SpecificationVersion.satisfies("1.1", "> 1.0"), true);
+ t.is(SpecificationVersion.satisfies("2.2", "> 1.0"), true);
+
+ // range: 2.2 - 2.4
+ t.is(SpecificationVersion.satisfies("2.1", "2.2 - 2.4"), false);
+ t.is(SpecificationVersion.satisfies("2.2", "2.2 - 2.4"), true);
+ t.is(SpecificationVersion.satisfies("2.3", "2.2 - 2.4"), true);
+ t.is(SpecificationVersion.satisfies("2.4", "2.2 - 2.4"), true);
+ t.is(SpecificationVersion.satisfies("2.5", "2.2 - 2.4"), false);
+
+ // range: 0.1 || 1.0 - 1.1 || ^2.5
+ t.is(SpecificationVersion.satisfies("0.1", "0.1 || 1.0 - 1.1 || ^2.5"), true);
+ t.is(SpecificationVersion.satisfies("1.0", "0.1 || 1.0 - 1.1 || ^2.5"), true);
+ t.is(SpecificationVersion.satisfies("1.1", "0.1 || 1.0 - 1.1 || ^2.5"), true);
+ t.is(SpecificationVersion.satisfies("2.4", "0.1 || 1.0 - 1.1 || ^2.5"), false);
+ t.is(SpecificationVersion.satisfies("2.5", "0.1 || 1.0 - 1.1 || ^2.5"), true);
+ t.is(SpecificationVersion.satisfies("2.6", "0.1 || 1.0 - 1.1 || ^2.5"), true);
+
+ // unsupported spec version
+ t.is(t.throws(() => {
+ SpecificationVersion.satisfies("0.2", "1.x");
+ }).message, unsupportedSpecVersionText("0.2"));
+});
+
+test("(static) low level comparator", (t) => {
+ t.is(SpecificationVersion.gt("2.1", "2.2"), false);
+ t.is(SpecificationVersion.gt("2.2", "2.2"), false);
+ t.is(SpecificationVersion.gt("2.3", "2.2"), true);
+
+ t.is(SpecificationVersion.gte("2.1", "2.2"), false);
+ t.is(SpecificationVersion.gte("2.2", "2.2"), true);
+ t.is(SpecificationVersion.gte("2.3", "2.2"), true);
+
+ t.is(SpecificationVersion.lt("2.1", "2.2"), true);
+ t.is(SpecificationVersion.lt("2.2", "2.2"), false);
+ t.is(SpecificationVersion.lt("2.3", "2.2"), false);
+
+ t.is(SpecificationVersion.lte("2.1", "2.2"), true);
+ t.is(SpecificationVersion.lte("2.2", "2.2"), true);
+ t.is(SpecificationVersion.lte("2.3", "2.2"), false);
+
+ t.is(SpecificationVersion.eq("2.0", "2.2"), false);
+ t.is(SpecificationVersion.eq("2.2", "2.2"), true);
+
+ t.is(SpecificationVersion.neq("2.0", "2.2"), true);
+ t.is(SpecificationVersion.neq("2.2", "2.2"), false);
+});
+
+test("(static) getVersionsForRange", (t) => {
+ // range: 1.x
+ t.deepEqual(SpecificationVersion.getVersionsForRange("1.x"), [
+ "1.0", "1.1"
+ ]);
+
+ // range: ^2.2
+ t.deepEqual(SpecificationVersion.getVersionsForRange("^2.2"), [
+ "2.2", "2.3", "2.4", "2.5", "2.6"
+ ]);
+
+ // range: >=2.2
+ t.deepEqual(SpecificationVersion.getVersionsForRange(">=2.2"), [
+ "2.2", "2.3", "2.4", "2.5", "2.6",
+ "3.0", "3.1", "3.2", "4.0"
+ ]);
+
+ // range: > 1.0
+ t.deepEqual(SpecificationVersion.getVersionsForRange("> 1.0"), [
+ "1.1",
+ "2.0", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6",
+ "3.0", "3.1", "3.2", "4.0"
+ ]);
+
+ // range: 2.2 - 2.4
+ t.deepEqual(SpecificationVersion.getVersionsForRange("2.2 - 2.4"), [
+ "2.2", "2.3", "2.4"
+ ]);
+
+ // range: 0.1 || 1.0 - 1.1 || ^2.5
+ t.deepEqual(SpecificationVersion.getVersionsForRange("0.1 || 1.0 - 1.1 || ^2.5"), [
+ "0.1", "1.0", "1.1",
+ "2.5", "2.6"
+ ]);
+
+ // Incorrect range returns empty array
+ t.deepEqual(SpecificationVersion.getVersionsForRange("not a range"), []);
+});
+
+test("getSemverCompatibleVersion", (t) => {
+ t.is(__localFunctions__.getSemverCompatibleVersion("0.1"), "0.1.0");
+ t.is(__localFunctions__.getSemverCompatibleVersion("1.1"), "1.1.0");
+ t.is(__localFunctions__.getSemverCompatibleVersion("2.0"), "2.0.0");
+
+ t.is(t.throws(() => {
+ __localFunctions__.getSemverCompatibleVersion("1.2.3");
+ }).message, unsupportedSpecVersionText("1.2.3"));
+ t.is(t.throws(() => {
+ __localFunctions__.getSemverCompatibleVersion("0.99");
+ }).message, unsupportedSpecVersionText("0.99"));
+ t.is(t.throws(() => {
+ __localFunctions__.getSemverCompatibleVersion("foo");
+ }).message, unsupportedSpecVersionText("foo"));
+ t.is(t.throws(() => {
+ __localFunctions__.getSemverCompatibleVersion();
+ }).message, unsupportedSpecVersionText("undefined"));
+});
+
+test("handleSemverComparator", (t) => {
+ const comparatorStub = t.context.sinon.stub().returns("foobar");
+ t.is(__localFunctions__.handleSemverComparator(comparatorStub, "1.1.0", "2.2"), "foobar");
+ t.deepEqual(comparatorStub.getCall(0).args, ["1.1.0", "2.2.0"]);
+
+ t.is(t.throws(() => {
+ __localFunctions__.handleSemverComparator(undefined, undefined, "a.b");
+ }).message, "Invalid spec version expectation given in comparator: a.b");
+});
diff --git a/packages/project/test/lib/specifications/extensions/ProjectShim.js b/packages/project/test/lib/specifications/extensions/ProjectShim.js
new file mode 100644
index 00000000000..f9afa782b47
--- /dev/null
+++ b/packages/project/test/lib/specifications/extensions/ProjectShim.js
@@ -0,0 +1,89 @@
+import test from "ava";
+import path from "node:path";
+import sinon from "sinon";
+import Specification from "../../../../lib/specifications/Specification.js";
+import ProjectShim from "../../../../lib/specifications/extensions/ProjectShim.js";
+
+function clone(obj) {
+ return JSON.parse(JSON.stringify(obj));
+}
+
+const __dirname = import.meta.dirname;
+
+const nonExistingPath = path.join(__dirname, "..", "..", "..", "fixtures", "does-not-exist");
+const basicProjectShimInput = {
+ id: "shim.a",
+ version: "1.0.0",
+ modulePath: nonExistingPath, // should not matter
+ configuration: {
+ specVersion: "2.6",
+ kind: "extension",
+ type: "project-shim",
+ metadata: {
+ name: "project-shim-a"
+ },
+ shims: {
+ dependencies: {
+ "module.a": ["dependencies"]
+ },
+ configurations: {
+ "module.b": {
+ configuration: "configuration"
+ }
+ },
+ collections: {
+ "module.c": {
+ modules: {
+ "module.x": "some/path"
+ }
+ }
+ }
+ }
+ }
+};
+
+test.afterEach.always((t) => {
+ sinon.restore();
+});
+
+test("Correct class", async (t) => {
+ const extension = await Specification.create(clone(basicProjectShimInput));
+ t.true(extension instanceof ProjectShim, `Is an instance of the ProjectShim class`);
+});
+
+test("Defaults", async (t) => {
+ const projectShimInput = clone(basicProjectShimInput);
+ projectShimInput.configuration.shims = {};
+
+ const extension = await Specification.create(projectShimInput);
+ t.deepEqual(extension.getDependencyShims(), {}, "Returned correct default value for dependencies");
+ t.deepEqual(extension.getConfigurationShims(), {}, "Returned correct default value for configuration");
+ t.deepEqual(extension.getCollectionShims(), {}, "Returned correct default value for collection");
+});
+
+test("getDependencyShims", async (t) => {
+ const extension = await Specification.create(clone(basicProjectShimInput));
+ t.deepEqual(extension.getDependencyShims(), {
+ "module.a": ["dependencies"]
+ }, "Returned correct value for dependencies shim configuration");
+});
+
+test("getConfigurationShims", async (t) => {
+ const extension = await Specification.create(clone(basicProjectShimInput));
+ t.deepEqual(extension.getConfigurationShims(), {
+ "module.b": {
+ configuration: "configuration"
+ }
+ }, "Returned correct value for configuration shim configuration");
+});
+
+test("getCollectionShims", async (t) => {
+ const extension = await Specification.create(clone(basicProjectShimInput));
+ t.deepEqual(extension.getCollectionShims(), {
+ "module.c": {
+ modules: {
+ "module.x": "some/path"
+ }
+ }
+ }, "Returned correct value for collection shim configuration");
+});
diff --git a/packages/project/test/lib/specifications/extensions/ServerMiddleware.js b/packages/project/test/lib/specifications/extensions/ServerMiddleware.js
new file mode 100644
index 00000000000..d6c25d9bcee
--- /dev/null
+++ b/packages/project/test/lib/specifications/extensions/ServerMiddleware.js
@@ -0,0 +1,83 @@
+import test from "ava";
+import path from "node:path";
+import sinon from "sinon";
+import Specification from "../../../../lib/specifications/Specification.js";
+import ServerMiddleware from "../../../../lib/specifications/extensions/ServerMiddleware.js";
+
+function clone(obj) {
+ return JSON.parse(JSON.stringify(obj));
+}
+
+const __dirname = import.meta.dirname;
+
+const genericCjsExtensionPath = path.join(__dirname, "..", "..", "..", "fixtures", "extension.a");
+const genericEsmExtensionPath = path.join(__dirname, "..", "..", "..", "fixtures", "extension.a.esm");
+const basicCjsServerMiddlewareInput = {
+ id: "server.middleware.a",
+ version: "1.0.0",
+ modulePath: genericCjsExtensionPath,
+ configuration: {
+ specVersion: "2.6",
+ kind: "extension",
+ type: "server-middleware",
+ metadata: {
+ name: "middleware-a"
+ },
+ middleware: {
+ path: "lib/extensionModule.js"
+ }
+ }
+};
+const basicEsmServerMiddlewareInput = {
+ id: "server.middleware.a",
+ version: "1.0.0",
+ modulePath: genericEsmExtensionPath,
+ configuration: {
+ specVersion: "2.6",
+ kind: "extension",
+ type: "server-middleware",
+ metadata: {
+ name: "middleware-a"
+ },
+ middleware: {
+ path: "lib/extensionModule.js"
+ }
+ }
+};
+
+test.afterEach.always((t) => {
+ sinon.restore();
+});
+
+test("Correct class (CJS)", async (t) => {
+ const extension = await Specification.create(clone(basicCjsServerMiddlewareInput));
+ t.true(extension instanceof ServerMiddleware, `Is an instance of the ServerMiddleware class`);
+});
+test("Correct class (ESM)", async (t) => {
+ const extension = await Specification.create(clone(basicEsmServerMiddlewareInput));
+ t.true(extension instanceof ServerMiddleware, `Is an instance of the ServerMiddleware class`);
+});
+
+test("getMiddleware (CJS)", async (t) => {
+ const extension = await Specification.create(clone(basicCjsServerMiddlewareInput));
+ const middleware = await extension.getMiddleware();
+ t.is(middleware(), "extension module",
+ "Returned correct module");
+});
+
+test("getMiddleware (ESM)", async (t) => {
+ const extension = await Specification.create(clone(basicEsmServerMiddlewareInput));
+ const middleware = await extension.getMiddleware();
+ t.is(middleware(), "extension module",
+ "Returned correct module");
+});
+
+test("Middleware with illegal suffix", async (t) => {
+ const serverMiddlewareInput = clone(basicCjsServerMiddlewareInput);
+ serverMiddlewareInput.configuration.metadata.name += "--1";
+ const err = await t.throwsAsync(Specification.create(serverMiddlewareInput));
+ t.is(err.message,
+ "Failed to validate configuration of server-middleware extension middleware-a--1: " +
+ "Server middleware name must not end with '--'",
+ "Threw with expected error message");
+});
diff --git a/packages/project/test/lib/specifications/extensions/Task.js b/packages/project/test/lib/specifications/extensions/Task.js
new file mode 100644
index 00000000000..24cf306523a
--- /dev/null
+++ b/packages/project/test/lib/specifications/extensions/Task.js
@@ -0,0 +1,100 @@
+import test from "ava";
+import path from "node:path";
+import sinon from "sinon";
+import Specification from "../../../../lib/specifications/Specification.js";
+import Task from "../../../../lib/specifications/extensions/Task.js";
+
+function clone(obj) {
+ return JSON.parse(JSON.stringify(obj));
+}
+
+const __dirname = import.meta.dirname;
+
+const genericCjsExtensionPath = path.join(__dirname, "..", "..", "..", "fixtures", "extension.a");
+const genericEsmExtensionPath = path.join(__dirname, "..", "..", "..", "fixtures", "extension.a.esm");
+
+const basicCjsTaskInput = {
+ id: "task.a",
+ version: "1.0.0",
+ modulePath: genericCjsExtensionPath,
+ configuration: {
+ specVersion: "2.6",
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task-a"
+ },
+ task: {
+ path: "lib/extensionModule.js"
+ }
+ }
+};
+
+const basicEsmTaskInput = {
+ id: "task.a",
+ version: "1.0.0",
+ modulePath: genericEsmExtensionPath,
+ configuration: {
+ specVersion: "2.6",
+ kind: "extension",
+ type: "task",
+ metadata: {
+ name: "task-a"
+ },
+ task: {
+ path: "lib/extensionModule.js"
+ }
+ }
+};
+
+test.afterEach.always((t) => {
+ sinon.restore();
+});
+
+test("Correct class (CJS)", async (t) => {
+ const extension = await Specification.create(clone(basicCjsTaskInput));
+ t.true(extension instanceof Task, `Is an instance of the Task class`);
+});
+
+test("Correct class (ESM)", async (t) => {
+ const extension = await Specification.create(clone(basicEsmTaskInput));
+ t.true(extension instanceof Task, `Is an instance of the Task class`);
+});
+
+test("getTask (CJS)", async (t) => {
+ const extension = await Specification.create(clone(basicCjsTaskInput));
+ const task = await extension.getTask();
+ t.is(task(), "extension module",
+ "Returned correct module");
+});
+
+test("getTask (ESM)", async (t) => {
+ const extension = await Specification.create(clone(basicEsmTaskInput));
+ const task = await extension.getTask();
+ t.is(task(), "extension module",
+ "Returned correct module");
+});
+
+test("getRequiredDependenciesCallback (CJS)", async (t) => {
+ const extension = await Specification.create(clone(basicCjsTaskInput));
+ const requiredDependenciesCallback = await extension.getRequiredDependenciesCallback();
+ t.is(requiredDependenciesCallback(), "required dependencies function",
+ "Returned correct module");
+});
+
+test("getRequiredDependenciesCallback (ESM)", async (t) => {
+ const extension = await Specification.create(clone(basicEsmTaskInput));
+ const requiredDependenciesCallback = await extension.getRequiredDependenciesCallback();
+ t.is(requiredDependenciesCallback(), "required dependencies function",
+ "Returned correct module");
+});
+
+test("Task with illegal suffix", async (t) => {
+ const TaskInput = clone(basicCjsTaskInput);
+ TaskInput.configuration.metadata.name += "--1";
+ const err = await t.throwsAsync(Specification.create(TaskInput));
+ t.is(err.message,
+ "Failed to validate configuration of task extension task-a--1: " +
+ "Task name must not end with '--'",
+ "Threw with expected error message");
+});
diff --git a/packages/project/test/lib/specifications/types/Application.js b/packages/project/test/lib/specifications/types/Application.js
new file mode 100644
index 00000000000..0a53ae309b0
--- /dev/null
+++ b/packages/project/test/lib/specifications/types/Application.js
@@ -0,0 +1,681 @@
+import test from "ava";
+import path from "node:path";
+import {createResource} from "@ui5/fs/resourceFactory";
+import sinonGlobal from "sinon";
+import Specification from "../../../../lib/specifications/Specification.js";
+import Application from "../../../../lib/specifications/types/Application.js";
+
+const __dirname = import.meta.dirname;
+const applicationAPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.a");
+const applicationHPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.h");
+
+test.beforeEach((t) => {
+ t.context.sinon = sinonGlobal.createSandbox();
+ t.context.projectInput = {
+ id: "application.a.id",
+ version: "1.0.0",
+ modulePath: applicationAPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "application",
+ metadata: {name: "application.a"}
+ }
+ };
+
+ t.context.applicationHInput = {
+ id: "application.h.id",
+ version: "1.0.0",
+ modulePath: applicationHPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "application",
+ metadata: {name: "application.h"},
+ resources: {
+ configuration: {
+ paths: {
+ webapp: "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 Application, `Is an instance of the Application 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(applicationAPath, "webapp"),
+ "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("/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 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"]
+ }
+ };
+ 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/ legacy 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");
+
+ // 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("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("Application 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("/index.html");
+ t.truthy(runtimeReaderResource, "Found the requested resource byPath");
+ t.is(runtimeReaderResource.getPath(), "/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(), "/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._webappPath, "webapp", "Correct default path");
+});
+
+test("_configureAndValidatePaths: Custom webapp directory", async (t) => {
+ const applicationHPath = path.join(__dirname, "..", "..", "..", "fixtures", "application.h");
+ const projectInput = {
+ id: "application.h.id",
+ version: "1.0.0",
+ modulePath: applicationHPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "application",
+ metadata: {name: "application.h"},
+ resources: {
+ configuration: {
+ paths: {
+ webapp: "webapp-properties.componentName"
+ }
+ }
+ }
+ }
+ };
+
+ const project = await Specification.create(projectInput);
+
+ t.is(project._webappPath, "webapp-properties.componentName", "Correct path for src");
+});
+
+test("_configureAndValidatePaths: Webapp directory does not exist", async (t) => {
+ const {projectInput} = t.context;
+ projectInput.configuration.resources = {
+ configuration: {
+ paths: {
+ webapp: "does/not/exist"
+ }
+ }
+ };
+ const err = await t.throwsAsync(Specification.create(projectInput));
+
+ t.is(err.message, "Unable to find source directory 'does/not/exist' in application project application.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 application.a",
+ "Rejected with correct error message");
+});
+
+test("_getNamespaceFromManifestJson: No application 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 application.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 application.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 application.a: " +
+ "No such stable or directory: manifest.json" +
+ "\n\n" +
+ "If you are about to start a new project, please refer to:\n" +
+ "https://ui5.github.io/cli/v4/pages/GettingStarted/#starting-a-new-project",
+ "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 application\.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.is(error.message,
+ "Could not find resource /does-not-exist.json in project application.a",
+ "Rejected with correct error message");
+ t.is(error.code, "ENOENT");
+});
+
+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 application.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 application.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 {applicationHInput} = t.context;
+ applicationHInput.configuration.resources.configuration.paths.webapp = "webapp-project.artifactId";
+ const project = await Specification.create(applicationHInput);
+
+ t.is(project.getNamespace(), "application/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 {applicationHInput} = t.context;
+ applicationHInput.configuration.resources.configuration.paths.webapp = "webapp-properties.componentName";
+ const project = await Specification.create(applicationHInput);
+
+ t.is(project.getNamespace(), "application/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 {applicationHInput} = t.context;
+ applicationHInput.configuration.resources.configuration.paths.webapp = "webapp-properties.appId";
+
+ const error = await t.throwsAsync(Specification.create(applicationHInput));
+ t.deepEqual(error.message, "Failed to resolve namespace of project application.h: \"${appId}\"" +
+ " couldn't be resolved from maven property \"appId\" of pom.xml of project application.h");
+});
diff --git a/packages/project/test/lib/specifications/types/Library.js b/packages/project/test/lib/specifications/types/Library.js
new file mode 100644
index 00000000000..aaeed466701
--- /dev/null
+++ b/packages/project/test/lib/specifications/types/Library.js
@@ -0,0 +1,1569 @@
+import test from "ava";
+import path from "node:path";
+import sinonGlobal from "sinon";
+import Library from "../../../../lib/specifications/types/Library.js";
+
+const __dirname = import.meta.dirname;
+const libraryDPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.d");
+const libraryHPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.h");
+
+test.beforeEach((t) => {
+ t.context.sinon = sinonGlobal.createSandbox();
+ t.context.projectInput = {
+ id: "library.d.id",
+ version: "1.0.0",
+ modulePath: libraryDPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "library",
+ metadata: {
+ name: "library.d",
+ },
+ resources: {
+ configuration: {
+ paths: {
+ src: "main/src",
+ test: "main/test"
+ }
+ }
+ },
+ }
+ };
+ t.context.flatProjectInput = {
+ id: "library.d.id",
+ version: "1.0.0",
+ modulePath: libraryHPath,
+ configuration: {
+ specVersion: "2.6",
+ kind: "project",
+ type: "library",
+ metadata: {
+ name: "library.h",
+ }
+ }
+ };
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test("getNamespace", async (t) => {
+ const {projectInput} = t.context;
+ const project = await (new Library().init(projectInput));
+ t.is(project.getNamespace(), "library/d",
+ "Returned correct namespace");
+});
+
+test("getSourcePath", async (t) => {
+ const {projectInput} = t.context;
+ const project = await (new Library().init(projectInput));
+ t.is(project.getSourcePath(), path.join(libraryDPath, "main", "src"),
+ "Returned correct source path");
+});
+
+test("getPropertiesFileSourceEncoding: Default", async (t) => {
+ const {projectInput} = t.context;
+ const project = await (new Library().init(projectInput));
+ t.is(project.getPropertiesFileSourceEncoding(), "UTF-8",
+ "Returned correct default propertiesFileSourceEncoding configuration");
+});
+
+test("getPropertiesFileSourceEncoding: Configuration", async (t) => {
+ const {projectInput} = t.context;
+ projectInput.configuration.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1";
+ const project = await (new Library().init(projectInput));
+ t.is(project.getPropertiesFileSourceEncoding(), "ISO-8859-1",
+ "Returned correct default propertiesFileSourceEncoding configuration");
+});
+
+test("getJsdocExcludes", async (t) => {
+ const {projectInput} = t.context;
+ projectInput.configuration.builder = {
+ jsdoc: {
+ excludes: ["excludes"]
+ }
+ };
+ const project = await (new Library().init(projectInput));
+ t.deepEqual(project.getJsdocExcludes(), ["excludes"],
+ "Returned correct jsdocExcludes configuration");
+});
+
+test("getJsdocExcludes: default", async (t) => {
+ const {projectInput} = t.context;
+ const project = await (new Library().init(projectInput));
+ t.deepEqual(project.getJsdocExcludes(), [],
+ "Returned correct jsdocExcludes configuration");
+});
+
+test("Access project resources via reader: buildtime style", async (t) => {
+ const {projectInput} = t.context;
+ const project = await (new Library().init(projectInput));
+ const reader = project.getReader();
+ const resource = await reader.byPath("/resources/library/d/.library");
+ t.truthy(resource, "Found the requested resource");
+ t.is(resource.getPath(), "/resources/library/d/.library", "Resource has correct path");
+});
+
+test("Access project resources via reader: flat style", async (t) => {
+ const {projectInput} = t.context;
+ const project = await (new Library().init(projectInput));
+ const reader = project.getReader({style: "flat"});
+ const resource = await reader.byPath("/.library");
+ t.truthy(resource, "Found the requested resource");
+ t.is(resource.getPath(), "/.library", "Resource has correct path");
+});
+
+test("Access project test-resources via reader: buildtime style", async (t) => {
+ const {projectInput} = t.context;
+ const project = await (new Library().init(projectInput));
+ const reader = project.getReader({style: "buildtime"});
+ const resource = await reader.byPath("/test-resources/library/d/Test.html");
+ t.truthy(resource, "Found the requested resource");
+ t.is(resource.getPath(), "/test-resources/library/d/Test.html", "Resource has correct path");
+});
+
+test("Access project test-resources via reader: runtime style", async (t) => {
+ const {projectInput} = t.context;
+ const project = await (new Library().init(projectInput));
+ const reader = project.getReader({style: "runtime"});
+ const resource = await reader.byPath("/test-resources/library/d/Test.html");
+ t.truthy(resource, "Found the requested resource");
+ t.is(resource.getPath(), "/test-resources/library/d/Test.html", "Resource has correct path");
+});
+
+test("Access project resources via reader w/ builder excludes", async (t) => {
+ const {projectInput} = t.context;
+ const baselineProject = await (new Library().init(projectInput));
+
+ projectInput.configuration.builder = {
+ resources: {
+ excludes: ["**/.library"]
+ }
+ };
+ const excludesProject = await (new Library().init(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("**/.library")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getReader({}).byGlob("**/.library")).length, 0,
+ "Did not find excluded resource for default style");
+
+ t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 1,
+ "Found resource in baseline project for buildtime style");
+ t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 0,
+ "Did not find excluded resource for buildtime style");
+
+ t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/.library")).length, 1,
+ "Found resource in baseline project for dist style");
+ t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/.library")).length, 0,
+ "Did not find excluded resource for dist style");
+
+ t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/.library")).length, 1,
+ "Found resource in baseline project for flat style");
+ t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/.library")).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("**/.library")).length, 1,
+ "Found resource in baseline project for runtime style");
+ t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/.library")).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 (new Library().init(projectInput));
+
+ projectInput.configuration.builder = {
+ resources: {
+ excludes: ["**/.library"]
+ }
+ };
+ const excludesProject = await (new Library().init(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("**/.library")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getWorkspace().byGlob("**/.library")).length, 0,
+ "Did not find excluded resource for default style");
+});
+
+test("Access project resources via reader and workspace w/ absolute builder excludes", async (t) => {
+ const {projectInput} = t.context;
+ const baselineProject = await (new Library().init(projectInput));
+
+ projectInput.configuration.builder = {
+ resources: {
+ excludes: ["/resources/library/d/.library"]
+ }
+ };
+ const excludesProject = await (new Library().init(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("**/.library")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getReader({}).byGlob("**/.library")).length, 0,
+ "Did not find excluded resource for default style");
+
+ t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 1,
+ "Found resource in baseline project for buildtime style");
+ t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 0,
+ "Did not find excluded resource for buildtime style");
+
+ t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/.library")).length, 1,
+ "Found resource in baseline project for dist style");
+ t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/.library")).length, 0,
+ "Did not find excluded resource for dist style");
+
+ t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/.library")).length, 1,
+ "Found resource in baseline project for flat style");
+ t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/.library")).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("**/.library")).length, 1,
+ "Found resource in baseline project for runtime style");
+ t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/.library")).length, 1,
+ "Found excluded resource for runtime style");
+
+ t.is((await baselineProject.getWorkspace().byGlob("**/.library")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getWorkspace().byGlob("**/.library")).length, 0,
+ "Did not find excluded resource for default style");
+});
+
+test("Access project resources via reader and workspace w/ incorrect builder excludes", async (t) => {
+ const {projectInput} = t.context;
+ const baselineProject = await (new Library().init(projectInput));
+
+ projectInput.configuration.builder = {
+ resources: {
+ excludes: ["/.library"] // Absolute path does not match base path
+ }
+ };
+ const excludesProject = await (new Library().init(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("**/.library")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getReader({}).byGlob("**/.library")).length, 1,
+ "Found resource in project with incorrect exclude for default style");
+
+ t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 1,
+ "Found resource in baseline project for buildtime style");
+ t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/.library")).length, 1,
+ "Found resource in project with incorrect exclude for buildtime style");
+
+ t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/.library")).length, 1,
+ "Found resource in baseline project for dist style");
+ t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/.library")).length, 1,
+ "Found resource in project with incorrect exclude for dist style");
+
+ t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/.library")).length, 1,
+ "Can not read any test-resources for flat style");
+ t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/.library")).length, 1,
+ "Can not read any test-resources for flat style");
+
+ // Excludes are not applied for "runtime" style
+ t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/.library")).length, 1,
+ "Found resource in baseline project for runtime style");
+ t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/.library")).length, 1,
+ "Found resource for runtime style");
+
+ t.is((await baselineProject.getWorkspace().byGlob("**/.library")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getWorkspace().byGlob("**/.library")).length, 1,
+ "Found resource in project with incorrect exclude for default style");
+});
+
+test("Access project test-resources via reader and workspace w/ absolute builder excludes", async (t) => {
+ const {projectInput} = t.context;
+ const baselineProject = await (new Library().init(projectInput));
+
+ projectInput.configuration.builder = {
+ resources: {
+ excludes: ["/test-resources/library/d/Test.html"]
+ }
+ };
+ const excludesProject = await (new Library().init(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("**/Test.html")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getReader({}).byGlob("**/Test.html")).length, 0,
+ "Did not find excluded resource for default style");
+
+ t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/Test.html")).length, 1,
+ "Found resource in baseline project for buildtime style");
+ t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/Test.html")).length, 0,
+ "Did not find excluded resource for buildtime style");
+
+ t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/Test.html")).length, 1,
+ "Found resource in baseline project for dist style");
+ t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/Test.html")).length, 0,
+ "Did not find excluded resource for dist style");
+
+ // Test resources are not available in flat reader
+ t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/Test.html")).length, 0,
+ "Can not read any test-resources for flat style");
+ t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/Test.html")).length, 0,
+ "Can not read any test-resources for flat style");
+
+ // Excludes are not applied for "runtime" style
+ t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/Test.html")).length, 1,
+ "Found resource in baseline project for runtime style");
+ t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/Test.html")).length, 1,
+ "Found excluded resource for runtime style");
+
+ t.is((await baselineProject.getWorkspace().byGlob("**/Test.html")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getWorkspace().byGlob("**/Test.html")).length, 0,
+ "Did not find excluded resource for default style");
+});
+
+test("Access project test-resources via reader and workspace w/ relative builder excludes", async (t) => {
+ const {projectInput} = t.context;
+ const baselineProject = await (new Library().init(projectInput));
+
+ projectInput.configuration.builder = {
+ resources: {
+ excludes: ["Test.html"] // Has no effect since library excludes must be absolute or use wildcards
+ }
+ };
+ const excludesProject = await (new Library().init(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("**/Test.html")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getReader({}).byGlob("**/Test.html")).length, 1,
+ "Did not find excluded resource for default style");
+
+ t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/Test.html")).length, 1,
+ "Found resource in baseline project for buildtime style");
+ t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/Test.html")).length, 1,
+ "Did not find excluded resource for buildtime style");
+
+ t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/Test.html")).length, 1,
+ "Found resource in baseline project for dist style");
+ t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/Test.html")).length, 1,
+ "Did not find excluded resource for dist style");
+
+ // Test resources are not available in flat reader
+ t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/Test.html")).length, 0,
+ "Can not read any test-resources for flat style");
+ t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/Test.html")).length, 0,
+ "Can not read any test-resources for flat style");
+
+ // Excludes are not applied for "runtime" style
+ t.is((await baselineProject.getReader({style: "runtime"}).byGlob("**/Test.html")).length, 1,
+ "Found resource in baseline project for runtime style");
+ t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/Test.html")).length, 1,
+ "Found excluded resource for runtime style");
+
+ t.is((await baselineProject.getWorkspace().byGlob("**/Test.html")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getWorkspace().byGlob("**/Test.html")).length, 1,
+ "Did not find excluded resource for default style");
+});
+
+test("Modify project resources via workspace and access via flat and runtime reader", async (t) => {
+ const {projectInput} = t.context;
+ const project = await (new Library().init(projectInput));
+ const workspace = project.getWorkspace();
+ const workspaceResource = await workspace.byPath("/resources/library/d/.library");
+ t.truthy(workspaceResource, "Found resource in workspace");
+
+ const newContent = (await workspaceResource.getString()).replace("fancy", "fancy dancy");
+ workspaceResource.setString(newContent);
+ await workspace.write(workspaceResource);
+
+ const flatReader = project.getReader({style: "flat"});
+ const flatReaderResource = await flatReader.byPath("/.library");
+ t.truthy(flatReaderResource, "Found the requested resource byPath (flat)");
+ t.is(flatReaderResource.getPath(), "/.library", "Resource (byPath) has correct path (flat)");
+ t.is(await flatReaderResource.getString(), newContent,
+ "Found resource (byPath) has expected (changed) content (flat)");
+
+ const flatGlobResult = await flatReader.byGlob("**/.library");
+ t.is(flatGlobResult.length, 1, "Found the requested resource byGlob (flat)");
+ t.is(flatGlobResult[0].getPath(), "/.library", "Resource (byGlob) has correct path (flat)");
+ t.is(await flatGlobResult[0].getString(), newContent,
+ "Found resource (byGlob) has expected (changed) content (flat)");
+
+ const runtimeReader = project.getReader({style: "runtime"});
+ const runtimeReaderResource = await runtimeReader.byPath("/resources/library/d/.library");
+ t.truthy(runtimeReaderResource, "Found the requested resource byPath (runtime)");
+ t.is(runtimeReaderResource.getPath(), "/resources/library/d/.library",
+ "Resource (byPath) has correct path (runtime)");
+ t.is(await runtimeReaderResource.getString(), newContent,
+ "Found resource (byPath) has expected (changed) content (runtime)");
+
+ const runtimeGlobResult = await runtimeReader.byGlob("**/.library");
+ t.is(runtimeGlobResult.length, 1, "Found the requested resource byGlob (runtime)");
+ t.is(runtimeGlobResult[0].getPath(), "/resources/library/d/.library",
+ "Resource (byGlob) has correct path (runtime)");
+ t.is(await runtimeGlobResult[0].getString(), newContent,
+ "Found resource (byGlob) has expected (changed) content (runtime)");
+});
+
+test("Access flat project resources via reader: buildtime style", async (t) => {
+ const {flatProjectInput} = t.context;
+ const project = await (new Library().init(flatProjectInput));
+ const reader = project.getReader({style: "buildtime"});
+ const resource = await reader.byPath("/resources/library/h/some.js");
+ t.truthy(resource, "Found the requested resource");
+ t.is(resource.getPath(), "/resources/library/h/some.js", "Resource has correct path");
+});
+
+test("_configureAndValidatePaths: Default paths", async (t) => {
+ const libraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "library.e");
+ const projectInput = {
+ id: "library.e.id",
+ version: "1.0.0",
+ modulePath: libraryEPath,
+ configuration: {
+ specVersion: "2.6",
+ kind: "project",
+ type: "library",
+ metadata: {
+ name: "library.e",
+ }
+ }
+ };
+
+ const project = await (new Library().init(projectInput));
+
+ t.is(project._srcPath, "src", "Correct default path for src");
+ t.is(project._testPath, "test", "Correct default path for test");
+ t.true(project._testPathExists, "Test path detected as existing");
+});
+
+test("_configureAndValidatePaths: Test directory does not exist", async (t) => {
+ const {projectInput} = t.context;
+ projectInput.configuration.resources.configuration.paths.test = "does/not/exist";
+ const project = await (new Library().init(projectInput));
+
+ t.is(project._srcPath, "main/src", "Correct path for src");
+ t.is(project._testPath, "does/not/exist", "Correct path for test");
+ t.false(project._testPathExists, "Test path detected as non-existent");
+});
+
+test("_configureAndValidatePaths: Source directory does not exist", async (t) => {
+ const {projectInput} = t.context;
+ projectInput.configuration.resources.configuration.paths.src = "does/not/exist";
+ const err = await t.throwsAsync(new Library().init(projectInput));
+
+ t.is(err.message, "Unable to find source directory 'does/not/exist' in library project library.d");
+});
+
+test("_parseConfiguration: Get copyright", async (t) => {
+ const {projectInput} = t.context;
+ const project = await (new Library().init(projectInput));
+
+ t.is(project.getCopyright(), "Some fancy copyright", "Copyright was read correctly");
+});
+
+test("_parseConfiguration: Copyright already configured", async (t) => {
+ const {projectInput} = t.context;
+ projectInput.configuration.metadata.copyright = "My copyright";
+ const project = await (new Library().init(projectInput));
+
+ t.is(project.getCopyright(), "My copyright", "Copyright was not altered");
+});
+
+test.serial("_parseConfiguration: Copyright retrieval fails", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ sinon.stub(Library.prototype, "_getCopyrightFromDotLibrary").resolves(null);
+ const project = await (new Library().init(projectInput));
+
+ t.is(project.getCopyright(), undefined, "Copyright was not altered");
+});
+
+test.serial("_parseConfiguration: Preload excludes from .library", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ sinon.stub(Library.prototype, "isFrameworkProject").returns(true);
+ sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves(["test/exclude/**"]);
+
+ const project = new Library();
+
+ const loggerVerboseSpy = sinon.spy(project._log, "verbose");
+
+ await project.init(projectInput);
+
+ t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"],
+ "Correct library preload excludes have been set");
+
+ t.deepEqual(loggerVerboseSpy.getCall(10).args, [
+ "No preload excludes defined in project configuration of framework library library.d. " +
+ "Falling back to .library..."
+ ]);
+});
+
+test("_parseConfiguration: Preload excludes from project configuration (non-framework library)", async (t) => {
+ const {projectInput} = t.context;
+ projectInput.configuration.builder = {
+ libraryPreload: {
+ excludes: ["test/exclude/**"]
+ }
+ };
+ const project = await (new Library().init(projectInput));
+
+ t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"],
+ "Correct library preload excludes have been set");
+});
+
+test.serial("_parseConfiguration: Preload exclude fallback to .library (framework libraries only)", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ sinon.stub(Library.prototype, "isFrameworkProject").returns(true);
+ sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves(["test/exclude/**"]);
+
+ const project = new Library();
+
+ const loggerVerboseSpy = sinon.spy(project._log, "verbose");
+
+ await project.init(projectInput);
+
+ t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"],
+ "Correct library preload excludes have been set");
+
+ t.deepEqual(loggerVerboseSpy.getCall(10).args, [
+ "No preload excludes defined in project configuration of framework library library.d. " +
+ "Falling back to .library..."
+ ]);
+});
+
+test.serial("_parseConfiguration: No preload excludes from .library", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ sinon.stub(Library.prototype, "isFrameworkProject").returns(true);
+ sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves(null);
+
+ const project = new Library();
+
+ const loggerVerboseSpy = sinon.spy(project._log, "verbose");
+
+ await project.init(projectInput);
+
+ t.deepEqual(project.getLibraryPreloadExcludes(), [],
+ "No library preload excludes have been set");
+
+ t.deepEqual(loggerVerboseSpy.getCall(10).args, [
+ "No preload excludes defined in project configuration of framework library library.d. " +
+ "Falling back to .library..."
+ ]);
+});
+
+test.serial("_parseConfiguration: Preload excludes from project configuration (framework library)", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ sinon.stub(Library.prototype, "isFrameworkProject").returns(true);
+ const getPreloadExcludesFromDotLibraryStub =
+ sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary").resolves([]);
+
+ projectInput.configuration.builder = {
+ libraryPreload: {
+ excludes: ["test/exclude/**"]
+ }
+ };
+ const project = new Library();
+
+ const loggerVerboseSpy = sinon.spy(project._log, "verbose");
+
+ await project.init(projectInput);
+
+ t.deepEqual(project.getLibraryPreloadExcludes(), ["test/exclude/**"],
+ "Correct library preload excludes have been set");
+
+ t.deepEqual(loggerVerboseSpy.getCall(10).args, [
+ "Using preload excludes for framework library library.d from project configuration"
+ ]);
+
+ t.is(getPreloadExcludesFromDotLibraryStub.callCount, 0, "_getPreloadExcludesFromDotLibrary has not been called");
+});
+
+test.serial("_parseConfiguration: No preload exclude fallback for non-framework libraries", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ sinon.stub(Library.prototype, "isFrameworkProject").returns(false);
+ const getPreloadExcludesFromDotLibraryStub = sinon.stub(Library.prototype, "_getPreloadExcludesFromDotLibrary")
+ .resolves(["test/exclude/**"]);
+ const project = await (new Library().init(projectInput));
+
+ t.deepEqual(project.getLibraryPreloadExcludes(), [],
+ "No library preload excludes have been set");
+ t.is(getPreloadExcludesFromDotLibraryStub.callCount, 0, "_getPreloadExcludesFromDotLibrary has not been called");
+});
+
+test("_getManifest: Reads correctly", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getString: async () => `{"pony": "no unicorn"}`,
+ getPath: () => "some path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pManifest = null; // Clear cache from instantiation
+ const {content, filePath} = await project._getManifest();
+ t.is(content.pony, "no unicorn", "manifest.json content has been read");
+ t.is(filePath, "some path", "Correct path");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments");
+});
+
+test("_getManifest: No manifest.json", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pManifest = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getManifest());
+ t.is(error.message,
+ "Could not find manifest.json file for project library.d",
+ "Rejected with correct error message");
+});
+
+test("_getManifest: Invalid JSON", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getString: async () => `no pony`,
+ getPath: () => "some path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pManifest = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getManifest());
+ t.regex(error.message, /^Failed to read some path for project library\.d: /,
+ "Rejected with correct error message");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments");
+});
+
+test("_getManifest: Propagates exception", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().rejects(new Error("because shark"));
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pManifest = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getManifest());
+ t.is(error.message,
+ "because shark",
+ "Rejected with correct error message");
+});
+
+test("_getManifest: Multiple manifest.json files", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getString: async () => `{"pony": "no unicorn"}`,
+ getPath: () => "some path"
+ }, {
+ getString: async () => `{"pony": "no shark"}`,
+ getPath: () => "some other path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pManifest = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getManifest());
+ t.is(error.message, "Found multiple (2) manifest.json files for project library.d",
+ "Rejected with correct error message");
+});
+
+test("_getManifest: Result is cached", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getString: async () => `{"pony": "no unicorn"}`,
+ getPath: () => "some path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pManifest = null; // Clear cache from instantiation
+ const {content: content1, filePath: filePath1} = await project._getManifest();
+ t.is(content1.pony, "no unicorn", "manifest.json content has been read");
+ t.is(filePath1, "some path", "Correct path");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments");
+ const {content: content2, filePath: filePath2} = await project._getManifest();
+
+ t.is(content2.pony, "no unicorn", "manifest.json content has been read");
+ t.is(filePath2, "some path", "Correct path");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/manifest.json", "byGlob got called with the expected arguments");
+});
+
+test("_getDotLibrary: Reads correctly", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getString: async () => `Fancy `,
+ getPath: () => "some path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pDotLibrary = null; // Clear cache from instantiation
+ const {content, filePath} = await project._getDotLibrary();
+ t.deepEqual(content, {chicken: {_: "Fancy"}}, ".library content has been read");
+ t.is(filePath, "some path", "Correct path");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments");
+});
+
+test("_getDotLibrary: No .library file", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pDotLibrary = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getDotLibrary());
+ t.is(error.message,
+ "Could not find .library file for project library.d",
+ "Rejected with correct error message");
+});
+
+test("_getDotLibrary: Invalid XML", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getString: async () => `no pony`,
+ getPath: () => "some path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pDotLibrary = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getDotLibrary());
+ t.is(error.message,
+ "Failed to read some path for project library.d: " +
+ "Non-whitespace before first tag.\nLine: 0\nColumn: 1\nChar: n",
+ "Rejected with correct error message");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments");
+});
+
+test("_getDotLibrary: Propagates exception", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().rejects(new Error("because shark"));
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pDotLibrary = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getDotLibrary());
+ t.is(error.message,
+ "because shark",
+ "Rejected with correct error message");
+});
+
+test("_getDotLibrary: Multiple .library files", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getString: async () => `Fancy `,
+ getPath: () => "some path"
+ }, {
+ getString: async () => `Hungry `,
+ getPath: () => "some other path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pDotLibrary = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getDotLibrary());
+ t.is(error.message, "Found multiple (2) .library files for project library.d",
+ "Rejected with correct error message");
+});
+
+test("_getDotLibrary: Result is cached", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getString: async () => `Fancy `,
+ getPath: () => "some path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pDotLibrary = null; // Clear cache from instantiation
+ const {content: content1, filePath: filePath1} = await project._getDotLibrary();
+ t.deepEqual(content1, {chicken: {_: "Fancy"}}, ".library content has been read");
+ t.is(filePath1, "some path", "Correct path");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments");
+ const {content: content2, filePath: filePath2} = await project._getDotLibrary();
+
+ t.deepEqual(content2, {chicken: {_: "Fancy"}}, ".library content has been read");
+ t.is(filePath2, "some path", "Correct path");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/.library", "byGlob got called with the expected arguments");
+});
+
+test("_getLibraryJsPath: Reads correctly", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getPath: () => "some path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pLibraryJs = null; // Clear cache from instantiation
+ const filePath = await project._getLibraryJsPath();
+ t.is(filePath, "some path", "Expected library.js path");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/library.js", "byGlob got called with the expected arguments");
+});
+
+test("_getLibraryJsPath: No library.js file", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pLibraryJs = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getLibraryJsPath());
+ t.is(error.message,
+ "Could not find library.js file for project library.d",
+ "Rejected with correct error message");
+});
+
+test("_getLibraryJsPath: Propagates exception", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().rejects(new Error("because shark"));
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pLibraryJs = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getLibraryJsPath());
+ t.is(error.message,
+ "because shark",
+ "Rejected with correct error message");
+});
+
+test("_getLibraryJsPath: Multiple library.js files", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getPath: () => "some path"
+ }, {
+ getPath: () => "some other path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pLibraryJs = null; // Clear cache from instantiation
+ const error = await t.throwsAsync(project._getLibraryJsPath());
+ t.is(error.message, "Found multiple (2) library.js files for project library.d",
+ "Rejected with correct error message");
+});
+
+test("_getLibraryJsPath: Result is cached", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ const byGlobStub = sinon.stub().resolves([{
+ getPath: () => "some path"
+ }]);
+
+ project._getRawSourceReader = () => {
+ return {
+ byGlob: byGlobStub
+ };
+ };
+ project._pLibraryJs = null; // Clear cache from instantiation
+ const filePath1 = await project._getLibraryJsPath();
+ t.is(filePath1, "some path", "Expected library.js path");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/library.js", "byGlob got called with the expected arguments");
+
+ const filePath2 = await project._getLibraryJsPath();
+ t.is(filePath2, "some path", "Expected library.js path");
+ t.is(filePath2, "some path", "Correct path");
+ t.is(byGlobStub.callCount, 1, "byGlob got called once");
+ t.is(byGlobStub.getCall(0).args[0], "**/library.js", "byGlob got called with the expected arguments");
+});
+
+test.serial("_getNamespace: namespace resolution fails", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+
+ const loggerVerboseSpy = sinon.stub(project._log, "verbose");
+
+ sinon.stub(project, "_getNamespaceFromManifest").resolves({});
+ sinon.stub(project, "_getNamespaceFromDotLibrary").resolves({});
+ sinon.stub(project, "_getLibraryJsPath").rejects(new Error("pony error"));
+
+ const error = await t.throwsAsync(project._getNamespace());
+ t.deepEqual(error.message, "Failed to detect namespace or namespace is empty for project library.d." +
+ " Check verbose log for details.");
+
+ t.is(loggerVerboseSpy.callCount, 2, "2 calls to log.verbose should be done");
+ const logVerboseCalls = loggerVerboseSpy.getCalls().map((call) => call.args[0]);
+
+ t.true(logVerboseCalls.includes(
+ "Failed to resolve namespace of project library.d from manifest.json or .library file. " +
+ "Falling back to library.js file path..."),
+ "should contain message for missing manifest.json");
+
+ t.true(logVerboseCalls.includes(
+ "Namespace resolution from library.js file path failed for project library.d: pony error"),
+ "should contain message for missing library.js");
+});
+
+test("_getNamespace: from manifest.json with .library on same level", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").resolves({
+ content: {
+ "sap.app": {
+ id: "mani-pony"
+ }
+ },
+ filePath: "/mani-pony/manifest.json"
+ });
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {name: {_: "dot-pony"}}
+ },
+ filePath: "/mani-pony/.library"
+ });
+ const res = await project._getNamespace();
+ t.is(res, "mani-pony", "Returned correct namespace");
+ t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure");
+});
+
+test("_getNamespace: from manifest.json for flat project", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").resolves({
+ content: {
+ "sap.app": {
+ id: "mani-pony"
+ }
+ },
+ filePath: "/manifest.json"
+ });
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {name: {_: "dot-pony"}}
+ },
+ filePath: "/.library"
+ });
+ const res = await project._getNamespace();
+ t.is(res, "mani-pony", "Returned correct namespace");
+ t.false(project._isSourceNamespaced, "Project flagged as flat source structure");
+});
+
+test("_getNamespace: from .library for flat project", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").rejects("No manifest aint' here");
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {name: {_: "dot-pony"}}
+ },
+ filePath: "/.library"
+ });
+ const res = await project._getNamespace();
+ t.is(res, "dot-pony", "Returned correct namespace");
+ t.false(project._isSourceNamespaced, "Project flagged as flat source structure");
+});
+
+test("_getNamespace: from manifest.json with .library on same level but different directory", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").resolves({
+ content: {
+ "sap.app": {
+ id: "mani-pony"
+ }
+ },
+ filePath: "/mani-pony/manifest.json"
+ });
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {name: {_: "dot-pony"}}
+ },
+ filePath: "/different-pony/.library"
+ });
+
+ const err = await t.throwsAsync(project._getNamespace());
+
+ t.deepEqual(err.message,
+ `Failed to detect namespace for project library.d: Found a manifest.json on the same directory level ` +
+ `but in a different directory than the .library file. They should be in the same directory.\n` +
+ ` manifest.json path: /mani-pony/manifest.json\n` +
+ ` is different to\n` +
+ ` .library path: /different-pony/.library`,
+ "Rejected with correct error message");
+});
+
+test("_getNamespace: from manifest.json with not matching file path", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").resolves({
+ content: {
+ "sap.app": {
+ id: "mani-pony"
+ }
+ },
+ filePath: "/different/namespace/manifest.json"
+ });
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {name: {_: "dot-pony"}}
+ },
+ filePath: "/different/namespace/.library"
+ });
+ const err = await t.throwsAsync(project._getNamespace());
+
+ t.deepEqual(err.message, `Detected namespace "mani-pony" does not match detected directory structure ` +
+ `"different/namespace" for project library.d`, "Rejected with correct error message");
+});
+
+test.serial("_getNamespace: from manifest.json without sap.app id", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+
+ const manifestPath = "/different/namespace/manifest.json";
+ sinon.stub(project, "_getManifest").resolves({
+ content: {
+ "sap.app": {
+ }
+ },
+ filePath: manifestPath
+ });
+ sinon.stub(project, "_getDotLibrary").resolves({});
+
+ const loggerStub = sinon.stub(project._log, "verbose");
+
+ const err = await t.throwsAsync(project._getNamespace());
+
+ t.is(err.message,
+ `Failed to detect namespace or namespace is empty for project library.d. Check verbose log for details.`,
+ "Rejected with correct error message");
+ t.is(loggerStub.callCount, 4, "calls to verbose");
+
+
+ t.is(loggerStub.getCall(0).args[0],
+ `Namespace resolution from manifest.json failed for project library.d: ` +
+ `No sap.app/id configuration found in manifest.json of project library.d at ${manifestPath}`,
+ "correct verbose message");
+ t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure");
+});
+
+test("_getNamespace: from .library", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").rejects("No manifest aint' here");
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {name: {_: "dot-pony"}}
+ },
+ filePath: "/dot-pony/.library"
+ });
+ const res = await project._getNamespace();
+ t.is(res, "dot-pony", "Returned correct namespace");
+ t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure");
+});
+
+test("_getNamespace: from .library with ignored manifest.json on lower level", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").resolves({
+ content: {
+ "sap.app": {
+ id: "mani-pony"
+ }
+ },
+ filePath: "/namespace/somedir/manifest.json"
+ });
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {name: {_: "dot-pony"}}
+ },
+ filePath: "/dot-pony/.library"
+ });
+ const res = await project._getNamespace();
+ t.is(res, "dot-pony", "Returned correct namespace");
+ t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure");
+});
+
+test("_getNamespace: manifest.json on higher level than .library", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const manifestFsPath = "/namespace/manifest.json";
+ const dotLibraryFsPath = "/namespace/morenamespace/.library";
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").resolves({
+ content: {
+ "sap.app": {
+ id: "mani-pony"
+ }
+ },
+ filePath: manifestFsPath
+ });
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {name: {_: "dot-pony"}}
+ },
+ filePath: dotLibraryFsPath
+ });
+ const err = await t.throwsAsync(project._getNamespace());
+
+ t.deepEqual(err.message,
+ `Failed to detect namespace for project library.d: ` +
+ `Found a manifest.json on a higher directory level than the .library file. ` +
+ `It should be on the same or a lower level. ` +
+ `Note that a manifest.json on a lower level will be ignored.\n` +
+ ` manifest.json path: ${manifestFsPath}\n` +
+ ` is higher than\n` +
+ ` .library path: ${dotLibraryFsPath}`,
+ "Rejected with correct error message");
+});
+
+test("_getNamespace: from .library with maven placeholder", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").rejects("No manifest aint' here");
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {name: {_: "${mvn-pony}"}}
+ },
+ filePath: "/mvn-unicorn/.library"
+ });
+ const resolveMavenPlaceholderStub =
+ sinon.stub(project, "_resolveMavenPlaceholder").resolves("mvn-unicorn");
+ const res = await project._getNamespace();
+
+ t.is(resolveMavenPlaceholderStub.getCall(0).args[0], "${mvn-pony}",
+ "resolveMavenPlaceholder called with correct argument");
+ t.is(res, "mvn-unicorn", "Returned correct namespace");
+ t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure");
+});
+
+test("_getNamespace: from .library with not matching file path", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").rejects("No manifest aint' here");
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {name: {_: "mvn-pony"}}
+ },
+ filePath: "/different/namespace/.library"
+ });
+ const err = await t.throwsAsync(project._getNamespace());
+
+ t.deepEqual(err.message, `Detected namespace "mvn-pony" does not match detected directory structure ` +
+ `"different/namespace" for project library.d`,
+ "Rejected with correct error message");
+ t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure");
+});
+
+test("_getNamespace: from library.js", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").resolves({}); // Empty result or exception should not matter
+ sinon.stub(project, "_getDotLibrary").rejects(new Error("Because bird"));
+ sinon.stub(project, "_getLibraryJsPath").resolves("/my/namespace/library.js");
+ const res = await project._getNamespace();
+ t.is(res, "my/namespace", "Returned correct namespace");
+ t.true(project._isSourceNamespaced, "Project still flagged as namespaced source structure");
+});
+
+test("_getNamespace: from project root level library.js", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = new Library();
+
+ const loggerStub = sinon.stub(project._log, "verbose");
+
+ await project.init(projectInput);
+
+ sinon.stub(project, "_getManifest").resolves({});
+ sinon.stub(project, "_getDotLibrary").resolves({});
+ sinon.stub(project, "_getLibraryJsPath").resolves("/library.js");
+ const err = await t.throwsAsync(project._getNamespace());
+
+ t.is(err.message,
+ "Failed to detect namespace or namespace is empty for project library.d. Check verbose log for details.",
+ "Rejected with correct error message");
+
+ const logCalls = loggerStub.getCalls().map((call) => call.args[0]);
+ t.true(logCalls.includes(
+ "Namespace resolution from library.js file path failed for project library.d: " +
+ "Found library.js file in root directory. " +
+ "Expected it to be in namespace directory."),
+ "should contain message for root level library.js");
+});
+
+test("_getNamespace: neither manifest nor .library or library.js path contain it", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").resolves({});
+ sinon.stub(project, "_getDotLibrary").resolves({});
+ sinon.stub(project, "_getLibraryJsPath").rejects(new Error("Not found bla"));
+ const err = await t.throwsAsync(project._getNamespace());
+ t.is(err.message,
+ "Failed to detect namespace or namespace is empty for project library.d. Check verbose log for details.",
+ "Rejected with correct error message");
+});
+
+test("_getNamespace: maven placeholder resolution fails", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").resolves({
+ content: {
+ "sap.app": {
+ id: "${mvn-pony}"
+ }
+ },
+ filePath: "/not/used"
+ });
+ sinon.stub(project, "_getDotLibrary").resolves({});
+ const resolveMavenPlaceholderStub =
+ sinon.stub(project, "_resolveMavenPlaceholder")
+ .rejects(new Error("because squirrel"));
+ const err = await t.throwsAsync(project._getNamespace());
+ t.is(err.message,
+ "Failed to resolve namespace maven placeholder of project library.d: because squirrel",
+ "Rejected with correct error message");
+ t.is(resolveMavenPlaceholderStub.getCall(0).args[0], "${mvn-pony}",
+ "resolveMavenPlaceholder called with correct argument");
+});
+
+test("_getCopyrightFromDotLibrary", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {
+ copyright: {
+ _: "copyleft"
+ }
+ }
+ }
+ });
+ const copyright = await project._getCopyrightFromDotLibrary();
+ t.is(copyright, "copyleft", "Returned correct copyright");
+});
+
+test("_getCopyrightFromDotLibrary: No copyright in .library file", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {}
+ },
+ filePath: "some path"
+ });
+ const copyright = await project._getCopyrightFromDotLibrary();
+ t.is(copyright, null, "No copyright returned");
+});
+
+test("_getCopyrightFromDotLibrary: Does not propagate exception", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+
+ sinon.stub(project, "_getDotLibrary").rejects(new Error("because shark"));
+ const res = await project._getCopyrightFromDotLibrary();
+ t.is(res, null, "Returned with null");
+});
+
+test("_getPreloadExcludesFromDotLibrary: Single exclude", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {
+ appData: {
+ packaging: {
+ "all-in-one": {
+ exclude: {
+ $: {
+ name: "test/exclude/**"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ });
+ const excludes = await project._getPreloadExcludesFromDotLibrary();
+ t.deepEqual(excludes, [
+ "test/exclude/**",
+ ], "_getPreloadExcludesFromDotLibrary should return array with excludes");
+});
+
+test("_getPreloadExcludesFromDotLibrary: Multiple excludes", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {
+ appData: {
+ packaging: {
+ "all-in-one": {
+ exclude: [
+ {
+ $: {
+ name: "test/exclude1/**"
+ }
+ },
+ {
+ $: {
+ name: "test/exclude2/**"
+ }
+ },
+ {
+ $: {
+ name: "test/exclude3/**"
+ }
+ }
+ ]
+ }
+ }
+ }
+ }
+ }
+ });
+ const excludes = await project._getPreloadExcludesFromDotLibrary();
+ t.deepEqual(excludes, [
+ "test/exclude1/**",
+ "test/exclude2/**",
+ "test/exclude3/**"
+ ], "_getPreloadExcludesFromDotLibrary should return array with excludes");
+});
+
+test("_getPreloadExcludesFromDotLibrary: No excludes in .library file", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {}
+ },
+ filePath: "some path"
+ });
+ const excludes = await project._getPreloadExcludesFromDotLibrary();
+ t.is(excludes, null, "No excludes returned");
+});
+
+test("_getPreloadExcludesFromDotLibrary: Propagates exception", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+
+ sinon.stub(project, "_getDotLibrary").rejects(new Error("because shark"));
+ const err = await t.throwsAsync(project._getPreloadExcludesFromDotLibrary());
+ t.is(err.message, "because shark",
+ "Threw with excepted error message");
+});
+
+test("_getNamespaceFromManifest", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").resolves({
+ content: {
+ "sap.app": {
+ id: "library namespace"
+ }
+ },
+ filePath: "some path"
+ });
+ const {namespace, filePath} = await project._getNamespaceFromManifest();
+ t.is(namespace, "library namespace", "Returned correct namespace");
+ t.is(filePath, "some path", "Returned correct file path");
+});
+
+test("_getNamespaceFromManifest: No ID in manifest.json file", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getManifest").resolves({
+ content: {
+ "sap.app": {}
+ },
+ filePath: "some path"
+ });
+ const res = await project._getNamespaceFromManifest();
+ t.deepEqual(res, {}, "Empty object returned");
+});
+
+test("_getNamespaceFromManifest: Does not propagate exception", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+
+ sinon.stub(project, "_getManifest").rejects(new Error("because shark"));
+ const res = await project._getNamespaceFromManifest();
+ t.deepEqual(res, {}, "Empty object returned");
+});
+
+test("_getNamespaceFromDotLibrary", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {
+ name: {
+ _: "library namespace"
+ }
+ }
+ },
+ filePath: "some path"
+ });
+ const {namespace, filePath} = await project._getNamespaceFromDotLibrary();
+ t.is(namespace, "library namespace",
+ "Returned correct namespace");
+ t.is(filePath, "some path",
+ "Returned correct file path");
+});
+
+test("_getNamespaceFromDotLibrary: No library name in .library file", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+ sinon.stub(project, "_getDotLibrary").resolves({
+ content: {
+ library: {}
+ },
+ filePath: "some path"
+ });
+ const res = await project._getNamespaceFromDotLibrary();
+ t.deepEqual(res, {}, "Empty object returned");
+});
+
+test("_getNamespaceFromDotLibrary: Does not propagate exception", async (t) => {
+ const {projectInput, sinon} = t.context;
+
+ const project = await (new Library().init(projectInput));
+
+ sinon.stub(project, "_getDotLibrary").rejects(new Error("because shark"));
+ const res = await project._getNamespaceFromDotLibrary();
+ t.deepEqual(res, {}, "Empty object returned");
+});
diff --git a/packages/project/test/lib/specifications/types/Module.js b/packages/project/test/lib/specifications/types/Module.js
new file mode 100644
index 00000000000..deff4f7fdb8
--- /dev/null
+++ b/packages/project/test/lib/specifications/types/Module.js
@@ -0,0 +1,313 @@
+import test from "ava";
+import path from "node:path";
+import sinonGlobal from "sinon";
+import Specification from "../../../../lib/specifications/Specification.js";
+
+const __dirname = import.meta.dirname;
+const moduleAPath = path.join(__dirname, "..", "..", "..", "fixtures", "module.a");
+
+test.beforeEach((t) => {
+ t.context.sinon = sinonGlobal.createSandbox();
+ t.context.projectInput = {
+ id: "module.a.id",
+ version: "1.0.0",
+ modulePath: moduleAPath,
+ configuration: {
+ specVersion: "2.3",
+ kind: "project",
+ type: "module",
+ metadata: {
+ name: "module.a",
+ copyright: "Some fancy copyright" // allowed but ignored
+ },
+ resources: {
+ configuration: {
+ paths: {
+ "/": "dist",
+ "/dev/": "dev"
+ }
+ }
+ }
+ }
+ };
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test("Correct class", async (t) => {
+ const {projectInput} = t.context;
+ const {default: Module} = await import("../../../../lib/specifications/types/Module.js");
+ const project = await Specification.create(projectInput);
+ t.true(project instanceof Module, `Is an instance of the Module class`);
+});
+
+test("getSourcePath: Throws", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+ const err = t.throws(() => {
+ project.getSourcePath();
+ });
+ t.is(err.message, "Projects of type module have more than one source path",
+ "Threw with expected error message");
+});
+
+test("getNamespace", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+ t.is(project.getNamespace(), null,
+ "Returned no namespace");
+});
+
+test("Access project resources via reader", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+ t.throws(() => {
+ project.getSourcePath();
+ }, {
+ message: "Projects of type module have more than one source path"
+ }, "Threw with expected error message");
+});
+
+test("Access project resources via reader (multiple mappings)", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+ const reader = project.getReader();
+ const resource1 = await reader.byPath("/dev/devTools.js");
+ t.truthy(resource1, "Found the requested resource");
+ t.is(resource1.getPath(), "/dev/devTools.js", "Resource has correct path");
+
+ const resource2 = await reader.byPath("/index.js");
+ t.truthy(resource2, "Found the requested resource");
+ t.is(resource2.getPath(), "/index.js", "Resource has correct path");
+});
+
+test("Access project resources via reader (one mapping)", async (t) => {
+ const {projectInput} = t.context;
+ delete projectInput.configuration.resources.configuration.paths["/"];
+ const project = await Specification.create(projectInput);
+ const reader = project.getReader();
+ const resource1 = await reader.byPath("/dev/devTools.js");
+ t.truthy(resource1, "Found the requested resource");
+ t.is(resource1.getPath(), "/dev/devTools.js", "Resource has correct path");
+
+ const resource2 = await reader.byPath("/index.js");
+ t.falsy(resource2, "Could not find resource in unmapped path");
+});
+
+test("Access project resources via reader w/ builder excludes", async (t) => {
+ const {projectInput, sinon} = t.context;
+ const baselineProject = await Specification.create(projectInput);
+ const excludesProject = await Specification.create(projectInput);
+
+ // As of specVersion 3.0, modules are not allowed to have a "builder.resources" configuration.
+ // Hence modules can't practically be configured with builder excludes.
+ // We still simply stub the respective API call to test the code and be prepared
+ //
+ // projectInput.configuration.builder = {
+ // resources: {
+ // excludes: ["**/devTools.js"]
+ // }
+ // };
+ // So stub instead:
+ sinon.stub(excludesProject, "getBuilderResourcesExcludes").returns(["**/devTools.js"]);
+
+ // 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("**/devTools.js")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getReader({}).byGlob("**/devTools.js")).length, 0,
+ "Did not find excluded resource for default style");
+
+ t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/devTools.js")).length, 1,
+ "Found resource in baseline project for buildtime style");
+ t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/devTools.js")).length, 0,
+ "Did not find excluded resource for buildtime style");
+
+ t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/devTools.js")).length, 1,
+ "Found resource in baseline project for dist style");
+ t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/devTools.js")).length, 0,
+ "Did not find excluded resource for dist style");
+
+ t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/devTools.js")).length, 1,
+ "Found resource in baseline project for flat style");
+ t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/devTools.js")).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("**/devTools.js")).length, 1,
+ "Found resource in baseline project for runtime style");
+ t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/devTools.js")).length, 1,
+ "Found excluded resource for runtime style");
+});
+
+test("Access project resources via workspace w/ builder excludes", async (t) => {
+ const {projectInput, sinon} = t.context;
+ const baselineProject = await Specification.create(projectInput);
+ const excludesProject = await Specification.create(projectInput);
+
+ // As of specVersion 3.0, modules are not allowed to have a "builder.resources" configuration.
+ // Hence modules can't practically be configured with builder excludes.
+ // We still simply stub the respective API call to test the code and be prepared
+ //
+ // projectInput.configuration.builder = {
+ // resources: {
+ // excludes: ["**/devTools.js"]
+ // }
+ // };
+ // So stub instead:
+ sinon.stub(excludesProject, "getBuilderResourcesExcludes").returns(["**/devTools.js"]);
+
+ // 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("**/devTools.js")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getWorkspace().byGlob("**/devTools.js")).length, 0,
+ "Did not find excluded resource for default style");
+});
+
+test("Access project resources via reader w/ absolute builder excludes", async (t) => {
+ const {projectInput, sinon} = t.context;
+ const baselineProject = await Specification.create(projectInput);
+ const excludesProject = await Specification.create(projectInput);
+
+ // As of specVersion 3.0, modules are not allowed to have a "builder.resources" configuration.
+ // Hence modules can't practically be configured with builder excludes.
+ // We still simply stub the respective API call to test the code and be prepared
+ //
+ // projectInput.configuration.builder = {
+ // resources: {
+ // excludes: ["/dev/devTools.js"]
+ // }
+ // };
+ // So stub instead:
+ sinon.stub(excludesProject, "getBuilderResourcesExcludes").returns(["/dev/devTools.js"]);
+
+ // 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("**/devTools.js")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getReader({}).byGlob("**/devTools.js")).length, 0,
+ "Did not find excluded resource for default style");
+
+ t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/devTools.js")).length, 1,
+ "Found resource in baseline project for buildtime style");
+ t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/devTools.js")).length, 0,
+ "Did not find excluded resource for buildtime style");
+
+ t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/devTools.js")).length, 1,
+ "Found resource in baseline project for dist style");
+ t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/devTools.js")).length, 0,
+ "Did not find excluded resource for dist style");
+
+ t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/devTools.js")).length, 1,
+ "Found resource in baseline project for flat style");
+ t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/devTools.js")).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("**/devTools.js")).length, 1,
+ "Found resource in baseline project for runtime style");
+ t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/devTools.js")).length, 1,
+ "Found excluded resource for runtime style");
+
+ t.is((await baselineProject.getWorkspace().byGlob("**/devTools.js")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getWorkspace().byGlob("**/devTools.js")).length, 0,
+ "Did not find excluded resource for default style");
+});
+
+test("Modify project resources via workspace and access via reader", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+ const workspace = project.getWorkspace();
+ const workspaceResource = await workspace.byPath("/dev/devTools.js");
+ t.truthy(workspaceResource, "Found resource in workspace");
+
+ const newContent = (await workspaceResource.getString()).replace(/dev/g, "duck duck");
+ workspaceResource.setString(newContent);
+ await workspace.write(workspaceResource);
+
+ const reader = project.getReader();
+ const readerResource = await reader.byPath("/dev/devTools.js");
+ t.truthy(readerResource, "Found the requested resource byPath");
+ t.is(readerResource.getPath(), "/dev/devTools.js", "Resource (byPath) has correct path");
+ t.is(await readerResource.getString(), newContent,
+ "Found resource (byPath) has expected (changed) content");
+
+ const gGlobResult = await reader.byGlob("**/devTools.js");
+ t.is(gGlobResult.length, 1, "Found the requested resource byGlob");
+ t.is(gGlobResult[0].getPath(), "/dev/devTools.js", "Resource (byGlob) has correct path");
+ t.is(await gGlobResult[0].getString(), newContent,
+ "Found resource (byGlob) has expected (changed) content");
+});
+
+test("Modify project resources via workspace and access via reader for other path mapping", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+ const workspace = project.getWorkspace();
+ const workspaceResource = await workspace.byPath("/index.js");
+ t.truthy(workspaceResource, "Found resource in workspace");
+
+ const newContent = (await workspaceResource.getString()).replace("world", "duck");
+ workspaceResource.setString(newContent);
+ await workspace.write(workspaceResource);
+
+ const reader = project.getReader();
+ const readerResource = await reader.byPath("/index.js");
+ t.truthy(readerResource, "Found the requested resource byPath");
+ t.is(readerResource.getPath(), "/index.js", "Resource (byPath) has correct path");
+ t.is(await readerResource.getString(), newContent,
+ "Found resource (byPath) has expected (changed) content");
+
+ const gGlobResult = await reader.byGlob("**/index.js");
+ t.is(gGlobResult.length, 1, "Found the requested resource byGlob");
+ t.is(gGlobResult[0].getPath(), "/index.js", "Resource (byGlob) has correct path");
+ t.is(await gGlobResult[0].getString(), newContent,
+ "Found resource (byGlob) has expected (changed) content");
+});
+
+test("_configureAndValidatePaths: Default path mapping", async (t) => {
+ const {projectInput} = t.context;
+ projectInput.configuration.resources = {};
+ const project = await Specification.create(projectInput);
+
+ t.is(project._paths.length, 1, "One default path mapping");
+ t.is(project._paths[0].virBasePath, "/", "Default path mapping for /");
+ t.is(project._paths[0].fsBasePath, projectInput.modulePath, "Correct fs path");
+});
+
+test("_configureAndValidatePaths: Configured path mapping", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+
+ t.is(project._paths.length, 2, "Two path mappings");
+ t.is(project._paths[0].virBasePath, "/", "Correct virtual base path for /");
+ t.is(project._paths[0].fsBasePath, path.join(projectInput.modulePath, "dist"), "Correct fs path");
+ t.is(project._paths[1].virBasePath, "/dev/", "Correct virtual base path for /dev/");
+ t.is(project._paths[1].fsBasePath, path.join(projectInput.modulePath, "dev"), "Correct fs path");
+});
+
+test("_configureAndValidatePaths: Default directory does not exist", async (t) => {
+ const {projectInput} = t.context;
+ projectInput.configuration.resources = {};
+ projectInput.modulePath = "/does/not/exist";
+ const err = await t.throwsAsync(Specification.create(projectInput));
+
+ t.is(err.message, "Unable to find root directory of module project module.a");
+});
+
+test("_configureAndValidatePaths: Directory does not exist", async (t) => {
+ const {projectInput} = t.context;
+ projectInput.configuration.resources.configuration.paths.doesNotExist = "does/not/exist";
+ const err = await t.throwsAsync(Specification.create(projectInput));
+
+ t.is(err.message, "Unable to find source directory 'does/not/exist' in module project module.a");
+});
diff --git a/packages/project/test/lib/specifications/types/ThemeLibrary.js b/packages/project/test/lib/specifications/types/ThemeLibrary.js
new file mode 100644
index 00000000000..10559e29c3e
--- /dev/null
+++ b/packages/project/test/lib/specifications/types/ThemeLibrary.js
@@ -0,0 +1,250 @@
+import test from "ava";
+import path from "node:path";
+import sinonGlobal from "sinon";
+import Specification from "../../../../lib/specifications/Specification.js";
+
+const __dirname = import.meta.dirname;
+const themeLibraryEPath = path.join(__dirname, "..", "..", "..", "fixtures", "theme.library.e");
+
+test.beforeEach((t) => {
+ t.context.sinon = sinonGlobal.createSandbox();
+ t.context.projectInput = {
+ id: "theme.library.e.id",
+ version: "1.0.0",
+ modulePath: themeLibraryEPath,
+ configuration: {
+ specVersion: "2.6",
+ kind: "project",
+ type: "theme-library",
+ metadata: {
+ name: "theme.library.e",
+ copyright: "Some fancy copyright"
+ }
+ }
+ };
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+});
+
+test("Correct class", async (t) => {
+ const {projectInput} = t.context;
+ const {default: ThemeLibrary} = await import("../../../../lib/specifications/types/ThemeLibrary.js");
+ const project = await Specification.create(projectInput);
+ t.true(project instanceof ThemeLibrary, `Is an instance of the ThemeLibrary class`);
+});
+
+test("getCopyright", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+
+ t.is(project.getCopyright(), "Some fancy copyright", "Copyright was read correctly");
+});
+
+test("getSourcePath", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+ t.is(project.getSourcePath(), path.join(themeLibraryEPath, "src"), "Correct source path");
+});
+
+test("getNamespace", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+ t.is(project.getNamespace(), null,
+ "Returned no namespace");
+});
+
+test("Access project resources via reader", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+ const reader = project.getReader();
+ const resource = await reader.byPath("/resources/theme/library/e/themes/my_theme/.theme");
+ t.truthy(resource, "Found the requested resource");
+ t.is(resource.getPath(), "/resources/theme/library/e/themes/my_theme/.theme", "Resource has correct path");
+});
+
+test("Access project test-resources via reader", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+ const reader = project.getReader();
+ const resource = await reader.byPath("/test-resources/theme/library/e/Test.html");
+ t.truthy(resource, "Found the requested resource");
+ t.is(resource.getPath(), "/test-resources/theme/library/e/Test.html", "Resource has correct path");
+});
+
+test("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: ["**/.theme"]
+ }
+ };
+ 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("**/.theme")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getReader({}).byGlob("**/.theme")).length, 0,
+ "Did not find excluded resource for default style");
+
+ t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/.theme")).length, 1,
+ "Found resource in baseline project for buildtime style");
+ t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/.theme")).length, 0,
+ "Did not find excluded resource for buildtime style");
+
+ t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/.theme")).length, 1,
+ "Found resource in baseline project for dist style");
+ t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/.theme")).length, 0,
+ "Did not find excluded resource for dist style");
+
+ t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/.theme")).length, 1,
+ "Found resource in baseline project for flat style");
+ t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/.theme")).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("**/.theme")).length, 1,
+ "Found resource in baseline project for runtime style");
+ t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/.theme")).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: ["**/.theme"]
+ }
+ };
+ 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("**/.theme")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getWorkspace().byGlob("**/.theme")).length, 0,
+ "Did not find excluded resource for default style");
+});
+
+test("Access project resources via reader w/ absolute builder excludes", async (t) => {
+ const {projectInput} = t.context;
+ const baselineProject = await Specification.create(projectInput);
+
+ projectInput.configuration.builder = {
+ resources: {
+ excludes: ["/resources/theme/library/e/themes/my_theme/.theme"]
+ }
+ };
+ 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("**/.theme")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getReader({}).byGlob("**/.theme")).length, 0,
+ "Did not find excluded resource for default style");
+
+ t.is((await baselineProject.getReader({style: "buildtime"}).byGlob("**/.theme")).length, 1,
+ "Found resource in baseline project for buildtime style");
+ t.is((await excludesProject.getReader({style: "buildtime"}).byGlob("**/.theme")).length, 0,
+ "Did not find excluded resource for buildtime style");
+
+ t.is((await baselineProject.getReader({style: "dist"}).byGlob("**/.theme")).length, 1,
+ "Found resource in baseline project for dist style");
+ t.is((await excludesProject.getReader({style: "dist"}).byGlob("**/.theme")).length, 0,
+ "Did not find excluded resource for dist style");
+
+ t.is((await baselineProject.getReader({style: "flat"}).byGlob("**/.theme")).length, 1,
+ "Found resource in baseline project for flat style");
+ t.is((await excludesProject.getReader({style: "flat"}).byGlob("**/.theme")).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("**/.theme")).length, 1,
+ "Found resource in baseline project for runtime style");
+ t.is((await excludesProject.getReader({style: "runtime"}).byGlob("**/.theme")).length, 1,
+ "Found excluded resource for runtime style");
+
+ t.is((await baselineProject.getWorkspace().byGlob("**/.theme")).length, 1,
+ "Found resource in baseline project for default style");
+ t.is((await excludesProject.getWorkspace().byGlob("**/.theme")).length, 0,
+ "Did not find excluded resource for default style");
+});
+
+test("Modify project resources via workspace and access via flat and runtime reader", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+ const workspace = project.getWorkspace();
+ const workspaceResource = await workspace.byPath("/resources/theme/library/e/themes/my_theme/library.source.less");
+ t.truthy(workspaceResource, "Found resource in workspace");
+
+ const newContent = (await workspaceResource.getString()).replace("fancy", "fancy dancy");
+ workspaceResource.setString(newContent);
+ await workspace.write(workspaceResource);
+
+ const reader = project.getReader();
+ const readerResource = await reader.byPath("/resources/theme/library/e/themes/my_theme/library.source.less");
+ t.truthy(readerResource, "Found the requested resource byPath");
+ t.is(readerResource.getPath(), "/resources/theme/library/e/themes/my_theme/library.source.less",
+ "Resource (byPath) has correct path");
+ t.is(await readerResource.getString(), newContent,
+ "Found resource (byPath) has expected (changed) content");
+
+ const globResult = await reader.byGlob("**/library.source.less");
+ t.is(globResult.length, 1, "Found the requested resource byGlob");
+ t.is(globResult[0].getPath(), "/resources/theme/library/e/themes/my_theme/library.source.less",
+ "Resource (byGlob) has correct path");
+ t.is(await globResult[0].getString(), newContent,
+ "Found resource (byGlob) has expected (changed) content");
+});
+
+test("_configureAndValidatePaths: Default paths", async (t) => {
+ const {projectInput} = t.context;
+ const project = await Specification.create(projectInput);
+
+ t.is(project._srcPath, "src", "Correct default path for src");
+ t.is(project._testPath, "test", "Correct default path for test");
+ t.true(project._testPathExists, "Test path detected as existing");
+});
+
+test("_configureAndValidatePaths: Test directory does not exist", async (t) => {
+ const {projectInput} = t.context;
+ projectInput.configuration.resources = {
+ configuration: {
+ paths: {
+ test: "does/not/exist"
+ }
+ }
+ };
+ const project = await Specification.create(projectInput);
+
+ t.is(project._srcPath, "src", "Correct path for src");
+ t.is(project._testPath, "does/not/exist", "Correct path for test");
+ t.false(project._testPathExists, "Test path detected as non-existent");
+});
+
+test("_configureAndValidatePaths: Source directory does not exist", async (t) => {
+ const {projectInput} = 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 theme-library project theme.library.e");
+});
diff --git a/packages/project/test/lib/ui5framework/AbstractInstaller.js b/packages/project/test/lib/ui5framework/AbstractInstaller.js
new file mode 100644
index 00000000000..691e40984a5
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/AbstractInstaller.js
@@ -0,0 +1,11 @@
+import test from "ava";
+import AbstractInstaller from "../../../lib/ui5Framework/AbstractInstaller.js";
+
+test("AbstractInstaller: constructor throws an error", (t) => {
+ t.throws(() => {
+ new AbstractInstaller();
+ }, {
+ instanceOf: TypeError,
+ message: "Class 'AbstractInstaller' is abstract"
+ });
+});
diff --git a/packages/project/test/lib/ui5framework/AbstractResolver.js b/packages/project/test/lib/ui5framework/AbstractResolver.js
new file mode 100644
index 00000000000..7156df0a545
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/AbstractResolver.js
@@ -0,0 +1,1206 @@
+import test from "ava";
+import sinon from "sinon";
+import path from "node:path";
+import os from "node:os";
+import esmock from "esmock";
+
+test.beforeEach(async (t) => {
+ t.context.osHomeDirStub = sinon.stub().callsFake(() => os.homedir());
+ t.context.AbstractResolver = await esmock.p("../../../lib/ui5Framework/AbstractResolver.js", {
+ "node:os": {
+ homedir: t.context.osHomeDirStub
+ }
+ });
+
+ class MyResolver extends t.context.AbstractResolver {
+ static async fetchAllVersions() {}
+ }
+
+ t.context.MyResolver = MyResolver;
+});
+
+test.afterEach.always((t) => {
+ delete process.env.UI5_PROJECT_USE_FRAMEWORK_SOURCES;
+ esmock.purge(t.context.AbstractResolver);
+ sinon.restore();
+});
+
+test("AbstractResolver: abstract constructor should throw", async (t) => {
+ const {AbstractResolver} = t.context;
+ await t.throwsAsync(async () => {
+ new AbstractResolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+ }, {message: `Class 'AbstractResolver' is abstract`});
+});
+
+test("AbstractResolver: constructor", (t) => {
+ const {MyResolver, AbstractResolver} = t.context;
+ const providedLibraryMetadata = {"test": "data"};
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ version: "1.75.0",
+ providedLibraryMetadata,
+ sources: true
+ });
+ t.true(resolver instanceof MyResolver, "Constructor returns instance of sub-class");
+ t.true(resolver instanceof AbstractResolver, "Constructor returns instance of abstract class");
+ t.is(resolver._version, "1.75.0");
+ t.true(resolver._sources, "Correct value for 'sources' flag");
+});
+
+test("AbstractResolver: constructor overwrites sources with env variable", (t) => {
+ const {MyResolver, AbstractResolver} = t.context;
+
+ process.env.UI5_PROJECT_USE_FRAMEWORK_SOURCES = true;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ version: "1.75.0",
+ sources: false // Environment variable overrules parameter
+ });
+ t.true(resolver instanceof MyResolver, "Constructor returns instance of sub-class");
+ t.true(resolver instanceof AbstractResolver, "Constructor returns instance of abstract class");
+ t.is(resolver._version, "1.75.0");
+ t.true(resolver._sources, "Correct value for 'sources' flag");
+});
+
+test("AbstractResolver: constructor without version", (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/"
+ });
+ t.is(resolver._version, undefined);
+});
+
+test("AbstractResolver: Set absolute 'cwd'", (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ version: "1.75.0",
+ cwd: "/my-cwd"
+ });
+ t.is(resolver._cwd, path.resolve("/my-cwd"), "Should be resolved 'cwd'");
+});
+
+test("AbstractResolver: Set relative 'cwd'", (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ version: "1.75.0",
+ cwd: "./my-cwd"
+ });
+ t.is(resolver._cwd, path.resolve("./my-cwd"), "Should be resolved 'cwd'");
+});
+
+test("AbstractResolver: Defaults 'cwd' to process.cwd()", (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ version: "1.75.0",
+ ui5DataDir: "/ui5data"
+ });
+ t.is(resolver._cwd, process.cwd(), "Should default to process.cwd()");
+});
+
+test("AbstractResolver: Set absolute 'ui5DataDir'", (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ version: "1.75.0",
+ ui5DataDir: "/my-ui5DataDir"
+ });
+ t.is(resolver._ui5DataDir, path.resolve("/my-ui5DataDir"), "Should be resolved 'ui5DataDir'");
+});
+
+test("AbstractResolver: Set relative 'ui5DataDir'", (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ version: "1.75.0",
+ ui5DataDir: "./my-ui5DataDir"
+ });
+ t.is(resolver._ui5DataDir, path.resolve("./my-ui5DataDir"), "Should be resolved 'ui5DataDir'");
+});
+
+test("AbstractResolver: 'ui5DataDir' overriden os.homedir()", (t) => {
+ const {MyResolver, osHomeDirStub} = t.context;
+
+ osHomeDirStub.returns("./");
+
+ const resolver = new MyResolver({
+ version: "1.75.0"
+ });
+ t.is(resolver._ui5DataDir, path.resolve("./.ui5"), "Should be resolved 'ui5DataDir'");
+});
+
+test("AbstractResolver: Defaults 'ui5DataDir' to ~/.ui5", (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ version: "1.75.0",
+ cwd: "/test-project/"
+ });
+ t.is(resolver._ui5DataDir, path.join(os.homedir(), ".ui5"), "Should default to ~/.ui5");
+});
+
+test("AbstractResolver: getLibraryMetadata should throw an Error when not implemented", async (t) => {
+ const {MyResolver} = t.context;
+ await t.throwsAsync(async () => {
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+ await resolver.getLibraryMetadata();
+ }, {message: `AbstractResolver: getLibraryMetadata must be implemented!`});
+});
+
+test("AbstractResolver: handleLibrary should throw an Error when not implemented", async (t) => {
+ const {MyResolver} = t.context;
+ await t.throwsAsync(async () => {
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+ await resolver.handleLibrary();
+ }, {message: `AbstractResolver: handleLibrary must be implemented!`});
+});
+
+test("AbstractResolver: install", async (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ const metadata = {
+ libraries: {
+ "sap.ui.lib1": {
+ "npmPackageName": "@openui5/sap.ui.lib1",
+ "version": "1.75.0",
+ "dependencies": [],
+ "optionalDependencies": []
+ },
+ "sap.ui.lib2": {
+ "npmPackageName": "@openui5/sap.ui.lib2",
+ "version": "1.75.0",
+ "dependencies": [
+ "sap.ui.lib3"
+ ],
+ "optionalDependencies": []
+ },
+ "sap.ui.lib3": {
+ "npmPackageName": "@openui5/sap.ui.lib3",
+ "version": "1.75.0",
+ "dependencies": [],
+ "optionalDependencies": [
+ "sap.ui.lib4"
+ ]
+ },
+ "sap.ui.lib4": {
+ "npmPackageName": "@openui5/sap.ui.lib4",
+ "version": "1.75.0",
+ "dependencies": [
+ "sap.ui.lib1"
+ ],
+ "optionalDependencies": []
+ }
+ }
+ };
+
+ const handleLibraryStub = sinon.stub(resolver, "handleLibrary");
+ handleLibraryStub
+ .callsFake(async (libraryName) => {
+ throw new Error(`Unknown handleLibrary call: ${libraryName}`);
+ })
+ .withArgs("sap.ui.lib1").resolves({
+ metadata: Promise.resolve(metadata.libraries["sap.ui.lib1"]),
+ install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"})
+ })
+ .withArgs("sap.ui.lib2").resolves({
+ metadata: Promise.resolve(metadata.libraries["sap.ui.lib2"]),
+ install: Promise.resolve({pkgPath: "/foo/sap.ui.lib2"})
+ })
+ .withArgs("sap.ui.lib3").resolves({
+ metadata: Promise.resolve(metadata.libraries["sap.ui.lib3"]),
+ install: Promise.resolve({pkgPath: "/foo/sap.ui.lib3"})
+ })
+ .withArgs("sap.ui.lib4").resolves({
+ metadata: Promise.resolve(metadata.libraries["sap.ui.lib4"]),
+ install: Promise.resolve({pkgPath: "/foo/sap.ui.lib4"})
+ });
+
+ const result = await resolver.install(["sap.ui.lib1", "sap.ui.lib2", "sap.ui.lib4"]);
+
+ t.is(handleLibraryStub.callCount, 4, "Each library should be handled once");
+ t.deepEqual(result, {
+ libraryMetadata: {
+ "sap.ui.lib1": {
+ dependencies: [],
+ npmPackageName: "@openui5/sap.ui.lib1",
+ optionalDependencies: [],
+ path: "/foo/sap.ui.lib1",
+ version: "1.75.0",
+ },
+ "sap.ui.lib2": {
+ dependencies: [
+ "sap.ui.lib3",
+ ],
+ npmPackageName: "@openui5/sap.ui.lib2",
+ optionalDependencies: [],
+ path: "/foo/sap.ui.lib2",
+ version: "1.75.0",
+ },
+ "sap.ui.lib3": {
+ dependencies: [],
+ npmPackageName: "@openui5/sap.ui.lib3",
+ optionalDependencies: [
+ "sap.ui.lib4",
+ ],
+ path: "/foo/sap.ui.lib3",
+ version: "1.75.0",
+ },
+ "sap.ui.lib4": {
+ dependencies: [
+ "sap.ui.lib1",
+ ],
+ npmPackageName: "@openui5/sap.ui.lib4",
+ optionalDependencies: [],
+ path: "/foo/sap.ui.lib4",
+ version: "1.75.0",
+ },
+ }
+ });
+});
+
+test("AbstractResolver: install (with providedLibraryMetadata)", async (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ version: "1.75.0",
+ providedLibraryMetadata: {
+ "sap.ui.lib1": {
+ "npmPackageName": "@openui5/sap.ui.lib1",
+ "version": "1.75.0-workspace",
+ "dependencies": [
+ "sap.ui.lib3"
+ ],
+ "optionalDependencies": [],
+ "path": "/workspace/sap.ui.lib1"
+ },
+ "sap.ui.lib4": {
+ "npmPackageName": "@openui5/sap.ui.lib4",
+ "version": "1.75.0-workspace",
+ "dependencies": [
+ "sap.ui.lib5"
+ ],
+ "optionalDependencies": [],
+ "path": "/workspace/sap.ui.lib4"
+ },
+ "sap.ui.lib5": {
+ "npmPackageName": "@openui5/sap.ui.lib5",
+ "version": "1.75.0-workspace",
+ "dependencies": [],
+ "optionalDependencies": [],
+ "path": "/workspace/sap.ui.lib5"
+ },
+ }
+ });
+
+ const metadata = {
+ libraries: {
+ "sap.ui.lib2": {
+ "npmPackageName": "@openui5/sap.ui.lib2",
+ "version": "1.75.0",
+ "dependencies": [],
+ "optionalDependencies": []
+ },
+ "sap.ui.lib3": {
+ "npmPackageName": "@openui5/sap.ui.lib3",
+ "version": "1.75.0",
+ "dependencies": [
+ "sap.ui.lib4"
+ ],
+ "optionalDependencies": []
+ },
+ }
+ };
+
+ const handleLibraryStub = sinon.stub(resolver, "handleLibrary");
+ handleLibraryStub
+ .callsFake(async (libraryName) => {
+ throw new Error(`Unknown handleLibrary call: ${libraryName}`);
+ })
+ .withArgs("sap.ui.lib2").resolves({
+ metadata: Promise.resolve(metadata.libraries["sap.ui.lib2"]),
+ install: Promise.resolve({pkgPath: "/foo/sap.ui.lib2"})
+ })
+ .withArgs("sap.ui.lib3").resolves({
+ metadata: Promise.resolve(metadata.libraries["sap.ui.lib3"]),
+ install: Promise.resolve({pkgPath: "/foo/sap.ui.lib3"})
+ });
+
+ const result = await resolver.install(["sap.ui.lib1", "sap.ui.lib2"]);
+
+ t.is(handleLibraryStub.callCount, 2, "Each library not part of providedLibraryMetadata should be handled once");
+ t.deepEqual(result, {
+ libraryMetadata: {
+ "sap.ui.lib1": {
+ dependencies: ["sap.ui.lib3"],
+ npmPackageName: "@openui5/sap.ui.lib1",
+ optionalDependencies: [],
+ path: "/workspace/sap.ui.lib1",
+ version: "1.75.0-workspace",
+ },
+ "sap.ui.lib2": {
+ dependencies: [],
+ npmPackageName: "@openui5/sap.ui.lib2",
+ optionalDependencies: [],
+ path: "/foo/sap.ui.lib2",
+ version: "1.75.0",
+ },
+ "sap.ui.lib3": {
+ dependencies: ["sap.ui.lib4",],
+ npmPackageName: "@openui5/sap.ui.lib3",
+ optionalDependencies: [],
+ path: "/foo/sap.ui.lib3",
+ version: "1.75.0",
+ },
+ "sap.ui.lib4": {
+ dependencies: [
+ "sap.ui.lib5",
+ ],
+ npmPackageName: "@openui5/sap.ui.lib4",
+ optionalDependencies: [],
+ path: "/workspace/sap.ui.lib4",
+ version: "1.75.0-workspace",
+ },
+ "sap.ui.lib5": {
+ dependencies: [],
+ npmPackageName: "@openui5/sap.ui.lib5",
+ optionalDependencies: [],
+ path: "/workspace/sap.ui.lib5",
+ version: "1.75.0-workspace",
+ },
+ }
+ });
+});
+
+test("AbstractResolver: install error handling (rejection of metadata/install)", async (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ const handleLibraryStub = sinon.stub(resolver, "handleLibrary");
+ handleLibraryStub
+ .callsFake(async (libraryName) => {
+ throw new Error(`Unknown handleLibrary call: ${libraryName}`);
+ })
+ .withArgs("sap.ui.lib1").resolves({
+ metadata: Promise.reject(new Error("Error loading metadata for sap.ui.lib1")),
+ install: Promise.reject(new Error("Error installing sap.ui.lib1"))
+ })
+ .withArgs("sap.ui.lib2").resolves({
+ metadata: Promise.reject(new Error("Error loading metadata for sap.ui.lib2")),
+ install: Promise.reject(new Error("Error installing sap.ui.lib2"))
+ });
+
+ await t.throwsAsync(async () => {
+ await resolver.install(["sap.ui.lib1", "sap.ui.lib2"]);
+ }, {message: `Resolution of framework libraries failed with errors:
+ 1. Failed to resolve library sap.ui.lib1: Error installing sap.ui.lib1
+ 2. Failed to resolve library sap.ui.lib2: Error installing sap.ui.lib2`});
+
+ t.is(handleLibraryStub.callCount, 2, "Each library should be handled once");
+});
+
+test("AbstractResolver: install error handling (rejection of dependency metadata/install)", async (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ const handleLibraryStub = sinon.stub(resolver, "handleLibrary");
+ handleLibraryStub
+ .callsFake(async (libraryName) => {
+ throw new Error(`Unknown handleLibrary call: ${libraryName}`);
+ })
+ .withArgs("sap.ui.lib1").resolves({
+ metadata: Promise.resolve({
+ dependencies: ["sap.ui.lib2"]
+ }),
+ install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"})
+ })
+ .withArgs("sap.ui.lib2").resolves({
+ metadata: Promise.reject(new Error("Error loading metadata for sap.ui.lib2")),
+ install: Promise.reject(new Error("Error installing sap.ui.lib2"))
+ });
+
+ await t.throwsAsync(async () => {
+ await resolver.install(["sap.ui.lib1"]);
+ }, {message: `Failed to resolve library sap.ui.lib2: Error installing sap.ui.lib2`});
+
+ t.is(handleLibraryStub.callCount, 2, "Each library should be handled once");
+});
+
+test("AbstractResolver: install error handling (rejection of dependency install)", async (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ const handleLibraryStub = sinon.stub(resolver, "handleLibrary");
+ handleLibraryStub
+ .callsFake(async (libraryName) => {
+ throw new Error(`Unknown handleLibrary call: ${libraryName}`);
+ })
+ .withArgs("sap.ui.lib1").resolves({
+ metadata: Promise.resolve({
+ dependencies: ["sap.ui.lib2"]
+ }),
+ install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"})
+ })
+ .withArgs("sap.ui.lib2").callsFake(() => {
+ return {
+ metadata: Promise.resolve({
+ dependencies: ["sap.ui.lib3"]
+ }),
+ install: Promise.resolve({pkgPath: "/foo/sap.ui.lib1"})
+ };
+ })
+ .withArgs("sap.ui.lib3").callsFake(() => {
+ return {
+ metadata: Promise.resolve({
+ dependencies: []
+ }),
+ install: Promise.reject(new Error("Error installing sap.ui.lib3"))
+ };
+ });
+
+ await t.throwsAsync(async () => {
+ await resolver.install(["sap.ui.lib1"]);
+ }, {message: `Failed to resolve library sap.ui.lib3: Error installing sap.ui.lib3`});
+
+ t.is(handleLibraryStub.callCount, 3, "Each library should be handled once");
+});
+
+test("AbstractResolver: install error handling (handleLibrary throws error)", async (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ const handleLibraryStub = sinon.stub(resolver, "handleLibrary");
+ handleLibraryStub
+ .callsFake(async (libraryName) => {
+ throw new Error(`Error within handleLibrary: ${libraryName}`);
+ });
+
+ await t.throwsAsync(async () => {
+ await resolver.install(["sap.ui.lib1", "sap.ui.lib2"]);
+ }, {message: `Resolution of framework libraries failed with errors:
+ 1. Failed to resolve library sap.ui.lib1: Error within handleLibrary: sap.ui.lib1
+ 2. Failed to resolve library sap.ui.lib2: Error within handleLibrary: sap.ui.lib2`});
+
+ t.is(handleLibraryStub.callCount, 2, "Each library should be handled once");
+});
+
+test("AbstractResolver: install error handling " +
+"(no version, no providedLibraryMetadata)", async (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ });
+
+ const handleLibraryStub = sinon.stub(resolver, "handleLibrary");
+
+ await t.throwsAsync(resolver.install(["sap.ui.lib1", "sap.ui.lib2"]), {
+ message: `Resolution of framework libraries failed with errors:
+ 1. Failed to resolve library sap.ui.lib1: Unable to install library sap.ui.lib1. No framework version provided.
+ 2. Failed to resolve library sap.ui.lib2: Unable to install library sap.ui.lib2. No framework version provided.`
+ });
+
+ t.is(handleLibraryStub.callCount, 0, "Handle library should not be called when no version is available");
+});
+
+test("AbstractResolver: install error handling " +
+"(no version, one lib not part of providedLibraryMetadata)", async (t) => {
+ const {MyResolver} = t.context;
+ const resolver = new MyResolver({
+ cwd: "/test-project/",
+ providedLibraryMetadata: {
+ "sap.ui.lib1": {
+ "npmPackageName": "@openui5/sap.ui.lib1",
+ "version": "1.75.0-SNAPSHOT",
+ "dependencies": [],
+ "optionalDependencies": []
+ }
+ }
+ });
+
+ const handleLibraryStub = sinon.stub(resolver, "handleLibrary");
+
+ await t.throwsAsync(resolver.install(["sap.ui.lib1", "sap.ui.lib2"]), {
+ message:
+ "Failed to resolve library sap.ui.lib2:" +
+ " Unable to install library sap.ui.lib2. No framework version provided.",
+ });
+
+ t.is(handleLibraryStub.callCount, 0, "Handle library should not be called when no version is available");
+});
+
+test("AbstractResolver: static fetchAllVersions should throw an Error when not implemented", async (t) => {
+ const {AbstractResolver} = t.context;
+ await t.throwsAsync(async () => {
+ await AbstractResolver.fetchAllVersions();
+ }, {message: `AbstractResolver: static fetchAllVersions must be implemented!`});
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'latest'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0"]);
+
+ const version = await MyResolver.resolveVersion("latest", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.76.0", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0"]);
+
+ const version = await MyResolver.resolveVersion("1", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.76.0", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR-SNAPSHOT'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.76.0", "1.77.0", "1.77.0-SNAPSHOT", "1.78.0", "1.79.0-SNAPSHOT"]);
+
+ const version = await MyResolver.resolveVersion("1-SNAPSHOT", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.79.0-SNAPSHOT", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR.MINOR'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0"]);
+
+ const version = await MyResolver.resolveVersion("1.75", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.75.1", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR.MINOR-SNAPSHOT'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.76.0", "1.77.0", "1.77.0-SNAPSHOT", "1.78.0", "1.79.0-SNAPSHOT"]);
+
+ const version = await MyResolver.resolveVersion("1.79-SNAPSHOT", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.79.0-SNAPSHOT", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR.MINOR.PATCH'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0"]);
+
+ const version = await MyResolver.resolveVersion("1.75.0", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.75.0", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'MAJOR.MINOR.PATCH-SNAPSHOT'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.76.0", "1.77.0", "1.77.0-SNAPSHOT", "1.78.0", "1.79.0-SNAPSHOT"]);
+
+ const version = await MyResolver.resolveVersion("1.79.0-SNAPSHOT", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.79.0-SNAPSHOT", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion does not include prereleases for 'latest' version", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.76.0", "1.77.0", "1.78.0", "1.79.0-SNAPSHOT"]);
+
+ const version = await MyResolver.resolveVersion("latest", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.78.0", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'latest-snapshot'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0-SNAPSHOT", "1.75.1-SNAPSHOT", "1.76.0-SNAPSHOT", "1.76.1-SNAPSHOT"]);
+
+ const version = await MyResolver.resolveVersion("latest-snapshot", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.76.1-SNAPSHOT", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion includes non-prereleases for 'latest-snapshot'", async (t) => {
+ // Realistically this should never happen, since the Sapui5MavenSnapshotResolver would never return
+ // non-snapshot versions. This test therefore simply illustrates the current behavior for this theoretic case
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.76.0", "1.77.0", "1.78.0", "1.79.0-SNAPSHOT", "1.79.1"]);
+
+ const version = await MyResolver.resolveVersion("latest-snapshot", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.79.1", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion without options", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0"]);
+
+ await MyResolver.resolveVersion("1.75.0");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: undefined,
+ ui5DataDir: undefined
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion throws error for 'lts'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions");
+
+ const error = await t.throwsAsync(MyResolver.resolveVersion("lts", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }));
+
+ t.is(error.message, `Framework version specifier "lts" is incorrect or not supported`);
+
+ t.is(fetchAllVersionsStub.callCount, 0, "fetchAllVersions should not be called");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves '1.x'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0", "2.0.0"]);
+
+ const version = await MyResolver.resolveVersion("1.x", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.76.0", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves '1.75.x'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0", "2.0.0"]);
+
+ const version = await MyResolver.resolveVersion("1.75.x", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.75.1", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves '^1.75.0'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0", "2.0.0"]);
+
+ const version = await MyResolver.resolveVersion("^1.75.0", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.76.0", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves '~1.75.0'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0", "2.0.0"]);
+
+ const version = await MyResolver.resolveVersion("~1.75.0", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.75.1", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves '> 1.75.0 < 1.75.3'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.75.2", "1.75.3"]);
+
+ const version = await MyResolver.resolveVersion("> 1.75.0 < 1.75.3", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "1.75.2", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'x.x.x-SNAPSHOT'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0-SNAPSHOT", "1.76.0-SNAPSHOT", "1.77.0-SNAPSHOT"]);
+
+ const version = await MyResolver.resolveVersion("x.x.x-SNAPSHOT", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ // All ranges ending with -SNAPSHOT should use "includePrerelease" in order to
+ // properly match prerelease (i.e. -SNAPSHOT) versions.
+ t.is(version, "1.77.0-SNAPSHOT", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves '^2.0.0-SNAPSHOT'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["2.0.0-SNAPSHOT", "2.0.1-SNAPSHOT", "2.1.0-SNAPSHOT"]);
+
+ const version = await MyResolver.resolveVersion("^2.0.0-SNAPSHOT", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ // All ranges ending with -SNAPSHOT should use "includePrerelease" in order to
+ // properly match prerelease (i.e. -SNAPSHOT) versions.
+ t.is(version, "2.1.0-SNAPSHOT", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves '2.x.x-alpha'", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["2.0.0-alpha", "2.0.1-alpha", "2.1.0-alpha"]);
+
+ const version = await MyResolver.resolveVersion("^2.0.0-alpha", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ // Prerelease ranges other than -SNAPSHOT should not use "includePrerelease"
+ // and therefore not match pre-releases like normal versions
+ t.is(version, "2.0.0-alpha", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'next' using tags", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.0.0", "2.0.0"]);
+ const fetchAllTagsStub = sinon.stub(MyResolver, "fetchAllTags")
+ .resolves({
+ "latest": "1.0.0",
+ "next": "2.0.0"
+ });
+
+ const version = await MyResolver.resolveVersion("next", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "2.0.0", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+ t.is(fetchAllTagsStub.callCount, 1, "fetchAllTagsStub should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllTags should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'next' to a pre-release using tags", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.0.0", "2.0.0-SNAPSHOT"]);
+ const fetchAllTagsStub = sinon.stub(MyResolver, "fetchAllTags")
+ .resolves({
+ "latest": "1.0.0",
+ "next": "2.0.0-SNAPSHOT"
+ });
+
+ const version = await MyResolver.resolveVersion("next", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+
+ t.is(version, "2.0.0-SNAPSHOT", "Resolved version should be correct");
+
+ t.is(fetchAllVersionsStub.callCount, 1, "fetchAllVersions should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllVersions should be called with expected arguments");
+ t.is(fetchAllTagsStub.callCount, 1, "fetchAllTagsStub should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllTags should be called with expected arguments");
+});
+
+
+test.serial("AbstractResolver: Static resolveVersion resolves 'latest' using tags only " +
+"when the resolver supports them", async (t) => {
+ const {MyResolver} = t.context;
+ const fetchAllVersionsStub = sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0", "2.0.0"]);
+ const fetchAllTagsStub = sinon.stub(MyResolver, "fetchAllTags")
+ .resolves(null);
+
+ // Resolver does not support tags (resolves with "null" instead of an object)
+ // 'latest' should resolve to the highest version available
+ const version1 = await MyResolver.resolveVersion("latest", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+ t.is(version1, "2.0.0", "Resolved version should be correct");
+
+ t.is(fetchAllTagsStub.callCount, 1, "fetchAllTagsStub should be called once");
+ t.deepEqual(fetchAllVersionsStub.getCall(0).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllTags should be called with expected arguments");
+
+ // Change behavior of Resolver to support tags, so that version should be used now
+ // instead of the highest version
+ fetchAllTagsStub.resolves({
+ "latest": "1.76.0"
+ });
+ const version2 = await MyResolver.resolveVersion("latest", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ });
+ t.is(version2, "1.76.0", "Resolved version should be correct");
+
+ t.is(fetchAllTagsStub.callCount, 2, "fetchAllTagsStub should be called twice");
+ t.deepEqual(fetchAllVersionsStub.getCall(1).args, [{
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }], "fetchAllTags should be called with expected arguments");
+});
+
+test.serial("AbstractResolver: Static resolveVersion throws error for empty string", async (t) => {
+ const {MyResolver} = t.context;
+ sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0"]);
+
+ const error = await t.throwsAsync(MyResolver.resolveVersion("", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }));
+
+ t.is(error.message, `Framework version specifier "" is incorrect or not supported`);
+});
+
+test.serial("AbstractResolver: Static resolveVersion throws error for invalid tag name", async (t) => {
+ const {MyResolver} = t.context;
+ sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0"]);
+
+ const error = await t.throwsAsync(MyResolver.resolveVersion("%20", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }));
+
+ t.is(error.message, `Framework version specifier "%20" is incorrect or not supported`);
+});
+
+test.serial("AbstractResolver: Static resolveVersion throws error for non-existing tag", async (t) => {
+ const {MyResolver} = t.context;
+ sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0"]);
+ sinon.stub(MyResolver, "fetchAllTags")
+ .resolves({"latest": "1.76.0"});
+
+ const error = await t.throwsAsync(MyResolver.resolveVersion("this-tag-does-not-exist", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }));
+
+ t.is(error.message, `Could not resolve framework version via tag 'this-tag-does-not-exist'. ` +
+ `Make sure the tag is available in the configured registry.`
+ );
+});
+
+test.serial("AbstractResolver: Static resolveVersion throws error for version not found", async (t) => {
+ const {MyResolver} = t.context;
+ sinon.stub(MyResolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0"]);
+
+ const error = await t.throwsAsync(MyResolver.resolveVersion("1.74.0", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }));
+
+ t.is(error.message, `Could not resolve framework version 1.74.0. ` +
+ `Make sure the version is valid and available in the configured registry.`);
+});
+
+test.serial(
+ "AbstractResolver: Static resolveVersion throws error for version lower than lowest OpenUI5 version", async (t) => {
+ const {AbstractResolver} = t.context;
+ class Openui5Resolver extends AbstractResolver {
+ static async fetchAllVersions() {}
+ }
+
+ sinon.stub(Openui5Resolver, "fetchAllVersions")
+ .returns(["1.75.0", "1.75.1", "1.76.0"]);
+
+ const error = await t.throwsAsync(Openui5Resolver.resolveVersion("1.50.0", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }));
+
+ t.is(error.message,
+ `Could not resolve framework version 1.50.0. Note that OpenUI5 framework libraries can only be ` +
+ `consumed by the UI5 CLI starting with OpenUI5 v1.52.5`);
+ });
+
+test.serial(
+ "AbstractResolver: Static resolveVersion throws error for version lower than lowest SAPUI5 version", async (t) => {
+ const {AbstractResolver} = t.context;
+ class Sapui5Resolver extends AbstractResolver {
+ static async fetchAllVersions() {}
+ }
+
+ sinon.stub(Sapui5Resolver, "fetchAllVersions")
+ .returns(["1.76.0", "1.76.1", "1.90.0"]);
+
+ const error = await t.throwsAsync(Sapui5Resolver.resolveVersion("1.75.0", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }));
+
+ t.is(error.message,
+ `Could not resolve framework version 1.75.0. Note that SAPUI5 framework libraries can only be ` +
+ `consumed by the UI5 CLI starting with SAPUI5 v1.76.0`);
+ });
+
+test.serial(
+ "AbstractResolver: Static resolveVersion throws error when latest OpenUI5 version cannot be found", async (t) => {
+ const {AbstractResolver} = t.context;
+ class Openui5Resolver extends AbstractResolver {
+ static async fetchAllVersions() {}
+ }
+
+ sinon.stub(Openui5Resolver, "fetchAllVersions")
+ .returns([]);
+
+ const error = await t.throwsAsync(Openui5Resolver.resolveVersion("latest", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }));
+
+ t.is(error.message, `Could not resolve framework version latest. ` +
+ `Make sure the version is valid and available in the configured registry.`);
+ });
+
+test.serial(
+ "AbstractResolver: Static resolveVersion throws error when latest SAPUI5 version cannot be found", async (t) => {
+ const {AbstractResolver} = t.context;
+ class Sapui5Resolver extends AbstractResolver {
+ static async fetchAllVersions() {}
+ }
+
+ sinon.stub(Sapui5Resolver, "fetchAllVersions")
+ .returns([]);
+
+ const error = await t.throwsAsync(Sapui5Resolver.resolveVersion("latest", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }));
+
+ t.is(error.message, `Could not resolve framework version latest. ` +
+ `Make sure the version is valid and available in the configured registry.`);
+ });
+
+test.serial(
+ "AbstractResolver: Static resolveVersion throws error when OpenUI5 version range cannot be resolved", async (t) => {
+ const {AbstractResolver} = t.context;
+ class Openui5Resolver extends AbstractResolver {
+ static async fetchAllVersions() {}
+ }
+
+ sinon.stub(Openui5Resolver, "fetchAllVersions")
+ .returns([]);
+
+ const error = await t.throwsAsync(Openui5Resolver.resolveVersion("1.99", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }));
+
+ t.is(error.message, `Could not resolve framework version 1.99. ` +
+ `Make sure the version is valid and available in the configured registry.`);
+ });
+
+test.serial(
+ "AbstractResolver: Static resolveVersion throws error when SAPUI5 version range cannot be resolved", async (t) => {
+ const {AbstractResolver} = t.context;
+ class Sapui5Resolver extends AbstractResolver {
+ static async fetchAllVersions() {}
+ }
+
+ sinon.stub(Sapui5Resolver, "fetchAllVersions")
+ .returns([]);
+
+ const error = await t.throwsAsync(Sapui5Resolver.resolveVersion("1.99", {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ }));
+
+ t.is(error.message, `Could not resolve framework version 1.99. ` +
+ `Make sure the version is valid and available in the configured registry.`);
+ });
diff --git a/packages/project/test/lib/ui5framework/Openui5Resolver.integration.js b/packages/project/test/lib/ui5framework/Openui5Resolver.integration.js
new file mode 100644
index 00000000000..e8049f0a412
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/Openui5Resolver.integration.js
@@ -0,0 +1,200 @@
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import path from "node:path";
+
+const __dirname = import.meta.dirname;
+
+// Use path within project as mocking base directory to reduce chance of side effects
+// in case mocks/stubs do not work and real fs is used
+const fakeBaseDir = path.join(__dirname, "fake-tmp");
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.logStub = {
+ info: sinon.stub(),
+ verbose: sinon.stub(),
+ silly: sinon.stub(),
+ warn: sinon.stub(),
+ error: sinon.stub(),
+ isLevelEnabled: sinon.stub().returns(false),
+ _getLogger: sinon.stub()
+ };
+ const ui5Logger = {
+ getLogger: sinon.stub().returns(t.context.logStub)
+ };
+
+ t.context.pacote = {
+ packument: sinon.stub().callsFake(async (...args) => {
+ throw new Error(`pacote.packument stub called with unknown args: ${args}`);
+ })
+ };
+
+ t.context.NpmcliConfig = sinon.stub().returns({
+ load: sinon.stub().resolves(),
+ flat: {
+ registry: "https://registry.fake"
+ }
+ });
+
+ t.context.Registry = await esmock.p("../../../lib/ui5Framework/npm/Registry.js", {
+ "@ui5/logger": ui5Logger,
+ "pacote": t.context.pacote,
+ "@npmcli/config": {
+ "default": t.context.NpmcliConfig
+ }
+ });
+
+ const AbstractInstaller = await esmock.p("../../../lib/ui5Framework/AbstractInstaller.js", {
+ "@ui5/logger": ui5Logger,
+ "../../../lib/utils/fs.js": {
+ mkdirp: sinon.stub().resolves()
+ },
+ "lockfile": {
+ lock: sinon.stub().yieldsAsync(),
+ unlock: sinon.stub().yieldsAsync()
+ }
+ });
+
+ t.context.Installer = await esmock.p("../../../lib/ui5Framework/npm/Installer.js", {
+ "@ui5/logger": ui5Logger,
+ "graceful-fs": {
+ rename: sinon.stub().yieldsAsync(),
+ },
+ "../../../lib/utils/fs.js": {
+ mkdirp: sinon.stub().resolves()
+ },
+ "../../../lib/ui5Framework/npm/Registry.js": t.context.Registry,
+ "../../../lib/ui5Framework/AbstractInstaller.js": AbstractInstaller
+ });
+
+ t.context.AbstractResolver = await esmock.p("../../../lib/ui5Framework/AbstractResolver.js", {
+ "@ui5/logger": ui5Logger,
+ "node:os": {
+ homedir: sinon.stub().returns(path.join(fakeBaseDir, "datadir"))
+ },
+ });
+
+ t.context.Openui5Resolver = await esmock.p("../../../lib/ui5Framework/Openui5Resolver.js", {
+ "@ui5/logger": ui5Logger,
+ "node:os": {
+ homedir: sinon.stub().returns(path.join(fakeBaseDir, "datadir"))
+ },
+ "../../../lib/ui5Framework/AbstractResolver.js": t.context.AbstractResolver,
+ "../../../lib/ui5Framework/npm/Installer.js": t.context.Installer
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.Registry);
+ esmock.purge(t.context.Installer);
+ esmock.purge(t.context.AbstractResolver);
+ esmock.purge(t.context.Openui5Resolver);
+});
+
+test.serial("resolveVersion", async (t) => {
+ const {Openui5Resolver, pacote, logStub, NpmcliConfig} = t.context;
+
+ pacote.packument
+ .withArgs("@openui5/sap.ui.core")
+ .resolves({
+ "versions": {
+ "1.120.1": "",
+ "1.120.0": "",
+ "1.119.0": "",
+ "1.118.0": "",
+ "2.0.0-rc.1": "",
+ "1.123.4-SNAPSHOT": ""
+ },
+ "dist-tags": {
+ // NOTE: latest does not correspond to highest version in order to verify
+ // that this tag is used instead of picking the highest version
+ "latest": "1.120.0",
+
+ "next": "2.0.0-rc.1",
+
+ // NOTE: Tag ends with "-snapshot" in order to verify that the special handling
+ // of that
+ "not-a-snapshot": "1.118.0"
+ }
+ });
+
+ const defaultCwd = process.cwd();
+ const defaultUi5DataDir = path.join(fakeBaseDir, "datadir", ".ui5");
+
+ // Generic testing without and with options argument
+ const optionsArguments = [
+ undefined,
+ {
+ cwd: path.join(fakeBaseDir, "custom-cwd"),
+ ui5DataDir: path.join(fakeBaseDir, "custom-datadir", ".ui5")
+ }
+ ];
+ for (const options of optionsArguments) {
+ // Reset calls to be able to check them per for-loop run
+ NpmcliConfig.resetHistory();
+ pacote.packument.resetHistory();
+
+ // Ranges
+ t.is(await Openui5Resolver.resolveVersion("1", options), "1.120.1");
+ t.is(await Openui5Resolver.resolveVersion("1.120", options), "1.120.1");
+ t.is(await Openui5Resolver.resolveVersion("1.x", options), "1.120.1");
+ t.is(await Openui5Resolver.resolveVersion("1.x.x", options), "1.120.1");
+ t.is(await Openui5Resolver.resolveVersion("^1", options), "1.120.1");
+ t.is(await Openui5Resolver.resolveVersion("*", options), "1.120.1");
+
+ // Tags
+ t.is(await Openui5Resolver.resolveVersion("latest", options), "1.120.0");
+ t.is(await Openui5Resolver.resolveVersion("next", options), "2.0.0-rc.1");
+ t.is(await Openui5Resolver.resolveVersion("not-a-snapshot", options), "1.118.0");
+
+ // Exact versions
+ t.is(await Openui5Resolver.resolveVersion("1.118.0", options), "1.118.0");
+ t.is(await Openui5Resolver.resolveVersion("2.0.0-rc.1", options), "2.0.0-rc.1");
+ t.is(await Openui5Resolver.resolveVersion("1.123.4-SNAPSHOT", options), "1.123.4-SNAPSHOT");
+
+ // SNAPSHOT ranges
+ t.is(await Openui5Resolver.resolveVersion("1-SNAPSHOT", options), "1.123.4-SNAPSHOT");
+ t.is(await Openui5Resolver.resolveVersion("1.123-SNAPSHOT", options), "1.123.4-SNAPSHOT");
+
+ // Error cases
+ await t.throwsAsync(Openui5Resolver.resolveVersion("", options), {
+ message: `Framework version specifier "" is incorrect or not supported`
+ });
+ await t.throwsAsync(Openui5Resolver.resolveVersion("tag-does-not-exist", options), {
+ message: `Could not resolve framework version via tag 'tag-does-not-exist'. ` +
+ `Make sure the tag is available in the configured registry.`
+ });
+ await t.throwsAsync(Openui5Resolver.resolveVersion("invalid-tag-%20", options), {
+ message: `Framework version specifier "invalid-tag-%20" is incorrect or not supported`
+ });
+
+ await t.throwsAsync(Openui5Resolver.resolveVersion("1.999.9", options), {
+ message: `Could not resolve framework version 1.999.9. ` +
+ `Make sure the version is valid and available in the configured registry.`
+ });
+ await t.throwsAsync(Openui5Resolver.resolveVersion("1.0.0", options), {
+ message: `Could not resolve framework version 1.0.0. ` +
+ `Note that OpenUI5 framework libraries can only be consumed by the UI5 CLI ` +
+ `starting with OpenUI5 v1.52.5`
+ });
+ await t.throwsAsync(Openui5Resolver.resolveVersion("^999", options), {
+ message: `Could not resolve framework version ^999. ` +
+ `Make sure the version is valid and available in the configured registry.`
+ });
+
+ // Check whether options have been passed as expected
+ t.true(NpmcliConfig.alwaysCalledWithNew());
+ t.true(NpmcliConfig.alwaysCalledWithMatch(sinonGlobal.match({
+ cwd: options?.cwd ?? defaultCwd
+ })));
+ t.true(pacote.packument.alwaysCalledWithMatch("@openui5/sap.ui.core", {
+ cache: path.join(options?.ui5DataDir ?? defaultUi5DataDir, "framework", "cacache")
+ }));
+ }
+
+ t.is(logStub.warn.callCount, 0);
+ t.is(logStub.error.callCount, 0);
+});
diff --git a/packages/project/test/lib/ui5framework/Openui5Resolver.js b/packages/project/test/lib/ui5framework/Openui5Resolver.js
new file mode 100644
index 00000000000..34d756987da
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/Openui5Resolver.js
@@ -0,0 +1,217 @@
+import test from "ava";
+import sinon from "sinon";
+import esmock from "esmock";
+import path from "node:path";
+import os from "node:os";
+
+test.beforeEach(async (t) => {
+ t.context.InstallerStub = sinon.stub();
+ t.context.fetchPackageDistTags = sinon.stub();
+ t.context.fetchPackageManifestStub = sinon.stub();
+ t.context.fetchPackageVersionsStub = sinon.stub();
+ t.context.installPackageStub = sinon.stub();
+ t.context.InstallerStub.callsFake(() => {
+ return {
+ fetchPackageDistTags: t.context.fetchPackageDistTags,
+ fetchPackageManifest: t.context.fetchPackageManifestStub,
+ fetchPackageVersions: t.context.fetchPackageVersionsStub,
+ installPackage: t.context.installPackageStub
+ };
+ });
+
+ t.context.Openui5Resolver = await esmock("../../../lib/ui5Framework/Openui5Resolver.js", {
+ "../../../lib/ui5Framework/npm/Installer": t.context.InstallerStub
+ });
+});
+
+test.afterEach.always(() => {
+ sinon.restore();
+});
+
+test.serial("Openui5Resolver: _getNpmPackageName", (t) => {
+ const {Openui5Resolver} = t.context;
+ t.is(Openui5Resolver._getNpmPackageName("foo"), "@openui5/foo");
+});
+
+test.serial("Openui5Resolver: _getLibaryName", (t) => {
+ const {Openui5Resolver} = t.context;
+ t.is(Openui5Resolver._getLibaryName("@openui5/foo"), "foo");
+ t.is(Openui5Resolver._getLibaryName("@something/else"), "@something/else");
+});
+
+test.serial("Openui5Resolver: getLibraryMetadata", async (t) => {
+ const {Openui5Resolver} = t.context;
+
+ const resolver = new Openui5Resolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ t.context.fetchPackageManifestStub
+ .callsFake(async ({pkgName}) => {
+ throw new Error(`Unknown install call: ${pkgName}`);
+ })
+ .withArgs({pkgName: "@openui5/sap.ui.lib1", version: "1.75.0"}).resolves({})
+ .withArgs({pkgName: "@openui5/sap.ui.lib2", version: "1.75.0"}).resolves({
+ dependencies: {
+ "sap.ui.lib3": "1.2.3"
+ },
+ devDependencies: {
+ "sap.ui.lib4": "4.5.6"
+ }
+ });
+
+ async function assert(libraryName, expectedMetadata) {
+ const pLibraryMetadata = resolver.getLibraryMetadata(libraryName);
+ const pLibraryMetadata2 = resolver.getLibraryMetadata(libraryName);
+
+ const libraryMetadata = await pLibraryMetadata;
+ t.deepEqual(libraryMetadata, expectedMetadata,
+ libraryName + ": First call should resolve with expected metadata");
+ const libraryMetadata2 = await pLibraryMetadata2;
+ t.deepEqual(libraryMetadata2, expectedMetadata,
+ libraryName + ": Second call should also resolve with expected metadata");
+
+ const libraryMetadata3 = await resolver.getLibraryMetadata(libraryName);
+
+ t.deepEqual(libraryMetadata3, expectedMetadata,
+ libraryName + ": Third call should still return the same metadata");
+ }
+
+ await assert("sap.ui.lib1", {
+ id: "@openui5/sap.ui.lib1",
+ version: "1.75.0",
+ dependencies: [],
+ optionalDependencies: []
+ });
+
+ await assert("sap.ui.lib2", {
+ id: "@openui5/sap.ui.lib2",
+ version: "1.75.0",
+ dependencies: [
+ "sap.ui.lib3"
+ ],
+ optionalDependencies: [
+ "sap.ui.lib4"
+ ]
+ });
+
+ t.is(t.context.fetchPackageManifestStub.callCount, 2, "fetchPackageManifest should be called twice");
+});
+
+test.serial("Openui5Resolver: handleLibrary", async (t) => {
+ const {Openui5Resolver} = t.context;
+
+ const resolver = new Openui5Resolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ const getLibraryMetadataStub = sinon.stub(resolver, "getLibraryMetadata");
+ getLibraryMetadataStub
+ .callsFake(async (libraryName) => {
+ throw new Error("getLibraryMetadata stub called with unknown libraryName: " + libraryName);
+ })
+ .withArgs("sap.ui.lib1").resolves({
+ "id": "@openui5/sap.ui.lib1",
+ "version": "1.75.0",
+ "dependencies": [],
+ "optionalDependencies": []
+ });
+
+ t.context.installPackageStub
+ .callsFake(async ({pkgName, version}) => {
+ throw new Error(`Unknown install call: ${pkgName}@${version}`);
+ })
+ .withArgs({pkgName: "@openui5/sap.ui.lib1", version: "1.75.0"}).resolves({pkgPath: "/foo/sap.ui.lib1"});
+
+ const promises = await resolver.handleLibrary("sap.ui.lib1");
+
+ t.true(promises.metadata instanceof Promise, "Metadata promise should be returned");
+ t.true(promises.install instanceof Promise, "Install promise should be returned");
+
+ const metadata = await promises.metadata;
+ t.deepEqual(metadata, {
+ "id": "@openui5/sap.ui.lib1",
+ "version": "1.75.0",
+ "dependencies": [],
+ "optionalDependencies": []
+ }, "Expected library metadata should be returned");
+
+ t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object");
+});
+
+test.serial("Openui5Resolver: Static _getInstaller", (t) => {
+ const {Openui5Resolver} = t.context;
+
+ const options = {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ };
+
+ const installer = Openui5Resolver._getInstaller(options);
+
+ t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once");
+ t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new");
+ t.is(installer, t.context.InstallerStub.getCall(0).returnValue, "Installer instance is returned");
+ t.deepEqual(t.context.InstallerStub.getCall(0).args, [{
+ cwd: path.resolve("/cwd"),
+ ui5DataDir: path.resolve("/ui5DataDir")
+ }], "Installer should be called with expected arguments");
+});
+
+test.serial("Openui5Resolver: Static _getInstaller without options", (t) => {
+ const {Openui5Resolver} = t.context;
+
+ const installer = Openui5Resolver._getInstaller();
+
+ t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once");
+ t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new");
+ t.is(installer, t.context.InstallerStub.getCall(0).returnValue, "Installer instance is returned");
+ t.deepEqual(t.context.InstallerStub.getCall(0).args, [{
+ cwd: process.cwd(),
+ ui5DataDir: path.join(os.homedir(), ".ui5")
+ }], "Installer should be called with expected arguments");
+});
+
+test.serial("Openui5Resolver: Static fetchAllVersions", async (t) => {
+ const {Openui5Resolver} = t.context;
+
+ const expectedVersions = ["1.75.0", "1.75.1", "1.76.0"];
+
+ t.context.fetchPackageVersionsStub.returns(expectedVersions);
+
+ const getInstallerSpy = sinon.spy(Openui5Resolver, "_getInstaller");
+
+ const versions = await Openui5Resolver.fetchAllVersions();
+
+ t.deepEqual(versions, expectedVersions, "Fetched versions should be correct");
+
+ t.is(t.context.fetchPackageVersionsStub.callCount, 1, "fetchPackageVersions should be called once");
+ t.deepEqual(t.context.fetchPackageVersionsStub.getCall(0).args, [{pkgName: "@openui5/sap.ui.core"}],
+ "fetchPackageVersions should be called with expected arguments");
+
+ t.is(getInstallerSpy.callCount, 1, "_getInstaller should be called once");
+ t.is(getInstallerSpy.getCall(0).args[0], undefined, "_getInstaller should be called without any options");
+});
+
+test.serial("Openui5Resolver: Static fetchAllTags", async (t) => {
+ const {Openui5Resolver} = t.context;
+
+ const expectedTags = ["latest", "latest-1.71", "latest-1"];
+
+ t.context.fetchPackageDistTags.returns(expectedTags);
+
+ const getInstallerSpy = sinon.spy(Openui5Resolver, "_getInstaller");
+
+ const tags = await Openui5Resolver.fetchAllTags();
+
+ t.deepEqual(tags, expectedTags, "Fetched tags should be correct");
+
+ t.is(t.context.fetchPackageDistTags.callCount, 1, "fetchPackageVersions should be called once");
+ t.deepEqual(t.context.fetchPackageDistTags.getCall(0).args, [{pkgName: "@openui5/sap.ui.core"}],
+ "fetchPackageVersions should be called with expected arguments");
+
+ t.is(getInstallerSpy.callCount, 1, "_getInstaller should be called once");
+ t.is(getInstallerSpy.getCall(0).args[0], undefined, "_getInstaller should be called without any options");
+});
diff --git a/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.integration.js b/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.integration.js
new file mode 100644
index 00000000000..c82fc42ab9a
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.integration.js
@@ -0,0 +1,157 @@
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import path from "node:path";
+
+const __dirname = import.meta.dirname;
+
+// Use path within project as mocking base directory to reduce chance of side effects
+// in case mocks/stubs do not work and real fs is used
+const fakeBaseDir = path.join(__dirname, "fake-tmp");
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL = "_SNAPSHOT_URL_";
+
+ t.context.logStub = {
+ info: sinon.stub(),
+ verbose: sinon.stub(),
+ silly: sinon.stub(),
+ warn: sinon.stub(),
+ error: sinon.stub(),
+ isLevelEnabled: sinon.stub().returns(false),
+ _getLogger: sinon.stub()
+ };
+ const ui5Logger = {
+ getLogger: sinon.stub().returns(t.context.logStub)
+ };
+
+ t.context.makeFetchHappen = sinon.stub();
+
+ t.context.gracefulFs = {
+ stat: sinon.stub().yieldsAsync(),
+ readFile: sinon.stub().yieldsAsync(),
+ writeFile: sinon.stub().yieldsAsync(),
+ rename: sinon.stub().yieldsAsync(),
+ rm: sinon.stub().yieldsAsync(),
+ createWriteStream: sinon.stub()
+ };
+
+ t.context.Registry = await esmock.p("../../../lib/ui5Framework/maven/Registry.js", {
+ "@ui5/logger": ui5Logger,
+ "graceful-fs": t.context.gracefulFs,
+ "make-fetch-happen": t.context.makeFetchHappen,
+ });
+
+ const AbstractInstaller = await esmock.p("../../../lib/ui5Framework/AbstractInstaller.js", {
+ "@ui5/logger": ui5Logger,
+ "../../../lib/utils/fs.js": {
+ mkdirp: sinon.stub().resolves()
+ },
+ "lockfile": {
+ lock: sinon.stub().yieldsAsync(),
+ unlock: sinon.stub().yieldsAsync()
+ }
+ });
+
+ t.context.Installer = await esmock.p("../../../lib/ui5Framework/maven/Installer.js", {
+ "@ui5/logger": ui5Logger,
+ "graceful-fs": t.context.gracefulFs,
+ "../../../lib/utils/fs.js": {
+ mkdirp: sinon.stub().resolves()
+ },
+ "../../../lib/ui5Framework/maven/Registry.js": t.context.Registry,
+ "../../../lib/ui5Framework/AbstractInstaller.js": AbstractInstaller
+ });
+
+ t.context.AbstractResolver = await esmock.p("../../../lib/ui5Framework/AbstractResolver.js", {
+ "@ui5/logger": ui5Logger,
+ "node:os": {
+ homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir"))
+ },
+ });
+
+ t.context.Sapui5MavenSnapshotResolver = await esmock.p("../../../lib/ui5Framework/Sapui5MavenSnapshotResolver.js", {
+ "@ui5/logger": ui5Logger,
+ "node:os": {
+ homedir: sinon.stub().returns(path.join(fakeBaseDir, "homedir"))
+ },
+ "../../../lib/ui5Framework/AbstractResolver.js": t.context.AbstractResolver,
+ "../../../lib/ui5Framework/maven/Installer.js": t.context.Installer
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.Registry);
+ esmock.purge(t.context.Installer);
+ esmock.purge(t.context.AbstractResolver);
+ esmock.purge(t.context.Sapui5MavenSnapshotResolver);
+ delete process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL;
+});
+
+test.serial("resolveVersion", async (t) => {
+ const {Sapui5MavenSnapshotResolver, makeFetchHappen, logStub, sinon} = t.context;
+
+ makeFetchHappen.withArgs("_SNAPSHOT_URL_/com/sap/ui5/dist/sapui5-sdk-dist/maven-metadata.xml")
+ .resolves({
+ ok: true,
+ buffer: sinon.stub().resolves(`
+
+
+
+
+ 1.120.1
+ 2.0.0-rc.1
+ 1.120.1-SNAPSHOT
+ 1.123.4-SNAPSHOT
+ 2.0.0-SNAPSHOT
+ 2.0.1-SNAPSHOT
+ 2.1.2-SNAPSHOT
+
+
+
+ `)
+ });
+
+
+ // Exact SNAPSHOT versions
+ t.is(await Sapui5MavenSnapshotResolver.resolveVersion("1.123.4-SNAPSHOT"), "1.123.4-SNAPSHOT");
+ t.is(await Sapui5MavenSnapshotResolver.resolveVersion("2.0.1-SNAPSHOT"), "2.0.1-SNAPSHOT");
+
+ // latest-snapshot
+ t.is(await Sapui5MavenSnapshotResolver.resolveVersion("latest-snapshot"), "2.1.2-SNAPSHOT");
+
+ // SNAPSHOT ranges
+ t.is(await Sapui5MavenSnapshotResolver.resolveVersion("1-SNAPSHOT"), "1.123.4-SNAPSHOT");
+ t.is(await Sapui5MavenSnapshotResolver.resolveVersion("2-SNAPSHOT"), "2.1.2-SNAPSHOT");
+ t.is(await Sapui5MavenSnapshotResolver.resolveVersion("1.123-SNAPSHOT"), "1.123.4-SNAPSHOT");
+
+ // Error cases
+ await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion(""), {
+ message: `Framework version specifier "" is incorrect or not supported`
+ });
+ await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion("tag-does-not-exist"), {
+ message: `Framework version specifier "tag-does-not-exist" is incorrect or not supported`
+ });
+ await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion("invalid-tag-%20"), {
+ message: `Framework version specifier "invalid-tag-%20" is incorrect or not supported`
+ });
+
+ await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion("1.999.9"), {
+ message: `Could not resolve framework version 1.999.9. ` +
+ `Make sure the version is valid and available in the configured registry.`
+ });
+ await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion("1.0.0-SNAPSHOT"), {
+ message: `Could not resolve framework version 1.0.0-SNAPSHOT. ` +
+ `Make sure the version is valid and available in the configured registry.`
+ });
+ await t.throwsAsync(Sapui5MavenSnapshotResolver.resolveVersion("3-SNAPSHOT"), {
+ message: `Could not resolve framework version 3-SNAPSHOT. ` +
+ `Make sure the version is valid and available in the configured registry.`
+ });
+
+ t.is(logStub.warn.callCount, 0);
+ t.is(logStub.error.callCount, 0);
+});
diff --git a/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js b/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js
new file mode 100644
index 00000000000..ed9a4de6cd9
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/Sapui5MavenSnapshotResolver.js
@@ -0,0 +1,648 @@
+import test from "ava";
+import sinon from "sinon";
+import esmock from "esmock";
+import path from "node:path";
+import os from "node:os";
+
+test.beforeEach(async (t) => {
+ t.context.InstallerStub = sinon.stub();
+ t.context.fetchPackageVersionsStub = sinon.stub();
+ t.context.installPackageStub = sinon.stub();
+ t.context.readJsonStub = sinon.stub();
+ t.context.InstallerStub.callsFake(() => {
+ return {
+ fetchPackageVersions: t.context.fetchPackageVersionsStub,
+ installPackage: t.context.installPackageStub,
+ readJson: t.context.readJsonStub
+ };
+ });
+
+ process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL = "_SNAPSHOT_URL_";
+
+ t.context.yesnoStub = sinon.stub();
+ t.context.promisifyStub = sinon.stub();
+ t.context.loggerVerbose = sinon.stub();
+ t.context.loggerWarn = sinon.stub();
+ t.context.loggerInfo = sinon.stub();
+
+ t.context.Configuration = await esmock.p("../../../lib/config/Configuration.js", {});
+ t.context.configFromFile = sinon.stub(t.context.Configuration, "fromFile")
+ .resolves(new t.context.Configuration({}));
+ t.context.configToFile = sinon.stub(t.context.Configuration, "toFile").resolves();
+
+ t.context.Sapui5MavenSnapshotResolver = await esmock.p("../../../lib/ui5Framework/Sapui5MavenSnapshotResolver.js", {
+ "../../../lib/ui5Framework/maven/Installer": t.context.InstallerStub,
+ "yesno": t.context.yesnoStub,
+ "node:util": {
+ "promisify": t.context.promisifyStub
+ },
+ "@ui5/logger": {
+ getLogger: () => ({
+ verbose: t.context.loggerVerbose,
+ warning: t.context.loggerWarn,
+ info: t.context.loggerInfo,
+ })
+ },
+ "../../../lib/config/Configuration": t.context.Configuration
+ });
+
+ t.context.originalIsTty = process.stdout.isTTY;
+});
+
+test.afterEach.always((t) => {
+ process.stdout.isTTY = t.context.originalIsTty;
+ delete process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL;
+ esmock.purge(t.context.Sapui5MavenSnapshotResolver);
+ sinon.restore();
+});
+
+test.serial(
+ "Sapui5MavenSnapshotResolver: loadDistMetadata loads metadata "+
+ "once from @sapui5/distribution-metadata package", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+
+ const resolver = new Sapui5MavenSnapshotResolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ const expectedMetadata = {
+ libraries: {
+ "sap.ui.foo": {
+ "npmPackageName": "@openui5/sap.ui.foo",
+ "version": "1.75.0",
+ "dependencies": [],
+ "optionalDependencies": []
+ }
+ }
+ };
+
+ t.context.installPackageStub
+ .withArgs({
+ pkgName: "@sapui5/distribution-metadata",
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ classifier: "npm-sources",
+ extension: "zip",
+ })
+ .resolves({pkgPath: "/path/to/distribution-metadata/1.75.0"});
+
+ t.context.readJsonStub
+ .withArgs(path.join("/path", "to", "distribution-metadata", "1.75.0", "metadata.json"))
+ .resolves(expectedMetadata);
+
+ let distMetadata = await resolver.loadDistMetadata();
+ t.is(t.context.installPackageStub.callCount, 1, "Distribution metadata package should be installed once");
+ t.deepEqual(distMetadata, expectedMetadata,
+ "loadDistMetadata should resolve with expected metadata");
+
+ // Calling loadDistMetadata again should not load package again
+ distMetadata = await resolver.loadDistMetadata();
+
+ t.is(t.context.installPackageStub.callCount, 1, "Distribution metadata package should still be installed once");
+ t.deepEqual(distMetadata, expectedMetadata,
+ "Metadata should still be the expected metadata after calling loadDistMetadata again");
+
+ const libraryMetadata = await resolver.getLibraryMetadata("sap.ui.foo");
+ t.deepEqual(libraryMetadata, expectedMetadata.libraries["sap.ui.foo"],
+ "getLibraryMetadata returns metadata for one library");
+ });
+
+test.serial("Sapui5MavenSnapshotResolver: getLibraryMetadata throws", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+
+ const resolver = new Sapui5MavenSnapshotResolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata");
+ loadDistMetadataStub.resolves({
+ libraries: {}
+ });
+
+ await t.throwsAsync(resolver.getLibraryMetadata("sap.ui.foo"), {
+ message: "Could not find library \"sap.ui.foo\"",
+ });
+});
+
+test.serial("Sapui5MavenSnapshotResolver: handleLibrary", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+
+ const resolver = new Sapui5MavenSnapshotResolver({
+ cwd: "/test-project/",
+ version: "1.116.0-SNAPSHOT"
+ });
+
+ const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata");
+ loadDistMetadataStub.resolves({
+ libraries: {
+ "sap.ui.lib1": {
+ "npmPackageName": "@openui5/sap.ui.lib1",
+ "version": "1.116.0-SNAPSHOT",
+ "dependencies": [],
+ "optionalDependencies": [],
+ "gav": "x:y:z"
+ }
+ }
+ });
+
+ t.context.installPackageStub
+ .callsFake(async ({pkgName, version}) => {
+ throw new Error(`Unknown install call: ${pkgName}@${version}`);
+ })
+ .withArgs({
+ pkgName: "@openui5/sap.ui.lib1-prebuilt",
+ groupId: "x",
+ artifactId: "y",
+ version: "1.116.0-SNAPSHOT",
+ classifier: "npm-dist",
+ extension: "zip",
+ })
+ .resolves({pkgPath: "/foo/sap.ui.lib1"});
+
+
+ const promises = await resolver.handleLibrary("sap.ui.lib1");
+
+ t.true(promises.metadata instanceof Promise, "Metadata promise should be returned");
+ t.true(promises.install instanceof Promise, "Install promise should be returned");
+
+ const metadata = await promises.metadata;
+ t.deepEqual(metadata, {
+ "id": "@openui5/sap.ui.lib1-prebuilt",
+ "version": "1.116.0-SNAPSHOT",
+ "dependencies": [],
+ "optionalDependencies": []
+ }, "Expected library metadata should be returned");
+
+ t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object");
+ t.is(loadDistMetadataStub.callCount, 1, "loadDistMetadata should be called once");
+});
+
+
+test.serial("Sapui5MavenSnapshotResolver: handleLibrary - legacy version", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+
+ const resolver = new Sapui5MavenSnapshotResolver({
+ cwd: "/test-project/",
+ version: "1.75.0-SNAPSHOT"
+ });
+
+ const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata");
+ loadDistMetadataStub.resolves({
+ libraries: {
+ "sap.ui.lib1": {
+ "npmPackageName": "@openui5/sap.ui.lib1",
+ "version": "1.75.0-SNAPSHOT",
+ "dependencies": [],
+ "optionalDependencies": [],
+ "gav": "x:y:z"
+ }
+ }
+ });
+
+ t.context.installPackageStub
+ .callsFake(async ({pkgName, version}) => {
+ throw new Error(`Unknown install call: ${pkgName}@${version}`);
+ })
+ .withArgs({
+ pkgName: "@openui5/sap.ui.lib1-prebuilt",
+ groupId: "x",
+ artifactId: "y",
+ version: "1.75.0-SNAPSHOT",
+ classifier: null,
+ extension: "jar",
+ })
+ .resolves({pkgPath: "/foo/sap.ui.lib1"});
+
+
+ const promises = await resolver.handleLibrary("sap.ui.lib1");
+
+ t.true(promises.metadata instanceof Promise, "Metadata promise should be returned");
+ t.true(promises.install instanceof Promise, "Install promise should be returned");
+
+ const metadata = await promises.metadata;
+ t.deepEqual(metadata, {
+ "id": "@openui5/sap.ui.lib1-prebuilt",
+ "version": "1.75.0-SNAPSHOT",
+ "dependencies": [],
+ "optionalDependencies": []
+ }, "Expected library metadata should be returned");
+
+ t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object");
+ t.is(loadDistMetadataStub.callCount, 1, "loadDistMetadata should be called once");
+});
+
+test.serial("Sapui5MavenSnapshotResolver: handleLibrary - sources requested", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+
+ const resolver = new Sapui5MavenSnapshotResolver({
+ cwd: "/test-project/",
+ version: "1.116.0-SNAPSHOT",
+ sources: true
+ });
+
+ const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata");
+ loadDistMetadataStub.resolves({
+ libraries: {
+ "sap.ui.lib1": {
+ "npmPackageName": "@openui5/sap.ui.lib1",
+ "version": "1.116.0-SNAPSHOT",
+ "dependencies": [],
+ "optionalDependencies": [],
+ "gav": "x:y:z"
+ }
+ }
+ });
+
+ t.context.installPackageStub
+ .callsFake(async ({pkgName, version}) => {
+ throw new Error(`Unknown install call: ${pkgName}@${version}`);
+ })
+ .withArgs({
+ pkgName: "@openui5/sap.ui.lib1",
+ groupId: "x",
+ artifactId: "y",
+ version: "1.116.0-SNAPSHOT",
+ classifier: "npm-sources",
+ extension: "zip",
+ })
+ .resolves({pkgPath: "/foo/sap.ui.lib1"});
+
+
+ const promises = await resolver.handleLibrary("sap.ui.lib1");
+
+ t.true(promises.metadata instanceof Promise, "Metadata promise should be returned");
+ t.true(promises.install instanceof Promise, "Install promise should be returned");
+
+ const metadata = await promises.metadata;
+ t.deepEqual(metadata, {
+ "id": "@openui5/sap.ui.lib1",
+ "version": "1.116.0-SNAPSHOT",
+ "dependencies": [],
+ "optionalDependencies": []
+ }, "Expected library metadata should be returned");
+
+ t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object");
+ t.is(loadDistMetadataStub.callCount, 1, "loadDistMetadata should be called once");
+});
+
+test.serial("Sapui5MavenSnapshotResolver: handleLibrary - sources requested with legacy version", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+
+ const resolver = new Sapui5MavenSnapshotResolver({
+ cwd: "/test-project/",
+ version: "1.75.0-SNAPSHOT",
+ sources: true
+ });
+
+ const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata");
+ loadDistMetadataStub.resolves({
+ libraries: {
+ "sap.ui.lib1": {
+ "npmPackageName": "@openui5/sap.ui.lib1",
+ "version": "1.75.0-SNAPSHOT",
+ "dependencies": [],
+ "optionalDependencies": [],
+ "gav": "x:y:z"
+ }
+ }
+ });
+
+ t.context.installPackageStub
+ .callsFake(async ({pkgName, version}) => {
+ throw new Error(`Unknown install call: ${pkgName}@${version}`);
+ })
+ .withArgs({
+ pkgName: "@openui5/sap.ui.lib1",
+ groupId: "x",
+ artifactId: "y",
+ version: "1.75.0-SNAPSHOT",
+ classifier: "npm-sources",
+ extension: "zip",
+ })
+ .resolves({pkgPath: "/foo/sap.ui.lib1"});
+
+
+ const promises = await resolver.handleLibrary("sap.ui.lib1");
+
+ t.true(promises.metadata instanceof Promise, "Metadata promise should be returned");
+ t.true(promises.install instanceof Promise, "Install promise should be returned");
+
+ const metadata = await promises.metadata;
+ t.deepEqual(metadata, {
+ "id": "@openui5/sap.ui.lib1",
+ "version": "1.75.0-SNAPSHOT",
+ "dependencies": [],
+ "optionalDependencies": []
+ }, "Expected library metadata should be returned");
+
+ t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object");
+ t.is(loadDistMetadataStub.callCount, 1, "loadDistMetadata should be called once");
+});
+
+test.serial("Sapui5MavenSnapshotResolver: handleLibrary throws", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+
+ const resolver = new Sapui5MavenSnapshotResolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ sinon.stub(resolver, "getLibraryMetadata").resolves({});
+
+ await t.throwsAsync(resolver.handleLibrary("sap.ui.lib1"), {
+ message:
+ "Metadata is missing GAV (group, artifact and version) information. "+
+ "This might indicate an unsupported SNAPSHOT version.",
+ });
+});
+
+test.serial("Sapui5MavenSnapshotResolver: Static fetchAllVersions", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+
+ const expectedVersions = ["1.75.0-SNAPSHOT", "1.75.1-SNAPSHOT", "1.76.0-SNAPSHOT"];
+ const options = {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ };
+
+ t.context.fetchPackageVersionsStub.returns(expectedVersions);
+ sinon.stub(Sapui5MavenSnapshotResolver, "_createSnapshotEndpointUrlCallback")
+ .returns("snapshotEndpointUrlCallback");
+
+ const versions = await Sapui5MavenSnapshotResolver.fetchAllVersions(options);
+
+ t.deepEqual(versions, expectedVersions, "Fetched versions should be correct");
+
+ t.is(t.context.fetchPackageVersionsStub.callCount, 1, "fetchPackageVersions should be called once");
+ t.deepEqual(
+ t.context.fetchPackageVersionsStub.getCall(0).args,
+ [{artifactId: "sapui5-sdk-dist", groupId: "com.sap.ui5.dist"}],
+ "fetchPackageVersions should be called with expected arguments"
+ );
+
+ t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once");
+ t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new");
+ t.deepEqual(t.context.InstallerStub.getCall(0).args, [{
+ cwd: path.resolve("/cwd"),
+ snapshotEndpointUrlCb: "snapshotEndpointUrlCallback",
+ ui5DataDir: path.resolve("/ui5DataDir")
+ }], "Installer should be called with expected arguments");
+});
+
+test.serial("Sapui5MavenSnapshotResolver: Static fetchAllVersions without options", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+
+ const expectedVersions = ["1.75.0-SNAPSHOT", "1.75.1-SNAPSHOT", "1.76.0-SNAPSHOT"];
+
+ t.context.fetchPackageVersionsStub.returns(expectedVersions);
+ sinon.stub(Sapui5MavenSnapshotResolver, "_createSnapshotEndpointUrlCallback")
+ .returns("snapshotEndpointUrlCallback");
+
+ const versions = await Sapui5MavenSnapshotResolver.fetchAllVersions();
+
+ t.deepEqual(versions, expectedVersions, "Fetched versions should be correct");
+
+ t.is(t.context.fetchPackageVersionsStub.callCount, 1, "fetchPackageVersions should be called once");
+ t.deepEqual(t.context.fetchPackageVersionsStub.getCall(0).args,
+ [{artifactId: "sapui5-sdk-dist", groupId: "com.sap.ui5.dist"}],
+ "fetchPackageVersions should be called with expected arguments");
+
+ t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once");
+ t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new");
+ t.deepEqual(t.context.InstallerStub.getCall(0).args, [{
+ cwd: process.cwd(),
+ snapshotEndpointUrlCb: "snapshotEndpointUrlCallback",
+ ui5DataDir: path.join(os.homedir(), ".ui5")
+ }], "Installer should be called with expected arguments");
+});
+
+test.serial("_createSnapshotEndpointUrlCallback: Environment variable", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+ const createSnapshotEndpointUrlCallback = Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback;
+
+ const endpointCallback = await createSnapshotEndpointUrlCallback("my url");
+
+ t.is(await endpointCallback(), "_SNAPSHOT_URL_",
+ "Returned a callback resolving to value of env variable");
+});
+
+test.serial("_createSnapshotEndpointUrlCallback: Parameter", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+ const createSnapshotEndpointUrlCallback = Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback;
+
+ delete process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL; // Delete env variable for this test
+ const endpointCallback = await createSnapshotEndpointUrlCallback("my url");
+
+ t.is(await endpointCallback(), "my url",
+ "Returned a callback resolving to value of env variable");
+});
+
+test.serial("_createSnapshotEndpointUrlCallback: Fallback to configuration files", async (t) => {
+ const {Sapui5MavenSnapshotResolver} = t.context;
+ const createSnapshotEndpointUrlCallback = Sapui5MavenSnapshotResolver._createSnapshotEndpointUrlCallback;
+
+ delete process.env.UI5_MAVEN_SNAPSHOT_ENDPOINT_URL; // Delete env variable for this test
+ const resolveUrlStub = sinon.stub(Sapui5MavenSnapshotResolver, "_resolveSnapshotEndpointUrl").resolves("🐱");
+
+ const endpointCallback = await createSnapshotEndpointUrlCallback();
+
+ t.is(endpointCallback, resolveUrlStub, "Returned correct callback");
+ t.is(await endpointCallback(), "🐱", "Callback can be executed correctly");
+});
+
+test.serial("_resolveSnapshotEndpointUrl: From configuration", async (t) => {
+ const {configFromFile, configToFile, Configuration, Sapui5MavenSnapshotResolver} = t.context;
+ const resolveSnapshotEndpointUrl = Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrl;
+
+ configFromFile.resolves(new Configuration({mavenSnapshotEndpointUrl: "config-url"}));
+ const fromMavenStub = sinon.stub(Sapui5MavenSnapshotResolver, "_resolveSnapshotEndpointUrlFromMaven").resolves();
+
+ const endpoint = await resolveSnapshotEndpointUrl();
+
+ t.is(endpoint, "config-url", "Returned URL extracted from UI5 CLI configuration");
+ t.is(configFromFile.callCount, 1, "Configuration has been read once");
+ t.is(configToFile.callCount, 0, "Configuration has not been written");
+ t.is(fromMavenStub.callCount, 0, "Maven configuration has not been requested");
+});
+
+test.serial("_resolveSnapshotEndpointUrl: Maven fallback with config update", async (t) => {
+ const {configFromFile, configToFile, Sapui5MavenSnapshotResolver} = t.context;
+ const resolveSnapshotEndpointUrl = Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrl;
+
+ sinon.stub(Sapui5MavenSnapshotResolver, "_resolveSnapshotEndpointUrlFromMaven").resolves("maven-url");
+
+ const endpoint = await resolveSnapshotEndpointUrl();
+
+ t.is(endpoint, "maven-url", "Returned URL extracted from Maven settings.xml");
+ t.is(configFromFile.callCount, 1, "Configuration has been read once");
+ t.is(configToFile.callCount, 1, "Configuration has been written once");
+ t.deepEqual(configToFile.firstCall.firstArg.toJson(), {
+ mavenSnapshotEndpointUrl: "maven-url",
+ ui5DataDir: undefined
+ }, "Correct configuration has been written");
+});
+
+test.serial("_resolveSnapshotEndpointUrl: Maven fallback without config update", async (t) => {
+ const {configFromFile, configToFile, Sapui5MavenSnapshotResolver} = t.context;
+ const resolveSnapshotEndpointUrl = Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrl;
+
+ // Resolving with null
+ sinon.stub(Sapui5MavenSnapshotResolver, "_resolveSnapshotEndpointUrlFromMaven").resolves(null);
+
+ const endpoint = await resolveSnapshotEndpointUrl();
+
+ t.is(endpoint, null, "No URL resolved");
+ t.is(configFromFile.callCount, 1, "Configuration has been read once");
+ t.is(configToFile.callCount, 0, "Configuration has not been written");
+});
+
+test.serial("_resolveSnapshotEndpointUrlFromMaven", async (t) => {
+ const resolveSnapshotEndpointUrl = t.context.Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven;
+ const {promisifyStub, yesnoStub} = t.context;
+
+ process.stdout.isTTY = true;
+
+ const readStub = sinon.stub().resolves(`
+
+
+ snapshot.build
+
+
+ artifactory
+ /build-snapshots/
+
+
+
+
+ `);
+ promisifyStub.callsFake(() => readStub);
+ yesnoStub.resolves(true);
+
+ const endpoint = await resolveSnapshotEndpointUrl();
+
+ t.is(endpoint, "/build-snapshots/", "URL Extracted from settings.xml");
+});
+
+test.serial("_resolveSnapshotEndpointUrlFromMaven: No snapshot.build attribute", async (t) => {
+ const resolveSnapshotEndpointUrl = t.context.Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven;
+ const {promisifyStub, yesnoStub} = t.context;
+
+ process.stdout.isTTY = true;
+
+ const readStub = sinon.stub().resolves(`
+
+
+ deploy.build
+
+
+ artifactory
+ /build-snapshots/
+
+
+
+
+ `);
+ promisifyStub.callsFake(() => readStub);
+ yesnoStub.resolves(true);
+
+ const endpoint = await resolveSnapshotEndpointUrl();
+
+ t.is(endpoint, null, "No URL Extracted from settings.xml");
+});
+
+test.serial("_resolveSnapshotEndpointUrlFromMaven fails", async (t) => {
+ const resolveSnapshotEndpointUrl = t.context.Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven;
+ const {promisifyStub, yesnoStub, loggerVerbose, loggerWarn} = t.context;
+
+ process.stdout.isTTY = true;
+
+ const readStub = sinon.stub()
+ .onFirstCall().throws({code: "ENOENT"})
+ .onSecondCall().throws(new Error("Error"))
+ .resolves(`
+
+
+ snapshot.build
+
+
+ artifactory
+ /build-snapshots/
+
+
+
+
+ `);
+ promisifyStub.callsFake(() => readStub);
+
+ let endpoint;
+ endpoint = await resolveSnapshotEndpointUrl(".m2/settings.xml");
+ t.is(endpoint, null, "No endpoint resolved");
+ t.is(
+ loggerVerbose.getCall(0).args[0],
+ "Attempting to resolve snapshot endpoint URL from Maven configuration file at .m2/settings.xml..."
+ );
+ t.is(
+ loggerVerbose.getCall(1).args[0],
+ `File does not exist: .m2/settings.xml`
+ );
+
+ loggerVerbose.reset();
+ loggerWarn.reset();
+ endpoint = await resolveSnapshotEndpointUrl("settings.xml");
+ t.is(endpoint, null, "No endpoint resolved");
+ t.is(
+ loggerVerbose.getCall(0).args[0],
+ "Attempting to resolve snapshot endpoint URL from Maven configuration file at settings.xml..."
+ );
+ t.is(
+ loggerWarn.getCall(0).args[0],
+ "Failed to read Maven configuration file from settings.xml: Error"
+ );
+
+ loggerVerbose.reset();
+ loggerWarn.reset();
+ yesnoStub.resolves(false);
+ endpoint = await resolveSnapshotEndpointUrl();
+
+ t.is(endpoint, null, "URL is not extracted after user rejection");
+ t.is(
+ loggerVerbose.getCall(1).args[0],
+ "User rejected usage of the resolved URL"
+ );
+});
+
+test.serial("_resolveSnapshotEndpointUrlFromMaven no TTY", async (t) => {
+ const resolveSnapshotEndpointUrl = t.context.Sapui5MavenSnapshotResolver._resolveSnapshotEndpointUrlFromMaven;
+ const {promisifyStub, yesnoStub} = t.context;
+
+ process.stdout.isTTY = false;
+
+ const readStub = sinon.stub().resolves(`
+
+
+ snapshot.build
+
+
+ artifactory
+ /build-snapshots/
+
+
+
+
+ `);
+ promisifyStub.callsFake(() => readStub);
+ yesnoStub.resolves(true);
+
+ const endpoint = await resolveSnapshotEndpointUrl(".m2/settings.xml");
+
+ t.is(readStub.callCount, 0, "read did not get called");
+ t.is(yesnoStub.callCount, 0, "yesno did not get called");
+ t.is(endpoint, null, "No URL got extracted");
+});
diff --git a/packages/project/test/lib/ui5framework/Sapui5Resolver.integration.js b/packages/project/test/lib/ui5framework/Sapui5Resolver.integration.js
new file mode 100644
index 00000000000..3b04805ad42
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/Sapui5Resolver.integration.js
@@ -0,0 +1,200 @@
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+import path from "node:path";
+
+const __dirname = import.meta.dirname;
+
+// Use path within project as mocking base directory to reduce chance of side effects
+// in case mocks/stubs do not work and real fs is used
+const fakeBaseDir = path.join(__dirname, "fake-tmp");
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.logStub = {
+ info: sinon.stub(),
+ verbose: sinon.stub(),
+ silly: sinon.stub(),
+ warn: sinon.stub(),
+ error: sinon.stub(),
+ isLevelEnabled: sinon.stub().returns(false),
+ _getLogger: sinon.stub()
+ };
+ const ui5Logger = {
+ getLogger: sinon.stub().returns(t.context.logStub)
+ };
+
+ t.context.pacote = {
+ packument: sinon.stub().callsFake(async (...args) => {
+ throw new Error(`pacote.packument stub called with unknown args: ${args}`);
+ })
+ };
+
+ t.context.NpmcliConfig = sinon.stub().returns({
+ load: sinon.stub().resolves(),
+ flat: {
+ registry: "https://registry.fake"
+ }
+ });
+
+ t.context.Registry = await esmock.p("../../../lib/ui5Framework/npm/Registry.js", {
+ "@ui5/logger": ui5Logger,
+ "pacote": t.context.pacote,
+ "@npmcli/config": {
+ "default": t.context.NpmcliConfig
+ }
+ });
+
+ const AbstractInstaller = await esmock.p("../../../lib/ui5Framework/AbstractInstaller.js", {
+ "@ui5/logger": ui5Logger,
+ "../../../lib/utils/fs.js": {
+ mkdirp: sinon.stub().resolves()
+ },
+ "lockfile": {
+ lock: sinon.stub().yieldsAsync(),
+ unlock: sinon.stub().yieldsAsync()
+ }
+ });
+
+ t.context.Installer = await esmock.p("../../../lib/ui5Framework/npm/Installer.js", {
+ "@ui5/logger": ui5Logger,
+ "graceful-fs": {
+ rename: sinon.stub().yieldsAsync(),
+ },
+ "../../../lib/utils/fs.js": {
+ mkdirp: sinon.stub().resolves()
+ },
+ "../../../lib/ui5Framework/npm/Registry.js": t.context.Registry,
+ "../../../lib/ui5Framework/AbstractInstaller.js": AbstractInstaller
+ });
+
+ t.context.AbstractResolver = await esmock.p("../../../lib/ui5Framework/AbstractResolver.js", {
+ "@ui5/logger": ui5Logger,
+ "node:os": {
+ homedir: sinon.stub().returns(path.join(fakeBaseDir, "datadir"))
+ },
+ });
+
+ t.context.Sapui5Resolver = await esmock.p("../../../lib/ui5Framework/Sapui5Resolver.js", {
+ "@ui5/logger": ui5Logger,
+ "node:os": {
+ homedir: sinon.stub().returns(path.join(fakeBaseDir, "datadir"))
+ },
+ "../../../lib/ui5Framework/AbstractResolver.js": t.context.AbstractResolver,
+ "../../../lib/ui5Framework/npm/Installer.js": t.context.Installer
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.Registry);
+ esmock.purge(t.context.Installer);
+ esmock.purge(t.context.AbstractResolver);
+ esmock.purge(t.context.Sapui5Resolver);
+});
+
+test.serial("resolveVersion", async (t) => {
+ const {Sapui5Resolver, pacote, logStub, NpmcliConfig} = t.context;
+
+ pacote.packument
+ .withArgs("@sapui5/distribution-metadata")
+ .resolves({
+ "versions": {
+ "1.120.1": "",
+ "1.120.0": "",
+ "1.119.0": "",
+ "1.118.0": "",
+ "2.0.0-rc.1": "",
+ "1.123.4-SNAPSHOT": ""
+ },
+ "dist-tags": {
+ // NOTE: latest does not correspond to highest version in order to verify
+ // that this tag is used instead of picking the highest version
+ "latest": "1.120.0",
+
+ "next": "2.0.0-rc.1",
+
+ // NOTE: Tag ends with "-snapshot" in order to verify that the special handling
+ // of that
+ "not-a-snapshot": "1.118.0"
+ }
+ });
+
+ const defaultCwd = process.cwd();
+ const defaultUi5DataDir = path.join(fakeBaseDir, "datadir", ".ui5");
+
+ // Generic testing without and with options argument
+ const optionsArguments = [
+ undefined,
+ {
+ cwd: path.join(fakeBaseDir, "custom-cwd"),
+ ui5DataDir: path.join(fakeBaseDir, "custom-datadir", ".ui5")
+ }
+ ];
+ for (const options of optionsArguments) {
+ // Reset calls to be able to check them per for-loop run
+ NpmcliConfig.resetHistory();
+ pacote.packument.resetHistory();
+
+ // Ranges
+ t.is(await Sapui5Resolver.resolveVersion("1", options), "1.120.1");
+ t.is(await Sapui5Resolver.resolveVersion("1.120", options), "1.120.1");
+ t.is(await Sapui5Resolver.resolveVersion("1.x", options), "1.120.1");
+ t.is(await Sapui5Resolver.resolveVersion("1.x.x", options), "1.120.1");
+ t.is(await Sapui5Resolver.resolveVersion("^1", options), "1.120.1");
+ t.is(await Sapui5Resolver.resolveVersion("*", options), "1.120.1");
+
+ // Tags
+ t.is(await Sapui5Resolver.resolveVersion("latest", options), "1.120.0");
+ t.is(await Sapui5Resolver.resolveVersion("next", options), "2.0.0-rc.1");
+ t.is(await Sapui5Resolver.resolveVersion("not-a-snapshot", options), "1.118.0");
+
+ // Exact versions
+ t.is(await Sapui5Resolver.resolveVersion("1.118.0", options), "1.118.0");
+ t.is(await Sapui5Resolver.resolveVersion("2.0.0-rc.1", options), "2.0.0-rc.1");
+ t.is(await Sapui5Resolver.resolveVersion("1.123.4-SNAPSHOT", options), "1.123.4-SNAPSHOT");
+
+ // SNAPSHOT ranges
+ t.is(await Sapui5Resolver.resolveVersion("1-SNAPSHOT", options), "1.123.4-SNAPSHOT");
+ t.is(await Sapui5Resolver.resolveVersion("1.123-SNAPSHOT", options), "1.123.4-SNAPSHOT");
+
+ // Error cases
+ await t.throwsAsync(Sapui5Resolver.resolveVersion("", options), {
+ message: `Framework version specifier "" is incorrect or not supported`
+ });
+ await t.throwsAsync(Sapui5Resolver.resolveVersion("tag-does-not-exist", options), {
+ message: `Could not resolve framework version via tag 'tag-does-not-exist'. ` +
+ `Make sure the tag is available in the configured registry.`
+ });
+ await t.throwsAsync(Sapui5Resolver.resolveVersion("invalid-tag-%20", options), {
+ message: `Framework version specifier "invalid-tag-%20" is incorrect or not supported`
+ });
+
+ await t.throwsAsync(Sapui5Resolver.resolveVersion("1.999.9", options), {
+ message: `Could not resolve framework version 1.999.9. ` +
+ `Make sure the version is valid and available in the configured registry.`
+ });
+ await t.throwsAsync(Sapui5Resolver.resolveVersion("1.0.0", options), {
+ message: `Could not resolve framework version 1.0.0. ` +
+ `Note that SAPUI5 framework libraries can only be consumed by the UI5 CLI ` +
+ `starting with SAPUI5 v1.76.0`
+ });
+ await t.throwsAsync(Sapui5Resolver.resolveVersion("^999", options), {
+ message: `Could not resolve framework version ^999. ` +
+ `Make sure the version is valid and available in the configured registry.`
+ });
+
+ // Check whether options have been passed as expected
+ t.true(NpmcliConfig.alwaysCalledWithNew());
+ t.true(NpmcliConfig.alwaysCalledWithMatch(sinonGlobal.match({
+ cwd: options?.cwd ?? defaultCwd
+ })));
+ t.true(pacote.packument.alwaysCalledWithMatch("@sapui5/distribution-metadata", {
+ cache: path.join(options?.ui5DataDir ?? defaultUi5DataDir, "framework", "cacache")
+ }));
+ }
+
+ t.is(logStub.warn.callCount, 0);
+ t.is(logStub.error.callCount, 0);
+});
diff --git a/packages/project/test/lib/ui5framework/Sapui5Resolver.js b/packages/project/test/lib/ui5framework/Sapui5Resolver.js
new file mode 100644
index 00000000000..63288d356ed
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/Sapui5Resolver.js
@@ -0,0 +1,257 @@
+import test from "ava";
+import sinon from "sinon";
+import esmock from "esmock";
+import path from "node:path";
+import os from "node:os";
+import Openui5Resolver from "../../../lib/ui5Framework/Openui5Resolver.js";
+
+test.beforeEach(async (t) => {
+ t.context.InstallerStub = sinon.stub();
+ t.context.fetchPackageDistTags = sinon.stub();
+ t.context.fetchPackageVersionsStub = sinon.stub();
+ t.context.installPackageStub = sinon.stub();
+ t.context.getTargetDirForPackageStub = sinon.stub();
+ t.context.readJsonStub = sinon.stub();
+ t.context.InstallerStub.callsFake(() => {
+ return {
+ fetchPackageDistTags: t.context.fetchPackageDistTags,
+ fetchPackageVersions: t.context.fetchPackageVersionsStub,
+ installPackage: t.context.installPackageStub,
+ getTargetDirForPackage: t.context.getTargetDirForPackageStub,
+ readJson: t.context.readJsonStub
+ };
+ });
+
+ t.context.Sapui5Resolver = await esmock("../../../lib/ui5Framework/Sapui5Resolver.js", {
+ "../../../lib/ui5Framework/npm/Installer": t.context.InstallerStub
+ });
+});
+
+test.afterEach.always(() => {
+ sinon.restore();
+});
+
+test.serial(
+ "Sapui5Resolver: loadDistMetadata loads metadata once from @sapui5/distribution-metadata package", async (t) => {
+ const {Sapui5Resolver} = t.context;
+
+ const resolver = new Sapui5Resolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ t.context.getTargetDirForPackageStub.callsFake(({pkgName, version}) => {
+ throw new Error(
+ `getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}`);
+ }).withArgs({
+ pkgName: "@sapui5/distribution-metadata",
+ version: "1.75.0"
+ }).returns(path.join("/path", "to", "distribution-metadata", "1.75.0"));
+ t.context.installPackageStub.withArgs({
+ pkgName: "@sapui5/distribution-metadata",
+ version: "1.75.0"
+ }).resolves({pkgPath: path.join("/path", "to", "distribution-metadata", "1.75.0")});
+
+ const expectedMetadata = {
+ libraries: {
+ "sap.ui.foo": {
+ "npmPackageName": "@openui5/sap.ui.foo",
+ "version": "1.75.0",
+ "dependencies": [],
+ "optionalDependencies": []
+ }
+ }
+ };
+ t.context.readJsonStub
+ .withArgs(path.join("/path", "to", "distribution-metadata", "1.75.0", "metadata.json"))
+ .resolves(expectedMetadata);
+
+ let distMetadata = await resolver.loadDistMetadata();
+ t.is(t.context.installPackageStub.callCount, 1, "Distribution metadata package should be installed once");
+ t.deepEqual(distMetadata, expectedMetadata,
+ "loadDistMetadata should resolve with expected metadata");
+
+ // Calling loadDistMetadata again should not load package again
+ distMetadata = await resolver.loadDistMetadata();
+
+ t.is(t.context.installPackageStub.callCount, 1, "Distribution metadata package should still be installed once");
+ t.deepEqual(distMetadata, expectedMetadata,
+ "Metadata should still be the expected metadata after calling loadDistMetadata again");
+
+ const libraryMetadata = await resolver.getLibraryMetadata("sap.ui.foo");
+ t.deepEqual(libraryMetadata, expectedMetadata.libraries["sap.ui.foo"],
+ "getLibraryMetadata returns metadata for one library");
+ });
+
+test.serial("Sapui5Resolver: handleLibrary", async (t) => {
+ const {Sapui5Resolver} = t.context;
+
+ const resolver = new Sapui5Resolver({
+ cwd: "/test-project/",
+ version: "1.75.0"
+ });
+
+ const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata");
+ loadDistMetadataStub.resolves({
+ libraries: {
+ "sap.ui.lib1": {
+ "npmPackageName": "@openui5/sap.ui.lib1",
+ "version": "1.75.0",
+ "dependencies": [],
+ "optionalDependencies": []
+ }
+ }
+ });
+
+ t.context.installPackageStub
+ .callsFake(async ({pkgName, version}) => {
+ throw new Error(`Unknown install call: ${pkgName}@${version}`);
+ })
+ .withArgs({pkgName: "@openui5/sap.ui.lib1", version: "1.75.0"}).resolves({pkgPath: "/foo/sap.ui.lib1"});
+
+
+ const promises = await resolver.handleLibrary("sap.ui.lib1");
+
+ t.true(promises.metadata instanceof Promise, "Metadata promise should be returned");
+ t.true(promises.install instanceof Promise, "Install promise should be returned");
+
+ const metadata = await promises.metadata;
+ t.deepEqual(metadata, {
+ "id": "@openui5/sap.ui.lib1",
+ "version": "1.75.0",
+ "dependencies": [],
+ "optionalDependencies": []
+ }, "Expected library metadata should be returned");
+
+ t.deepEqual(await promises.install, {pkgPath: "/foo/sap.ui.lib1"}, "Install should resolve with expected object");
+ t.is(loadDistMetadataStub.callCount, 1, "loadDistMetadata should be called once");
+});
+
+test.serial("Sapui5Resolver: Static _getInstaller", (t) => {
+ const {Sapui5Resolver} = t.context;
+
+ const options = {
+ cwd: "/cwd",
+ ui5DataDir: "/ui5DataDir"
+ };
+
+ const installer = Sapui5Resolver._getInstaller(options);
+
+ t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once");
+ t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new");
+ t.is(installer, t.context.InstallerStub.getCall(0).returnValue, "Installer instance is returned");
+ t.deepEqual(t.context.InstallerStub.getCall(0).args, [{
+ cwd: path.resolve("/cwd"),
+ ui5DataDir: path.resolve("/ui5DataDir")
+ }], "Installer should be called with expected arguments");
+});
+
+test.serial("Sapui5Resolver: Static _getInstaller without options", (t) => {
+ const {Sapui5Resolver} = t.context;
+
+ const installer = Sapui5Resolver._getInstaller();
+
+ t.is(t.context.InstallerStub.callCount, 1, "Installer should be called once");
+ t.true(t.context.InstallerStub.calledWithNew(), "Installer should be called with new");
+ t.is(installer, t.context.InstallerStub.getCall(0).returnValue, "Installer instance is returned");
+ t.deepEqual(t.context.InstallerStub.getCall(0).args, [{
+ cwd: process.cwd(),
+ ui5DataDir: path.join(os.homedir(), ".ui5")
+ }], "Installer should be called with expected arguments");
+});
+
+test.serial("Sapui5Resolver: Static fetchAllVersions", async (t) => {
+ const {Sapui5Resolver} = t.context;
+
+ const expectedVersions = ["1.75.0", "1.75.1", "1.76.0"];
+
+ t.context.fetchPackageVersionsStub.returns(expectedVersions);
+
+ const getInstallerSpy = sinon.spy(Sapui5Resolver, "_getInstaller");
+
+ const versions = await Sapui5Resolver.fetchAllVersions();
+
+ t.deepEqual(versions, expectedVersions, "Fetched versions should be correct");
+
+ t.is(t.context.fetchPackageVersionsStub.callCount, 1, "fetchPackageVersions should be called once");
+ t.deepEqual(t.context.fetchPackageVersionsStub.getCall(0).args, [{pkgName: "@sapui5/distribution-metadata"}],
+ "fetchPackageVersions should be called with expected arguments");
+ t.is(getInstallerSpy.callCount, 1, "_getInstaller should be called once");
+ t.is(getInstallerSpy.getCall(0).args[0], undefined, "_getInstaller should be called without any options");
+});
+
+test.serial("Sapui5Resolver: Static fetchAllTags", async (t) => {
+ const {Sapui5Resolver} = t.context;
+
+ const expectedTags = ["latest", "latest-1.71", "latest-1"];
+
+ t.context.fetchPackageDistTags.returns(expectedTags);
+
+ const getInstallerSpy = sinon.spy(Sapui5Resolver, "_getInstaller");
+
+ const tags = await Sapui5Resolver.fetchAllTags();
+
+ t.deepEqual(tags, expectedTags, "Fetched tags should be correct");
+
+ t.is(t.context.fetchPackageDistTags.callCount, 1, "fetchPackageVersions should be called once");
+ t.deepEqual(t.context.fetchPackageDistTags.getCall(0).args, [{pkgName: "@sapui5/distribution-metadata"}],
+ "fetchPackageVersions should be called with expected arguments");
+
+ t.is(getInstallerSpy.callCount, 1, "_getInstaller should be called once");
+ t.is(getInstallerSpy.getCall(0).args[0], undefined, "_getInstaller should be called without any options");
+});
+
+test.serial(
+ "Sapui5Resolver: getLibraryMetadata should use Openui5Resolver for @openui5/ modules in 1.77.x", async (t) => {
+ const {Sapui5Resolver} = t.context;
+
+ const resolver = new Sapui5Resolver({
+ cwd: "/test-project/",
+ version: "1.77.7"
+ });
+
+ const openui5LibraryMetadata = {
+ "id": "@openui5/sap.ui.lib3",
+ "version": "1.77.4",
+ "dependencies": [
+ "@openui5/sap.ui.lib1"
+ ],
+ "optionalDependencies": [
+ "@openui5/sap.ui.lib2"
+ ]
+ };
+ const expectedMetadata = {
+ "npmPackageName": "@openui5/sap.ui.lib3",
+ "version": "1.77.4",
+ "dependencies": [
+ "@openui5/sap.ui.lib1"
+ ],
+ "optionalDependencies": [
+ "@openui5/sap.ui.lib2"
+ ]
+ };
+
+ const openui5GetLibraryMetadataStub = sinon.stub(Openui5Resolver.prototype, "getLibraryMetadata");
+ openui5GetLibraryMetadataStub.resolves(openui5LibraryMetadata);
+
+ const loadDistMetadataStub = sinon.stub(resolver, "loadDistMetadata");
+ loadDistMetadataStub.resolves({
+ libraries: {
+ "sap.ui.lib1": {
+ "npmPackageName": "@openui5/sap.ui.lib1",
+ "version": "1.77.4",
+ "dependencies": [],
+ "optionalDependencies": []
+ }
+ }
+ });
+
+ const metadata = await resolver.getLibraryMetadata("sap.ui.lib1");
+ t.deepEqual(metadata, expectedMetadata, "Metadata should be equal to expected OpenUI5 metadata");
+
+ t.is(openui5GetLibraryMetadataStub.callCount, 1, "Openui5Resolver#getLibraryMetadata should be called once");
+ t.deepEqual(openui5GetLibraryMetadataStub.getCall(0).args, ["sap.ui.lib1"],
+ "Openui5Resolver#getLibraryMetadata should be called with library name");
+ t.is(openui5GetLibraryMetadataStub.getCall(0).thisValue._version, "1.77.4",
+ "Openui5Resolver should be created with @openui5 library version");
+ });
diff --git a/packages/project/test/lib/ui5framework/maven/Installer.js b/packages/project/test/lib/ui5framework/maven/Installer.js
new file mode 100644
index 00000000000..86b00754cdb
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/maven/Installer.js
@@ -0,0 +1,1084 @@
+import test from "ava";
+import sinon from "sinon";
+import esmock from "esmock";
+import path from "node:path";
+import fs from "graceful-fs";
+
+test.beforeEach(async (t) => {
+ t.context.mkdirpStub = sinon.stub().resolves();
+ t.context.rmrfStub = sinon.stub().resolves();
+ t.context.readFileStub = sinon.stub();
+ t.context.writeFileStub = sinon.stub();
+ t.context.renameStub = sinon.stub().returns();
+ t.context.rmStub = sinon.stub().returns();
+ t.context.statStub = sinon.stub().returns();
+
+ t.context.promisifyStub = sinon.stub();
+ t.context.promisifyStub.withArgs(fs.readFile).callsFake(() => t.context.readFileStub);
+ t.context.promisifyStub.withArgs(fs.writeFile).callsFake(() => t.context.writeFileStub);
+ t.context.promisifyStub.withArgs(fs.rename).callsFake(() => t.context.renameStub);
+ t.context.promisifyStub.withArgs(fs.rm).callsFake(() => t.context.rmStub);
+ t.context.promisifyStub.withArgs(fs.stat).callsFake(() => t.context.statStub);
+
+ t.context.lockStub = sinon.stub();
+ t.context.unlockStub = sinon.stub();
+ t.context.zipStub = class StreamZipStub {
+ extract = sinon.stub().resolves();
+ close = sinon.stub().resolves();
+ };
+
+ t.context.registryRequestMavenMetadataStub = sinon.stub().resolves();
+ t.context.registryRequestArtifactStub = sinon.stub().resolves();
+
+ t.context.RegistryConstructorStub = sinon.stub().returns({
+ requestMavenMetadata: t.context.registryRequestMavenMetadataStub,
+ requestArtifact: t.context.registryRequestArtifactStub
+ });
+
+ t.context.AbstractInstaller = await esmock.p("../../../../lib/ui5Framework/AbstractInstaller.js", {
+ "../../../../lib/utils/fs.js": {
+ mkdirp: t.context.mkdirpStub,
+ rmrf: t.context.rmrfStub
+ },
+ "lockfile": {
+ lock: t.context.lockStub,
+ unlock: t.context.unlockStub
+ }
+ });
+
+ t.context.Installer = await esmock.p("../../../../lib/ui5Framework/maven/Installer.js", {
+ "../../../../lib/ui5Framework/maven/Registry.js": t.context.RegistryConstructorStub,
+ "../../../../lib/ui5Framework/AbstractInstaller.js": t.context.AbstractInstaller,
+ "../../../../lib/utils/fs.js": {
+ mkdirp: t.context.mkdirpStub
+ },
+ "node:util": {
+ "promisify": t.context.promisifyStub,
+ },
+ "node-stream-zip": {
+ "async": t.context.zipStub
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ sinon.restore();
+ esmock.purge(t.context.AbstractInstaller);
+ esmock.purge(t.context.Installer);
+});
+
+test.serial("constructor", (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+ t.true(installer instanceof Installer, "Constructor returns instance of class");
+ t.is(installer._artifactsDir, path.join("/ui5Data/", "framework", "artifacts"));
+ t.is(installer._packagesDir, path.join("/ui5Data/", "framework", "packages"));
+ t.is(installer._stagingDir, path.join("/ui5Data/", "framework", "staging"));
+ t.is(installer._metadataDir, path.join("/ui5Data/", "framework", "metadata"));
+ t.is(installer._lockDir, path.join("/ui5Data/", "framework", "locks"));
+});
+
+test.serial("constructor requires 'ui5DataDir'", (t) => {
+ const {Installer} = t.context;
+
+ t.throws(() => {
+ new Installer({
+ cwd: "/cwd/"
+ });
+ }, {message: `Installer: Missing parameter "ui5DataDir"`});
+});
+
+test.serial("constructor requires 'snapshotEndpointUrlCb'", (t) => {
+ const {Installer} = t.context;
+
+ t.throws(() => {
+ new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data"
+ });
+ }, {message: `Installer: Missing Snapshot-Endpoint URL callback parameter`});
+});
+
+test.serial("getRegistry", async (t) => {
+ const {Installer, RegistryConstructorStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url")
+ });
+
+ const registry1 = await installer.getRegistry();
+
+ t.truthy(registry1, "Created registry");
+ t.is(RegistryConstructorStub.callCount, 1, "Registry constructor called once");
+ t.deepEqual(RegistryConstructorStub.firstCall.firstArg, {
+ endpointUrl: "endpoint-url"
+ }, "Registry constructor called with correct endpoint URL");
+
+ const registry2 = await installer.getRegistry();
+ t.is(registry2, registry1, "Registry instance is cached");
+ t.is(RegistryConstructorStub.callCount, 1, "Registry constructor still only called once");
+});
+
+test.serial("getRegistry: Missing endpoint URL", async (t) => {
+ const {Installer, RegistryConstructorStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => Promise.resolve(null)
+ });
+
+ const err = await t.throwsAsync(installer.getRegistry());
+ t.is(err.message, "Installer: Missing or empty Maven repository URL for snapshot consumption. " +
+ "This URL is required for consuming snapshot versions of UI5 libraries. " +
+ "Please configure the correct URL using the following command: " +
+ "'ui5 config set mavenSnapshotEndpointUrl '",
+ "Threw with expected error message");
+
+ t.is(RegistryConstructorStub.callCount, 0, "Registry constructor did not get called");
+});
+
+test.serial("fetchPackageVersions", async (t) => {
+ const {Installer, registryRequestMavenMetadataStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url")
+ });
+
+ registryRequestMavenMetadataStub
+ .resolves({
+ versioning: {
+ versions: {
+ version: ["1.0.0", "2.0.0", "2.0.0-SNAPSHOT", "3.0.0", "5.0.0-SNAPSHOT"]
+ }
+ }
+ });
+
+ const packageVersions = await installer.fetchPackageVersions({groupId: "ui5.corp", artifactId: "great-thing"});
+
+ t.deepEqual(packageVersions, ["2.0.0-SNAPSHOT", "5.0.0-SNAPSHOT"], "Should resolve with expected versions");
+
+ t.is(registryRequestMavenMetadataStub.callCount, 1, "requestPackagePackument should be called once");
+ t.deepEqual(registryRequestMavenMetadataStub.getCall(0).args[0], {groupId: "ui5.corp", artifactId: "great-thing"},
+ "requestMavenMetadata was called with correct arguments");
+});
+
+test.serial("fetchPackageVersions throws", async (t) => {
+ const {Installer, registryRequestMavenMetadataStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url")
+ });
+
+ registryRequestMavenMetadataStub.resolves({});
+
+ await t.throwsAsync(
+ installer.fetchPackageVersions({
+ groupId: "ui5.corp",
+ artifactId: "great-thing",
+ }),
+ {message: "Missing Maven metadata for artifact ui5.corp:great-thing"}
+ );
+});
+
+test.serial("_getLockPath", (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ const lockPath = installer._getLockPath("package-@openui5/sap.ui.lib1@1.2.3-SNAPSHOT");
+
+ t.is(lockPath, path.join("/ui5Data/", "framework", "locks", "package-@openui5-sap.ui.lib1@1.2.3-SNAPSHOT.lock"));
+});
+
+test.serial("readJson", async (t) => {
+ const jsonStub = {json: "response"};
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ t.context.readFileStub.resolves(JSON.stringify(jsonStub));
+
+ const jsonResponse = await installer.readJson("package-@openui5/sap.ui.lib1@1.2.3-SNAPSHOT");
+
+ t.deepEqual(jsonResponse, jsonStub);
+});
+
+test.serial("installPackage", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ const removeArtifactStub = sinon.stub().resolves();
+ const fetchArtifactMetadataStub = sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"});
+ sinon.stub(installer, "_pathExists").resolves(false);
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+ const installArtifactStub = sinon.stub(installer, "installArtifact").resolves({
+ artifactPath: "/ui5Data/framework/artifacts/com_sap_ui5_dist-sapui5-sdk-dist/5/npm-sources.zip",
+ removeArtifact: removeArtifactStub
+ });
+
+ const installedPackage = await installer.installPackage({
+ pkgName: "@sapui5/distribution-metadata",
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ classifier: "npm-sources",
+ extension: "zip",
+ });
+
+ t.deepEqual(
+ installedPackage,
+ {pkgPath:
+ path.join("/ui5Data/", "framework", "packages", "@sapui5", "distribution-metadata", "5")},
+ "Install the correct package"
+ );
+
+ t.is(fetchArtifactMetadataStub.callCount, 1, "fetchArtifactMetadataStub got called once");
+ t.deepEqual(fetchArtifactMetadataStub.firstCall.firstArg, {
+ pkgName: "@sapui5/distribution-metadata",
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ classifier: "npm-sources",
+ extension: "zip",
+ }, "fetchArtifactMetadataStub got called with expected arguments");
+
+ t.is(installArtifactStub.callCount, 1, "installArtifact got called once");
+ t.deepEqual(installArtifactStub.firstCall.firstArg, {
+ revision: "5",
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ classifier: "npm-sources",
+ extension: "zip",
+ }, "installArtifact got called with the expected parameters");
+ t.is(removeArtifactStub.callCount, 1, "removeArtifact got called once");
+});
+
+test.serial("installPackage: No classifier", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ const removeArtifactStub = sinon.stub().resolves();
+ const fetchArtifactMetadataStub = sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"});
+ sinon.stub(installer, "_pathExists").resolves(false);
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+ const installArtifactStub = sinon.stub(installer, "installArtifact").resolves({
+ artifactPath: "/ui5Data/framework/artifacts/com_sap_ui5_dist-sapui5-sdk-dist/5/npm-sources.zip",
+ removeArtifact: removeArtifactStub
+ });
+
+ const installedPackage = await installer.installPackage({
+ pkgName: "@sapui5/distribution-metadata",
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ classifier: null,
+ extension: "jar",
+ });
+
+ t.deepEqual(
+ installedPackage,
+ {pkgPath:
+ path.join("/ui5Data/", "framework", "packages", "@sapui5", "distribution-metadata", "5")},
+ "Install the correct package"
+ );
+
+ t.is(fetchArtifactMetadataStub.callCount, 1, "fetchArtifactMetadataStub got called once");
+ t.deepEqual(fetchArtifactMetadataStub.firstCall.firstArg, {
+ pkgName: "@sapui5/distribution-metadata",
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ classifier: null,
+ extension: "jar",
+ }, "fetchArtifactMetadataStub got called with expected arguments");
+
+ t.is(installArtifactStub.callCount, 1, "installArtifact got called once");
+ t.deepEqual(installArtifactStub.firstCall.firstArg, {
+ revision: "5",
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ classifier: null,
+ extension: "jar",
+ }, "installArtifact got called with the expected parameters");
+ t.is(removeArtifactStub.callCount, 1, "removeArtifact got called once");
+});
+
+test.serial("installPackage: Already installed", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"});
+ sinon.stub(installer, "_projectExists").resolves(true);
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+ const installArtifactStub = sinon.stub(installer, "installArtifact");
+
+ const installedPackage = await installer.installPackage({
+ pkgName: "@sapui5/distribution-metadata",
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ classifier: "npm-sources",
+ extension: "jar",
+ });
+
+ t.deepEqual(
+ installedPackage,
+ {pkgPath:
+ path.join("/ui5Data/", "framework", "packages", "@sapui5", "distribution-metadata", "5")},
+ "Install the correct package"
+ );
+
+ t.is(installArtifactStub.callCount, 0, "installArtifact did not get called");
+});
+
+test.serial("installPackage: Already installed only after lock acquired", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"});
+ sinon.stub(installer, "_projectExists")
+ .onFirstCall().resolves(false)
+ .onSecondCall().resolves(true);
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+ const installArtifactStub = sinon.stub(installer, "installArtifact");
+
+ const installedPackage = await installer.installPackage({
+ pkgName: "@sapui5/distribution-metadata",
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ classifier: "npm-sources",
+ extension: "jar",
+ });
+
+ t.deepEqual(
+ installedPackage,
+ {pkgPath:
+ path.join("/ui5Data/", "framework", "packages", "@sapui5", "distribution-metadata", "5")},
+ "Install the correct package"
+ );
+
+ t.is(installArtifactStub.callCount, 0, "installArtifact did not get called");
+});
+
+test.serial("installArtifact", async (t) => {
+ const {Installer, rmStub, registryRequestArtifactStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: async () => "url"
+ });
+
+ const fetchArtifactMetadataStub = sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"});
+ sinon.stub(installer, "_pathExists").resolves(false);
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+
+ const installedArtifact = await installer.installArtifact({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "jar",
+ classifier: null
+ });
+
+ const expectedPath = path.join("/ui5Data/", "framework", "artifacts", "com_sap_ui5_dist-sapui5-sdk-dist", "5.jar");
+ t.is(
+ installedArtifact.artifactPath,
+ expectedPath,
+ "artifactPath correctly resolved"
+ );
+
+ t.is(fetchArtifactMetadataStub.callCount, 1, "fetchArtifactMetadataStub got called once");
+ t.deepEqual(fetchArtifactMetadataStub.firstCall.firstArg, {
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ classifier: null,
+ extension: "jar",
+ }, "fetchArtifactMetadataStub got called with expected arguments");
+
+ t.is(registryRequestArtifactStub.callCount, 1, "Registry#requestArtifact got called once");
+ t.deepEqual(registryRequestArtifactStub.firstCall.firstArg, {
+ revision: "5",
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ classifier: null,
+ version: "1.75.0",
+ extension: "jar",
+ }, "Registry#requestArtifact got called with expected coordinates");
+ t.is(registryRequestArtifactStub.firstCall.args[1],
+ path.join("/ui5Data/", "framework", "staging", "com.sap.ui5.dist_sapui5-sdk-dist_5_jar"),
+ "Registry#requestArtifact got called with expected target directory");
+
+ t.is(
+ typeof installedArtifact.removeArtifact,
+ "function",
+ "removeArtifact method"
+ );
+ rmStub.resetHistory();
+ await installedArtifact.removeArtifact();
+ t.is(rmStub.callCount, 1, "fs.rm got called once");
+ t.is(rmStub.firstCall.firstArg, expectedPath, "fs.rm got called with expected argument");
+});
+
+
+test.serial("installArtifact: Target revision provided", async (t) => {
+ const {Installer, rmStub, registryRequestArtifactStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: async () => "url"
+ });
+
+ const fetchArtifactMetadataStub = sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"});
+ sinon.stub(installer, "_pathExists").resolves(false);
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+
+ const installedArtifact = await installer.installArtifact({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "zip",
+ classifier: "npm-sources",
+ revision: "16"
+ });
+
+ const expectedPath = path.join("/ui5Data/", "framework", "artifacts",
+ "com_sap_ui5_dist-sapui5-sdk-dist", "16", "npm-sources.zip");
+ t.is(
+ installedArtifact.artifactPath,
+ expectedPath,
+ "artifactPath correctly resolved"
+ );
+
+ t.is(fetchArtifactMetadataStub.callCount, 0, "fetchArtifactMetadataStub did not get called");
+
+ t.is(registryRequestArtifactStub.callCount, 1, "Registry#requestArtifact got called once");
+ t.deepEqual(registryRequestArtifactStub.firstCall.firstArg, {
+ revision: "16",
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ classifier: "npm-sources",
+ version: "1.75.0",
+ extension: "zip",
+ }, "Registry#requestArtifact got called with expected coordinates");
+ t.is(registryRequestArtifactStub.firstCall.args[1],
+ path.join("/ui5Data/", "framework", "staging", "com.sap.ui5.dist_sapui5-sdk-dist_16_npm-sources.zip"),
+ "Registry#requestArtifact got called with expected target directory");
+
+ t.is(
+ typeof installedArtifact.removeArtifact,
+ "function",
+ "removeArtifact method"
+ );
+ rmStub.resetHistory();
+ await installedArtifact.removeArtifact();
+ t.is(rmStub.callCount, 1, "fs.rm got called once");
+ t.is(rmStub.firstCall.firstArg, expectedPath, "fs.rm got called with expected argument");
+});
+
+test.serial("installArtifact: Already installed", async (t) => {
+ const {Installer, rmStub, registryRequestArtifactStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"});
+ sinon.stub(installer, "_pathExists").resolves(true);
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+
+ const installedArtifact = await installer.installArtifact({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "jar",
+ });
+
+ const expectedPath = path.join("/ui5Data/", "framework", "artifacts", "com_sap_ui5_dist-sapui5-sdk-dist", "5.jar");
+ t.is(
+ installedArtifact.artifactPath,
+ expectedPath,
+ "artifactPath correctly resolved"
+ );
+
+ t.is(registryRequestArtifactStub.callCount, 0, "Registry#requestArtifact did not get called");
+
+ t.is(
+ typeof installedArtifact.removeArtifact,
+ "function",
+ "removeArtifact method"
+ );
+ rmStub.resetHistory();
+ await installedArtifact.removeArtifact();
+ t.is(rmStub.callCount, 1, "fs.rm got called once");
+ t.is(rmStub.firstCall.firstArg, expectedPath, "fs.rm got called with expected argument");
+});
+
+test.serial("installArtifact: Already installed only after lock acquired", async (t) => {
+ const {Installer, rmStub, registryRequestArtifactStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ sinon.stub(installer, "_fetchArtifactMetadata").resolves({revision: "5"});
+ sinon.stub(installer, "_pathExists")
+ .onFirstCall().resolves(false)
+ .onSecondCall().resolves(true);
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+
+ const installedArtifact = await installer.installArtifact({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "jar",
+ });
+
+ const expectedPath = path.join("/ui5Data/", "framework", "artifacts", "com_sap_ui5_dist-sapui5-sdk-dist", "5.jar");
+ t.is(
+ installedArtifact.artifactPath,
+ expectedPath,
+ "artifactPath correctly resolved"
+ );
+
+ t.is(registryRequestArtifactStub.callCount, 0, "Registry#requestArtifact did not get called");
+
+ t.is(
+ typeof installedArtifact.removeArtifact,
+ "function",
+ "removeArtifact method"
+ );
+ rmStub.resetHistory();
+ await installedArtifact.removeArtifact();
+ t.is(rmStub.callCount, 1, "fs.rm got called once");
+ t.is(rmStub.firstCall.firstArg, expectedPath, "fs.rm got called with expected argument");
+});
+
+test.serial("_fetchArtifactMetadata", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+ sinon.stub(installer, "_getLocalArtifactMetadata")
+ .resolves({
+ lastCheck: 0,
+ lastUpdate: 0,
+ revision: "2",
+ staleRevisions: [],
+ });
+
+ const getRemoteArtifactMetadataStub = sinon.stub(installer, "_getRemoteArtifactMetadata")
+ .resolves({revision: "5", lastUpdate: 0});
+ sinon.stub(installer, "_removeStaleRevisions").resolves();
+ sinon.stub(installer, "_writeLocalArtifactMetadata").resolves();
+
+ const artifactMetadata = await installer._fetchArtifactMetadata({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "jar",
+ });
+
+ t.truthy(artifactMetadata.lastCheck, "Proper metadata: lastCheck");
+ t.is(artifactMetadata.lastUpdate, 0, "Proper metadata: lastUpdate");
+ t.is(artifactMetadata.revision, "5", "Proper metadata: revision");
+
+ t.is(getRemoteArtifactMetadataStub.callCount, 1, "getRemoteArtifactMetadata got called once");
+});
+
+test.serial("_fetchArtifactMetadata: Cached", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {},
+ });
+
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+ sinon.stub(installer, "_getLocalArtifactMetadata")
+ .resolves({
+ lastCheck: new Date().getTime(),
+ lastUpdate: 0,
+ revision: "2",
+ staleRevisions: [],
+ });
+ const getRemoteArtifactMetadataStub = sinon.stub(installer, "_getRemoteArtifactMetadata")
+ .resolves({revision: "5", lastUpdate: 0});
+ sinon.stub(installer, "_removeStaleRevisions").resolves();
+ sinon.stub(installer, "_writeLocalArtifactMetadata").resolves();
+
+ const artifactMetadata = await installer._fetchArtifactMetadata({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "jar",
+ });
+
+ t.truthy(artifactMetadata.lastCheck, "Proper metadata: lastCheck");
+ t.is(artifactMetadata.lastUpdate, 0, "Proper metadata: lastUpdate");
+ t.is(artifactMetadata.revision, "2", "Proper metadata: revision");
+
+ t.is(getRemoteArtifactMetadataStub.callCount, 0, "getRemoteArtifactMetadata did not get called");
+});
+
+test.serial("_fetchArtifactMetadata: Cache available but disabled", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {},
+ cacheMode: "Off"
+ });
+
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+ sinon.stub(installer, "_getLocalArtifactMetadata")
+ .resolves({
+ lastCheck: new Date().getTime(),
+ lastUpdate: 0,
+ revision: "2",
+ staleRevisions: [],
+ });
+ const getRemoteArtifactMetadataStub = sinon.stub(installer, "_getRemoteArtifactMetadata")
+ .resolves({revision: "5", lastUpdate: 0});
+ sinon.stub(installer, "_removeStaleRevisions").resolves();
+ sinon.stub(installer, "_writeLocalArtifactMetadata").resolves();
+
+ const artifactMetadata = await installer._fetchArtifactMetadata({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "jar",
+ });
+
+ t.truthy(artifactMetadata.lastCheck, "Proper metadata: lastCheck");
+ t.is(artifactMetadata.lastUpdate, 0, "Proper metadata: lastUpdate");
+ t.is(artifactMetadata.revision, "5", "Proper metadata: revision");
+ t.is(getRemoteArtifactMetadataStub.callCount, 1, "getRemoteArtifactMetadata got called once");
+});
+
+test.serial("_fetchArtifactMetadata: Cache outdated but enforced", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {},
+ cacheMode: "Force"
+ });
+
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+ sinon.stub(installer, "_getLocalArtifactMetadata")
+ .resolves({
+ lastCheck: 1, // first millisecond to indicate a cache is present but outdated
+ lastUpdate: 0,
+ revision: "2",
+ staleRevisions: [],
+ });
+ const getRemoteArtifactMetadataStub = sinon.stub(installer, "_getRemoteArtifactMetadata")
+ .resolves({revision: "5", lastUpdate: 0});
+ sinon.stub(installer, "_removeStaleRevisions").resolves();
+ sinon.stub(installer, "_writeLocalArtifactMetadata").resolves();
+
+ const artifactMetadata = await installer._fetchArtifactMetadata({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "jar",
+ });
+
+ t.truthy(artifactMetadata.lastCheck, "Proper metadata: lastCheck");
+ t.is(artifactMetadata.lastUpdate, 0, "Proper metadata: lastUpdate");
+ t.is(artifactMetadata.revision, "2", "Proper metadata: revision");
+
+ t.is(getRemoteArtifactMetadataStub.callCount, 0, "getRemoteArtifactMetadata did not get called");
+});
+
+test.serial("_fetchArtifactMetadata throws", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {},
+ cacheMode: "Force"
+ });
+
+ sinon.stub(installer, "_synchronize").callsFake( async (pckg, callback) => await callback());
+ sinon.stub(installer, "_getLocalArtifactMetadata").resolves({});
+
+ await t.throwsAsync(installer._fetchArtifactMetadata({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "jar",
+ }), {
+ message:
+ "Could not find artifact com.sap.ui5.dist:sapui5-sdk-dist:1.75.0:jar in local cache",
+ });
+});
+
+test.serial("_getRemoteArtifactMetadata", async (t) => {
+ const {Installer, registryRequestMavenMetadataStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url")
+ });
+
+ registryRequestMavenMetadataStub
+ .resolves({
+ versioning: {
+ snapshotVersions: {
+ snapshotVersion: [{"extension": "jar", "updated": "20220828080910", "value": "5.0.0-SNAPSHOT"}]
+ }
+ }
+ });
+
+ const remoteArtifactMetadata = await installer._getRemoteArtifactMetadata({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "jar",
+ });
+
+ t.truthy(remoteArtifactMetadata.lastUpdate, "Proper metadata: lastUpdate");
+ t.is(remoteArtifactMetadata.revision, "5.0.0-SNAPSHOT", "Proper metadata: revision");
+
+ t.is(registryRequestMavenMetadataStub.callCount, 1, "requestPackagePackument should be called once");
+ t.deepEqual(registryRequestMavenMetadataStub.getCall(0).args[0],
+ {groupId: "com.sap.ui5.dist", artifactId: "sapui5-sdk-dist", version: "1.75.0"},
+ "requestMavenMetadata was called with correct arguments");
+});
+
+test.serial("_getRemoteArtifactMetadata throws", async (t) => {
+ const {Installer, registryRequestMavenMetadataStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url")
+ });
+
+ registryRequestMavenMetadataStub.resolves({});
+
+ await t.throwsAsync(installer._getRemoteArtifactMetadata({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "jar",
+ }), {message: "Missing Maven snapshot metadata for artifact com.sap.ui5.dist:sapui5-sdk-dist:1.75.0"});
+});
+
+test.serial("_getRemoteArtifactMetadata throws missing deployment metadata", async (t) => {
+ const {Installer, registryRequestMavenMetadataStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => Promise.resolve("endpoint-url")
+ });
+
+ registryRequestMavenMetadataStub
+ .resolves({
+ versioning: {
+ snapshotVersions: {
+ snapshotVersion: [
+ {"extension": "jar", "updated": "20220828080910", "value": "5.0.0-SNAPSHOT"},
+ {
+ "classifier": "pony-sources", "extension": "zip", "updated": "20220828080910",
+ "value": "5.0.0-SNAPSHOT"
+ }
+ ]
+ }
+ }
+ });
+
+ await t.throwsAsync(installer._getRemoteArtifactMetadata({
+ groupId: "com.sap.ui5.dist",
+ artifactId: "sapui5-sdk-dist",
+ version: "1.75.0",
+ extension: "zip",
+ classifier: "npm-sources",
+ }), {
+ message: "Could not find npm-sources.zip deployment for artifact " +
+ "com.sap.ui5.dist:sapui5-sdk-dist:1.75.0 in snapshot metadata:\n" +
+ `[{"extension":"jar","updated":"20220828080910","value":"5.0.0-SNAPSHOT"},` +
+ `{"classifier":"pony-sources","extension":"zip","updated":"20220828080910","value":"5.0.0-SNAPSHOT"}]`
+ });
+});
+
+test.serial("_getLocalArtifactMetadata", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ sinon.stub(installer, "readJson").resolves({foo: "bar"});
+ const localArtifactMetadata = await installer._getLocalArtifactMetadata();
+
+ t.deepEqual(localArtifactMetadata, {foo: "bar"}, "Returns the correct metadata");
+});
+
+test.serial("_getLocalArtifactMetadata file not found", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ sinon.stub(installer, "readJson").throws({code: "ENOENT"});
+ const localArtifactMetadata = await installer._getLocalArtifactMetadata();
+
+ t.deepEqual(
+ localArtifactMetadata,
+ {lastCheck: 0, lastUpdate: 0, revision: null, staleRevisions: []},
+ "Returns an 'empty' localArtifactMetadata"
+ );
+});
+
+test.serial("_getLocalArtifactMetadata throws", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ sinon.stub(installer, "readJson").throws(() => {
+ throw new Error("Error message");
+ });
+
+ await t.throwsAsync(installer._getLocalArtifactMetadata(), {
+ message: "Error message",
+ });
+});
+
+
+test.serial("_writeLocalArtifactMetadata", async (t) => {
+ const {Installer, writeFileStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ // const writeJsonStub = sinon.stub(installer, "_writeJson").resolves("/path/to/file");
+ writeFileStub.resolves("/path/to/file");
+
+ const fsWriteRsource = await installer._writeLocalArtifactMetadata("Id", {foo: "bar"});
+
+ t.is(fsWriteRsource, "/path/to/file");
+ t.is(writeFileStub.callCount, 1, "_writeJson called");
+ t.deepEqual(
+ writeFileStub.args,
+ [[path.join("/ui5Data/", "framework", "metadata", "Id.json"), "{\"foo\":\"bar\"}"]],
+ "_writeJson called with correct arguments"
+ );
+});
+
+test.serial("_removeStaleRevisions", async (t) => {
+ const {Installer, rmStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ const pathForArtifact = sinon.stub(installer, "_getTargetPathForArtifact")
+ .onCall(0).resolves("/path/to/artifact/1")
+ .onCall(1).resolves("/path/to/artifact/2");
+
+ let metadata = {
+ staleRevisions: ["1", "2", "3", "4", "5"],
+ };
+
+ await installer._removeStaleRevisions("Id", metadata, {pkgName: "myPkg"});
+
+ t.is(metadata.staleRevisions.length, 3, "Metadata's staleRevisions cut");
+ t.is(pathForArtifact.callCount, 2, "requested path for 2 artifacts");
+ t.is(pathForArtifact.getCall(0).args[0].revision, "1", "Resolved revison 1");
+ t.is(pathForArtifact.getCall(1).args[0].revision, "2", "Resolved revison 2");
+
+ t.is(await rmStub.getCall(0).args[0], "/path/to/artifact/1", "Rm artifact 1");
+ t.is(await rmStub.getCall(1).args[0], "/path/to/artifact/2", "Rm artifact 2");
+
+ metadata = {
+ staleRevisions: ["1"],
+ };
+ await installer._removeStaleRevisions("Id", metadata, {pkgName: "myPkg"});
+
+ t.deepEqual(metadata, {staleRevisions: ["1"]}, "Stale revisions stay untouched if 1 or less");
+});
+
+test.serial("_pathExists", async (t) => {
+ const {Installer, statStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ statStub.resolves();
+ const pathExists = await installer._pathExists("/target/path/");
+
+ t.is(pathExists, true, "Target path exists");
+ t.is(statStub.callCount, 1, "stat got called once");
+ t.is(statStub.firstCall.firstArg, "/target/path/", "stat got called with expected argument");
+});
+
+test.serial("_pathExists file not found", async (t) => {
+ const {Installer, statStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ statStub.throws({code: "ENOENT"});
+ const pathExists = await installer._pathExists("/target/path/");
+
+ t.is(pathExists, false, "Target path does not exist");
+});
+
+test.serial("_pathExists throws", async (t) => {
+ const {Installer, statStub} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ statStub.throws(() => {
+ throw new Error("Error message");
+ });
+
+ await t.throwsAsync(installer._pathExists("/target/path/"), {
+ message: "Error message",
+ }, "Threw with expected error message");
+});
+
+test.serial("_projectExists", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(true);
+ const projectExists = await installer._projectExists("/target/path/");
+
+ t.is(projectExists, true, "Resolves the target path");
+ t.is(pathExistsStub.callCount, 1, "_pathExists got called once");
+ t.is(pathExistsStub.firstCall.firstArg, path.join("/target/path/package.json"),
+ "_pathExists got called with expected argument");
+});
+
+test.serial("_projectExists: Does not exist", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(false);
+ const projectExists = await installer._projectExists("/target/path/");
+
+ t.is(projectExists, false, "Resolves the target path");
+ t.is(pathExistsStub.callCount, 1, "_pathExists got called once");
+ t.is(pathExistsStub.firstCall.firstArg, path.join("/target/path/package.json"),
+ "_pathExists got called with expected argument");
+});
+
+test.serial("_projectExists: Throws", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/",
+ snapshotEndpointUrlCb: () => {}
+ });
+
+ const pathExistsStub = sinon.stub(installer, "_pathExists").throws(() => {
+ throw new Error("Error message");
+ });
+
+ await t.throwsAsync(installer._projectExists("/target/path/"), {
+ message: "Error message",
+ }, "Threw with expected error message");
+
+ t.is(pathExistsStub.callCount, 1, "_pathExists got called once");
+ t.is(pathExistsStub.firstCall.firstArg, path.join("/target/path/package.json"),
+ "_pathExists got called with expected argument");
+});
diff --git a/packages/project/test/lib/ui5framework/maven/Registry.js b/packages/project/test/lib/ui5framework/maven/Registry.js
new file mode 100644
index 00000000000..51a63e10f5d
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/maven/Registry.js
@@ -0,0 +1,240 @@
+import test from "ava";
+import sinon from "sinon";
+import esmock from "esmock";
+import {promisify} from "node:util";
+
+test.beforeEach(async (t) => {
+ t.context.pipelineStub = sinon.stub().resolves();
+ t.context.streamPipelineStub = sinon.stub().resolves();
+
+ t.context.promisifyStub = sinon.stub();
+
+ t.context.fetchStub = sinon.stub().resolves({
+ ok: true,
+ buffer: sinon.stub().resolves("Some metadata ")
+ });
+
+ t.context.fsCreateWriteStreamStub = sinon.stub().resolves();
+
+ t.context.Registry = await esmock.p("../../../../lib/ui5Framework/maven/Registry.js", {
+ "make-fetch-happen": t.context.fetchStub,
+ "node:stream/promises": {
+ "pipeline": t.context.streamPipelineStub
+ },
+ "node:util": {
+ "promisify": t.context.promisifyStub
+ },
+ "graceful-fs": {
+ "createWriteStream": t.context.fsCreateWriteStreamStub
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ sinon.restore();
+ esmock.purge(t.context.Registry);
+});
+
+test.serial("Registry: constructor", (t) => {
+ const {Registry} = t.context;
+
+ const reg = new Registry({
+ cwd: "/cwd/",
+ endpointUrl: "some-url"
+ });
+ t.true(reg instanceof Registry, "Constructor returns instance of class");
+ t.is(reg._endpointUrl, "some-url/");
+});
+
+test.serial("Registry: constructor requires 'endpointUrl'", (t) => {
+ const {Registry} = t.context;
+
+ t.throws(() => {
+ new Registry({cwd: "/"});
+ }, {message: `Registry: Missing parameter "endpointUrl"`});
+});
+
+test.serial("Registry: requestMavenMetadata", async (t) => {
+ const {Registry, promisifyStub} = t.context;
+
+ promisifyStub.callsFake((fn) => promisify(fn)); // Use the native promisify
+
+ const reg = new Registry({
+ cwd: "/cwd/",
+ endpointUrl: "some-url"
+ });
+
+ const resolvedMetadata = await reg.requestMavenMetadata({
+ groupId: "ui5.corp",
+ artifactId: "great-thing",
+ version: "1.75.0-SNAPSHOT",
+ });
+
+ t.is(resolvedMetadata, "Some metadata");
+});
+
+test.serial("Registry: requestMavenMetadata bad request", async (t) => {
+ const {Registry, fetchStub} = t.context;
+
+ fetchStub.resolves({status: "500", statusText: "Bad request"});
+
+ const reg = new Registry({
+ cwd: "/cwd/",
+ endpointUrl: "some-url"
+ });
+
+ await t.throwsAsync(
+ reg.requestMavenMetadata({
+ groupId: "ui5.corp",
+ artifactId: "great-thing",
+ version: "1.75.0-SNAPSHOT",
+ }),
+ {
+ message:
+ "Failed to retrieve maven-metadata.xml for ui5.corp:great-thing:1.75.0-SNAPSHOT:" +
+ " [HTTP Error] 500 Bad request",
+ }
+ );
+});
+
+test.serial("Registry: requestMavenMetadata not found", async (t) => {
+ const {Registry, fetchStub} = t.context;
+
+ fetchStub.throws({code: "ENOTFOUND"});
+
+ const reg = new Registry({
+ cwd: "/cwd/",
+ endpointUrl: "some-url"
+ });
+
+ await t.throwsAsync(
+ reg.requestMavenMetadata({
+ groupId: "ui5.corp",
+ artifactId: "great-thing"
+ }),
+ {
+ message:
+ "Failed to connect to Maven registry at some-url/. " +
+ "Please check the correct endpoint URL is maintained and can be reached. "+
+ "You can change the configured URL " +
+ "using the following command: 'ui5 config set mavenSnapshotEndpointUrl '"
+ }
+ );
+});
+
+test.serial("Registry: requestMavenMetadata No metadata/bad xml", async (t) => {
+ const {Registry, fetchStub, promisifyStub} = t.context;
+
+ const reg = new Registry({
+ cwd: "/cwd/",
+ endpointUrl: "some-url"
+ });
+
+ promisifyStub.callsFake((fn) => promisify(fn)); // Use the native promisify
+
+ fetchStub.resolves({
+ ok: true,
+ buffer: sinon.stub().resolves(" ")
+ });
+
+ await t.throwsAsync(
+ reg.requestMavenMetadata({
+ groupId: "ui5.corp",
+ artifactId: "great-thing",
+ version: "1.75.0-SNAPSHOT",
+ }),
+ {
+ message:
+ "Failed to retrieve maven-metadata.xml for ui5.corp:great-thing:1.75.0-SNAPSHOT: " +
+ "Empty or unexpected response body:\n" +
+ " \n" +
+ "Parsed as:\n" +
+ "{\"metadata\":\"\"}"
+ }
+ );
+});
+
+test.serial("Registry: requestArtifact", async (t) => {
+ const {Registry, fetchStub, streamPipelineStub, fsCreateWriteStreamStub} = t.context;
+
+ fetchStub.resolves({
+ ok: true,
+ body: "content body"
+ });
+
+ const reg = new Registry({
+ cwd: "/cwd/",
+ endpointUrl: "some-url"
+ });
+
+ await reg.requestArtifact({
+ groupId: "ui5.corp",
+ artifactId: "great-thing",
+ revision: "2",
+ extension: "jar"
+ }, "/target/path/");
+
+ t.is(streamPipelineStub.callCount, 1, "Pipeline is called");
+ t.is(streamPipelineStub.args[0][0], "content body", "Pipeline called with response body as argument");
+ t.is(fsCreateWriteStreamStub.callCount, 1, "writeStream called");
+ t.deepEqual(fsCreateWriteStreamStub.args[0], ["/target/path/"], "writeStream called with the target path");
+});
+
+test.serial("Registry: requestArtifact bad request", async (t) => {
+ const {Registry, fetchStub} = t.context;
+
+ fetchStub.resolves({status: "500", statusText: "Bad request"});
+
+ const reg = new Registry({
+ cwd: "/cwd/",
+ endpointUrl: "some-url"
+ });
+
+ await t.throwsAsync(
+ reg.requestArtifact({
+ groupId: "ui5.corp",
+ artifactId: "great-thing",
+ revision: "2",
+ version: "2",
+ classifier: "classifier",
+ extension: "jar"
+ }, "/target/path/"),
+ {
+ message:
+ "Failed to retrieve artifact ui5.corp:great-thing:2:classifier:jar" +
+ " [HTTP Error] 500 Bad request",
+ }
+ );
+});
+
+test.serial("Registry: requestArtifact not found", async (t) => {
+ const {Registry, fetchStub} = t.context;
+
+ fetchStub.throws({code: "ENOTFOUND"});
+
+ const reg = new Registry({
+ cwd: "/cwd/",
+ endpointUrl: "some-url"
+ });
+
+ await t.throwsAsync(
+ reg.requestArtifact(
+ {
+ groupId: "ui5.corp",
+ artifactId: "great-thing",
+ revision: "2",
+ version: "2",
+ classifier: "",
+ extension: "jar",
+ },
+ "/target/path/"
+ ),
+ {
+ message:
+ "Failed to connect to Maven registry at some-url/. " +
+ "Please check the correct endpoint URL is maintained and can be reached. "+
+ "You can change the configured URL " +
+ "using the following command: 'ui5 config set mavenSnapshotEndpointUrl '"
+ }
+ );
+});
diff --git a/packages/project/test/lib/ui5framework/npm/Installer.js b/packages/project/test/lib/ui5framework/npm/Installer.js
new file mode 100644
index 00000000000..c06b36ae33d
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/npm/Installer.js
@@ -0,0 +1,801 @@
+import test from "ava";
+import sinon from "sinon";
+import esmock from "esmock";
+import path from "node:path";
+
+const __dirname = import.meta.dirname;
+
+test.beforeEach(async (t) => {
+ t.context.mkdirpStub = sinon.stub().resolves();
+ t.context.rmrfStub = sinon.stub().resolves();
+
+ t.context.lockStub = sinon.stub();
+ t.context.unlockStub = sinon.stub();
+ t.context.renameStub = sinon.stub().yieldsAsync();
+ t.context.statStub = sinon.stub().yieldsAsync();
+
+ t.context.AbstractResolver = await esmock.p("../../../../lib/ui5Framework/AbstractInstaller.js", {
+ "../../../../lib/utils/fs.js": {
+ mkdirp: t.context.mkdirpStub,
+ rmrf: t.context.rmrfStub
+ },
+ "lockfile": {
+ lock: t.context.lockStub,
+ unlock: t.context.unlockStub
+ }
+ });
+ t.context.Installer = await esmock.p("../../../../lib/ui5Framework/npm/Installer.js", {
+ "../../../../lib/ui5Framework/AbstractInstaller.js": t.context.AbstractResolver,
+ "../../../../lib/utils/fs.js": {
+ mkdirp: t.context.mkdirpStub,
+ rmrf: t.context.rmrfStub
+ },
+ "graceful-fs": {
+ rename: t.context.renameStub,
+ stat: t.context.statStub
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ sinon.restore();
+ esmock.purge(t.context.AbstractResolver);
+ esmock.purge(t.context.Installer);
+});
+
+test.serial("Installer: constructor", (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+ t.true(installer instanceof Installer, "Constructor returns instance of class");
+ t.is(installer._packagesDir, path.join("/ui5Data/", "framework", "packages"));
+ t.is(installer._lockDir, path.join("/ui5Data/", "framework", "locks"));
+ t.is(installer._stagingDir, path.join("/ui5Data/", "framework", "staging"));
+});
+
+test.serial("Installer: constructor requires 'cwd'", (t) => {
+ const {Installer} = t.context;
+
+ t.throws(() => {
+ new Installer({
+ ui5DataDir: "/ui5Data/"
+ });
+ }, {message: `Installer: Missing parameter "cwd"`});
+});
+
+test.serial("Installer: constructor requires 'ui5DataDir'", (t) => {
+ const {Installer} = t.context;
+
+ t.throws(() => {
+ new Installer({
+ cwd: "/cwd/"
+ });
+ }, {message: `Installer: Missing parameter "ui5DataDir"`});
+});
+
+test.serial("Installer: fetchPackageVersions", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ const registry = installer.getRegistry();
+ const requestPackagePackumentStub = sinon.stub().resolves({
+ versions: {
+ "1.0.0": {},
+ "2.0.0": {},
+ "3.0.0": {},
+ },
+ });
+ sinon
+ .stub(registry, "_getPacote")
+ .resolves({
+ pacote: {
+ packument: requestPackagePackumentStub
+ },
+ pacoteOptions: {},
+ });
+
+ const packageVersions = await installer.fetchPackageVersions({pkgName: "@openui5/sap.ui.lib1"});
+
+ t.deepEqual(packageVersions, ["1.0.0", "2.0.0", "3.0.0"], "Should resolve with expected versions");
+
+ t.is(requestPackagePackumentStub.callCount, 1, "requestPackagePackument should be called once");
+ t.is(requestPackagePackumentStub.getCall(0).args[0], "@openui5/sap.ui.lib1",
+ "requestPackagePackument should be called with pkgName");
+});
+
+test.serial("Installer: _getLockPath", (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ const lockPath = installer._getLockPath("lo/ck-n@me");
+
+ t.is(lockPath, path.join("/ui5Data/", "framework", "locks", "lo-ck-n@me.lock"));
+});
+
+test.serial("Installer: _getLockPath with illegal characters", (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ t.throws(() => installer._getLockPath("lock.näme"), {
+ message: "Illegal file name: lock.näme"
+ });
+ t.throws(() => installer._getLockPath(".lock.name"), {
+ message: "Illegal file name: .lock.name"
+ });
+});
+
+test.serial("Installer: fetchPackageManifest (without existing package.json)", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ const mockedManifest = {
+ name: "myPackage",
+ dependencies: {
+ "foo": "1.2.3"
+ },
+ devDependencies: {
+ "bar": "4.5.6"
+ },
+ foo: "bar"
+ };
+
+ const expectedManifest = {
+ name: "myPackage",
+ dependencies: {
+ "foo": "1.2.3"
+ },
+ devDependencies: {
+ "bar": "4.5.6"
+ }
+ };
+
+ const registry = installer.getRegistry();
+ const requestPackageManifestStub = sinon.stub(registry, "requestPackageManifest")
+ .callsFake((pkgName, version) => {
+ throw new Error(
+ "_cachedRegistry.requestPackageManifest stub called with unknown arguments " +
+ `pkgName: ${pkgName}, version: ${version}}`
+ );
+ })
+ .withArgs("myPackage", "1.2.3").resolves(mockedManifest);
+
+ const readJsonStub = sinon.stub(installer, "readJson")
+ .callsFake((path) => {
+ throw new Error(
+ `readJson stub called with unknown path: ${path}`
+ );
+ })
+ .withArgs(path.join("/path", "to", "myPackage", "1.2.3", "package.json"))
+ .callsFake(async (path) => {
+ const error = new Error(`ENOENT: no such file or directory, open '${path}'`);
+ error.code = "ENOENT";
+ throw error;
+ });
+
+ const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage")
+ .callsFake(({pkgName, version}) => {
+ throw new Error(
+ `_getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}`
+ );
+ })
+ .withArgs({
+ pkgName: "myPackage",
+ version: "1.2.3"
+ }).returns(path.join("/path", "to", "myPackage", "1.2.3"));
+
+ const manifest = await installer.fetchPackageManifest({pkgName: "myPackage", version: "1.2.3"});
+
+ t.deepEqual(manifest, expectedManifest, "Should return expected manifest object");
+ t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once");
+ t.is(readJsonStub.callCount, 1, "readJson should be called once");
+ t.is(requestPackageManifestStub.callCount, 1, "requestPackageManifest should be called once");
+});
+
+test.serial("Installer: fetchPackageManifest (with existing package.json)", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ const mockedManifest = {
+ name: "myPackage",
+ dependencies: {
+ "foo": "1.2.3"
+ },
+ devDependencies: {
+ "bar": "4.5.6"
+ },
+ foo: "bar"
+ };
+
+ const expectedManifest = {
+ name: "myPackage",
+ dependencies: {
+ "foo": "1.2.3"
+ },
+ devDependencies: {
+ "bar": "4.5.6"
+ }
+ };
+
+ const registry = installer.getRegistry();
+ const requestPackageManifestStub = sinon.stub(registry, "requestPackageManifest")
+ .rejects(new Error("Unexpected call"));
+
+ const readJsonStub = sinon.stub(installer, "readJson")
+ .callsFake((path) => {
+ throw new Error(
+ `readJson stub called with unknown path: ${path}`
+ );
+ })
+ .withArgs(path.join("/path", "to", "myPackage", "1.2.3", "package.json"))
+ .resolves(mockedManifest);
+
+ const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage")
+ .callsFake(({pkgName, version}) => {
+ throw new Error(
+ `_getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}`
+ );
+ })
+ .withArgs({
+ pkgName: "myPackage",
+ version: "1.2.3"
+ }).returns(path.join("/path", "to", "myPackage", "1.2.3"));
+
+ const manifest = await installer.fetchPackageManifest({pkgName: "myPackage", version: "1.2.3"});
+
+ t.deepEqual(manifest, expectedManifest, "Should return expected manifest object");
+ t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once");
+ t.is(readJsonStub.callCount, 1, "readJson should be called once");
+ t.is(requestPackageManifestStub.callCount, 0, "requestPackageManifest should not be called");
+});
+
+test.serial("Installer: fetchPackageManifest (readJson throws error)", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ const registry = installer.getRegistry();
+ const requestPackageManifestStub = sinon.stub(registry, "requestPackageManifest")
+ .rejects(new Error("Unexpected call"));
+
+ const readJsonStub = sinon.stub(installer, "readJson")
+ .rejects(new Error("Error from readJson"));
+
+ const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage")
+ .callsFake(({pkgName, version}) => {
+ throw new Error(
+ `_getTargetDirForPackage stub called with unknown arguments pkgName: ${pkgName}, version: ${version}}`
+ );
+ })
+ .withArgs({
+ pkgName: "myPackage",
+ version: "1.2.3"
+ }).returns(path.join("/path", "to", "myPackage", "1.2.3"));
+
+ await t.throwsAsync(async () => {
+ await installer.fetchPackageManifest({pkgName: "myPackage", version: "1.2.3"});
+ }, {message: "Error from readJson"});
+
+ t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once");
+ t.is(readJsonStub.callCount, 1, "readJson should be called once");
+ t.is(requestPackageManifestStub.callCount, 0, "requestPackageManifest should not be called");
+});
+
+test.serial("Installer: _synchronize", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ t.context.lockStub.yieldsAsync();
+ t.context.unlockStub.yieldsAsync();
+
+ const getLockPathStub = sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock");
+
+ const callback = sinon.stub().resolves();
+
+ await installer._synchronize("lock/name", callback);
+
+ t.is(getLockPathStub.callCount, 1, "_getLockPath should be called once");
+ t.is(getLockPathStub.getCall(0).args[0], "lock/name",
+ "_getLockPath should be called with expected args");
+
+ t.is(t.context.mkdirpStub.callCount, 1, "_mkdirp should be called once");
+ t.deepEqual(t.context.mkdirpStub.getCall(0).args, [path.join("/ui5Data/", "framework", "locks")],
+ "_mkdirp should be called with expected args");
+
+ t.is(t.context.lockStub.callCount, 1, "lock should be called once");
+ t.is(t.context.lockStub.getCall(0).args[0], "/locks/lockfile.lock",
+ "lock should be called with expected path");
+ t.deepEqual(t.context.lockStub.getCall(0).args[1], {wait: 10000, stale: 60000, retries: 10},
+ "lock should be called with expected options");
+
+ t.is(t.context.unlockStub.callCount, 1, "unlock should be called once");
+ t.is(t.context.unlockStub.getCall(0).args[0], "/locks/lockfile.lock",
+ "unlock should be called with expected path");
+
+ t.is(callback.callCount, 1, "callback should be called once");
+
+ t.true(t.context.lockStub.calledBefore(callback), "Lock should be called before invoking the callback");
+ t.true(t.context.unlockStub.calledAfter(callback), "Unlock should be called after invoking the callback");
+});
+
+test.serial("Installer: _synchronize should unlock when callback promise has resolved", async (t) => {
+ const {Installer} = t.context;
+
+ t.plan(4);
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ t.context.lockStub.yieldsAsync();
+ t.context.unlockStub.yieldsAsync();
+
+ sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock");
+
+ const callback = sinon.stub().callsFake(async () => {
+ t.is(t.context.lockStub.callCount, 1, "lock should have been called when the callback is invoked");
+ await Promise.resolve();
+ t.is(t.context.unlockStub.callCount, 0,
+ "unlock should not be called when the callback did not fully resolve, yet");
+ });
+
+ await installer._synchronize("lock/name", callback);
+
+ t.is(callback.callCount, 1, "callback should be called once");
+ t.is(t.context.unlockStub.callCount, 1, "unlock should be called after _synchronize has resolved");
+});
+
+test.serial("Installer: _synchronize should throw when locking fails", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ t.context.lockStub.yieldsAsync(new Error("Locking error"));
+
+ sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock");
+
+ const callback = sinon.stub();
+
+ await t.throwsAsync(async () => {
+ await installer._synchronize("lock/name", callback);
+ }, {message: "Locking error"});
+
+ t.is(callback.callCount, 0, "callback should not be called");
+ t.is(t.context.unlockStub.callCount, 0, "unlock should not be called");
+});
+
+test.serial("Installer: _synchronize should still unlock when callback throws an error", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ t.context.lockStub.yieldsAsync();
+ t.context.unlockStub.yieldsAsync();
+
+ sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock");
+
+ const callback = sinon.stub().throws(new Error("Callback throws error"));
+
+ await t.throwsAsync(async () => {
+ await installer._synchronize("lock/name", callback);
+ }, {message: "Callback throws error"});
+
+ t.is(callback.callCount, 1, "callback should be called once");
+ t.is(t.context.lockStub.callCount, 1, "lock should be called once");
+ t.is(t.context.unlockStub.callCount, 1, "unlock should be called once");
+});
+
+test.serial("Installer: _synchronize should still unlock when callback rejects with error", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ t.context.lockStub.yieldsAsync();
+ t.context.unlockStub.yieldsAsync();
+
+ sinon.stub(installer, "_getLockPath").returns("/locks/lockfile.lock");
+
+ const callback = sinon.stub().rejects(new Error("Callback rejects with error"));
+
+ await t.throwsAsync(async () => {
+ await installer._synchronize("lock/name", callback);
+ }, {message: "Callback rejects with error"});
+
+ t.is(callback.callCount, 1, "callback should be called once");
+ t.is(t.context.lockStub.callCount, 1, "lock should be called once");
+ t.is(t.context.unlockStub.callCount, 1, "unlock should be called once");
+});
+
+test.serial("Installer: installPackage with new package", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ t.context.lockStub.yieldsAsync();
+ t.context.unlockStub.yieldsAsync();
+
+ const targetDir = path.join("my", "package", "dir");
+ const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage")
+ .returns(targetDir);
+
+ const packageJsonExistsStub = sinon.stub(installer, "_packageJsonExists").resolves(false);
+ const synchronizeSpy = sinon.spy(installer, "_synchronize");
+
+ const getStagingDirForPackageStub = sinon.stub(installer, "_getStagingDirForPackage")
+ .returns("staging-dir-path");
+ const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(false);
+
+ const registry = installer.getRegistry();
+ const extractPackageStub = sinon.stub(registry, "extractPackage").resolves();
+
+ const res = await installer.installPackage({
+ pkgName: "myPackage",
+ version: "1.2.3"
+ });
+
+ t.deepEqual(res, {
+ pkgPath: targetDir
+ }, "Should return correct values");
+
+ t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once");
+ t.deepEqual(getTargetDirForPackageStub.getCall(0).args[0], {
+ pkgName: "myPackage",
+ version: "1.2.3"
+ }, "_getTargetDirForPackage should be called with the correct arguments");
+
+ t.is(packageJsonExistsStub.callCount, 2, "_packageJsonExists should be called twice");
+ t.is(packageJsonExistsStub.getCall(0).args[0], targetDir,
+ "_packageJsonExists should be called with the correct arguments on first call");
+ t.is(packageJsonExistsStub.getCall(1).args[0], targetDir,
+ "_packageJsonExists should be called with the correct arguments on second call");
+
+ t.is(synchronizeSpy.callCount, 1, "_synchronize should be called once");
+ t.is(synchronizeSpy.getCall(0).args[0], "package-myPackage@1.2.3",
+ "_synchronize should be called with the correct first argument");
+ t.is(t.context.lockStub.callCount, 1, "lock should be called once");
+ t.is(t.context.unlockStub.callCount, 1, "unlock should be called once");
+
+ t.is(getStagingDirForPackageStub.callCount, 1, "_getStagingDirForPackage should be called once");
+ t.deepEqual(getStagingDirForPackageStub.getCall(0).args[0], {
+ pkgName: "myPackage",
+ version: "1.2.3"
+ }, "_getStagingDirForPackage should be called with the correct arguments");
+
+ t.is(pathExistsStub.callCount, 2, "_pathExists should be called twice");
+ t.is(pathExistsStub.getCall(0).args[0], "staging-dir-path",
+ "_packageJsonExists should be called with the correct arguments");
+ t.is(pathExistsStub.getCall(1).args[0], targetDir,
+ "_packageJsonExists should be called with the correct arguments");
+ t.is(t.context.rmrfStub.callCount, 0, "rmrf should never be called");
+
+ t.is(extractPackageStub.callCount, 1, "_extractPackage should be called once");
+
+ t.is(t.context.mkdirpStub.callCount, 2, "mkdirp should be called twice");
+ t.is(t.context.mkdirpStub.getCall(0).args[0], path.join("/", "ui5Data", "framework", "locks"),
+ "mkdirp should be called with the correct arguments on first call");
+ t.is(t.context.mkdirpStub.getCall(1).args[0], path.join("my", "package"),
+ "mkdirp should be called with the correct arguments on second call");
+
+ t.is(t.context.renameStub.callCount, 1, "fs.rename should be called once");
+ t.is(t.context.renameStub.getCall(0).args[0], "staging-dir-path",
+ "fs.rename should be called with the correct first argument");
+ t.is(t.context.renameStub.getCall(0).args[1], targetDir,
+ "fs.rename should be called with the correct second argument");
+});
+
+test.serial("Installer: installPackage with already installed package", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ t.context.lockStub.yieldsAsync();
+ t.context.unlockStub.yieldsAsync();
+
+ const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage")
+ .returns("package-dir-path");
+
+ const packageJsonExistsStub = sinon.stub(installer, "_packageJsonExists").resolves(true);
+ const synchronizeSpy = sinon.spy(installer, "_synchronize");
+
+ const getStagingDirForPackageStub = sinon.stub(installer, "_getStagingDirForPackage")
+ .returns("staging-dir-path");
+ const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(false);
+
+ const registry = installer.getRegistry();
+ const extractPackageStub = sinon.stub(registry, "extractPackage").resolves();
+
+ const res = await installer.installPackage({
+ pkgName: "myPackage",
+ version: "1.2.3"
+ });
+
+ t.deepEqual(res, {
+ pkgPath: "package-dir-path"
+ }, "Should return correct values");
+
+ t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once");
+ t.deepEqual(getTargetDirForPackageStub.getCall(0).args[0], {
+ pkgName: "myPackage",
+ version: "1.2.3"
+ }, "_getTargetDirForPackage should be called with the correct arguments");
+
+ t.is(packageJsonExistsStub.callCount, 1, "_packageJsonExists should be called once");
+ t.is(packageJsonExistsStub.getCall(0).args[0], "package-dir-path",
+ "_packageJsonExists should be called with the correct arguments on first call");
+
+ t.is(synchronizeSpy.callCount, 0, "_synchronize should never be called");
+ t.is(t.context.lockStub.callCount, 0, "lock should never be called");
+ t.is(t.context.unlockStub.callCount, 0, "unlock should never be called");
+ t.is(getStagingDirForPackageStub.callCount, 0, "_getStagingDirForPackage should never be called");
+ t.is(pathExistsStub.callCount, 0, "_pathExists should never be called");
+ t.is(t.context.rmrfStub.callCount, 0, "rmrf should never be called");
+ t.is(extractPackageStub.callCount, 0, "_extractPackage should never be called");
+ t.is(t.context.mkdirpStub.callCount, 0, "mkdirp should never be called");
+ t.is(t.context.renameStub.callCount, 0, "fs.rename should never be called");
+});
+
+test.serial("Installer: installPackage with install already in progress", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ t.context.lockStub.yieldsAsync();
+ t.context.unlockStub.yieldsAsync();
+
+ const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage")
+ .returns("package-dir-path");
+
+ const packageJsonExistsStub = sinon.stub(installer, "_packageJsonExists")
+ .onFirstCall().resolves(false)
+ .onSecondCall().resolves(true); // After lock got acquired, package has been installed
+
+ const synchronizeSpy = sinon.spy(installer, "_synchronize");
+
+ const getStagingDirForPackageStub = sinon.stub(installer, "_getStagingDirForPackage")
+ .returns("staging-dir-path");
+ const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(false);
+
+ const registry = installer.getRegistry();
+ const extractPackageStub = sinon.stub(registry, "extractPackage").resolves();
+
+ await installer.installPackage({
+ pkgName: "myPackage",
+ version: "1.2.3"
+ });
+
+ t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once");
+ t.deepEqual(getTargetDirForPackageStub.getCall(0).args[0], {
+ pkgName: "myPackage",
+ version: "1.2.3"
+ }, "_getTargetDirForPackage should be called with the correct arguments");
+
+ t.is(packageJsonExistsStub.callCount, 2, "_packageJsonExists should be called twice");
+ t.is(packageJsonExistsStub.getCall(0).args[0], "package-dir-path",
+ "_packageJsonExists should be called with the correct arguments on first call");
+ t.is(packageJsonExistsStub.getCall(1).args[0], "package-dir-path",
+ "_packageJsonExists should be called with the correct arguments on second call");
+
+ t.is(synchronizeSpy.callCount, 1, "_synchronize should be called once");
+ t.is(synchronizeSpy.getCall(0).args[0], "package-myPackage@1.2.3",
+ "_synchronize should be called with the correct first argument");
+ t.is(t.context.lockStub.callCount, 1, "lock should be called once");
+ t.is(t.context.unlockStub.callCount, 1, "unlock should be called once");
+
+ t.is(t.context.rmrfStub.callCount, 0, "rmrf should never be called");
+
+ t.is(t.context.mkdirpStub.callCount, 1, "mkdirp should be called once");
+ t.is(t.context.mkdirpStub.getCall(0).args[0], path.join("/", "ui5Data", "framework", "locks"),
+ "mkdirp should be called with the correct arguments");
+
+ t.is(getStagingDirForPackageStub.callCount, 0, "_getStagingDirForPackage should never be called");
+ t.is(pathExistsStub.callCount, 0, "_pathExists should never be called");
+ t.is(extractPackageStub.callCount, 0, "_extractPackage should never be called");
+ t.is(t.context.renameStub.callCount, 0, "fs.rename should never be called");
+});
+
+test.serial("Installer: installPackage with new package and existing target and staging", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ t.context.lockStub.yieldsAsync();
+ t.context.unlockStub.yieldsAsync();
+
+ const targetDir = path.join("my", "package", "dir");
+ const getTargetDirForPackageStub = sinon.stub(installer, "_getTargetDirForPackage")
+ .returns(targetDir);
+
+ const packageJsonExistsStub = sinon.stub(installer, "_packageJsonExists").resolves(false);
+ const synchronizeSpy = sinon.spy(installer, "_synchronize");
+
+ const getStagingDirForPackageStub = sinon.stub(installer, "_getStagingDirForPackage")
+ .returns("staging-dir-path");
+ const pathExistsStub = sinon.stub(installer, "_pathExists").resolves(true); // Staging dir exists
+
+ const registry = installer.getRegistry();
+ const extractPackageStub = sinon.stub(registry, "extractPackage").resolves();
+
+ const res = await installer.installPackage({
+ pkgName: "myPackage",
+ version: "1.2.3"
+ });
+
+ t.deepEqual(res, {
+ pkgPath: targetDir
+ }, "Should return correct values");
+
+ t.is(getTargetDirForPackageStub.callCount, 1, "_getTargetDirForPackage should be called once");
+ t.deepEqual(getTargetDirForPackageStub.getCall(0).args[0], {
+ pkgName: "myPackage",
+ version: "1.2.3"
+ }, "_getTargetDirForPackage should be called with the correct arguments");
+
+ t.is(packageJsonExistsStub.callCount, 2, "_packageJsonExists should be called twice");
+ t.is(packageJsonExistsStub.getCall(0).args[0], targetDir,
+ "_packageJsonExists should be called with the correct arguments on first call");
+ t.is(packageJsonExistsStub.getCall(1).args[0], targetDir,
+ "_packageJsonExists should be called with the correct arguments on second call");
+
+ t.is(synchronizeSpy.callCount, 1, "_synchronize should be called once");
+ t.is(synchronizeSpy.getCall(0).args[0], "package-myPackage@1.2.3",
+ "_synchronize should be called with the correct first argument");
+ t.is(t.context.lockStub.callCount, 1, "lock should be called once");
+ t.is(t.context.unlockStub.callCount, 1, "unlock should be called once");
+
+ t.is(getStagingDirForPackageStub.callCount, 1, "_getStagingDirForPackage should be called once");
+ t.deepEqual(getStagingDirForPackageStub.getCall(0).args[0], {
+ pkgName: "myPackage",
+ version: "1.2.3"
+ }, "_getStagingDirForPackage should be called with the correct arguments");
+
+ t.is(pathExistsStub.callCount, 2, "_pathExists should be called twice");
+ t.is(pathExistsStub.getCall(0).args[0], "staging-dir-path",
+ "_packageJsonExists should be called with the correct arguments");
+ t.is(pathExistsStub.getCall(1).args[0], targetDir,
+ "_packageJsonExists should be called with the correct arguments");
+
+ t.is(t.context.rmrfStub.callCount, 2, "rmrf should be called twice");
+ t.is(t.context.rmrfStub.getCall(0).args[0], "staging-dir-path",
+ "rmrf should be called with the correct arguments");
+ t.is(t.context.rmrfStub.getCall(1).args[0], targetDir,
+ "rmrf should be called with the correct arguments");
+
+ t.is(extractPackageStub.callCount, 1, "_extractPackage should be called once");
+
+ t.is(t.context.mkdirpStub.callCount, 2, "mkdirp should be called twice");
+ t.is(t.context.mkdirpStub.getCall(0).args[0], path.join("/", "ui5Data", "framework", "locks"),
+ "mkdirp should be called with the correct arguments on first call");
+ t.is(t.context.mkdirpStub.getCall(1).args[0], path.join("my", "package"),
+ "mkdirp should be called with the correct arguments on second call");
+
+ t.is(t.context.renameStub.callCount, 1, "fs.rename should be called once");
+ t.is(t.context.renameStub.getCall(0).args[0], "staging-dir-path",
+ "fs.rename should be called with the correct first argument");
+ t.is(t.context.renameStub.getCall(0).args[1], targetDir,
+ "fs.rename should be called with the correct second argument");
+});
+
+test.serial("Installer: _pathExists - exists", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ const res = await installer._pathExists(__dirname);
+
+ t.is(res, true, "Path should exist");
+ t.is(t.context.statStub.getCall(0).args[0], __dirname,
+ "fs.stat should be called with correct arguments");
+});
+
+test.serial("Installer: _pathExists - does not exist", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ const notFoundError = new Error("Not found");
+ notFoundError.code = "ENOENT";
+ t.context.statStub.yieldsAsync(notFoundError);
+
+ const res = await installer._pathExists("my-path");
+
+ t.is(res, false, "Path should not exist");
+ t.is(t.context.statStub.getCall(0).args[0], "my-path",
+ "fs.stat should be called with correct arguments");
+});
+
+test.serial("Installer: _pathExists - re-throws unexpected errors", async (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ const notFoundError = new Error("Pony Error");
+ notFoundError.code = "PONY";
+ t.context.statStub.yieldsAsync(notFoundError);
+
+ const err = await t.throwsAsync(installer._pathExists("my-path"));
+
+ t.is(err, notFoundError, "Should throw with expected exception");
+ t.is(t.context.statStub.getCall(0).args[0], "my-path",
+ "fs.stat should be called with correct arguments");
+});
+
+
+test.serial("Installer: Registry throws", (t) => {
+ const {Installer} = t.context;
+
+ const installer = new Installer({
+ cwd: "/cwd/",
+ ui5DataDir: "/ui5Data/"
+ });
+
+ installer._cwd = null;
+ t.throws(() => installer.getRegistry(), {
+ message: "Registry: Missing parameter \"cwd\"",
+ }, "Registry requires cwd");
+
+ installer._cwd = "/cwd/";
+ installer._caCacheDir = null;
+ t.throws(() => installer.getRegistry(), {
+ message: "Registry: Missing parameter \"cacheDir\"",
+ }, "Registry requires cahceDir");
+});
diff --git a/packages/project/test/lib/ui5framework/npm/Registry.js b/packages/project/test/lib/ui5framework/npm/Registry.js
new file mode 100644
index 00000000000..2a3465e0860
--- /dev/null
+++ b/packages/project/test/lib/ui5framework/npm/Registry.js
@@ -0,0 +1,190 @@
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+
+test.beforeEach(async (t) => {
+ const sinon = (t.context.sinon = sinonGlobal.createSandbox());
+
+ t.context.pacote = {
+ packument: sinon.stub(),
+ manifest: sinon.stub(),
+ extract: sinon.stub(),
+ };
+
+ class Config {
+ constructor(...args) {
+ t.context.npmConfigConstructor(...args);
+ }
+
+ static get typeDefs() {
+ return {path: "string"};
+ }
+
+ async load() {}
+
+ get flat() {
+ return {};
+ }
+ }
+
+ t.context.npmConfigConstructor = sinon.stub();
+ t.context.npmConfigFlat = sinon.stub(Config.prototype, "flat");
+ t.context.Registry = await esmock.p("../../../../lib/ui5Framework/npm/Registry.js", {
+ "pacote": {
+ "default": t.context.pacote
+ },
+ "@npmcli/config": {
+ "default": Config
+ },
+ "@npmcli/config/lib/definitions/index.js": {
+ default: {
+ flatten: "flatten",
+ definitions: "definitions",
+ shorthands: "shorthands",
+ defaults: "defaults",
+ }
+ }
+ });
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.Registry);
+});
+
+test.serial("Constructor", (t) => {
+ const {Registry} = t.context;
+
+ const registry = new Registry({
+ cwd: "cwd",
+ cacheDir: "cacheDir"
+ });
+
+ t.true(registry instanceof Registry);
+});
+
+test.serial("_getPacoteOptions", async (t) => {
+ const {Registry, npmConfigFlat, npmConfigConstructor} = t.context;
+
+ const registry = new Registry({
+ cwd: "cwd",
+ cacheDir: "cacheDir"
+ });
+
+ const npmConfig = {
+ "fake": "config"
+ };
+
+ const expectedPacoteOptions = {
+ fake: "config",
+ cache: "cacheDir"
+ };
+ npmConfigFlat.value(npmConfig);
+
+ const pacoteOptions = await registry._getPacoteOptions();
+
+ t.is(npmConfigConstructor.callCount, 1);
+ t.deepEqual(npmConfigConstructor.firstCall.firstArg, {
+ cwd: "cwd",
+ npmPath: "cwd",
+ flatten: "flatten",
+ definitions: "definitions",
+ shorthands: "shorthands",
+ defaults: "defaults",
+ });
+
+ t.deepEqual(pacoteOptions, expectedPacoteOptions);
+});
+
+test.serial("_getPacoteOptions (proxy config set)", async (t) => {
+ const {Registry, npmConfigFlat, npmConfigConstructor} = t.context;
+
+ const registry = new Registry({
+ cwd: "cwd",
+ cacheDir: "cacheDir"
+ });
+
+ const npmConfig = {
+ "proxy": "http://localhost:9999"
+ };
+
+ const expectedPacoteOptions = {
+ proxy: "http://localhost:9999",
+ cache: "cacheDir"
+ };
+
+ npmConfigFlat.value(npmConfig);
+
+ const pacoteOptions = await registry._getPacoteOptions();
+
+ t.is(npmConfigConstructor.callCount, 1);
+
+ t.deepEqual(pacoteOptions, expectedPacoteOptions);
+});
+
+test.serial("_getPacoteOptions (https-proxy config set)", async (t) => {
+ const {Registry, npmConfigFlat, npmConfigConstructor} = t.context;
+
+ const registry = new Registry({
+ cwd: "cwd",
+ cacheDir: "cacheDir"
+ });
+
+ const npmConfig = {
+ "httpsProxy": "http://localhost:9999"
+ };
+
+ const expectedPacoteOptions = {
+ httpsProxy: "http://localhost:9999",
+ cache: "cacheDir"
+ };
+
+ npmConfigFlat.value(npmConfig);
+
+ const pacoteOptions = await registry._getPacoteOptions();
+
+ t.is(npmConfigConstructor.callCount, 1);
+
+ t.deepEqual(pacoteOptions, expectedPacoteOptions);
+});
+
+test.serial("_getPacote", async (t) => {
+ const {Registry, sinon} = t.context;
+
+ const registry = new Registry({
+ cwd: "cwd",
+ cacheDir: "cacheDir"
+ });
+
+ const expectedPacoteOptions = {"fake": "options"};
+
+ sinon.stub(registry, "_getPacoteOptions").resolves(expectedPacoteOptions);
+
+ const {pacote, pacoteOptions} = await registry._getPacote();
+
+ t.is(pacote, t.context.pacote);
+ t.is(pacoteOptions, expectedPacoteOptions);
+});
+
+test.serial("_getPacote caching", async (t) => {
+ const {Registry, sinon} = t.context;
+
+ const registry = new Registry({
+ cwd: "cwd",
+ cacheDir: "cacheDir"
+ });
+
+ const expectedPacoteOptions = {"fake": "options"};
+
+ const getPacoteOptionsStub = sinon.stub(registry, "_getPacoteOptions").resolves(expectedPacoteOptions);
+
+ const {pacote, pacoteOptions} = await registry._getPacote();
+
+ t.is(pacote, t.context.pacote);
+ t.is(pacoteOptions, expectedPacoteOptions);
+
+ await registry._getPacote();
+ await registry._getPacote();
+
+ t.is(getPacoteOptionsStub.callCount, 1, "_getPacoteOptions got called once");
+});
diff --git a/packages/project/test/lib/utils/fs.js b/packages/project/test/lib/utils/fs.js
new file mode 100644
index 00000000000..322beff2fcd
--- /dev/null
+++ b/packages/project/test/lib/utils/fs.js
@@ -0,0 +1,13 @@
+import test from "ava";
+import path from "node:path";
+import {stat} from "node:fs/promises";
+import {mkdirp} from "../../../lib/utils/fs.js";
+
+const __dirname = import.meta.dirname;
+
+test("mkdirp: Create directory hierarchy", async (t) => {
+ const targetPath = path.join(__dirname, "..", "..", "tmp", "mkdir-test", "this", "is", "a", "directory");
+ await mkdirp(targetPath);
+ const res = await stat(targetPath);
+ t.truthy(res, "Target directory has been created");
+});
diff --git a/packages/project/test/lib/validation/ValidationError.js b/packages/project/test/lib/validation/ValidationError.js
new file mode 100644
index 00000000000..dd32292ef8f
--- /dev/null
+++ b/packages/project/test/lib/validation/ValidationError.js
@@ -0,0 +1,939 @@
+import test from "ava";
+import sinon from "sinon";
+import chalk from "chalk";
+import ValidationError from "../../../lib/validation/ValidationError.js";
+
+test.afterEach.always((t) => {
+ sinon.restore();
+});
+
+test.serial("ValidationError constructor", (t) => {
+ const errors = [
+ {dataPath: "", keyword: "", message: "error1", params: {}},
+ {dataPath: "", keyword: "", message: "error2", params: {}}
+ ];
+ const project = {id: "id"};
+ const schema = {schema: "schema"};
+ const data = {data: "data"};
+ const yaml = {path: "path", source: "source", documentIndex: 0};
+
+ const filteredErrors = [{dataPath: "", keyword: "", message: "error1", params: {}}];
+
+ const filterErrorsStub = sinon.stub(ValidationError, "filterErrors");
+ filterErrorsStub.returns(filteredErrors);
+
+ const formatErrorsStub = sinon.stub(ValidationError.prototype, "formatErrors");
+ formatErrorsStub.returns("Formatted Message");
+
+ const validationError = new ValidationError({errors, schema, data, project, yaml});
+
+ t.true(validationError instanceof ValidationError, "ValidationError constructor returns instance");
+ t.true(validationError instanceof Error, "ValidationError inherits from Error");
+ t.is(validationError.name, "ValidationError", "ValidationError should have 'name' property");
+
+ t.deepEqual(validationError.errors, filteredErrors,
+ "ValidationError should have 'errors' property with filtered errors");
+ t.deepEqual(validationError.project, project, "ValidationError should have 'project' property");
+ t.deepEqual(validationError.yaml, yaml, "≈ should have 'yaml' property");
+ t.is(validationError.message, "Formatted Message", "ValidationError should have 'message' property");
+
+ t.is(filterErrorsStub.callCount, 1, "ValidationError.filterErrors should be called once");
+ t.deepEqual(filterErrorsStub.getCall(0).args, [errors],
+ "ValidationError.filterErrors should be called with errors, project and yaml");
+
+ t.is(formatErrorsStub.callCount, 1, "ValidationError#formatErrors should be called once");
+ t.deepEqual(formatErrorsStub.getCall(0).args, [],
+ "ValidationError.formatErrors should be called without args");
+});
+
+test.serial("ValidationError.filterErrors", (t) => {
+ const allErrors = [
+ {
+ keyword: "if"
+ },
+ {
+ dataPath: "dataPath1",
+ keyword: "keyword1"
+ },
+ {
+ dataPath: "dataPath1",
+ keyword: "keyword2"
+ },
+ {
+ dataPath: "dataPath3",
+ keyword: "keyword2"
+ },
+ {
+ dataPath: "dataPath1",
+ keyword: "keyword1"
+ },
+ {
+ dataPath: "dataPath1",
+ keyword: "keyword1",
+ params: {
+ type: "foo"
+ }
+ },
+ {
+ dataPath: "dataPath4",
+ keyword: "keyword5",
+ params: {
+ type: "foo"
+ }
+ },
+ {
+ dataPath: "dataPath6",
+ keyword: "keyword6",
+ params: {
+ errors: [
+ {
+ "type": "foo"
+ },
+ {
+ "type": "bar"
+ }
+ ]
+ }
+ },
+ {
+ dataPath: "dataPath6",
+ keyword: "keyword6",
+ params: {
+ errors: [
+ {
+ "type": "foo"
+ },
+ {
+ "type": "bar"
+ }
+ ]
+ }
+ },
+ {
+ dataPath: "dataPath6",
+ keyword: "keyword6",
+ params: {
+ errors: [
+ {
+ "type": "foo"
+ },
+ {
+ "type": "foo"
+ }
+ ]
+ }
+ }
+ ];
+
+ const expectedErrors = [
+ {
+ dataPath: "dataPath1",
+ keyword: "keyword1"
+ },
+ {
+ dataPath: "dataPath1",
+ keyword: "keyword2"
+ },
+ {
+ dataPath: "dataPath3",
+ keyword: "keyword2"
+ },
+ {
+ dataPath: "dataPath1",
+ keyword: "keyword1",
+ params: {
+ type: "foo"
+ }
+ },
+ {
+ dataPath: "dataPath4",
+ keyword: "keyword5",
+ params: {
+ type: "foo"
+ }
+ },
+ {
+ dataPath: "dataPath6",
+ keyword: "keyword6",
+ params: {
+ errors: [
+ {
+ "type": "foo"
+ },
+ {
+ "type": "bar"
+ }
+ ]
+ }
+ },
+ {
+ dataPath: "dataPath6",
+ keyword: "keyword6",
+ params: {
+ errors: [
+ {
+ "type": "foo"
+ },
+ {
+ "type": "foo"
+ }
+ ]
+ }
+ }
+ ];
+
+ const filteredErrors = ValidationError.filterErrors(allErrors);
+
+ t.deepEqual(filteredErrors, expectedErrors, "filterErrors should return expected errors");
+});
+
+test.serial("ValidationError.formatErrors", (t) => {
+ const fakeValidationErrorInstance = {
+ errors: [{}, {}],
+ project: {id: "my-project"}
+ };
+
+ const formatErrorStub = sinon.stub();
+ formatErrorStub.onFirstCall().returns("Error message 1");
+ formatErrorStub.onSecondCall().returns("Error message 2");
+ fakeValidationErrorInstance.formatError = formatErrorStub;
+
+ const message = ValidationError.prototype.formatErrors.apply(fakeValidationErrorInstance);
+
+ const expectedMessage =
+`${chalk.red("Invalid ui5.yaml configuration for project my-project")}
+
+Error message 1
+
+${process.stdout.isTTY ? chalk.grey.dim("─".repeat(process.stdout.columns || 80)) : ""}
+
+Error message 2`;
+
+ t.is(message, expectedMessage);
+
+ t.is(formatErrorStub.callCount, 2, "formatErrorStub should be called twice");
+ t.deepEqual(formatErrorStub.getCall(0).args, [
+ fakeValidationErrorInstance.errors[0]
+ ], "formatErrorStub should be called with first error");
+ t.deepEqual(formatErrorStub.getCall(1).args, [
+ fakeValidationErrorInstance.errors[1]
+ ], "formatErrorStub should be called with second error");
+});
+
+test.serial("ValidationError.formatError (with yaml)", (t) => {
+ const fakeValidationErrorInstance = {
+ yaml: {
+ path: "/path",
+ source: "source"
+ }
+ };
+ const error = {"error": true};
+
+ const formatMessageStub = sinon.stub(ValidationError, "formatMessage");
+ formatMessageStub.returns("First line\nSecond line\nThird line");
+
+ const getYamlExtractStub = sinon.stub(ValidationError, "getYamlExtract");
+ getYamlExtractStub.returns("YAML");
+
+ const message = ValidationError.prototype.formatError.call(fakeValidationErrorInstance, error);
+
+ const expectedMessage =
+`First line
+
+YAML
+Second line
+Third line`;
+
+ t.is(message, expectedMessage);
+
+ t.is(formatMessageStub.callCount, 1, "formatMessageStub should be called once");
+ t.deepEqual(formatMessageStub.getCall(0).args, [error], "formatMessageStub should be called with error");
+
+ t.is(getYamlExtractStub.callCount, 1, "getYamlExtractStub should be called once");
+ t.deepEqual(getYamlExtractStub.getCall(0).args, [
+ {error, yaml: fakeValidationErrorInstance.yaml}],
+ "getYamlExtractStub should be called with error and yaml");
+});
+
+test.serial("ValidationError.getYamlExtract", (t) => {
+ const error = {};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`property1: value1
+property2: value2
+property3: value3
+property4: value4
+property5: value5
+`,
+ documentIndex: 0
+ };
+
+ const analyzeYamlErrorStub = sinon.stub(ValidationError, "analyzeYamlError");
+ analyzeYamlErrorStub.returns({line: 3, column: 12});
+
+ const expectedYamlExtract =
+ chalk.grey("/my-project/ui5.yaml:3") +
+ "\n\n" +
+ chalk.grey("1:") + " property1: value1\n" +
+ chalk.grey("2:") + " property2: value2\n" +
+ chalk.bgRed(chalk.grey("3:") + " property3: value3\n") +
+ " ".repeat(14) + chalk.red("^");
+
+ const yamlExtract = ValidationError.getYamlExtract({error, yaml});
+
+ t.is(yamlExtract, expectedYamlExtract);
+});
+
+test.serial("ValidationError.getSourceExtract", (t) => {
+ const yamlSource =
+`property1: value1
+property2: value2
+`;
+ const line = 2;
+ const column = 1;
+
+ const expected =
+ chalk.grey("1:") + " property1: value1\n" +
+ chalk.bgRed(chalk.grey("2:") + " property2: value2\n") +
+ " ".repeat(3) + chalk.red("^");
+
+ const sourceExtract = ValidationError.getSourceExtract(yamlSource, line, column);
+
+ t.is(sourceExtract, expected, "getSourceExtract should return expected string");
+});
+
+test.serial("ValidationError.getSourceExtract (Windows Line-Endings)", (t) => {
+ const yamlSource =
+"property1: value1\r\n" +
+"property2: value2\r\n";
+ const line = 2;
+ const column = 1;
+
+ const expected =
+ chalk.grey("1:") + " property1: value1\n" +
+ chalk.bgRed(chalk.grey("2:") + " property2: value2\n") +
+ " ".repeat(3) + chalk.red("^");
+
+ const sourceExtract = ValidationError.getSourceExtract(yamlSource, line, column);
+
+ t.is(sourceExtract, expected, "getSourceExtract should return expected string");
+});
+
+test.serial("ValidationError.analyzeYamlError: Property", (t) => {
+ const error = {dataPath: "/property3"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`property1: value1
+property2: value2
+property3: value3
+property4: value4
+property5: value5
+`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 3, column: 1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Nested property", (t) => {
+ const error = {dataPath: "/property2/property3"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`property1: value1
+property2:
+ property3: value3
+property3: value3
+`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 3, column: 3},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Array", (t) => {
+ const error = {dataPath: "/property/list/2/name"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`property:
+ list:
+ - name: ' - - - - -'
+ - name: other - name- with- hyphens
+ - name: name3
+`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 5, column: 7},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Nested array", (t) => {
+ const error = {dataPath: "/items/2/subItems/1"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`items:
+ - subItems:
+ - foo
+ - bar
+ - subItems:
+ - foo
+ - bar
+ - subItems:
+ - foo
+ - bar
+`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 10, column: 7},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Nested array (Windows Line-Endings)", (t) => {
+ const error = {dataPath: "/items/2/subItems/1"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+"items:\r\n" +
+" - subItems:\r\n" +
+" - foo\r\n" +
+" - bar\r\n" +
+" - subItems:\r\n" +
+" - foo\r\n" +
+" - bar\r\n" +
+" - subItems:\r\n" +
+" - foo\r\n" +
+" - bar\r\n",
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 10, column: 7},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Array with square brackets (not supported)", (t) => {
+ const error = {dataPath: "/items/2"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`items: [1, 2, 3]
+`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: -1, column: -1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Multiline array with square brackets (not supported)", (t) => {
+ const error = {dataPath: "/items/2"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`items: [
+ 1,
+ 2,
+ 3
+]
+`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: -1, column: -1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Nested property with comments", (t) => {
+ const error = {dataPath: "/property1/property2/property3/property4"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`property1:
+ property2:
+ property3:
+ # property4: value4444
+ property4: value4
+`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 5, column: 7},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Nested properties with same name", (t) => {
+ const error = {dataPath: "/property/property/property/property"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`property:
+ property:
+ property:
+ # property: foo
+ property: bar
+`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 5, column: 7},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Error keyword=required, no dataPath", (t) => {
+ const error = {dataPath: "", keyword: "required"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source: ``,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: -1, column: -1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Error keyword=required", (t) => {
+ const error = {dataPath: "/property2", keyword: "required"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`property1: true
+property2:
+ property3: true
+`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 2, column: 1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Error keyword=additionalProperties", (t) => {
+ const error = {
+ dataPath: "/property2",
+ keyword: "additionalProperties",
+ params: {
+ additionalProperty: "property3"
+ }
+ };
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`property1: true
+property2:
+ property3: true
+`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 3, column: 3},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: documentIndex=0 (Without leading separator)", (t) => {
+ const error = {dataPath: "/property3"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`property1: value1document1
+property2: value2document1
+property3: value3document1
+property4: value4document1
+property5: value5document1
+---
+property1: value1document2
+property2: value2document2
+property3: value3document2
+property4: value4document2
+property5: value5document2`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 3, column: 1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: documentIndex=0 (With leading separator)", (t) => {
+ const error = {dataPath: "/property3"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`---
+property1: value1document1
+property2: value2document1
+property3: value3document1
+property4: value4document1
+property5: value5document1
+---
+property1: value1document2
+property2: value2document2
+property3: value3document2
+property4: value4document2
+property5: value5document2`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 4, column: 1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: documentIndex=0 (With leading separator and empty lines)", (t) => {
+ const error = {dataPath: "/property3"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`
+
+
+
+---
+property1: value1document1
+property2: value2document1
+property3: value3document1
+property4: value4document1
+property5: value5document1
+---
+property1: value1document2
+property2: value2document2
+property3: value3document2
+property4: value4document2
+property5: value5document2`,
+ documentIndex: 0
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 8, column: 1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: documentIndex=2 (Without leading separator)", (t) => {
+ const error = {dataPath: "/property3"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`property1: value1document1
+property2: value2document1
+property3: value3document1
+property4: value4document1
+property5: value5document1
+---
+property1: value1document2
+property2: value2document2
+property3: value3document2
+property4: value4document2
+property5: value5document2
+---
+property1: value1document3
+property2: value2document3
+property3: value3document3
+property4: value4document3
+property5: value5document3
+`,
+ documentIndex: 2
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 15, column: 1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: documentIndex=2 (With leading separator)", (t) => {
+ const error = {dataPath: "/property3"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`---
+property1: value1document1
+property2: value2document1
+property3: value3document1
+property4: value4document1
+property5: value5document1
+---
+property1: value1document2
+property2: value2document2
+property3: value3document2
+property4: value4document2
+property5: value5document2
+---
+property1: value1document3
+property2: value2document3
+property3: value3document3
+property4: value4document3
+property5: value5document3
+`,
+ documentIndex: 2
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 16, column: 1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: documentIndex=2 (With leading separator and empty lines)", (t) => {
+ const error = {dataPath: "/property3"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`
+
+
+
+
+---
+property1: value1document1
+property2: value2document1
+property3: value3document1
+property4: value4document1
+property5: value5document1
+---
+property1: value1document2
+property2: value2document2
+property3: value3document2
+property4: value4document2
+property5: value5document2
+---
+property1: value1document3
+property2: value2document3
+property3: value3document3
+property4: value4document3
+property5: value5document3
+`,
+ documentIndex: 2
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: 21, column: 1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Invalid documentIndex=1 (With leading separator)", (t) => {
+ const error = {dataPath: "/property3"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`---
+property1: value1document1
+property2: value2document1
+property3: value3document1
+property4: value4document1
+property5: value5document1
+`,
+ documentIndex: 1
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: -1, column: -1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.analyzeYamlError: Invalid documentIndex=1 (Without leading separator)", (t) => {
+ const error = {dataPath: "/property3"};
+ const yaml = {
+ path: "/my-project/ui5.yaml",
+ source:
+`property1: value1document1
+property2: value2document1
+property3: value3document1
+property4: value4document1
+property5: value5document1
+`,
+ documentIndex: 1
+ };
+
+ const info = ValidationError.analyzeYamlError({error, yaml});
+
+ t.deepEqual(info, {line: -1, column: -1},
+ "analyzeYamlError should return expected results");
+});
+
+test.serial("ValidationError.formatMessage: keyword=type dataPath=", (t) => {
+ const error = {
+ dataPath: "",
+ keyword: "type",
+ message: "should be object",
+ params: {
+ type: "object",
+ },
+ schemaPath: "#/type",
+ };
+
+ const expectedErrorMessage = "Configuration must be of type 'object'";
+
+ const errorMessage = ValidationError.formatMessage(error);
+ t.is(errorMessage, expectedErrorMessage);
+});
+
+test.serial("ValidationError.formatMessage: keyword=type", (t) => {
+ const error = {
+ dataPath: "/foo",
+ keyword: "type",
+ message: "should be object",
+ params: {
+ type: "object",
+ },
+ schemaPath: "#/type",
+ };
+
+ const expectedErrorMessage = `Configuration ${chalk.underline(chalk.red("foo"))} must be of type 'object'`;
+
+ const errorMessage = ValidationError.formatMessage(error);
+ t.is(errorMessage, expectedErrorMessage);
+});
+
+test.serial("ValidationError.formatMessage: keyword=required w/o dataPath", (t) => {
+ const error = {
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'specVersion'",
+ params: {
+ missingProperty: "specVersion",
+ },
+ schemaPath: "#/required",
+ };
+
+ const expectedErrorMessage = "Configuration must have required property 'specVersion'";
+
+ const errorMessage = ValidationError.formatMessage(error);
+ t.is(errorMessage, expectedErrorMessage);
+});
+
+test.serial("ValidationError.formatMessage: keyword=required", (t) => {
+ const error = {
+ keyword: "required",
+ dataPath: "/metadata",
+ schemaPath: "#/definitions/metadata/required",
+ params: {missingProperty: "name"},
+ message: "should have required property 'name'"
+ };
+
+ const expectedErrorMessage =
+ `Configuration ${chalk.underline(chalk.red("metadata"))} must have required property 'name'`;
+
+ const errorMessage = ValidationError.formatMessage(error);
+ t.is(errorMessage, expectedErrorMessage);
+});
+
+test.serial("ValidationError.formatMessage: keyword=errorMessage", (t) => {
+ const error = {
+ dataPath: "/specVersion",
+ keyword: "errorMessage",
+ message:
+`Unsupported "specVersion"
+Your UI5 CLI installation might be outdated.
+Supported specification versions: "2.0", "1.1", "1.0", "0.1"
+For details, see: https://ui5.github.io/cli/pages/Configuration/#specification-versions`,
+ params: {
+ errors: [
+ {
+ dataPath: "/specVersion",
+ keyword: "enum",
+ message: "should be equal to one of the allowed values",
+ params: {
+ allowedValues: [
+ "2.0",
+ "1.1",
+ "1.0",
+ "0.1",
+ ],
+ },
+ schemaPath: "#/properties/specVersion/enum",
+ },
+ ],
+ },
+ schemaPath: "#/properties/specVersion/errorMessage",
+ };
+
+ const expectedErrorMessage =
+`Unsupported "specVersion"
+Your UI5 CLI installation might be outdated.
+Supported specification versions: "2.0", "1.1", "1.0", "0.1"
+For details, see: https://ui5.github.io/cli/pages/Configuration/#specification-versions`;
+
+ const errorMessage = ValidationError.formatMessage(error, {});
+ t.is(errorMessage, expectedErrorMessage);
+});
+
+test.serial("ValidationError.formatMessage: keyword=additionalProperties", (t) => {
+ const error = {
+ keyword: "additionalProperties",
+ dataPath: "/resources/configuration",
+ schemaPath: "#/properties/configuration/additionalProperties",
+ params: {additionalProperty: "propertiesFileEncoding"},
+ message: "should NOT have additional properties"
+ };
+
+ const expectedErrorMessage =
+ `Configuration ${chalk.underline(chalk.red("resources/configuration"))} ` +
+ `property propertiesFileEncoding must not be provided here`;
+
+ const errorMessage = ValidationError.formatMessage(error);
+ t.is(errorMessage, expectedErrorMessage);
+});
+
+test.serial("ValidationError.formatMessage: keyword=enum", (t) => {
+ const error = {
+ keyword: "enum",
+ dataPath: "/type",
+ schemaPath: "#/properties/type/enum",
+ params: {
+ allowedValues: ["application", "library", "theme-library", "module"]
+ },
+ message: "should be equal to one of the allowed values"
+ };
+
+ const expectedErrorMessage =
+`Configuration ${chalk.underline(chalk.red("type"))} must be equal to one of the allowed values
+Allowed values: application, library, theme-library, module`;
+
+ const errorMessage = ValidationError.formatMessage(error);
+ t.is(errorMessage, expectedErrorMessage);
+});
+
+// test.serial.skip("ValidationError.formatMessage: keyword=pattern", (t) => {
+// const error = {};
+
+// const expectedErrorMessage =
+// ``;
+
+// const errorMessage = ValidationError.formatMessage(error);
+// t.is(errorMessage, expectedErrorMessage);
+// });
diff --git a/packages/project/test/lib/validation/schema/__helper__/builder-bundleOptions.js b/packages/project/test/lib/validation/schema/__helper__/builder-bundleOptions.js
new file mode 100644
index 00000000000..144590c1740
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/__helper__/builder-bundleOptions.js
@@ -0,0 +1,269 @@
+import SpecificationVersion from "../../../../../lib/specifications/SpecificationVersion.js";
+
+/**
+ * Common test functionality for builder/bundles/bundleOptions section in config
+ */
+export default {
+ /**
+ * Executes the tests for different kind of projects, e.g. "application", "library"
+ *
+ * @param {Function} test ava test
+ * @param {Function} assertValidation assertion function
+ * @param {string} type one of "application", "library"
+ */
+ defineTests: function(test, assertValidation, type) {
+ // Version specific tests
+ SpecificationVersion.getVersionsForRange(">=4.0").forEach(function(specVersion) {
+ test(`${type} (specVersion ${specVersion}): builder/bundles/bundleOptions`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": type,
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "bundles": [{
+ "bundleOptions": {
+ "optimize": false,
+ "decorateBootstrapModule": false,
+ "addTryCatchRestartWrapper": true,
+ "numberOfParts": 8,
+ "sourceMap": false
+ }
+ }]
+ }
+ });
+ });
+
+ test(`${type} (specVersion ${specVersion}): builder/bundles/bundleOptions properties removal`,
+ async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": type,
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "bundles": [{
+ "bundleOptions": {
+ "usePredefineCalls": true
+ }
+ }]
+ }
+ }, [
+ {
+ keyword: "additionalProperties",
+ dataPath: "/builder/bundles/0/bundleOptions",
+ params: {
+ additionalProperty: "usePredefineCalls",
+ },
+ message: "should NOT have additional properties",
+ },
+ ]);
+ });
+
+ test(`${type} invalid (specVersion ${specVersion}): builder/bundles/bundleOptions config`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": type,
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "bundles": [{
+ "bundleOptions": {
+ "optimize": "invalid value",
+ "decorateBootstrapModule": {"invalid": "value"},
+ "addTryCatchRestartWrapper": ["invalid value"],
+ "numberOfParts": true,
+ "sourceMap": 55
+ }
+ }]
+ }
+ }, [
+ {
+ keyword: "type",
+ dataPath: "/builder/bundles/0/bundleOptions/optimize",
+ params: {
+ type: "boolean",
+ },
+ message: "should be boolean"
+ },
+ {
+ keyword: "type",
+ dataPath:
+ "/builder/bundles/0/bundleOptions/decorateBootstrapModule",
+ params: {
+ type: "boolean",
+ },
+ message: "should be boolean"
+ },
+ {
+ keyword: "type",
+ dataPath:
+ "/builder/bundles/0/bundleOptions/addTryCatchRestartWrapper",
+ params: {
+ type: "boolean",
+ },
+ message: "should be boolean"
+ },
+ {
+ keyword: "type",
+ dataPath:
+ "/builder/bundles/0/bundleOptions/numberOfParts",
+ params: {
+ type: "number",
+ },
+ message: "should be number"
+ },
+ {
+ keyword: "type",
+ dataPath: "/builder/bundles/0/bundleOptions/sourceMap",
+ params: {
+ type: "boolean",
+ },
+ message: "should be boolean"
+ }
+ ]);
+ });
+ });
+
+ SpecificationVersion.getVersionsForRange("3.0 - 3.2").forEach(function(specVersion) {
+ test(`${type} (specVersion ${specVersion}): builder/bundles/bundleOptions`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": type,
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "bundles": [{
+ "bundleOptions": {
+ "optimize": false,
+ "decorateBootstrapModule": false,
+ "addTryCatchRestartWrapper": true,
+ "usePredefineCalls": true,
+ "numberOfParts": 8,
+ "sourceMap": false
+ }
+ }]
+ }
+ });
+ });
+
+ test(`${type} (specVersion ${specVersion}): builder/bundles/bundleOptions properties removal`,
+ async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": type,
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "bundles": [{
+ "bundleOptions": {
+ "debugMode": true
+ }
+ }]
+ }
+ }, [
+ {
+ keyword: "additionalProperties",
+ dataPath: "/builder/bundles/0/bundleOptions",
+ params: {
+ additionalProperty: "debugMode",
+ },
+ message: "should NOT have additional properties",
+ },
+ ]);
+ });
+
+ test(`${type} invalid (specVersion ${specVersion}): builder/bundles/bundleOptions config`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": type,
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "bundles": [{
+ "bundleOptions": {
+ "optimize": "invalid value",
+ "decorateBootstrapModule": {"invalid": "value"},
+ "addTryCatchRestartWrapper": ["invalid value"],
+ "usePredefineCalls": 12,
+ "numberOfParts": true,
+ "sourceMap": 55
+ }
+ }]
+ }
+ }, [
+ {
+ keyword: "type",
+ dataPath: "/builder/bundles/0/bundleOptions/optimize",
+ params: {
+ type: "boolean",
+ },
+ message: "should be boolean"
+ },
+ {
+ keyword: "type",
+ dataPath:
+ "/builder/bundles/0/bundleOptions/decorateBootstrapModule",
+ params: {
+ type: "boolean",
+ },
+ message: "should be boolean"
+ },
+ {
+ keyword: "type",
+ dataPath:
+ "/builder/bundles/0/bundleOptions/addTryCatchRestartWrapper",
+ params: {
+ type: "boolean",
+ },
+ message: "should be boolean"
+ },
+ {
+ keyword: "type",
+ dataPath:
+ "/builder/bundles/0/bundleOptions/usePredefineCalls",
+ params: {
+ type: "boolean",
+ },
+ message: "should be boolean"
+ },
+ {
+ keyword: "type",
+ dataPath:
+ "/builder/bundles/0/bundleOptions/numberOfParts",
+ params: {
+ type: "number",
+ },
+ message: "should be number"
+ },
+ {
+ keyword: "type",
+ dataPath: "/builder/bundles/0/bundleOptions/sourceMap",
+ params: {
+ type: "boolean",
+ },
+ message: "should be boolean"
+ }
+ ]);
+ });
+ });
+ }
+};
diff --git a/packages/project/test/lib/validation/schema/__helper__/customConfiguration.js b/packages/project/test/lib/validation/schema/__helper__/customConfiguration.js
new file mode 100644
index 00000000000..34db358c983
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/__helper__/customConfiguration.js
@@ -0,0 +1,55 @@
+import SpecificationVersion from "../../../../../lib/specifications/SpecificationVersion.js";
+
+/**
+ * Common test functionality for customConfiguration section in config
+ */
+export default {
+ /**
+ * Executes the tests for different kind of projects,
+ * e.g. "application", "library", "theme-library" and "module"
+ *
+ * @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"
+ * @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",
+ }
+ }
+ ]);
+ });
+
+ SpecificationVersion.getVersionsForRange(">=2.1").forEach((specVersion) => {
+ test(`${type}: Valid customConfiguration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, Object.assign( {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type
+ },
+ "customConfiguration": {
+ "foo": "bar"
+ }
+ }, additionalConfiguration));
+ });
+ });
+ }
+};
diff --git a/packages/project/test/lib/validation/schema/__helper__/extension.js b/packages/project/test/lib/validation/schema/__helper__/extension.js
new file mode 100644
index 00000000000..c36624a0f8a
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/__helper__/extension.js
@@ -0,0 +1,123 @@
+import SpecificationVersion from "../../../../../lib/specifications/SpecificationVersion.js";
+import customConfiguration from "./customConfiguration.js";
+
+/**
+ * Common test functionality to be able to run the same tests for different types of kind "extension"
+ */
+export default {
+ /**
+ * Executes the tests for different types of kind extension, e.g. "project-shim", "server-middleware" and "task"
+ *
+ * @param {Function} test ava test
+ * @param {Function} assertValidation assertion function
+ * @param {string} type one of "project-shim", "server-middleware" and "task"
+ * @param {object} additionalConfiguration additional configuration content
+ */
+ defineTests: function(test, assertValidation, type, additionalConfiguration) {
+ additionalConfiguration = additionalConfiguration || {};
+ additionalConfiguration = Object.assign({"kind": "extension"}, additionalConfiguration);
+
+ customConfiguration.defineTests(test, assertValidation, type, additionalConfiguration);
+
+ SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => {
+ test(`kind: extension / type: ${type} basic (${specVersion})`, async (t) => {
+ await assertValidation(t, Object.assign({
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type
+ }
+ }, additionalConfiguration));
+ });
+
+ test(`kind: extension / type: ${type} additionalProperties (${specVersion})`, async (t) => {
+ await assertValidation(t, Object.assign({
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type
+ },
+ "resources": {}
+ }, additionalConfiguration), [{
+ dataPath: "",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ "additionalProperty": "resources"
+ }
+ }]);
+ });
+
+ test(`kind: extension / type: ${type} Invalid configuration: Additional property (${specVersion})`,
+ async (t) => {
+ await assertValidation(t, Object.assign( {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type
+ },
+ "notAllowed": true
+ }, additionalConfiguration), [{
+ dataPath: "",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "notAllowed",
+ }
+ }]);
+ });
+ });
+
+ SpecificationVersion.getVersionsForRange("2.0 - 2.6").forEach((specVersion) => {
+ test(`kind: extension / type: ${type}: Invalid metadata.name (${specVersion})`, async (t) => {
+ await assertValidation(t, Object.assign({
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": {}
+ }
+ }, additionalConfiguration), [{
+ dataPath: "/metadata/name",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string"
+ }
+ }]);
+ });
+ });
+
+ SpecificationVersion.getVersionsForRange(">=3.0").forEach((specVersion) => {
+ test(`kind: extension / type: ${type}: Invalid metadata.name (${specVersion})`, async (t) => {
+ await assertValidation(t, Object.assign({
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": {}
+ }
+ }, additionalConfiguration), [{
+ dataPath: "/metadata/name",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ },
+ }, {
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: `Not a valid extension 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: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ }
+ }]
+ },
+ }]);
+ });
+ });
+ }
+};
diff --git a/packages/project/test/lib/validation/schema/__helper__/framework.js b/packages/project/test/lib/validation/schema/__helper__/framework.js
new file mode 100644
index 00000000000..841ce8fc790
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/__helper__/framework.js
@@ -0,0 +1,254 @@
+import SpecificationVersion from "../../../../../lib/specifications/SpecificationVersion.js";
+
+/**
+ * Common test functionality for framework section in config
+ */
+export default {
+ /**
+ * Executes the tests for different types of kind project,
+ * e.g. "application", library" and "theme-library"
+ *
+ * @param {Function} test ava test
+ * @param {Function} assertValidation assertion function
+ * @param {string} type one of "application", library" and "theme-library"
+ */
+ defineTests: function(test, assertValidation, type) {
+ SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => {
+ test(`${type} (specVersion ${specVersion}): framework configuration: OpenUI5`, async (t) => {
+ const config = {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type
+ },
+ "framework": {
+ "name": "OpenUI5",
+ "version": "1.75.0",
+ "libraries": [
+ {"name": "sap.ui.core"},
+ {"name": "sap.m"},
+ {"name": "sap.f", "optional": true},
+ {"name": "sap.ui.support", "development": true}
+ ]
+ }
+ };
+ await assertValidation(t, config);
+ });
+
+ test(`${type} (specVersion ${specVersion}): framework configuration: SAPUI5`, async (t) => {
+ const config = {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type
+ },
+ "framework": {
+ "name": "SAPUI5",
+ "version": "1.75.0",
+ "libraries": [
+ {"name": "sap.ui.core"},
+ {"name": "sap.m"},
+ {"name": "sap.f", "optional": true},
+ {"name": "sap.ui.support", "development": true},
+ {"name": "sap.ui.comp", "development": true, "optional": false},
+ {"name": "sap.fe", "development": false, "optional": true},
+ {
+ "name": "sap.ui.export",
+ "development": false,
+ "optional": false
+ }
+ ]
+ }
+ };
+ await assertValidation(t, config);
+ });
+
+ test(`${type} (specVersion ${specVersion}): framework configuration: Invalid`, async (t) => {
+ const config = {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type
+ },
+ "framework": {
+ "name": "FooUI5",
+ "version": "1.75",
+ "libraries": [
+ "sap.ui.core",
+ {"library": "sap.m"},
+ {"name": "sap.f", "optional": "x"},
+ {"name": "sap.f", "development": "no"}
+ ]
+ }
+ };
+
+ await assertValidation(t, config, [
+ {
+ dataPath: "/framework/name",
+ keyword: "enum",
+ message: "should be equal to one of the allowed values",
+ params: {
+ allowedValues: [
+ "OpenUI5",
+ "SAPUI5",
+ ],
+ }
+ },
+ {
+ dataPath: "/framework/version",
+ keyword: "errorMessage",
+ message: "Not a valid version according to the Semantic Versioning specification (https://semver.org/)",
+ params: {
+ errors: [
+ {
+ dataPath: "/framework/version",
+ keyword: "pattern",
+ message:
+ "should match pattern \"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)" +
+ "(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*" +
+ "[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$\"",
+ params: {
+ pattern:
+ "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*" +
+ "[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-]" +
+ "[0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$",
+ }
+ }
+ ]
+ }
+ },
+ {
+ dataPath: "/framework/libraries/0",
+ keyword: "type",
+ message: "should be object",
+ params: {
+ type: "object",
+ }
+ },
+ {
+ dataPath: "/framework/libraries/1",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "library",
+ }
+ },
+ {
+ dataPath: "/framework/libraries/1",
+ keyword: "required",
+ message: "should have required property 'name'",
+ params: {
+ missingProperty: "name",
+ }
+ },
+ {
+ dataPath: "/framework/libraries/2/optional",
+ keyword: "type",
+ message: "should be boolean",
+ params: {
+ type: "boolean"
+ }
+ },
+ {
+ dataPath: "/framework/libraries/3/development",
+ keyword: "type",
+ message: "should be boolean",
+ params: {
+ type: "boolean"
+ }
+ }
+ ]);
+ });
+
+ test(`${type} (specVersion ${specVersion}): framework configuration: Missing 'name'`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type
+ },
+ "framework": {}
+ }, [
+ {
+ dataPath: "/framework",
+ keyword: "required",
+ message: "should have required property 'name'",
+ params: {
+ missingProperty: "name"
+ }
+ }
+ ]);
+ });
+
+ test(
+ `${type} (specVersion ${specVersion}): framework configuration: library with optional and development`,
+ async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type
+ },
+ "framework": {
+ "name": "OpenUI5",
+ "libraries": [
+ {
+ name: "sap.ui.lib1",
+ development: true,
+ optional: true
+ },
+ {
+ // This should only complain about wrong types, not that both are true
+ name: "sap.ui.lib2",
+ development: "true",
+ optional: "true"
+ }
+ ]
+ }
+ }, [
+ {
+ dataPath: "/framework/libraries/0",
+ keyword: "errorMessage",
+ message: "Either \"development\" or \"optional\" can be true, but not both",
+ params: {
+ errors: [
+ {
+ dataPath: "/framework/libraries/0",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "development",
+ }
+ },
+ {
+ dataPath: "/framework/libraries/0",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "optional",
+ }
+ },
+ ],
+ }
+ },
+ {
+ dataPath: "/framework/libraries/1/optional",
+ keyword: "type",
+ message: "should be boolean",
+ params: {
+ type: "boolean",
+ }
+ },
+ {
+ dataPath: "/framework/libraries/1/development",
+ keyword: "type",
+ message: "should be boolean",
+ params: {
+ type: "boolean",
+ }
+ },
+ ]);
+ });
+ });
+ }
+};
diff --git a/packages/project/test/lib/validation/schema/__helper__/project.js b/packages/project/test/lib/validation/schema/__helper__/project.js
new file mode 100644
index 00000000000..cbac64534f7
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/__helper__/project.js
@@ -0,0 +1,313 @@
+import SpecificationVersion from "../../../../../lib/specifications/SpecificationVersion.js";
+import framework from "./framework.js";
+import customConfiguration from "./customConfiguration.js";
+import bundleOptions from "./builder-bundleOptions.js";
+
+/**
+ * Common test functionality to be able to run the same tests for different types of kind "project"
+ */
+export default {
+ /**
+ * Executes the tests for different types of kind project,
+ * e.g. "application", "library", "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"
+ */
+ defineTests: function(test, assertValidation, type) {
+ // framework tests
+ if (["application", "library", "theme-library"].includes(type)) {
+ framework.defineTests(test, assertValidation, type);
+ }
+
+ // customConfiguration tests
+ customConfiguration.defineTests(test, assertValidation, type);
+
+ // builder.bundleOptions tests
+ if (["application", "library"].includes(type)) {
+ bundleOptions.defineTests(test, assertValidation, type);
+ }
+
+ // version specific tests
+ SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => {
+ // tests for all kinds and version 2.0 and above
+ test(`${type} (specVersion ${specVersion}): No metadata`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type
+ }, [{
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'metadata'",
+ params: {
+ missingProperty: "metadata",
+ }
+ }]);
+ });
+
+ test(`${type} (specVersion ${specVersion}): Metadata not type object`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": "foo"
+ }, [{
+ dataPath: "/metadata",
+ keyword: "type",
+ message: "should be object",
+ params: {
+ type: "object",
+ }
+ }]);
+ });
+
+ test(`${type} (specVersion ${specVersion}): No metadata.name`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {}
+ }, [{
+ dataPath: "/metadata",
+ keyword: "required",
+ message: "should have required property 'name'",
+ params: {
+ missingProperty: "name",
+ }
+ }]);
+ });
+
+ test(`${type} (specVersion ${specVersion}): Invalid metadata.copyright`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "foo",
+ "copyright": 123
+ }
+ }, [
+ {
+ dataPath: "/metadata/copyright",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string"
+ }
+ }
+ ]);
+ });
+
+ test(`${type} (specVersion ${specVersion}): Additional metadata property`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "foo",
+ "copyrihgt": "typo"
+ }
+ }, [
+ {
+ dataPath: "/metadata",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "copyrihgt"
+ }
+ }
+ ]);
+ });
+
+ test(`${type} (specVersion ${specVersion}): metadata.deprecated: true`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type,
+ "deprecated": true
+ }
+ });
+ });
+
+ test(`${type} (specVersion ${specVersion}): metadata.deprecated: false`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type,
+ "deprecated": false
+ }
+ });
+ });
+
+ test(`${type} (specVersion ${specVersion}): Invalid metadata.deprecated`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type,
+ "deprecated": "Yes"
+ }
+ }, [
+ {
+ dataPath: "/metadata/deprecated",
+ keyword: "type",
+ message: "should be boolean",
+ params: {
+ type: "boolean",
+ }
+ }
+ ]);
+ });
+
+ test(`${type} (specVersion ${specVersion}): metadata.sapInternal: true`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type,
+ "sapInternal": true
+ }
+ });
+ });
+
+ test(`${type} (specVersion ${specVersion}): metadata.sapInternal: false`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type,
+ "sapInternal": false
+ }
+ });
+ });
+
+ test(`${type} (specVersion ${specVersion}): Invalid metadata.sapInternal`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type,
+ "sapInternal": "Yes"
+ }
+ }, [
+ {
+ dataPath: "/metadata/sapInternal",
+ keyword: "type",
+ message: "should be boolean",
+ params: {
+ type: "boolean",
+ }
+ }
+ ]);
+ });
+
+ test(`${type} (specVersion ${specVersion}): metadata.allowSapInternal: true`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type,
+ "allowSapInternal": true
+ }
+ });
+ });
+
+ test(`${type} (specVersion ${specVersion}): metadata.allowSapInternal: false`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type,
+ "allowSapInternal": false
+ }
+ });
+ });
+
+ test(`${type} (specVersion ${specVersion}): Invalid metadata.allowSapInternal`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type,
+ "allowSapInternal": "Yes"
+ }
+ }, [
+ {
+ dataPath: "/metadata/allowSapInternal",
+ keyword: "type",
+ message: "should be boolean",
+ params: {
+ type: "boolean",
+ }
+ }
+ ]);
+ });
+
+ test(`${type} (specVersion ${specVersion}) Invalid configuration: Additional property`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "name": "my-" + type
+ },
+ "notAllowed": true
+ }, [{
+ dataPath: "",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "notAllowed",
+ },
+ }]);
+ });
+ });
+
+ ["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) => {
+ test(`${type} (specVersion ${specVersion}): Invalid metadata.name`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": type,
+ "metadata": {
+ "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: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ }
+ }]
+ },
+ }
+ ]);
+ });
+ });
+ }
+};
diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/extension.js b/packages/project/test/lib/validation/schema/specVersion/kind/extension.js
new file mode 100644
index 00000000000..54fbb1fdc78
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/specVersion/kind/extension.js
@@ -0,0 +1,196 @@
+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";
+
+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;
+ });
+ 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/extension.json"]
+ });
+});
+
+test.after.always((t) => {
+ t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension"});
+ const thresholds = {
+ statements: 80,
+ branches: 70,
+ functions: 100,
+ lines: 80
+ };
+ t.context.ajvCoverage.verify(thresholds);
+});
+
+SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => {
+ test(`Type project-shim (${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "extension",
+ "type": "project-shim",
+ "metadata": {
+ "name": "my-project-shim"
+ },
+ "shims": {}
+ });
+ });
+
+ test(`Type server-middleware (${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "extension",
+ "type": "server-middleware",
+ "metadata": {
+ "name": "my-server-middleware"
+ },
+ "middleware": {
+ "path": "middleware.js"
+ }
+ });
+ });
+
+ test(`Type task (${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "extension",
+ "type": "task",
+ "metadata": {
+ "name": "my-task"
+ },
+ "task": {
+ "path": "task.js"
+ }
+ });
+ });
+
+ test(`No type (${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "extension",
+ "metadata": {
+ "name": "my-project"
+ }
+ }, [{
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'type'",
+ params: {
+ missingProperty: "type",
+ }
+ }]);
+ });
+
+ test(`Invalid type (${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "extension",
+ "type": "foo",
+ "metadata": {
+ "name": "my-project"
+ }
+ }, [{
+ dataPath: "/type",
+ keyword: "enum",
+ message: "should be equal to one of the allowed values",
+ params: {
+ allowedValues: [
+ "task",
+ "server-middleware",
+ "project-shim"
+ ],
+ }
+ }]);
+ });
+
+ test(`No specVersion (${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "kind": "extension",
+ "type": "project-shim",
+ "metadata": {
+ "name": "my-library"
+ },
+ "shims": {}
+ }, [{
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'specVersion'",
+ params: {
+ missingProperty: "specVersion",
+ }
+ }]);
+ });
+
+ test(`No metadata (${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "extension",
+ "type": "project-shim",
+ "shims": {}
+ }, [{
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'metadata'",
+ params: {
+ missingProperty: "metadata",
+ }
+ }]);
+ });
+});
+
+test("Legacy: Special characters in name (task)", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "kind": "extension",
+ "type": "task",
+ "metadata": {
+ "name": "ä".repeat(81)
+ },
+ "task": {
+ "path": "task.js"
+ }
+ });
+});
+
+test("Legacy: Special characters in name (server-middleware)", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "kind": "extension",
+ "type": "server-middleware",
+ "metadata": {
+ "name": "@my(middleware)"
+ },
+ "middleware": {
+ "path": "middleware.js"
+ }
+ });
+});
+
+test("Legacy: Special characters in name (project-shim)", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "kind": "extension",
+ "type": "project-shim",
+ "metadata": {
+ "name": "my/(project)-shim"
+ },
+ "shims": {}
+ });
+});
diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/extension/project-shim.js b/packages/project/test/lib/validation/schema/specVersion/kind/extension/project-shim.js
new file mode 100644
index 00000000000..286f3b0bddd
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/specVersion/kind/extension/project-shim.js
@@ -0,0 +1,243 @@
+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 extension from "../../../__helper__/extension.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/extension/project-shim.json"]
+ });
+});
+
+test.after.always((t) => {
+ t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension-project-shim"});
+ const thresholds = {
+ statements: 75,
+ branches: 60,
+ functions: 100,
+ lines: 70
+ };
+ t.context.ajvCoverage.verify(thresholds);
+});
+
+SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => {
+ test(`kind: extension / type: project-shim (${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "extension",
+ "type": "project-shim",
+ "metadata": {
+ "name": "my-project-shim"
+ },
+ "shims": {
+ "configurations": {
+ "invalid": {
+ "specVersion": "4.0",
+ "type": "does-not-exist",
+ "metadata": {
+ "name": "my-application"
+ }
+ }
+ },
+ "dependencies": {
+ "my-dependency": {
+ "foo": "bar"
+ }
+ },
+ "collections": {
+ "foo": {
+ "modules": {
+ "lib-1": {
+ "path": "src/lib1"
+ }
+ },
+ "notAllowed": true
+ }
+ },
+ "notAllowed": true
+ },
+ "middleware": {}
+ }, [
+ {
+ dataPath: "",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ "additionalProperty": "middleware"
+ }
+ },
+ {
+ dataPath: "/shims",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "notAllowed",
+ }
+ },
+ {
+ dataPath: "/shims/dependencies/my-dependency",
+ keyword: "type",
+ message: "should be array",
+ params: {
+ type: "array",
+ }
+ },
+ {
+ dataPath: "/shims/collections/foo",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "notAllowed",
+ }
+ },
+ {
+ dataPath: "/shims/collections/foo/modules/lib-1",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ }
+ }
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) {
+ test(`Invalid extension name (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "extension",
+ "type": "project-shim",
+ "metadata": {
+ "name": "illegal/name"
+ },
+ "shims": {}
+ }, [{
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: `Not a valid extension 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,
+ "kind": "extension",
+ "type": "project-shim",
+ "metadata": {
+ "name": "a"
+ },
+ "shims": {}
+ }, [{
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: `Not a valid extension 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,
+ "kind": "extension",
+ "type": "project-shim",
+ "metadata": {
+ "name": "a".repeat(81)
+ },
+ "shims": {}
+ }, [{
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: `Not a valid extension 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,
+ }
+ }]
+ },
+ }]);
+ });
+});
+
+const additionalConfiguration = {
+ "shims": {
+ "configurations": {
+ "my-dependency": {
+ "specVersion": "2.0",
+ "type": "application",
+ "metadata": {
+ "name": "my-application"
+ }
+ },
+ "my-other-dependency": {
+ "specVersion": "4.0",
+ "type": "does-not-exist",
+ "metadata": {
+ "name": "my-application"
+ }
+ }
+ },
+ "dependencies": {
+ "my-dependency": [
+ "my-other-dependency"
+ ],
+ "my-other-dependency": [
+ "some-lib",
+ "some-other-lib"
+ ]
+ },
+ "collections": {
+ "my-dependency": {
+ "modules": {
+ "lib-1": "src/lib1",
+ "lib-2": "src/lib2"
+ }
+ }
+ }
+ }
+};
+
+extension.defineTests(test, assertValidation, "project-shim", additionalConfiguration);
diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/extension/server-middleware.js b/packages/project/test/lib/validation/schema/specVersion/kind/extension/server-middleware.js
new file mode 100644
index 00000000000..ceb1e6dc3c5
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/specVersion/kind/extension/server-middleware.js
@@ -0,0 +1,135 @@
+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 extension from "../../../__helper__/extension.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/extension/server-middleware.json"]
+ });
+});
+
+test.after.always((t) => {
+ t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension-server-middleware"});
+ const thresholds = {
+ statements: 70,
+ branches: 55,
+ functions: 100,
+ lines: 70
+ };
+ t.context.ajvCoverage.verify(thresholds);
+});
+
+SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) {
+ test(`Invalid extension name (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "extension",
+ "type": "server-middleware",
+ "metadata": {
+ "name": "illegal-🦜"
+ },
+ "middleware": {
+ "path": "/bar"
+ }
+ }, [{
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: `Not a valid extension 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,
+ "kind": "extension",
+ "type": "server-middleware",
+ "metadata": {
+ "name": "a"
+ },
+ "middleware": {
+ "path": "/bar"
+ }
+ }, [{
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: `Not a valid extension 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,
+ "kind": "extension",
+ "type": "server-middleware",
+ "metadata": {
+ "name": "a".repeat(81)
+ },
+ "middleware": {
+ "path": "/bar"
+ }
+ }, [{
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: `Not a valid extension 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,
+ }
+ }]
+ },
+ }]);
+ });
+});
+
+const additionalConfiguration = {
+ "middleware": {
+ "path": "/foo"
+ }
+};
+
+extension.defineTests(test, assertValidation, "server-middleware", additionalConfiguration);
diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/extension/task.js b/packages/project/test/lib/validation/schema/specVersion/kind/extension/task.js
new file mode 100644
index 00000000000..e8ad322537e
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/specVersion/kind/extension/task.js
@@ -0,0 +1,135 @@
+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 extension from "../../../__helper__/extension.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/extension/task.json"]
+ });
+});
+
+test.after.always((t) => {
+ t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension-task"});
+ const thresholds = {
+ statements: 70,
+ branches: 55,
+ functions: 100,
+ lines: 70
+ };
+ t.context.ajvCoverage.verify(thresholds);
+});
+
+SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) {
+ test(`Invalid extension name (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "extension",
+ "type": "task",
+ "metadata": {
+ "name": "illegal-🦜"
+ },
+ "task": {
+ "path": "/bar"
+ }
+ }, [{
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: `Not a valid extension 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,
+ "kind": "extension",
+ "type": "task",
+ "metadata": {
+ "name": "a"
+ },
+ "task": {
+ "path": "/bar"
+ }
+ }, [{
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: `Not a valid extension 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,
+ "kind": "extension",
+ "type": "task",
+ "metadata": {
+ "name": "a".repeat(81)
+ },
+ "task": {
+ "path": "/bar"
+ }
+ }, [{
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: `Not a valid extension 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,
+ }
+ }]
+ },
+ }]);
+ });
+});
+
+const additionalConfiguration = {
+ "task": {
+ "path": "/foo"
+ }
+};
+
+extension.defineTests(test, assertValidation, "task", additionalConfiguration);
diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project.js b/packages/project/test/lib/validation/schema/specVersion/kind/project.js
new file mode 100644
index 00000000000..ba9d09ca579
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/specVersion/kind/project.js
@@ -0,0 +1,237 @@
+import test from "ava";
+import Ajv from "ajv";
+import ajvErrors from "ajv-errors";
+import AjvCoverage from "../../../../../utils/AjvCoverage.js";
+import {_Validator as Validator} from "../../../../../../lib/validation/validator.js";
+import ValidationError from "../../../../../../lib/validation/ValidationError.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;
+ });
+ 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.json"]
+ });
+});
+
+test.after.always((t) => {
+ t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project"});
+ const thresholds = {
+ statements: 85,
+ branches: 75,
+ functions: 100,
+ lines: 90
+ };
+ t.context.ajvCoverage.verify(thresholds);
+});
+
+test("Type application", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "kind": "project",
+ "type": "application",
+ "metadata": {
+ "name": "my-application"
+ }
+ });
+});
+
+test("Type application (no kind)", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "type": "application",
+ "metadata": {
+ "name": "my-application"
+ }
+ });
+});
+
+test("Type library", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "kind": "project",
+ "type": "library",
+ "metadata": {
+ "name": "my-library"
+ }
+ });
+});
+
+test("Type library (no kind)", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "type": "library",
+ "metadata": {
+ "name": "my-library"
+ }
+ });
+});
+
+test("Type theme-library", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "kind": "project",
+ "type": "theme-library",
+ "metadata": {
+ "name": "my-theme-library"
+ }
+ });
+});
+
+test("Type theme-library (no kind)", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "type": "theme-library",
+ "metadata": {
+ "name": "my-theme-library"
+ }
+ });
+});
+
+test("Type module", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "kind": "project",
+ "type": "module",
+ "metadata": {
+ "name": "my-module"
+ }
+ });
+});
+
+test("Type module (no kind)", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "type": "module",
+ "metadata": {
+ "name": "my-module"
+ }
+ });
+});
+
+test("No type", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "kind": "project",
+ "metadata": {
+ "name": "my-project"
+ }
+ }, [{
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'type'",
+ params: {
+ missingProperty: "type",
+ }
+ }]);
+});
+
+test("No type, no kind", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "metadata": {
+ "name": "my-project"
+ }
+ }, [{
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'type'",
+ params: {
+ missingProperty: "type",
+ }
+ }]);
+});
+
+test("Invalid type", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "kind": "project",
+ "type": "foo",
+ "metadata": {
+ "name": "my-project"
+ }
+ }, [{
+ dataPath: "/type",
+ keyword: "enum",
+ message: "should be equal to one of the allowed values",
+ params: {
+ allowedValues: [
+ "application",
+ "library",
+ "theme-library",
+ "module",
+ ],
+ }
+ }]);
+});
+
+test("No specVersion", async (t) => {
+ await assertValidation(t, {
+ "kind": "project",
+ "type": "library",
+ "metadata": {
+ "name": "my-library"
+ }
+ }, [{
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'specVersion'",
+ params: {
+ missingProperty: "specVersion",
+ }
+ }]);
+});
+
+test("Legacy: Special characters in name (application)", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "type": "application",
+ "metadata": {
+ "name": "/".repeat(81)
+ }
+ });
+});
+
+test("Legacy: Special characters in name (library)", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "type": "library",
+ "metadata": {
+ "name": "my/(library)"
+ }
+ });
+});
+
+test("Legacy: Special characters in name (theme-library)", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "type": "theme-library",
+ "metadata": {
+ "name": "my/(theme)-library"
+ }
+ });
+});
+
+test("Legacy: Special characters in name (module)", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "type": "module",
+ "metadata": {
+ "name": "my/(module)"
+ }
+ });
+});
diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project/application.js b/packages/project/test/lib/validation/schema/specVersion/kind/project/application.js
new file mode 100644
index 00000000000..db7a75b6826
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/specVersion/kind/project/application.js
@@ -0,0 +1,1523 @@
+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/application.json"]
+ });
+});
+
+test.after.always((t) => {
+ t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-application"});
+ const thresholds = {
+ statements: 80,
+ branches: 75,
+ functions: 100,
+ lines: 80
+ };
+ t.context.ajvCoverage.verify(thresholds);
+});
+
+SpecificationVersion.getVersionsForRange(">=4.0").forEach(function(specVersion) {
+ test(`Valid configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "application",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "okay"
+ },
+ "resources": {
+ "configuration": {
+ "propertiesFileSourceEncoding": "UTF-8",
+ "paths": {
+ "webapp": "/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,
+ "async": false
+ }
+ ]
+ },
+ "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: "application",
+ metadata: {
+ name: "com.sap.ui5.test",
+ },
+ resources: {
+ configuration: {
+ propertiesFileSourceEncoding: "FOO",
+ paths: {
+ app: "webapp",
+ webapp: {
+ path: "invalid",
+ },
+ },
+ notAllowed: true,
+ },
+ notAllowed: true,
+ },
+ builder: {
+ // cachebuster is only supported for type application
+ cachebuster: {
+ signatureType: "time",
+ },
+ bundles: [
+ {
+ bundleDefinition: {
+ name: "app.js",
+ defaultFileTypes: [".js"],
+ sections: [
+ {
+ name: "some-app-preload",
+ mode: "preload",
+ filters: ["some/app/Component.js"],
+ resolve: true,
+ sort: true,
+ declareRawModules: false,
+ async: false,
+ },
+ {
+ mode: "require",
+ filters: ["ui5loader-autoconfig.js"],
+ resolve: true,
+ async: false,
+ },
+ ],
+ },
+ bundleOptions: {
+ optimize: true,
+ numberOfParts: 3,
+ },
+ },
+ ],
+ },
+ },
+ [
+ {
+ 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/webapp",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ },
+ },
+ {
+ dataPath: "/builder/bundles/0/bundleDefinition/sections/0",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "async",
+ },
+ },
+ ]
+ );
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "application",
+ "metadata": {
+ "name": "com.sap.ui5.test"
+ },
+ "resources": {
+ "configuration": {
+ "paths": "webapp"
+ }
+ }
+ }, [
+ {
+ dataPath: "/resources/configuration/paths",
+ keyword: "type",
+ message: "should be object",
+ params: {
+ type: "object"
+ }
+ }
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange("2.0 - 3.2").forEach(function(specVersion) {
+ test(`Valid configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "application",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "okay"
+ },
+ "resources": {
+ "configuration": {
+ "propertiesFileSourceEncoding": "UTF-8",
+ "paths": {
+ "webapp": "/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,
+ "usePredefineCalls": 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": "application",
+ "metadata": {
+ "name": "com.sap.ui5.test"
+ },
+ "resources": {
+ "configuration": {
+ "propertiesFileSourceEncoding": "FOO",
+ "paths": {
+ "app": "webapp",
+ "webapp": {
+ "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/webapp",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string"
+ }
+ }
+ ]);
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "application",
+ "metadata": {
+ "name": "com.sap.ui5.test"
+ },
+ "resources": {
+ "configuration": {
+ "paths": "webapp"
+ }
+ }
+ }, [
+ {
+ dataPath: "/resources/configuration/paths",
+ keyword: "type",
+ message: "should be object",
+ params: {
+ type: "object"
+ }
+ }
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange("2.0 - 2.2").forEach(function(specVersion) {
+ test(`Unsupported builder/componentPreload/excludes configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "application",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "componentPreload": {
+ "excludes": [
+ "some/excluded/files/**",
+ "some/other/excluded/files/**"
+ ]
+ }
+ }
+ }, [
+ {
+ dataPath: "/builder/componentPreload",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "excludes",
+ },
+ },
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=2.3").forEach(function(specVersion) {
+ test(`application (specVersion ${specVersion}): builder/componentPreload/excludes`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "application",
+ "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": "application",
+ "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": "application",
+ "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",
+ },
+ },
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=2.4").forEach(function(specVersion) {
+ // Unsupported cases for older spec-versions already tested via "allowedValues" comparison above
+ test(`application (specVersion ${specVersion}): builder/bundles/bundleDefinition/sections/mode: bundleInfo`,
+ async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "application",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "bundles": [{
+ "bundleDefinition": {
+ "name": "my-bundle.js",
+ "sections": [{
+ "name": "my-bundle-info",
+ "mode": "bundleInfo",
+ "filters": []
+ }]
+ }
+ }]
+ }
+ });
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=2.5").forEach(function(specVersion) {
+ test(`application (specVersion ${specVersion}): builder/settings/includeDependency*`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "application",
+ "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": "application",
+ "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": "application",
+ "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",
+ },
+ },
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=2.6").forEach(function(specVersion) {
+ test(`application (specVersion ${specVersion}): builder/minification/excludes`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "application",
+ "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": "application",
+ "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": "application",
+ "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",
+ },
+ },
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) {
+ test(`Invalid project name (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "application",
+ "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": "application",
+ "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": "application",
+ "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,
+ },
+ }]
+ },
+ }]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange("2.0 - 3.1").forEach(function(specVersion) {
+ test(`Invalid builder configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "application",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ // jsdoc is not supported for type application
+ "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: ["3.1", "3.0", "2.6", "2.5", "2.4"].includes(specVersion) ? [
+ "raw",
+ "preload",
+ "require",
+ "provided",
+ "bundleInfo"
+ ] : [
+ "raw",
+ "preload",
+ "require",
+ "provided"
+ ]
+ }
+ },
+ {
+ 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",
+ }
+ }
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=3.2").forEach(function(specVersion) {
+ test(`Invalid builder configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "application",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ // jsdoc is not supported for type application
+ "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",
+ }
+ }
+ ]);
+ });
+});
+
+project.defineTests(test, assertValidation, "application");
diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project/library.js b/packages/project/test/lib/validation/schema/specVersion/kind/project/library.js
new file mode 100644
index 00000000000..b51e43bcb96
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/specVersion/kind/project/library.js
@@ -0,0 +1,1751 @@
+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/library.json"]
+ });
+});
+
+test.after.always((t) => {
+ t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-library"});
+ const thresholds = {
+ statements: 80,
+ branches: 75,
+ functions: 100,
+ lines: 80
+ };
+ t.context.ajvCoverage.verify(thresholds);
+});
+
+SpecificationVersion.getVersionsForRange(">=4.0").forEach(function(specVersion) {
+ test(`library (specVersion ${specVersion}): Valid configuration`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "resources": {
+ "configuration": {
+ "propertiesFileSourceEncoding": "UTF-8",
+ "paths": {
+ "src": "src/main/uilib",
+ "test": "src/test/uilib"
+ }
+ }
+ },
+ "builder": {
+ "resources": {
+ "excludes": [
+ "/resources/some/project/name/test_results/**",
+ "!/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,
+ "async": false
+ }
+ ]
+ },
+ "bundleOptions": {
+ "optimize": true,
+ "numberOfParts": 3
+ }
+ }
+ ],
+ "componentPreload": {
+ "paths": [
+ "some/glob/**/pattern/Component.js",
+ "some/other/glob/**/pattern/Component.js"
+ ],
+ "namespaces": [
+ "some/namespace",
+ "some/other/namespace"
+ ]
+ },
+ "jsdoc": {
+ "excludes": [
+ "some/project/name/thirdparty/**"
+ ]
+ },
+ "customTasks": [
+ {
+ "name": "custom-task-1",
+ "beforeTask": "replaceCopyright",
+ "configuration": {
+ "some-key": "some value"
+ }
+ },
+ {
+ "name": "custom-task-2",
+ "afterTask": "custom-task-1",
+ "configuration": {
+ "color": "blue"
+ }
+ }
+ ]
+ },
+ "server": {
+ "settings": {
+ "httpPort": 1337,
+ "httpsPort": 1443
+ },
+ "customMiddleware": [
+ {
+ "name": "myCustomMiddleware",
+ "mountPath": "/myapp",
+ "afterMiddleware": "compression",
+ "configuration": {
+ "debug": true
+ }
+ }
+ ]
+ }
+ });
+ });
+
+ test(`library (specVersion ${specVersion}): Invalid builder configuration`, async (t) => {
+ const config = {
+ "specVersion": specVersion,
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ // cachebuster is only supported for type application
+ "cachebuster": {
+ "signatureType": "time"
+ },
+ "bundles": [
+ {
+ "bundleDefinition": {
+ "name": "app.js",
+ "defaultFileTypes": [
+ ".js"
+ ],
+ "sections": [
+ {
+ "name": "some-app-preload",
+ "mode": "preload",
+ "filters": [
+ "some/app/Component.js"
+ ],
+ "resolve": true,
+ "sort": true,
+ "declareRawModules": false,
+ "async": false
+ },
+ {
+ "mode": "require",
+ "filters": [
+ "ui5loader-autoconfig.js"
+ ],
+ "resolve": true,
+ "async": false
+ }
+ ]
+ },
+ "bundleOptions": {
+ "optimize": true,
+ "numberOfParts": 3
+ }
+ }
+ ],
+ }
+ };
+ await assertValidation(t, config, [
+ {
+ dataPath: "/builder",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "cachebuster",
+ },
+ },
+ {
+ dataPath: "/builder/bundles/0/bundleDefinition/sections/0",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "async",
+ },
+ },
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange("2.0 - 3.2").forEach(function(specVersion) {
+ test(`library (specVersion ${specVersion}): Valid configuration`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "resources": {
+ "configuration": {
+ "propertiesFileSourceEncoding": "UTF-8",
+ "paths": {
+ "src": "src/main/uilib",
+ "test": "src/test/uilib"
+ }
+ }
+ },
+ "builder": {
+ "resources": {
+ "excludes": [
+ "/resources/some/project/name/test_results/**",
+ "!/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,
+ "usePredefineCalls": 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"
+ ]
+ },
+ "jsdoc": {
+ "excludes": [
+ "some/project/name/thirdparty/**"
+ ]
+ },
+ "customTasks": [
+ {
+ "name": "custom-task-1",
+ "beforeTask": "replaceCopyright",
+ "configuration": {
+ "some-key": "some value"
+ }
+ },
+ {
+ "name": "custom-task-2",
+ "afterTask": "custom-task-1",
+ "configuration": {
+ "color": "blue"
+ }
+ }
+ ]
+ },
+ "server": {
+ "settings": {
+ "httpPort": 1337,
+ "httpsPort": 1443
+ },
+ "customMiddleware": [
+ {
+ "name": "myCustomMiddleware",
+ "mountPath": "/myapp",
+ "afterMiddleware": "compression",
+ "configuration": {
+ "debug": true
+ }
+ }
+ ]
+ }
+ });
+ });
+
+ test(`library (specVersion ${specVersion}): Invalid builder configuration`, async (t) => {
+ const config = {
+ "specVersion": specVersion,
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ // cachebuster is only supported for type application
+ "cachebuster": {
+ "signatureType": "time"
+ }
+ }
+ };
+ await assertValidation(t, config, [{
+ dataPath: "/builder",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "cachebuster"
+ }
+ }]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange("2.0 - 2.2").forEach(function(specVersion) {
+ test(`Unsupported builder/libraryPreload configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "libraryPreload": {}
+ }
+ }, [
+ {
+ dataPath: "/builder",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "libraryPreload",
+ },
+ },
+ ]);
+ });
+ test(`Unsupported builder/componentPreload/excludes configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "componentPreload": {
+ "excludes": [
+ "some/excluded/files/**",
+ "some/other/excluded/files/**"
+ ]
+ }
+ }
+ }, [
+ {
+ dataPath: "/builder/componentPreload",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "excludes",
+ },
+ },
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=2.3").forEach(function(specVersion) {
+ test(`library (specVersion ${specVersion}): builder/libraryPreload/excludes`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "libraryPreload": {
+ "excludes": [
+ "some/excluded/files/**",
+ "some/other/excluded/files/**"
+ ]
+ }
+ }
+ });
+ });
+ test(`Invalid builder/libraryPreload/excludes configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "libraryPreload": {
+ "excludes": "some/excluded/files/**"
+ }
+ }
+ }, [
+ {
+ dataPath: "/builder/libraryPreload/excludes",
+ keyword: "type",
+ message: "should be array",
+ params: {
+ type: "array",
+ },
+ },
+ ]);
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "libraryPreload": {
+ "excludes": [
+ true,
+ 1,
+ {}
+ ],
+ "notAllowed": true
+ }
+ }
+ }, [
+ {
+ dataPath: "/builder/libraryPreload",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "notAllowed",
+ },
+ },
+ {
+ dataPath: "/builder/libraryPreload/excludes/0",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ },
+ },
+ {
+ dataPath: "/builder/libraryPreload/excludes/1",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ },
+ },
+ {
+ dataPath: "/builder/libraryPreload/excludes/2",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ },
+ },
+ ]);
+ });
+
+
+ test(`library (specVersion ${specVersion}): builder/componentPreload/excludes`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "library",
+ "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": "library",
+ "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": "library",
+ "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",
+ },
+ },
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=2.4").forEach(function(specVersion) {
+ // Unsupported cases for older spec-versions already tested via "allowedValues" comparison above
+ test(`library (specVersion ${specVersion}): builder/bundles/bundleDefinition/sections/mode: bundleInfo`,
+ async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ "bundles": [{
+ "bundleDefinition": {
+ "name": "my-bundle.js",
+ "sections": [{
+ "name": "my-bundle-info",
+ "mode": "bundleInfo",
+ "filters": []
+ }]
+ }
+ }]
+ }
+ });
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=2.5").forEach(function(specVersion) {
+ test(`library (specVersion ${specVersion}): builder/settings/includeDependency*`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "library",
+ "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": "library",
+ "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": "library",
+ "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",
+ },
+ },
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=2.6").forEach(function(specVersion) {
+ test(`library (specVersion ${specVersion}): builder/minification/excludes`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "library",
+ "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": "library",
+ "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": "library",
+ "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",
+ },
+ },
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) {
+ test(`Invalid project name (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "library",
+ "metadata": {
+ "name": "illegal-🦜"
+ }
+ }, [{
+ 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": "library",
+ "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": "library",
+ "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,
+ },
+ }]
+ },
+ }]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange("2.0 - 3.1").forEach(function(specVersion) {
+ test(`library (specVersion ${specVersion}): Invalid configuration`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "resources": {
+ "configuration": {
+ "propertiesFileSourceEncoding": "UTF8",
+ "paths": {
+ "src": {"path": "src"},
+ "test": {"path": "test"},
+ "webapp": "app"
+ }
+ }
+ },
+ "builder": {
+ "resources": {
+ "excludes": "/resources/some/project/name/test_results/**"
+ },
+ "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",
+ },
+ "jsdoc": {
+ "excludes": "some/project/name/thirdparty/**"
+ },
+ "customTasks": [
+ {
+ "name": "custom-task-1",
+ "beforeTask": "replaceCopyright",
+ "afterTask": "replaceCopyright",
+ },
+ {
+ "afterTask": "custom-task-1",
+ "configuration": {
+ "color": "blue"
+ }
+ },
+ "my-task"
+ ]
+ },
+ "server": {
+ "settings": {
+ "httpPort": "1337",
+ "httpsPort": "1443"
+ }
+ }
+ }, [
+ {
+ 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: "webapp",
+ }
+ },
+ {
+ dataPath: "/resources/configuration/paths/src",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ }
+ },
+ {
+ dataPath: "/resources/configuration/paths/test",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ }
+ },
+ {
+ dataPath: "/builder/resources/excludes",
+ keyword: "type",
+ message: "should be array",
+ params: {
+ type: "array",
+ }
+ },
+ {
+ dataPath: "/builder/jsdoc/excludes",
+ keyword: "type",
+ message: "should be array",
+ params: {
+ type: "array",
+ }
+ },
+ {
+ 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: ["3.1", "3.0", "2.6", "2.5", "2.4"].includes(specVersion) ? [
+ "raw",
+ "preload",
+ "require",
+ "provided",
+ "bundleInfo"
+ ] : [
+ "raw",
+ "preload",
+ "require",
+ "provided"
+ ]
+ }
+ },
+ {
+ 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",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/0",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "afterTask",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/0",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "beforeTask",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/1",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "afterTask",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/1",
+ keyword: "required",
+ message: "should have required property 'name'",
+ params: {
+ missingProperty: "name",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/1",
+ keyword: "required",
+ message: "should have required property 'beforeTask'",
+ params: {
+ missingProperty: "beforeTask",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/2",
+ keyword: "type",
+ message: "should be object",
+ params: {
+ type: "object",
+ }
+ },
+ {
+ dataPath: "/server/settings/httpPort",
+ keyword: "type",
+ message: "should be number",
+ params: {
+ type: "number",
+ }
+ },
+ {
+ dataPath: "/server/settings/httpsPort",
+ keyword: "type",
+ message: "should be number",
+ params: {
+ type: "number",
+ }
+ }
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=3.2").forEach(function(specVersion) {
+ test(`library (specVersion ${specVersion}): Invalid configuration`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "resources": {
+ "configuration": {
+ "propertiesFileSourceEncoding": "UTF8",
+ "paths": {
+ "src": {"path": "src"},
+ "test": {"path": "test"},
+ "webapp": "app"
+ }
+ }
+ },
+ "builder": {
+ "resources": {
+ "excludes": "/resources/some/project/name/test_results/**"
+ },
+ "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",
+ },
+ "jsdoc": {
+ "excludes": "some/project/name/thirdparty/**"
+ },
+ "customTasks": [
+ {
+ "name": "custom-task-1",
+ "beforeTask": "replaceCopyright",
+ "afterTask": "replaceCopyright",
+ },
+ {
+ "afterTask": "custom-task-1",
+ "configuration": {
+ "color": "blue"
+ }
+ },
+ "my-task"
+ ]
+ },
+ "server": {
+ "settings": {
+ "httpPort": "1337",
+ "httpsPort": "1443"
+ }
+ }
+ }, [
+ {
+ 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: "webapp",
+ }
+ },
+ {
+ dataPath: "/resources/configuration/paths/src",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ }
+ },
+ {
+ dataPath: "/resources/configuration/paths/test",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ }
+ },
+ {
+ dataPath: "/builder/resources/excludes",
+ keyword: "type",
+ message: "should be array",
+ params: {
+ type: "array",
+ }
+ },
+ {
+ dataPath: "/builder/jsdoc/excludes",
+ keyword: "type",
+ message: "should be array",
+ params: {
+ type: "array",
+ }
+ },
+ {
+ 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",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/0",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "afterTask",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/0",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "beforeTask",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/1",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "afterTask",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/1",
+ keyword: "required",
+ message: "should have required property 'name'",
+ params: {
+ missingProperty: "name",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/1",
+ keyword: "required",
+ message: "should have required property 'beforeTask'",
+ params: {
+ missingProperty: "beforeTask",
+ }
+ },
+ {
+ dataPath: "/builder/customTasks/2",
+ keyword: "type",
+ message: "should be object",
+ params: {
+ type: "object",
+ }
+ },
+ {
+ dataPath: "/server/settings/httpPort",
+ keyword: "type",
+ message: "should be number",
+ params: {
+ type: "number",
+ }
+ },
+ {
+ dataPath: "/server/settings/httpsPort",
+ keyword: "type",
+ message: "should be number",
+ params: {
+ type: "number",
+ }
+ }
+ ]);
+ });
+});
+
+project.defineTests(test, assertValidation, "library");
diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project/module.js b/packages/project/test/lib/validation/schema/specVersion/kind/project/module.js
new file mode 100644
index 00000000000..2899c22a040
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/specVersion/kind/project/module.js
@@ -0,0 +1,442 @@
+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/module.json"]
+ });
+});
+
+test.after.always((t) => {
+ t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-module"});
+ const thresholds = {
+ statements: 75,
+ branches: 65,
+ functions: 100,
+ lines: 75
+ };
+ t.context.ajvCoverage.verify(thresholds);
+});
+
+SpecificationVersion.getVersionsForRange(">=2.0").forEach((specVersion) => {
+ test(`Valid configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "module",
+ "metadata": {
+ "name": "my-module"
+ },
+ "resources": {
+ "configuration": {
+ "paths": {
+ "/resources/my/library/module-xy/": "lib",
+ "/resources/my/library/module-xy-min/": "dist"
+ }
+ }
+ }
+ });
+ });
+
+ test(`No framework configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "module",
+ "metadata": {
+ "name": "my-module"
+ },
+ "framework": {}
+ }, [{
+ dataPath: "",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ "additionalProperty": "framework"
+ }
+ }]);
+ });
+
+ test(`No propertiesFileSourceEncoding configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "module",
+ "metadata": {
+ "name": "my-module"
+ },
+ "resources": {
+ "configuration": {
+ "propertiesFileSourceEncoding": "UTF-8"
+ }
+ }
+ }, [{
+ dataPath: "/resources/configuration",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ "additionalProperty": "propertiesFileSourceEncoding"
+ }
+ }]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange("2.0 - 2.4").forEach((specVersion) => {
+ test(`No server configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "module",
+ "metadata": {
+ "name": "my-module"
+ },
+ "server": {}
+ }, [{
+ dataPath: "",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ "additionalProperty": "server"
+ }
+ }]);
+ });
+
+ test(`No builder configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "module",
+ "metadata": {
+ "name": "my-module"
+ },
+ "builder": {}
+ }, [{
+ dataPath: "",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ "additionalProperty": "builder"
+ }
+ }]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=2.5").forEach(function(specVersion) {
+ test(`Server configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "module",
+ "metadata": {
+ "name": "my-module"
+ },
+ "server": {
+ "settings": {
+ "httpPort": 1337,
+ "httpsPort": 1443
+ },
+ "customMiddleware": [
+ {
+ "name": "myCustomMiddleware",
+ "mountPath": "/myapp",
+ "afterMiddleware": "compression",
+ "configuration": {
+ "debug": true
+ }
+ }
+ ]
+ }
+ });
+ });
+
+ test(`module (specVersion ${specVersion}): builder/settings/includeDependency*`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "module",
+ "metadata": {
+ "name": "my-module"
+ },
+ "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": "module",
+ "metadata": {
+ "name": "my-module"
+ },
+ "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": "module",
+ "metadata": {
+ "name": "my-module"
+ },
+ "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",
+ },
+ },
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) {
+ test(`Invalid project name (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "module",
+ "metadata": {
+ "name": "illegal-🦜"
+ }
+ }, [{
+ 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": "module",
+ "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": "module",
+ "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,
+ },
+ }]
+ },
+ }]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=3.1").forEach(function(specVersion) {
+ test(`Builder resource excludes (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "module",
+ "metadata": {
+ "name": "my-module"
+ },
+ "builder": {
+ "resources": {
+ "excludes": [
+ "/resources/some/project/name/test_results/**",
+ "!/test-resources/some/project/name/demo-app/**"
+ ]
+ }
+ }
+ });
+ });
+});
+
+project.defineTests(test, assertValidation, "module");
diff --git a/packages/project/test/lib/validation/schema/specVersion/kind/project/theme-library.js b/packages/project/test/lib/validation/schema/specVersion/kind/project/theme-library.js
new file mode 100644
index 00000000000..19bc8a09467
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/specVersion/kind/project/theme-library.js
@@ -0,0 +1,418 @@
+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/theme-library.json"]
+ });
+});
+
+test.after.always((t) => {
+ t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-theme-library"});
+ const thresholds = {
+ statements: 80,
+ branches: 70,
+ functions: 100,
+ lines: 80
+ };
+ t.context.ajvCoverage.verify(thresholds);
+});
+
+SpecificationVersion.getVersionsForRange(">=2.0").forEach(function(specVersion) {
+ test(`Valid configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "theme-library",
+ "metadata": {
+ "name": "my-theme-library",
+ "copyright": "Copyright goes here"
+ },
+ "resources": {
+ "configuration": {
+ "propertiesFileSourceEncoding": "UTF-8",
+ "paths": {
+ "src": "src/main/uilib",
+ "test": "src/test/uilib"
+ }
+ }
+ },
+ "builder": {
+ "resources": {
+ "excludes": [
+ "/resources/some/project/name/test_results/**",
+ "/test-resources/**",
+ "!/test-resources/some/project/name/demo-app/**"
+ ]
+ },
+ "customTasks": [
+ {
+ "name": "custom-task-1",
+ "beforeTask": "replaceCopyright",
+ "configuration": {
+ "some-key": "some value"
+ }
+ },
+ {
+ "name": "custom-task-2",
+ "afterTask": "custom-task-1",
+ "configuration": {
+ "color": "blue"
+ }
+ }
+ ]
+ },
+ "server": {
+ "settings": {
+ "httpPort": 1337,
+ "httpsPort": 1443
+ },
+ "customMiddleware": [
+ {
+ "name": "myCustomMiddleware",
+ "mountPath": "/myapp",
+ "afterMiddleware": "compression",
+ "configuration": {
+ "debug": true
+ }
+ }
+ ]
+ }
+ });
+ });
+
+ test(`Invalid builder configuration (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "theme-library",
+ "metadata": {
+ "name": "com.sap.ui5.test",
+ "copyright": "yes"
+ },
+ "builder": {
+ // cachebuster is only supported for type application
+ "cachebuster": {
+ "signatureType": "time"
+ },
+ // jsdoc is only supported for type library
+ "jsdoc": {
+ "excludes": [
+ "some/project/name/thirdparty/**"
+ ]
+ },
+ // componentPreload is only supported for types application/library
+ "componentPreload": {},
+ // libraryPreload is only supported for type library
+ "libraryPreload": {},
+ }
+ }, [{
+ dataPath: "/builder",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "cachebuster"
+ }
+ },
+ {
+ 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: "componentPreload"
+ }
+ },
+ {
+ dataPath: "/builder",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "libraryPreload"
+ }
+ }]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=2.5").forEach(function(specVersion) {
+ test(`theme-library (specVersion ${specVersion}): builder/settings/includeDependency*`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "kind": "project",
+ "type": "theme-library",
+ "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": "theme-library",
+ "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": "theme-library",
+ "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",
+ },
+ },
+ ]);
+ });
+});
+
+SpecificationVersion.getVersionsForRange(">=3.0").forEach(function(specVersion) {
+ test(`Invalid project name (specVersion ${specVersion})`, async (t) => {
+ await assertValidation(t, {
+ "specVersion": specVersion,
+ "type": "theme-library",
+ "metadata": {
+ "name": "illegal-🦜"
+ }
+ }, [{
+ 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": "theme-library",
+ "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": "theme-library",
+ "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, "theme-library");
diff --git a/packages/project/test/lib/validation/schema/ui5-workspace.js b/packages/project/test/lib/validation/schema/ui5-workspace.js
new file mode 100644
index 00000000000..3bc5877542e
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/ui5-workspace.js
@@ -0,0 +1,464 @@
+import test from "ava";
+import Ajv from "ajv";
+import ajvErrors from "ajv-errors";
+import AjvCoverage from "../../../utils/AjvCoverage.js";
+import {_Validator as Validator} from "../../../../lib/validation/validator.js";
+import ValidationError from "../../../../lib/validation/ValidationError.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-workspace"});
+ t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, {
+ includes: ["schema/ui5-workspace.json"],
+ });
+});
+
+test.after.always((t) => {
+ t.context.ajvCoverage.createReport("html", {
+ dir: "coverage/ajv-ui5-workspace",
+ });
+ const thresholds = {
+ statements: 85,
+ branches: 75,
+ functions: 100,
+ lines: 85,
+ };
+ t.context.ajvCoverage.verify(thresholds);
+});
+
+test("Empty config", async (t) => {
+ await assertValidation(
+ t,
+ {
+ specVersion: "0.1",
+ },
+ [
+ {
+ dataPath: "/specVersion",
+ keyword: "errorMessage",
+ message: `Unsupported "specVersion"
+Your UI5 CLI installation might be outdated.
+Supported specification versions: "workspace/1.0"
+For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-specification-versions`,
+ params: {
+ errors: [
+ {
+ dataPath: "/specVersion",
+ keyword: "enum",
+ message:
+ "should be equal to one of the allowed values",
+ params: {
+ allowedValues: ["workspace/1.0"],
+ },
+ },
+ ],
+ },
+ },
+ {
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'metadata'",
+ params: {
+ missingProperty: "metadata",
+ },
+ },
+ {
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'dependencyManagement'",
+ params: {
+ missingProperty: "dependencyManagement",
+ },
+ },
+ ]
+ );
+});
+
+test("Valid spec", async (t) => {
+ await assertValidation(t, {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "test-spec-name",
+ },
+ dependencyManagement: {
+ resolutions: [
+ {
+ path: "path/to/resource/1",
+ },
+ {
+ path: "path/to/resource/2",
+ },
+ ],
+ },
+ });
+});
+
+test("Missing metadata.name", async (t) => {
+ await assertValidation(
+ t,
+ {
+ specVersion: "workspace/1.0",
+ metadata: {},
+ dependencyManagement: {
+ resolutions: [
+ {
+ path: "path/to/resource/1",
+ },
+ ],
+ },
+ },
+ [
+ {
+ dataPath: "/metadata",
+ keyword: "required",
+ message: "should have required property 'name'",
+ params: {
+ missingProperty: "name",
+ },
+ },
+ ]
+ );
+});
+
+test("Invalid metadata.name: Illegal characters", async (t) => {
+ await assertValidation(
+ t,
+ {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "🦭🦭🦭"
+ },
+ dependencyManagement: {
+ resolutions: [
+ {
+ path: "path/to/resource/1",
+ },
+ ],
+ },
+ },
+ [
+ {
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: "Not a valid workspace 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/Workspace/#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-_.]*$",
+ },
+ },
+ ],
+ },
+ },
+ ]
+ );
+});
+
+test("Invalid metadata.name: Too short", async (t) => {
+ await assertValidation(
+ t,
+ {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "a"
+ },
+ dependencyManagement: {
+ resolutions: [
+ {
+ path: "path/to/resource/1",
+ },
+ ],
+ },
+ },
+ [
+ {
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: "Not a valid workspace 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/Workspace/#name",
+ params: {
+ errors: [
+ {
+ dataPath: "/metadata/name",
+ keyword: "minLength",
+ message: "should NOT be shorter than 3 characters",
+ params: {
+ limit: 3,
+ },
+ },
+ ],
+ },
+ },
+ ]
+ );
+});
+
+
+test("Invalid metadata.name: Too long", async (t) => {
+ await assertValidation(
+ t,
+ {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "b".repeat(81)
+ },
+ dependencyManagement: {
+ resolutions: [
+ {
+ path: "path/to/resource/1",
+ },
+ ],
+ },
+ },
+ [
+ {
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: "Not a valid workspace 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/Workspace/#name",
+ params: {
+ errors: [
+ {
+ dataPath: "/metadata/name",
+ keyword: "maxLength",
+ message: "should NOT be longer than 80 characters",
+ params: {
+ limit: 80,
+ },
+ }
+ ],
+ },
+ },
+ ]
+ );
+});
+
+test("Invalid fields", async (t) => {
+ await assertValidation(
+ t,
+ {
+ specVersion: 12,
+ metadata: {
+ name: {},
+ },
+ dependencyManagement: {
+ resolutions: {
+ path: "path/to/resource/1",
+ },
+ },
+ },
+ [
+ {
+ dataPath: "/specVersion",
+ keyword: "errorMessage",
+ message: `Unsupported "specVersion"
+Your UI5 CLI installation might be outdated.
+Supported specification versions: "workspace/1.0"
+For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-specification-versions`,
+ params: {
+ errors: [
+ {
+ dataPath: "/specVersion",
+ keyword: "enum",
+ message:
+ "should be equal to one of the allowed values",
+ params: {
+ allowedValues: ["workspace/1.0"],
+ },
+ },
+ ],
+ },
+ },
+ {
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: "Not a valid workspace 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/Workspace/#name",
+ params: {
+ errors: [
+ {
+ dataPath: "/metadata/name",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ },
+ },
+ ],
+ },
+ },
+ {
+ dataPath: "/dependencyManagement/resolutions",
+ keyword: "type",
+ message: "should be array",
+ params: {
+ type: "array",
+ },
+ },
+ {
+ dataPath: "/dependencyManagement/resolutions",
+ keyword: "additionalProperties",
+ message: "should NOT have additional properties",
+ params: {
+ additionalProperty: "path",
+ },
+ },
+ ]
+ );
+});
+
+test("Invalid types", async (t) => {
+ await assertValidation(
+ t,
+ {
+ specVersion: 42,
+ metadata: {
+ name: 15,
+ },
+ dependencyManagement: "simple string",
+ },
+ [
+ {
+ dataPath: "/specVersion",
+ keyword: "errorMessage",
+ message: `Unsupported "specVersion"
+Your UI5 CLI installation might be outdated.
+Supported specification versions: "workspace/1.0"
+For details, see: https://ui5.github.io/cli/stable/pages/Workspace/#workspace-specification-versions`,
+ params: {
+ errors: [
+ {
+ dataPath: "/specVersion",
+ keyword: "enum",
+ message:
+ "should be equal to one of the allowed values",
+ params: {
+ allowedValues: ["workspace/1.0"],
+ },
+ },
+ ],
+ },
+ },
+ {
+ dataPath: "/metadata/name",
+ keyword: "errorMessage",
+ message: "Not a valid workspace 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/Workspace/#name",
+ params: {
+ errors: [
+ {
+ dataPath: "/metadata/name",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ },
+ },
+ ],
+ },
+ },
+ {
+ dataPath: "/dependencyManagement",
+ keyword: "type",
+ message: "should be object",
+ params: {
+ type: "object",
+ },
+ },
+ ]
+ );
+});
+
+test("Invalid dependencyManagement", async (t) => {
+ await assertValidation(
+ t,
+ {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "test-spec-name",
+ },
+ dependencyManagement: {
+ resolutions: "Invalid type",
+ },
+ },
+ [
+ {
+ dataPath: "/dependencyManagement/resolutions",
+ keyword: "type",
+ message: "should be array",
+ params: {
+ type: "array",
+ },
+ },
+ ]
+ );
+
+ await assertValidation(
+ t,
+ {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "test-spec-name",
+ },
+ dependencyManagement: {
+ resolutions: ["invalid type"],
+ },
+ },
+ [
+ {
+ dataPath: "/dependencyManagement/resolutions/0",
+ keyword: "type",
+ message: "should be object",
+ params: {
+ type: "object",
+ },
+ },
+ ]
+ );
+
+ await assertValidation(
+ t,
+ {
+ specVersion: "workspace/1.0",
+ metadata: {
+ name: "test-spec-name",
+ },
+ dependencyManagement: {
+ resolutions: [{path: 12}],
+ },
+ },
+ [
+ {
+ dataPath: "/dependencyManagement/resolutions/0/path",
+ keyword: "type",
+ message: "should be string",
+ params: {
+ type: "string",
+ },
+ },
+ ]
+ );
+});
diff --git a/packages/project/test/lib/validation/schema/ui5.js b/packages/project/test/lib/validation/schema/ui5.js
new file mode 100644
index 00000000000..7cdf1df7423
--- /dev/null
+++ b/packages/project/test/lib/validation/schema/ui5.js
@@ -0,0 +1,196 @@
+import test from "ava";
+import Ajv from "ajv";
+import ajvErrors from "ajv-errors";
+import AjvCoverage from "../../../utils/AjvCoverage.js";
+import {_Validator as Validator} from "../../../../lib/validation/validator.js";
+import ValidationError from "../../../../lib/validation/ValidationError.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/ui5.json"]
+ });
+});
+
+test.after.always((t) => {
+ t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-ui5"});
+ const thresholds = {
+ statements: 95,
+ branches: 80,
+ functions: 100,
+ lines: 95
+ };
+ t.context.ajvCoverage.verify(thresholds);
+});
+
+test("Undefined", async (t) => {
+ await assertValidation(t, undefined, [{
+ dataPath: "",
+ keyword: "type",
+ message: "should be object",
+ params: {
+ type: "object",
+ }
+ }]);
+});
+
+test("Missing specVersion, type", async (t) => {
+ await assertValidation(t, {}, [
+ {
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'specVersion'",
+ params: {
+ missingProperty: "specVersion",
+ }
+ },
+ {
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'type'",
+ params: {
+ missingProperty: "type",
+ }
+ }
+
+ ]);
+});
+
+test("Missing type", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0"
+ }, [
+ {
+ dataPath: "",
+ keyword: "required",
+ message: "should have required property 'type'",
+ params: {
+ missingProperty: "type",
+ }
+ }
+ ]);
+});
+
+test("Invalid specVersion", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "0.0"
+ }, [
+ {
+ dataPath: "/specVersion",
+ keyword: "errorMessage",
+ message:
+"Unsupported \"specVersion\"\n" +
+"Your UI5 CLI installation might be outdated.\n" +
+"Supported specification versions: \"4.0\", \"3.2\", \"3.1\", \"3.0\", \"2.6\", " +
+"\"2.5\", \"2.4\", \"2.3\", \"2.2\", \"2.1\", \"2.0\", \"1.1\", \"1.0\", \"0.1\"\n" +
+"For details, see: https://ui5.github.io/cli/pages/Configuration/#specification-versions",
+ params: {
+ errors: [
+ {
+ dataPath: "/specVersion",
+ keyword: "enum",
+ message: "should be equal to one of the allowed values",
+ params: {
+ allowedValues: [
+ "4.0",
+ "3.2",
+ "3.1",
+ "3.0",
+ "2.6",
+ "2.5",
+ "2.4",
+ "2.3",
+ "2.2",
+ "2.1",
+ "2.0",
+ "1.1",
+ "1.0",
+ "0.1",
+ ],
+ }
+ },
+ ],
+ }
+ }
+ ]);
+});
+
+test("Invalid type", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "type": "foo"
+ }, [
+ {
+ dataPath: "/type",
+ keyword: "enum",
+ message: "should be equal to one of the allowed values",
+ params: {
+ allowedValues: [
+ "application",
+ "library",
+ "theme-library",
+ "module"
+ ]
+ }
+ }
+ ]);
+});
+
+test("Invalid kind", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "2.0",
+ "kind": "foo"
+ }, [
+ {
+ dataPath: "/kind",
+ keyword: "enum",
+ message: "should be equal to one of the allowed values",
+ params: {
+ allowedValues: [
+ "project",
+ "extension",
+ null
+ ],
+ }
+ }
+ ]);
+});
+
+test("specVersion 0.1", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "0.1"
+ });
+});
+
+test("specVersion 1.0", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "1.0"
+ });
+});
+
+test("specVersion 1.1", async (t) => {
+ await assertValidation(t, {
+ "specVersion": "1.1"
+ });
+});
diff --git a/packages/project/test/lib/validation/validator.js b/packages/project/test/lib/validation/validator.js
new file mode 100644
index 00000000000..858ce8b3ed6
--- /dev/null
+++ b/packages/project/test/lib/validation/validator.js
@@ -0,0 +1,138 @@
+import test from "ava";
+import sinonGlobal from "sinon";
+import esmock from "esmock";
+
+test.beforeEach(async (t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ const Ajv = t.context.Ajv = sinon.stub();
+ const ajvErrors = t.context.ajvErrors = sinon.stub();
+
+ t.context.validatorModule = await esmock.p("../../../lib/validation/validator.js", {
+ "ajv": Ajv,
+ "ajv-errors": ajvErrors
+ });
+ const {validate, validateWorkspace, _Validator: Validator} = t.context.validatorModule;
+
+ t.context.validate = validate;
+ t.context.validateWorkspace = validateWorkspace;
+ t.context.Validator = Validator;
+});
+
+test.afterEach.always((t) => {
+ t.context.sinon.restore();
+ esmock.purge(t.context.validatorModule);
+});
+
+test("validate function calls Validator#validate method", async (t) => {
+ const {sinon, Validator, validate} = t.context;
+ const config = {config: true};
+ const project = {project: true};
+ const yaml = {yaml: true};
+
+ const validateStub = sinon.stub(Validator.prototype, "validate");
+ validateStub.resolves();
+
+ const result = await validate({config, project, yaml});
+
+ t.is(result, undefined, "validate should return undefined");
+ t.is(validateStub.callCount, 1, "validate should be called once");
+ t.deepEqual(validateStub.getCall(0).args, [{config, project, yaml}]);
+});
+
+test("validateWorkspace function calls Validator#validate method without project", async (t) => {
+ const {sinon, Validator, validateWorkspace} = t.context;
+ const config = {config: true};
+ const yaml = {yaml: true};
+
+ const validateStub = sinon.stub(Validator.prototype, "validate");
+ validateStub.resolves();
+
+ const result = await validateWorkspace({config, yaml});
+
+ t.is(result, undefined, "validate should return undefined");
+ t.is(validateStub.callCount, 1, "validate should be called once");
+ t.deepEqual(validateStub.getCall(0).args, [{config, yaml}]);
+});
+
+test("validateWorkspace throw an Error", async (t) => {
+ const {validateWorkspace} = await esmock("../../../lib/validation/validator.js");
+ const config = {config: true};
+ const yaml = {yaml: true};
+
+ const err = await t.throwsAsync(async () => {
+ return await validateWorkspace({config, yaml});
+ });
+
+ t.is(err.message.includes("Invalid workspace configuration."), true);
+});
+
+test("Validator requires schemaName", (t) => {
+ const {sinon, Validator} = t.context;
+
+ const Ajv = sinon.stub();
+ const ajvErrors = sinon.stub();
+ const invalidContructor = () => {
+ new Validator({Ajv, ajvErrors});
+ };
+
+ t.throws(invalidContructor, {
+ message:
+ "\"schemaName\" is missing or incorrect. The available schemaName variants are ui5, ui5-workspace",
+ });
+});
+
+test("Validator requires a valid schemaName", (t) => {
+ const {sinon, Validator} = t.context;
+
+ const Ajv = sinon.stub();
+ const ajvErrors = sinon.stub();
+ const invalidContructor = () => {
+ new Validator({Ajv, ajvErrors, schemaName: "invalid schema name"});
+ };
+
+ t.throws(invalidContructor, {
+ message:
+ "\"schemaName\" is missing or incorrect. The available schemaName variants are ui5, ui5-workspace",
+ });
+});
+
+test("Validator#_compileSchema cache test", async (t) => {
+ const {sinon, Validator} = t.context;
+
+ const schema1 = {schema1: true};
+
+ const loadSchemaStub = sinon.stub(Validator, "loadSchema");
+ loadSchemaStub.onCall(0).resolves(schema1);
+ loadSchemaStub.resolves({schema2: true});
+
+ const schema1Fn = sinon.stub().named("schema1Fn");
+
+ const compileAsyncStub = sinon.stub().resolves();
+ compileAsyncStub.onCall(0).resolves(schema1Fn);
+ compileAsyncStub.resolves(sinon.stub().named("schema2Fn"));
+
+ const Ajv = sinon.stub().returns({
+ compileAsync: compileAsyncStub
+ });
+ const ajvErrors = sinon.stub();
+
+ const validator = new Validator({Ajv, ajvErrors, schemaName: "ui5-workspace"});
+
+ const compile1 = validator._compileSchema();
+ const compile2 = validator._compileSchema();
+ const compile3 = validator._compileSchema();
+
+ const compile1Result = await compile1;
+ const compile2Result = await compile2;
+ const compile3Result = await compile3;
+
+ t.is(compile1Result, compile2Result);
+ t.is(compile2Result, compile3Result);
+
+ t.is(loadSchemaStub.callCount, 1);
+ t.deepEqual(loadSchemaStub.getCall(0).args, ["ui5-workspace.json"]);
+
+ t.is(compileAsyncStub.callCount, 1);
+ t.deepEqual(compileAsyncStub.getCall(0).args, [schema1]);
+});
diff --git a/packages/project/test/utils/AjvCoverage.js b/packages/project/test/utils/AjvCoverage.js
new file mode 100644
index 00000000000..90ff01fc234
--- /dev/null
+++ b/packages/project/test/utils/AjvCoverage.js
@@ -0,0 +1,145 @@
+// Inspired by https://github.com/epoberezkin/ajv-istanbul
+
+import crypto from "node:crypto";
+import beautify from "js-beautify";
+import libReport from "istanbul-lib-report";
+import reports from "istanbul-reports";
+import libCoverage from "istanbul-lib-coverage";
+import {createInstrumenter} from "istanbul-lib-instrument";
+
+const rSchemaName = new RegExp(/sourceURL=([^\s]*)/);
+const rRootDataUndefined = /\n(?:\s)*if \(rootData === undefined\) rootData = data;/g;
+const rEnsureErrorArray = /\n(?:\s)*if \(vErrors === null\) vErrors = \[err\];(?:\s)*else vErrors\.push\(err\);/g;
+const rDataPathOrEmptyString = /dataPath: \(dataPath \|\| ''\)/g;
+
+function hash(content) {
+ return crypto.createHash("sha1").update(content).digest("hex").substr(0, 16);
+}
+
+function randomCoverageVar() {
+ return "__ajv-coverage__" + hash((String(Date.now()) + Math.random()));
+}
+
+class AjvCoverage {
+ constructor(ajv, options = {}) {
+ this.ajv = ajv;
+ this.ajv._opts.processCode = this._processCode.bind(this);
+ if (options.meta === true) {
+ this.ajv._metaOpts.processCode = this._processCode.bind(this);
+ }
+ this._processFileName = options.processFileName;
+ this._includes = options.includes;
+ this._sources = {};
+ this._globalCoverageVar = options.globalCoverage === true ? "__coverage__" : randomCoverageVar();
+ this._instrumenter = createInstrumenter({
+ coverageVariable: this._globalCoverageVar
+ });
+ }
+ getSummary() {
+ const coverageMap = this._createCoverageMap();
+ const summary = libCoverage.createCoverageSummary();
+
+ const files = coverageMap.files();
+ files.forEach(function(file) {
+ const fileCoverageSummary = coverageMap.fileCoverageFor(file).toSummary();
+ summary.merge(fileCoverageSummary);
+ return;
+ });
+
+ if (files.length === 0 || summary.lines.covered === 0) {
+ throw new Error("AjvCoverage#getSummary: No coverage data found!");
+ }
+
+ return {
+ branches: summary.branches.pct,
+ lines: summary.lines.pct,
+ statements: summary.statements.pct,
+ functions: summary.functions.pct
+ };
+ }
+ verify(thresholds) {
+ const thresholdEntries = Object.entries(thresholds);
+ if (thresholdEntries.length === 0) {
+ throw new Error("AjvCoverage#verify: No thresholds defined!");
+ }
+
+ const summary = this.getSummary();
+ const errors = [];
+
+ thresholdEntries.forEach(function([threshold, expectedPct]) {
+ const pct = summary[threshold];
+ if (pct === undefined) {
+ errors.push(`Invalid coverage threshold '${threshold}'`);
+ } else if (pct < expectedPct) {
+ errors.push(
+ `Coverage for '${threshold}' (${pct}%) ` +
+ `does not meet global threshold (${expectedPct}%)`);
+ }
+ });
+
+ if (errors.length > 0) {
+ const errorMessage = "ERROR:\n" + errors.join("\n");
+ throw new Error(errorMessage);
+ }
+ }
+ createReport(name, contextOptions = {}, reportOptions = {}) {
+ const coverageMap = this._createCoverageMap();
+ const context = libReport.createContext(Object.assign({}, contextOptions, {
+ coverageMap,
+ sourceFinder: (filePath) => {
+ if (this._sources[filePath]) {
+ return this._sources[filePath];
+ }
+ const sourceFinder = contextOptions.sourceFinder;
+ if (typeof sourceFinder === "function") {
+ return sourceFinder(filePath);
+ }
+ }
+ }));
+ const report = reports.create(name, reportOptions);
+ report.execute(context);
+ }
+ _createCoverageMap() {
+ return libCoverage.createCoverageMap(global[this._globalCoverageVar]);
+ }
+ _processCode(originalCode) {
+ let fileName;
+ const schemaNameMatch = rSchemaName.exec(originalCode);
+ if (schemaNameMatch) {
+ fileName = schemaNameMatch[1];
+ } else {
+ // Probably a definition of a schema that is compiled separately
+ // Try to find the schema that is currently compiling
+ const schemas = Object.entries(this.ajv._schemas);
+ const compilingSchemas = schemas.filter(([, schema]) => schema.compiling);
+ if (compilingSchemas.length > 0) {
+ // Last schema is the current one
+ const lastSchemaEntry = compilingSchemas[compilingSchemas.length - 1];
+ fileName = lastSchemaEntry[0] + "-" + hash(originalCode);
+ } else {
+ fileName = hash(originalCode);
+ }
+ }
+
+ if (typeof this._processFileName === "function") {
+ fileName = this._processFileName.call(null, fileName);
+ }
+
+ if (this._includes && this._includes.every((pattern) => !fileName.includes(pattern))) {
+ return originalCode;
+ }
+
+ const code = AjvCoverage.insertIgnoreComments(beautify(originalCode, {indent_size: 2}));
+ const instrumentedCode = this._instrumenter.instrumentSync(code, fileName);
+ this._sources[fileName] = code;
+ return instrumentedCode;
+ }
+ static insertIgnoreComments(code) {
+ code = code.replace(rRootDataUndefined, "\n/* istanbul ignore next */$&");
+ code = code.replace(rEnsureErrorArray, "\n/* istanbul ignore next */$&");
+ code = code.replace(rDataPathOrEmptyString, "dataPath: (dataPath || /* istanbul ignore next */ '')");
+ return code;
+ }
+}
+
+export default AjvCoverage;