Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions packages/documentation/docs/pages/Project.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,23 @@ Types define how a project can be configured and how it is built. A type orchest

Also see [UI5 Project: Configuration](./Configuration.md#general-configuration)

### component
*Available since [Specification Version 5.0](./Configuration.md#specification-version-50)*

Projects of the `component` type are typical component-like UI5 applications. They usually run in a container-like root application, such as the SAP Fiori launchpad (FLP) sandbox, alongside other UI5 applications.

To allow multiple component projects to coexist in the same environment, each project is served under its own namespace, for example `/resources/my/bookstore/admin`. In contrast, `application`-type projects act as root projects and are served at `/`, without a namespace.

By default, component projects use the same directory structure as library projects: they include `src` and `test` directories in the root. The integrated server uses both directories. However, when you build the project, the `test` directory is ignored because it shouldn't be deployed to production environments. Both directories can have either a flat or a namespace structure. If you use a flat structure, the project namespace derives from the `"sap.app".id` property in the `manifest.json`.

A component project must contain both, a `Component.js` and a `manifest.json` file.

Unlike `application`-type projects, component projects typically don't have dedicated `index.html` files in their regular resources (`src/`). However, you can still run them standalone. You can do this by using a dedicated HTML file located in their test resources or by declaring a development dependency to an application-type project that can serve the component, such as the FLP sandbox.

Component projects support all [output styles](./CLI.md#ui5-build) that library projects currently support. This allows a deployment where you can omit the namespace from the final directory structure using the output style: `flat`.

### application
Projects of type `application` are typically the main or root project. In a projects dependency tree, there should only be one project of type `application`. If multiple are found, those further away from the root are ignored.
Projects of the type `application` typically serve as the main or root project. In a project's dependency tree, there should be only one project of this type. If the system detects additional application projects, it ignores those that are further away from the root.

The source directory of an application (typically named `webapp`) is mapped to the virtual root path `/`.

Expand All @@ -26,7 +41,7 @@ A project of type `library` must have a source directory (typically named `src`)
These directories should contain a directory structure representing the namespace of the library (e.g. `src/my/first/library`) to prevent name clashes between the resources of different libraries.

### theme-library
*Available since [Specification Version](./Configuration.md#specification-versions) 1.1*
*Available since [Specification Version 1.1](./Configuration.md#specification-version-11)*

UI5 theme libraries provide theming resources for the controls of one or multiple libraries.

Expand All @@ -50,6 +65,10 @@ In the table below you can find the available combinations of project type & out
| `Default` | Root project is written `Flat`-style. ^1^ |
| `Flat` | Same as `Default`. |
| `Namespace` | Root project is written `Namespace`-style (resources are prefixed with the project's namespace). ^1^ |
| **component** | |
| `Default` | Root project is written `Namespace`-style. ^1^ |
| `Flat` | Root project is written `Flat`-style (without its namespace, logging warnings for resources outside of it). ^1^ |
| `Namespace` | Same as `Default`. |
| **library** | |
| `Default` | Root project is written `Namespace`-style. ^1^ |
| `Flat` | Root project is written `Flat`-style (without its namespace, logging warnings for resources outside of it). ^1^ |
Expand Down
3 changes: 3 additions & 0 deletions packages/project/lib/build/TaskRunner.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ class TaskRunner {
case "library":
buildDefinition = "./definitions/library.js";
break;
case "component":
buildDefinition = "./definitions/application.js";
break;
case "module":
buildDefinition = "./definitions/module.js";
break;
Expand Down
3 changes: 3 additions & 0 deletions packages/project/lib/specifications/Specification.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ class Specification {
case "application": {
return createAndInitializeSpec("types/Application.js", parameters);
}
case "component": {
return createAndInitializeSpec("types/Component.js", parameters);
}
case "library": {
return createAndInitializeSpec("types/Library.js", parameters);
}
Expand Down
303 changes: 303 additions & 0 deletions packages/project/lib/specifications/types/Component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
import fsPath from "node:path";
import posixPath from "node:path/posix";
import ComponentProject from "../ComponentProject.js";
import {createReader} from "@ui5/fs/resourceFactory";

/**
* Component
*
* @public
* @class
* @alias @ui5/project/specifications/types/Component
* @extends @ui5/project/specifications/ComponentProject
* @hideconstructor
*/
class Component extends ComponentProject {
constructor(parameters) {
super(parameters);

this._pManifests = Object.create(null);

this._srcPath = "src";
this._testPath = "test";
this._testPathExists = false;

this._propertiesFilesSourceEncoding = "UTF-8";
}

/* === Attributes === */

/**
* Get the cachebuster signature type configuration of the project
*
* @returns {string} <code>time</code> or <code>hash</code>
*/
getCachebusterSignatureType() {
return this._config.builder && this._config.builder.cachebuster &&
this._config.builder.cachebuster.signatureType || "time";
}

/**
* Get the path of the project's source directory. This might not be POSIX-style on some platforms.
*
* @public
* @returns {string} Absolute path to the source directory of the project
*/
getSourcePath() {
return fsPath.join(this.getRootPath(), this._srcPath);
}

getSourcePaths() {
const paths = [this.getSourcePath()];
if (this._testPathExists) {
paths.push(fsPath.join(this.getRootPath(), this._testPath));
}
return paths;
}

getVirtualPath(sourceFilePath) {
const sourcePath = this.getSourcePath();
if (sourceFilePath.startsWith(sourcePath)) {
const relSourceFilePath = fsPath.relative(sourcePath, sourceFilePath);
let virBasePath = "/resources/";
if (!this._isSourceNamespaced) {
virBasePath += `${this._namespace}/`;
}
return posixPath.join(virBasePath, relSourceFilePath);
}

const testPath = fsPath.join(this.getRootPath(), this._testPath);
if (sourceFilePath.startsWith(testPath)) {
const relSourceFilePath = fsPath.relative(testPath, sourceFilePath);
let virBasePath = "/test-resources/";
if (!this._isSourceNamespaced) {
virBasePath += `${this._namespace}/`;
}
return posixPath.join(virBasePath, relSourceFilePath);
}

throw new Error(
`Unable to convert source path ${sourceFilePath} to virtual path for project ${this.getName()}`);
}

/* === Resource Access === */
/**
* Get a resource reader for the sources of the project (excluding any test resources)
*
* @param {string[]} excludes List of glob patterns to exclude
* @returns {@ui5/fs/ReaderCollection} Reader collection
*/
_getSourceReader(excludes) {
return createReader({
fsBasePath: this.getSourcePath(),
virBasePath: `/resources/${this._namespace}/`,
name: `Source reader for component project ${this.getName()}`,
project: this,
excludes
});
}

/**
* Get a resource reader for the test-resources of the project
*
* @param {string[]} excludes List of glob patterns to exclude
* @returns {@ui5/fs/ReaderCollection} Reader collection
*/
_getTestReader(excludes) {
if (!this._testPathExists) {
return null;
}
const testReader = createReader({
fsBasePath: fsPath.join(this.getRootPath(), this._testPath),
virBasePath: `/test-resources/${this._namespace}/`,
name: `Runtime test-resources reader for component project ${this.getName()}`,
project: this,
excludes
});
return testReader;
}

/**
* Get a resource reader for the sources of the project (excluding any test resources)
* without a virtual base path
*
* @returns {@ui5/fs/ReaderCollection} Reader collection
*/
_getRawSourceReader() {
return createReader({
fsBasePath: this.getSourcePath(),
virBasePath: "/",
name: `Raw source reader for component project ${this.getName()}`,
project: this
});
}

/* === Internals === */
/**
* @private
* @param {object} config Configuration object
*/
async _configureAndValidatePaths(config) {
await super._configureAndValidatePaths(config);

if (config.resources && config.resources.configuration && config.resources.configuration.paths) {
if (config.resources.configuration.paths.src) {
this._srcPath = config.resources.configuration.paths.src;
}
if (config.resources.configuration.paths.test) {
this._testPath = config.resources.configuration.paths.test;
}
}
if (!(await this._dirExists("/" + this._srcPath))) {
throw new Error(
`Unable to find source directory '${this._srcPath}' in component project ${this.getName()}`);
}
this._testPathExists = await this._dirExists("/" + this._testPath);

this._log.verbose(`Path mapping for component project ${this.getName()}:`);
this._log.verbose(` Physical root path: ${this.getRootPath()}`);
this._log.verbose(` Mapped to:`);
this._log.verbose(` /resources/ => ${this._srcPath}`);
this._log.verbose(
` /test-resources/ => ${this._testPath}${this._testPathExists ? "" : " [does not exist]"}`);
}

/**
* @private
* @param {object} config Configuration object
* @param {object} buildDescription Cache metadata object
*/
async _parseConfiguration(config, buildDescription) {
await super._parseConfiguration(config, buildDescription);

if (buildDescription) {
this._namespace = buildDescription.namespace;
return;
}
this._namespace = await this._getNamespace();
await this._ensureComponent();
}

/**
* Determine component namespace either based on a project`s
* manifest.json or manifest.appdescr_variant (fallback if present)
*
* @returns {string} Namespace of the project
* @throws {Error} if namespace can not be determined
*/
async _getNamespace() {
try {
return await this._getNamespaceFromManifestJson();
} catch (manifestJsonError) {
if (manifestJsonError.code !== "ENOENT") {
throw manifestJsonError;
}
// No manifest.json present
// => attempt fallback to manifest.appdescr_variant (typical for App Variants)
try {
return await this._getNamespaceFromManifestAppDescVariant();
} catch (appDescVarError) {
if (appDescVarError.code === "ENOENT") {
// Fallback not possible: No manifest.appdescr_variant present
// => Throw error indicating missing manifest.json
// (do not mention manifest.appdescr_variant since it is only
// relevant for the rather "uncommon" App Variants)
throw new Error(
`Could not find required manifest.json for project ` +
`${this.getName()}: ${manifestJsonError.message}`);
}
throw appDescVarError;
}
}
}

/**
* Determine application namespace by checking manifest.json.
* Any maven placeholders are resolved from the projects pom.xml
*
* @returns {string} Namespace of the project
* @throws {Error} if namespace can not be determined
*/
async _getNamespaceFromManifestJson() {
const manifest = await this._getManifest("/manifest.json");
let appId;
// check for a proper sap.app/id in manifest.json to determine namespace
if (manifest["sap.app"] && manifest["sap.app"].id) {
appId = manifest["sap.app"].id;
} else {
throw new Error(
`No sap.app/id configuration found in manifest.json of project ${this.getName()}`);
}

if (this._hasMavenPlaceholder(appId)) {
try {
appId = await this._resolveMavenPlaceholder(appId);
} catch (err) {
throw new Error(
`Failed to resolve namespace of project ${this.getName()}: ${err.message}`);
}
}
const namespace = appId.replace(/\./g, "/");
this._log.verbose(
`Namespace of project ${this.getName()} is ${namespace} (from manifest.json)`);
return namespace;
}

/**
* Determine application namespace by checking manifest.appdescr_variant.
*
* @returns {string} Namespace of the project
* @throws {Error} if namespace can not be determined
*/
async _getNamespaceFromManifestAppDescVariant() {
const manifest = await this._getManifest("/manifest.appdescr_variant");
let appId;
// check for the id property in manifest.appdescr_variant to determine namespace
if (manifest && manifest.id) {
appId = manifest.id;
} else {
throw new Error(
`No "id" property found in manifest.appdescr_variant of project ${this.getName()}`);
}

const namespace = appId.replace(/\./g, "/");
this._log.verbose(
`Namespace of project ${this.getName()} is ${namespace} (from manifest.appdescr_variant)`);
return namespace;
}

/**
* Reads and parses a JSON file with the provided name from the projects source directory
*
* @param {string} filePath Name of the JSON file to read. Typically "manifest.json" or "manifest.appdescr_variant"
* @returns {Promise<object>} 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) {
throw new Error(
`Could not find resource ${filePath} in project ${this.getName()}`);
}
return JSON.parse(await resource.getString());
}).catch((err) => {
throw new Error(
`Failed to read ${filePath} for project ` +
`${this.getName()}: ${err.message}`);
});
}

async _ensureComponent() {
// Ensure that a Component.js exists
const componentResource = await this._getRawSourceReader().byPath("/Component.js");
if (!componentResource) {
throw new Error(
`Unable to find required file Component.js in component project ${this.getName()}`);
}
}
}

export default Component;
Loading
Loading