UNPKG

@ui5/project

Version:
297 lines (272 loc) 9.53 kB
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://sap.github.io/ui5-tooling/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. * * <p><b>Note: </b>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.</p> * * 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 <code>project</code> or <code>extension</code> * * @public * @returns {string} Specification kind */ getKind() { return this._kind; } /** * Gets the type of this specification, * for example <code>application</code> or <code>library</code> in case of projects, * and <code>task</code> or <code>server-middleware</code> 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 <code>package.json</code> * * @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;