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;