Skip to content

Commit 6372e01

Browse files
committed
feat(project): Add component type
RFC: #1083 JIRA: CPOUI5FOUNDATION-1106
1 parent 31e22f6 commit 6372e01

File tree

65 files changed

+2605
-45
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+2605
-45
lines changed

packages/project/lib/build/TaskRunner.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ class TaskRunner {
5353
case "library":
5454
buildDefinition = "./definitions/library.js";
5555
break;
56+
case "component":
57+
buildDefinition = "./definitions/application.js";
58+
break;
5659
case "module":
5760
buildDefinition = "./definitions/module.js";
5861
break;

packages/project/lib/specifications/Specification.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ class Specification {
3939
case "application": {
4040
return createAndInitializeSpec("types/Application.js", parameters);
4141
}
42+
case "component": {
43+
return createAndInitializeSpec("types/Component.js", parameters);
44+
}
4245
case "library": {
4346
return createAndInitializeSpec("types/Library.js", parameters);
4447
}
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
import fsPath from "node:path";
2+
import posixPath from "node:path/posix";
3+
import ComponentProject from "../ComponentProject.js";
4+
import {createReader} from "@ui5/fs/resourceFactory";
5+
6+
/**
7+
* Component
8+
*
9+
* @public
10+
* @class
11+
* @alias @ui5/project/specifications/types/Component
12+
* @extends @ui5/project/specifications/ComponentProject
13+
* @hideconstructor
14+
*/
15+
class Component extends ComponentProject {
16+
constructor(parameters) {
17+
super(parameters);
18+
19+
this._pManifests = Object.create(null);
20+
21+
this._srcPath = "src";
22+
this._testPath = "test";
23+
this._testPathExists = false;
24+
25+
this._propertiesFilesSourceEncoding = "UTF-8";
26+
}
27+
28+
/* === Attributes === */
29+
30+
/**
31+
* Get the cachebuster signature type configuration of the project
32+
*
33+
* @returns {string} <code>time</code> or <code>hash</code>
34+
*/
35+
getCachebusterSignatureType() {
36+
return this._config.builder && this._config.builder.cachebuster &&
37+
this._config.builder.cachebuster.signatureType || "time";
38+
}
39+
40+
/**
41+
* Get the path of the project's source directory. This might not be POSIX-style on some platforms.
42+
*
43+
* @public
44+
* @returns {string} Absolute path to the source directory of the project
45+
*/
46+
getSourcePath() {
47+
return fsPath.join(this.getRootPath(), this._srcPath);
48+
}
49+
50+
getSourcePaths() {
51+
const paths = [this.getSourcePath()];
52+
if (this._testPathExists) {
53+
paths.push(fsPath.join(this.getRootPath(), this._testPath));
54+
}
55+
return paths;
56+
}
57+
58+
getVirtualPath(sourceFilePath) {
59+
const sourcePath = this.getSourcePath();
60+
if (sourceFilePath.startsWith(sourcePath)) {
61+
const relSourceFilePath = fsPath.relative(sourcePath, sourceFilePath);
62+
let virBasePath = "/resources/";
63+
if (!this._isSourceNamespaced) {
64+
virBasePath += `${this._namespace}/`;
65+
}
66+
return posixPath.join(virBasePath, relSourceFilePath);
67+
}
68+
69+
const testPath = fsPath.join(this.getRootPath(), this._testPath);
70+
if (sourceFilePath.startsWith(testPath)) {
71+
const relSourceFilePath = fsPath.relative(testPath, sourceFilePath);
72+
let virBasePath = "/test-resources/";
73+
if (!this._isSourceNamespaced) {
74+
virBasePath += `${this._namespace}/`;
75+
}
76+
return posixPath.join(virBasePath, relSourceFilePath);
77+
}
78+
79+
throw new Error(
80+
`Unable to convert source path ${sourceFilePath} to virtual path for project ${this.getName()}`);
81+
}
82+
83+
/* === Resource Access === */
84+
/**
85+
* Get a resource reader for the sources of the project (excluding any test resources)
86+
*
87+
* @param {string[]} excludes List of glob patterns to exclude
88+
* @returns {@ui5/fs/ReaderCollection} Reader collection
89+
*/
90+
_getSourceReader(excludes) {
91+
return createReader({
92+
fsBasePath: this.getSourcePath(),
93+
virBasePath: `/resources/${this._namespace}/`,
94+
name: `Source reader for component project ${this.getName()}`,
95+
project: this,
96+
excludes
97+
});
98+
}
99+
100+
/**
101+
* Get a resource reader for the test-resources of the project
102+
*
103+
* @param {string[]} excludes List of glob patterns to exclude
104+
* @returns {@ui5/fs/ReaderCollection} Reader collection
105+
*/
106+
_getTestReader(excludes) {
107+
if (!this._testPathExists) {
108+
return null;
109+
}
110+
const testReader = createReader({
111+
fsBasePath: fsPath.join(this.getRootPath(), this._testPath),
112+
virBasePath: `/test-resources/${this._namespace}/`,
113+
name: `Runtime test-resources reader for component project ${this.getName()}`,
114+
project: this,
115+
excludes
116+
});
117+
return testReader;
118+
}
119+
120+
/**
121+
* Get a resource reader for the sources of the project (excluding any test resources)
122+
* without a virtual base path
123+
*
124+
* @returns {@ui5/fs/ReaderCollection} Reader collection
125+
*/
126+
_getRawSourceReader() {
127+
return createReader({
128+
fsBasePath: this.getSourcePath(),
129+
virBasePath: "/",
130+
name: `Raw source reader for component project ${this.getName()}`,
131+
project: this
132+
});
133+
}
134+
135+
/* === Internals === */
136+
/**
137+
* @private
138+
* @param {object} config Configuration object
139+
*/
140+
async _configureAndValidatePaths(config) {
141+
await super._configureAndValidatePaths(config);
142+
143+
if (config.resources && config.resources.configuration && config.resources.configuration.paths) {
144+
if (config.resources.configuration.paths.src) {
145+
this._srcPath = config.resources.configuration.paths.src;
146+
}
147+
if (config.resources.configuration.paths.test) {
148+
this._testPath = config.resources.configuration.paths.test;
149+
}
150+
}
151+
if (!(await this._dirExists("/" + this._srcPath))) {
152+
throw new Error(
153+
`Unable to find source directory '${this._srcPath}' in component project ${this.getName()}`);
154+
}
155+
this._testPathExists = await this._dirExists("/" + this._testPath);
156+
157+
this._log.verbose(`Path mapping for component project ${this.getName()}:`);
158+
this._log.verbose(` Physical root path: ${this.getRootPath()}`);
159+
this._log.verbose(` Mapped to:`);
160+
this._log.verbose(` /resources/ => ${this._srcPath}`);
161+
this._log.verbose(
162+
` /test-resources/ => ${this._testPath}${this._testPathExists ? "" : " [does not exist]"}`);
163+
}
164+
165+
/**
166+
* @private
167+
* @param {object} config Configuration object
168+
* @param {object} buildDescription Cache metadata object
169+
*/
170+
async _parseConfiguration(config, buildDescription) {
171+
await super._parseConfiguration(config, buildDescription);
172+
173+
if (buildDescription) {
174+
this._namespace = buildDescription.namespace;
175+
return;
176+
}
177+
this._namespace = await this._getNamespace();
178+
await this._ensureComponent();
179+
}
180+
181+
/**
182+
* Determine component namespace either based on a project`s
183+
* manifest.json or manifest.appdescr_variant (fallback if present)
184+
*
185+
* @returns {string} Namespace of the project
186+
* @throws {Error} if namespace can not be determined
187+
*/
188+
async _getNamespace() {
189+
try {
190+
return await this._getNamespaceFromManifestJson();
191+
} catch (manifestJsonError) {
192+
if (manifestJsonError.code !== "ENOENT") {
193+
throw manifestJsonError;
194+
}
195+
// No manifest.json present
196+
// => attempt fallback to manifest.appdescr_variant (typical for App Variants)
197+
try {
198+
return await this._getNamespaceFromManifestAppDescVariant();
199+
} catch (appDescVarError) {
200+
if (appDescVarError.code === "ENOENT") {
201+
// Fallback not possible: No manifest.appdescr_variant present
202+
// => Throw error indicating missing manifest.json
203+
// (do not mention manifest.appdescr_variant since it is only
204+
// relevant for the rather "uncommon" App Variants)
205+
throw new Error(
206+
`Could not find required manifest.json for project ` +
207+
`${this.getName()}: ${manifestJsonError.message}`);
208+
}
209+
throw appDescVarError;
210+
}
211+
}
212+
}
213+
214+
/**
215+
* Determine application namespace by checking manifest.json.
216+
* Any maven placeholders are resolved from the projects pom.xml
217+
*
218+
* @returns {string} Namespace of the project
219+
* @throws {Error} if namespace can not be determined
220+
*/
221+
async _getNamespaceFromManifestJson() {
222+
const manifest = await this._getManifest("/manifest.json");
223+
let appId;
224+
// check for a proper sap.app/id in manifest.json to determine namespace
225+
if (manifest["sap.app"] && manifest["sap.app"].id) {
226+
appId = manifest["sap.app"].id;
227+
} else {
228+
throw new Error(
229+
`No sap.app/id configuration found in manifest.json of project ${this.getName()}`);
230+
}
231+
232+
if (this._hasMavenPlaceholder(appId)) {
233+
try {
234+
appId = await this._resolveMavenPlaceholder(appId);
235+
} catch (err) {
236+
throw new Error(
237+
`Failed to resolve namespace of project ${this.getName()}: ${err.message}`);
238+
}
239+
}
240+
const namespace = appId.replace(/\./g, "/");
241+
this._log.verbose(
242+
`Namespace of project ${this.getName()} is ${namespace} (from manifest.json)`);
243+
return namespace;
244+
}
245+
246+
/**
247+
* Determine application namespace by checking manifest.appdescr_variant.
248+
*
249+
* @returns {string} Namespace of the project
250+
* @throws {Error} if namespace can not be determined
251+
*/
252+
async _getNamespaceFromManifestAppDescVariant() {
253+
const manifest = await this._getManifest("/manifest.appdescr_variant");
254+
let appId;
255+
// check for the id property in manifest.appdescr_variant to determine namespace
256+
if (manifest && manifest.id) {
257+
appId = manifest.id;
258+
} else {
259+
throw new Error(
260+
`No "id" property found in manifest.appdescr_variant of project ${this.getName()}`);
261+
}
262+
263+
const namespace = appId.replace(/\./g, "/");
264+
this._log.verbose(
265+
`Namespace of project ${this.getName()} is ${namespace} (from manifest.appdescr_variant)`);
266+
return namespace;
267+
}
268+
269+
/**
270+
* Reads and parses a JSON file with the provided name from the projects source directory
271+
*
272+
* @param {string} filePath Name of the JSON file to read. Typically "manifest.json" or "manifest.appdescr_variant"
273+
* @returns {Promise<object>} resolves with an object containing the content requested manifest file
274+
*/
275+
async _getManifest(filePath) {
276+
if (this._pManifests[filePath]) {
277+
return this._pManifests[filePath];
278+
}
279+
return this._pManifests[filePath] = this._getRawSourceReader().byPath(filePath)
280+
.then(async (resource) => {
281+
if (!resource) {
282+
throw new Error(
283+
`Could not find resource ${filePath} in project ${this.getName()}`);
284+
}
285+
return JSON.parse(await resource.getString());
286+
}).catch((err) => {
287+
throw new Error(
288+
`Failed to read ${filePath} for project ` +
289+
`${this.getName()}: ${err.message}`);
290+
});
291+
}
292+
293+
async _ensureComponent() {
294+
// Ensure that a Component.js exists
295+
const componentResource = await this._getRawSourceReader().byPath("/Component.js");
296+
if (!componentResource) {
297+
throw new Error(
298+
`Unable to find required file Component.js in component project ${this.getName()}`);
299+
}
300+
}
301+
}
302+
303+
export default Component;

packages/project/lib/validation/schema/specVersion/kind/project.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"enum": [
1515
"application",
1616
"library",
17+
"component",
1718
"theme-library",
1819
"module"
1920
]
@@ -61,6 +62,16 @@
6162
},
6263
"then": {
6364
"$ref": "project/module.json"
65+
},
66+
"else": {
67+
"if": {
68+
"properties": {
69+
"type": {"const": "component"}
70+
}
71+
},
72+
"then": {
73+
"$ref": "project/component.json"
74+
}
6475
}
6576
}
6677
}

0 commit comments

Comments
 (0)