UNPKG

@ui5/project

Version:
381 lines (347 loc) 11 kB
import {promisify} from "node:util"; import Project from "./Project.js"; import * as resourceFactory from "@ui5/fs/resourceFactory"; /** * Subclass for projects potentially containing Components * * @public * @abstract * @class * @alias @ui5/project/specifications/ComponentProject * @extends @ui5/project/specifications/Project * @hideconstructor */ class ComponentProject extends Project { constructor(parameters) { super(parameters); if (new.target === ComponentProject) { throw new TypeError("Class 'ComponentProject' is abstract. Please use one of the 'types' subclasses"); } this._pPom = null; this._namespace = null; this._isRuntimeNamespaced = true; } /* === Attributes === */ /** * Get the project namespace * * @public * @returns {string} Project namespace in slash notation (e.g. <code>my/project/name</code>) */ getNamespace() { return this._namespace; } /** * @private */ getCopyright() { return this._config.metadata.copyright; } /** * @private */ getComponentPreloadPaths() { return this._config.builder && this._config.builder.componentPreload && this._config.builder.componentPreload.paths || []; } /** * @private */ getComponentPreloadNamespaces() { return this._config.builder && this._config.builder.componentPreload && this._config.builder.componentPreload.namespaces || []; } /** * @private */ getComponentPreloadExcludes() { return this._config.builder && this._config.builder.componentPreload && this._config.builder.componentPreload.excludes || []; } /** * @private */ getMinificationExcludes() { return this._config.builder && this._config.builder.minification && this._config.builder.minification.excludes || []; } /** * @private */ getBundles() { return this._config.builder && this._config.builder.bundles || []; } /** * @private */ getPropertiesFileSourceEncoding() { return this._config.resources && this._config.resources.configuration && this._config.resources.configuration.propertiesFileSourceEncoding || "UTF-8"; } /* === Resource Access === */ /** * Get a [ReaderCollection]{@link @ui5/fs/ReaderCollection} for accessing all resources of the * project in the specified "style": * * <ul> * <li><b>buildtime:</b> Resource paths are always prefixed with <code>/resources/</code> * or <code>/test-resources/</code> followed by the project's namespace. * Any configured build-excludes are applied</li> * <li><b>dist:</b> 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</li> * <li><b>runtime:</b> 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 * <li><b>flat:</b> 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</li> * </ul> * * 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"} = {}) { // TODO: Additional style 'ABAP' using "sap.platform.abap".uri from manifest.json? // Apply builder excludes to all styles but "runtime" const excludes = style === "runtime" ? [] : this.getBuilderResourcesExcludes(); if ((style === "runtime" || style === "dist") && this._isRuntimeNamespaced) { // If the project's type requires a namespace at runtime, the // dist- and runtime-style paths are identical to buildtime-style paths style = "buildtime"; } let reader = this._getReader(excludes); switch (style) { case "buildtime": break; case "runtime": case "dist": // Use buildtime reader and link it to / // No test-resources for runtime resource access, // unless runtime is namespaced reader = resourceFactory.createFlatReader({ reader, namespace: this._namespace }); break; case "flat": // Use buildtime reader and link it to / // No test-resources for runtime resource access, // unless runtime is namespaced reader = resourceFactory.createFlatReader({ reader, namespace: this._namespace }); break; default: throw new Error(`Unknown path mapping style ${style}`); } reader = this._addWriter(reader, style); return reader; } /** * Get a resource reader for the resources of the project * * @returns {@ui5/fs/ReaderCollection} Reader collection */ _getSourceReader() { throw new Error(`_getSourceReader must be implemented by subclass ${this.constructor.name}`); } /** * Get a resource reader for the test resources of the project * * @returns {@ui5/fs/ReaderCollection} Reader collection */ _getTestReader() { throw new Error(`_getTestReader must be implemented by subclass ${this.constructor.name}`); } /** * Get a resource reader/writer for accessing and modifying a project's resources * * @public * @returns {@ui5/fs/ReaderCollection} A reader collection instance */ getWorkspace() { // Workspace is always of style "buildtime" // Therefore builder resource-excludes are always to be applied const excludes = this.getBuilderResourcesExcludes(); return resourceFactory.createWorkspace({ name: `Workspace for project ${this.getName()}`, reader: this._getReader(excludes), writer: this._getWriter().collection }); } _getWriter() { if (!this._writers) { // writer is always of style "buildtime" const namespaceWriter = resourceFactory.createAdapter({ virBasePath: "/", project: this }); const generalWriter = resourceFactory.createAdapter({ virBasePath: "/", project: this }); const collection = resourceFactory.createWriterCollection({ name: `Writers for project ${this.getName()}`, writerMapping: { [`/resources/${this._namespace}/`]: namespaceWriter, [`/test-resources/${this._namespace}/`]: namespaceWriter, [`/`]: generalWriter } }); this._writers = { namespaceWriter, generalWriter, collection }; } return this._writers; } _getReader(excludes) { let reader = this._getSourceReader(excludes); const testReader = this._getTestReader(excludes); if (testReader) { reader = resourceFactory.createReaderCollection({ name: `Reader collection for project ${this.getName()}`, readers: [reader, testReader] }); } return reader; } _addWriter(reader, style) { const {namespaceWriter, generalWriter} = this._getWriter(); if ((style === "runtime" || style === "dist") && this._isRuntimeNamespaced) { // If the project's type requires a namespace at runtime, the // dist- and runtime-style paths are identical to buildtime-style paths style = "buildtime"; } const readers = []; switch (style) { case "buildtime": // Writer already uses buildtime style readers.push(namespaceWriter); readers.push(generalWriter); break; case "runtime": case "dist": // Runtime is not namespaced: link namespace to / readers.push(resourceFactory.createFlatReader({ reader: namespaceWriter, namespace: this._namespace })); // Add general writer as is readers.push(generalWriter); break; case "flat": // Rewrite paths from "flat" to "buildtime" readers.push(resourceFactory.createFlatReader({ reader: namespaceWriter, namespace: this._namespace })); // General writer resources can't be flattened, so they are not available break; default: throw new Error(`Unknown path mapping style ${style}`); } readers.push(reader); return resourceFactory.createReaderCollectionPrioritized({ name: `Reader/Writer collection for project ${this.getName()}`, readers }); } /* === Internals === */ /** * @private * @param {object} config Configuration object */ async _parseConfiguration(config) { await super._parseConfiguration(config); } async _getNamespace() { throw new Error(`_getNamespace must be implemented by subclass ${this.constructor.name}`); } /* === Helper === */ /** * Checks whether a given string contains a maven placeholder. * E.g. <code>${appId}</code>. * * @param {string} value String to check * @returns {boolean} True if given string contains a maven placeholder */ _hasMavenPlaceholder(value) { return !!value.match(/^\$\{(.*)\}$/); } /** * Resolves a maven placeholder in a given string using the projects pom.xml * * @param {string} value String containing a maven placeholder * @returns {Promise<string>} Resolved string */ async _resolveMavenPlaceholder(value) { const parts = value && value.match(/^\$\{(.*)\}$/); if (parts) { this._log.verbose( `"${value}" contains a maven placeholder "${parts[1]}". Resolving from projects pom.xml...`); const pom = await this._getPom(); let mvnValue; if (pom.project && pom.project.properties && pom.project.properties[parts[1]]) { mvnValue = pom.project.properties[parts[1]]; } else { let obj = pom; parts[1].split(".").forEach((part) => { obj = obj && obj[part]; }); mvnValue = obj; } if (!mvnValue) { throw new Error(`"${value}" couldn't be resolved from maven property ` + `"${parts[1]}" of pom.xml of project ${this.getName()}`); } return mvnValue; } else { throw new Error(`"${value}" is not a maven placeholder`); } } /** * Reads the projects pom.xml file * * @returns {Promise<object>} 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;