Skip to content

Commit

Permalink
[FIX] Detect library namespace automatically (#255)
Browse files Browse the repository at this point in the history
Like already done for applications, read namespace information from "sap.app".id
attribute of manifest.json.

Before, some tasks derived the namespace from a libraries name.
This contract was not documented and not enforced. Causing some confusion when
using those tasks.

If no manifest.json is present, fall back to library.name attribute of .library.
If .library is not present, fall back to the relative path of the library.js file.
If that is not present, the namespace is undefined and some tasks are never
executed for that library:
- generateJsdoc
- executeJsdocSdkTransformation
- generateManifestBundle

namespaces might become mandatory in a future major release.
  • Loading branch information
RandomByte authored Jun 13, 2019
1 parent 389e6c8 commit 604d4d3
Show file tree
Hide file tree
Showing 31 changed files with 1,481 additions and 333 deletions.
4 changes: 2 additions & 2 deletions lib/builder/builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -329,8 +329,8 @@ module.exports = {

return workspace.byGlob("/**/*.*").then((resources) => {
return Promise.all(resources.map((resource) => {
if (project === tree && project.metadata.namespace) {
// Root-project only: Remove namespace prefix if given
if (project === tree && project.type === "application" && project.metadata.namespace) {
// Root-application projects only: Remove namespace prefix if given
resource.setPath(resource.getPath().replace(
new RegExp(`^/resources/${project.metadata.namespace}`), ""));
}
Expand Down
34 changes: 33 additions & 1 deletion lib/types/AbstractFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,41 @@ const fs = require("graceful-fs");
* @abstract
*/
class AbstractFormatter {
/**
* Constructor
*
* @param {Object} parameters
* @param {Object} parameters.project Project
*/
constructor({project}) {
if (new.target === AbstractFormatter) {
throw new TypeError("Class 'AbstractFormatter' is abstract");
}
this._project = project;
}

/**
* Formats and validates the project
*
* @returns {Promise}
*/
format() {
throw new Error("AbstractFormatter: Function format Not implemented");
}

/**
* Validates the project
*
* @returns {Promise} resolves if successfully validated
* @throws {Error} if validation fails
*/
validate() {
throw new Error("AbstractFormatter: Function validate Not implemented");
}

/**
* Checks whether or not the given input is a directory on the file system.
*
*
* @param {string} dirPath directory
* @returns {Promise<boolean>} whether or not the given directory exists.
* <code>true</code> directory exists
Expand Down
95 changes: 95 additions & 0 deletions lib/types/AbstractUi5Formatter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
const log = require("@ui5/logger").getLogger("types:AbstractUi5Formatter");
const path = require("path");
const fs = require("graceful-fs");
const AbstractFormatter = require("./AbstractFormatter");
const {promisify} = require("util");
const readFile = promisify(fs.readFile);

/**
* Base class for formatters that require access to some UI5 specific resources
* like pom.xml
*
* @abstract
*/
class AbstractUi5Formatter extends AbstractFormatter {
/**
* Constructor
*
* @param {Object} parameters
* @param {Object} parameters.project Project
*/
constructor(parameters) {
super(parameters);
if (new.target === AbstractUi5Formatter) {
throw new TypeError("Class 'AbstractUi5Formatter' is abstract");
}
}

/**
* 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) {
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._project.metadata.name}`);
}
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;
}
const fsPath = path.join(this._project.path, "pom.xml");
return this._pPom = readFile(fsPath).then(async (content) => {
const xml2js = require("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._project.metadata.name}: ${err.message}`);
});
}
}

module.exports = AbstractUi5Formatter;
158 changes: 80 additions & 78 deletions lib/types/application/ApplicationFormatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,107 +3,109 @@ const path = require("path");
const fs = require("graceful-fs");
const {promisify} = require("util");
const readFile = promisify(fs.readFile);
const AbstractFormatter = require("../AbstractFormatter");
let readXML; // lazy definition of the readXML function (see readPOM)
const AbstractUi5Formatter = require("../AbstractUi5Formatter");

class ApplicationFormatter extends AbstractFormatter {
class ApplicationFormatter extends AbstractUi5Formatter {
/**
* Validates the project and retrieves its manifest
* Formats and validates the project
*
* @param {Object} project
* @returns {Promise} when validated and manifest has been read
* @returns {Promise}
*/
format(project) {
return this.validate(project).then(() => {
log.verbose("Formatting project %s...", project.metadata.name);
project.resources.pathMappings = {
"/": project.resources.configuration.paths.webapp
};
async format() {
const project = this._project;
await this.validate();
log.verbose("Formatting application project %s...", project.metadata.name);
project.resources.pathMappings = {
"/": project.resources.configuration.paths.webapp
};

return this.readManifest(project).then((manifest) => {
// check for a proper sap.app/id in manifest.json to determine namespace
const appId = manifest["sap.app"] && manifest["sap.app"].id;
if (!appId) {
log.warn(`No "sap.app" ID configuration found in manifest of project ${project.metadata.name}`);
return;
}
return appId;
}, (err) => {
log.verbose(`No manifest found for project ${project.metadata.name}.`);
}).then((appId) => {
// check app id for being a Maven placeholder and try to read the value from the pom.xml
const parts = appId && appId.match(/^\$\{(.*)\}$/);
if (parts) {
log.verbose(`"sap.app" ID configuration contains Maven placeholder "${parts[1]}". Resolving from pom.xml...`);
return this.readPOM(project).then((pom) => {
let mvnAppId;
if (pom.project && pom.project.properties && pom.project.properties[parts[1]]) {
mvnAppId = pom.project.properties[parts[1]];
} else {
let obj = pom;
parts[1].split(".").forEach((part) => {
obj = obj && obj[part];
});
mvnAppId = obj;
}
if (!mvnAppId) {
log.warn(`"sap.app" ID configuration couldn't be resolved from Maven property "${parts[1]}" of pom.xml of project ${project.metadata.name}`);
return;
}
return mvnAppId;
}, (err) => {
log.verbose(`No or invalid pom.xml found for project ${project.metadata.name}.`);
});
}
return appId;
}).then((appId) => {
if (appId) {
project.metadata.namespace = appId.replace(/\./g, "/");
log.verbose(`"sap.app" ID configuration found and set as namespace ${project.metadata.namespace} for project ${project.metadata.name}.`);
}
});
});
try {
project.metadata.namespace = await this.getNamespace();
} catch (err) {
// Catch error because namespace is optional
// TODO 2.0: Make namespace mandatory and just let the error throw
log.warn(err.message);
}
}

/**
* Reads the manifest
* Returns the base *source* path of the project. Runtime resources like manifest.json are expected
* to be located inside this path.
*
* @param {Object} project
* @returns {Promise<Object>} resolves with the json object
* @param {boolean} [posix] whether to return a POSIX path
* @returns {string} Base source path of the project
*/
readManifest(project) {
return readFile(path.join(project.path, project.resources.pathMappings["/"], "manifest.json"), "utf-8")
.then((file) => {
return JSON.parse(file);
});
getSourceBasePath(posix) {
let p = path;
if (posix) {
p = path.posix;
}
return p.join(this._project.path, this._project.resources.pathMappings["/"]);
}

/**
* Reads the pom.xml file
* Determine application namespace by checking manifest.json.
* Any maven placeholders are resolved from the projects pom.xml
*
* @param {Object} project
* @returns {Promise<Object>} resolves with the XML document from the pom.xml
* @returns {string} Namespace of the project
* @throws {Error} if namespace can not be determined
*/
readPOM(project) {
if (!readXML) {
const xml2js = require("xml2js");
const parser = new xml2js.Parser({
explicitArray: false,
ignoreAttrs: true
});
readXML = promisify(parser.parseString);
async getNamespace() {
const {content: manifest} = await this.getManifest();
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"] && manifest["sap.app"].id;
} else {
throw new Error(
`No "sap.app" ID configuration found in manifest.json of project ${this._project.metadata.name}`);
}
return readFile(path.join(project.path, "pom.xml"), "utf-8").then(readXML);

if (this.hasMavenPlaceholder(appId)) {
try {
appId = await this.resolveMavenPlaceholder(appId);
} catch (err) {
throw new Error(
`Failed to resolve namespace of project ${this._project.metadata.name}: ${err.message}`);
}
}
const namespace = appId.replace(/\./g, "/");
log.verbose(`Namespace of project ${this._project.metadata.name} is ${namespace}`);
return namespace;
}

/**
* Reads the projects manifest.json
*
* @returns {Promise<Object>} resolves with an object containing the <code>content</code> (as JSON) and
* <code>fsPath</code> (as string) of the manifest.json file
*/
async getManifest() {
if (this._pManifest) {
return this._pManifest;
}
const fsPath = path.join(this.getSourceBasePath(), "manifest.json");
return this._pManifest = readFile(fsPath)
.then((content) => {
return {
content: JSON.parse(content),
fsPath
};
})
.catch((err) => {
throw new Error(
`Failed to read manifest.json for project ${this._project.metadata.name}: ${err.message}`);
});
}

/**
* Validates a project
* Validates the project
*
* @param {Object} project
* @returns {Promise} resolves if successfully validated
* @throws {Error} if validation fails
*/
validate(project) {
validate() {
const project = this._project;
return Promise.resolve().then(() => {
if (!project) {
throw new Error("Project is undefined");
Expand Down
2 changes: 1 addition & 1 deletion lib/types/application/applicationType.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const ApplicationBuilder = require("./ApplicationBuilder");

module.exports = {
format: function(project) {
return new ApplicationFormatter().format(project);
return new ApplicationFormatter({project}).format();
},
build: function({resourceCollections, tasks, project, parentLogger, buildContext}) {
return new ApplicationBuilder({resourceCollections, project, parentLogger, buildContext}).build(tasks);
Expand Down
Loading

0 comments on commit 604d4d3

Please sign in to comment.