@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
168 lines (143 loc) • 6.83 kB
JavaScript
const path = require('path')
const cds = require('../../../cds')
const EdmxBuildPlugin = require('../edmxBuildPlugin')
const URL = require('url')
const { getProperty, relativePaths } = require('../../util')
const { OUTPUT_MODE, OUTPUT_MODE_RESULT, BUILD_TASK_FIORI, CDS_CONFIG_PATH_SEP, OUTPUT_MODE_FILESYSTEM } = require('../../constants')
const DEBUG = cds.debug('cli|build')
/**
* With cds 4 service metadata for the UI service binding is no longer created by default.
* For SAP Web IDE Full-Stack compatibility a corresponding metadata.xml is still generated.
* Though, 'fiori' build tasks can still be configured for UI5 mock server usage - see https://github.wdf.sap.corp/cap/issues/issues/8673
*/
class FioriBuildPlugin extends EdmxBuildPlugin {
init() {
// enforce
this._result = {
dest: this.task.dest,
edmx: new Map(),
languages: new Set(),
services: new Set()
}
}
async prepare() {
this.context.for = this.context.for || {}
this.context.for[BUILD_TASK_FIORI] = this.context.for[BUILD_TASK_FIORI] || {}
// cache for later use across multiple FioriBuildPlugin instances
const fioriBuildOptions = this.context.for[BUILD_TASK_FIORI]
fioriBuildOptions.appModel = new Map()
fioriBuildOptions.appEdmx = new Map()
// group tasks that have a common application root folder
const appTaskGroups = new Map()
this.context.tasks.forEach(task => {
if (task.for === BUILD_TASK_FIORI) {
const appFolder = path.relative(cds.root, task.src).split(CDS_CONFIG_PATH_SEP)[0]
appTaskGroups.has(appFolder) ? appTaskGroups.get(appFolder).push(task) : appTaskGroups.set(appFolder, [task])
}
})
// merge all model references to resolve the model later on
const appModelGroups = new Map()
for (let [appFolder, appTaskGroup] of appTaskGroups.entries()) {
const appModels = new Set()
appTaskGroup.forEach(task => {
if (Array.isArray(task.options.model)) {
task.options.model.forEach(model => appModels.add(model))
}
})
appModelGroups.set(appFolder, appModels)
}
// ensure edmx is cached as build result
const buildOption = this.context.options[OUTPUT_MODE]
this.context.options[OUTPUT_MODE] |= OUTPUT_MODE_RESULT
try {
for (let [appFolder, appModelGroup] of appModelGroups.entries()) {
DEBUG?.(`building module [${appFolder}] using [${this.constructor.name}]`)
const modelPaths = cds.resolve(Array.from(appModelGroup.values()))
if (!modelPaths || modelPaths.length === 0) {
continue
}
DEBUG?.(`model: ${relativePaths(cds.root, modelPaths).join(", ")}`)
//cache model per fiori app root folder
const options = this.options()
const model = await cds.load(modelPaths, options)
fioriBuildOptions.appModel.set(appFolder, model)
await this.compileToEdmx(model)
// cache edmx per fiori app root folder
fioriBuildOptions.appEdmx.set(appFolder, this._result.edmx)
}
} finally {
this.context.options[OUTPUT_MODE] = buildOption
}
return false
}
/**
* This version only creates a odata representation for the 'mainService' data source
* as defined by the fiori wizard - everything else is currently not supported.
* Therefore errors are only logged, the build does not fail in case a service
* cannot be resolved based on the defined service URI
*/
async build() {
const model = await this.model()
if (!model) {
return
}
await this._writeEdmxToWebapp(this.task.src, this.task.dest)
}
async _writeEdmxToWebapp(src, dest) {
const manifestPath = path.join(src, 'webapp', 'manifest.json')
let manifest
try {
manifest = require(manifestPath)
} catch (error) {
console.log(`UI module does not contain a manifest.json [${relativePaths(cds.root, manifestPath)}], skipping build`)
return
}
const mainService = getProperty(manifest, ['sap.app', 'dataSources', 'mainService'])
if (!mainService) {
// no mainService defined - not supported
DEBUG?.(`UI module does not have a datasource [mainService], [${relativePaths(cds.root, manifestPath)}], skipping build`)
return
}
const localUri = getProperty(mainService, ['settings', 'localUri'])
const uri = mainService.uri
if (!localUri || !uri) {
DEBUG?.(`local uri setting missing for data source [mainService], [${relativePaths(cds.root, manifestPath)}]`)
return
}
const appFolder = path.relative(cds.root, src).split(CDS_CONFIG_PATH_SEP)[0]
const model = this.context.for[BUILD_TASK_FIORI].appModel.get(appFolder)
if (!model) {
console.error(`failed to load model for service uri ${uri}, data source [mainService]`)
return
}
const edmx = this._getEdmxForUri(model, appFolder, uri)
if (!edmx) {
console.error(`failed to resolve service definition for service uri ${uri}, data source [mainService]`)
return
}
if (this.hasBuildOption(OUTPUT_MODE, OUTPUT_MODE_FILESYSTEM)) {
const edmxPath = path.resolve(path.join(dest, 'webapp'), this._strippedUrlPath(localUri))
return this.write(edmx).to(edmxPath)
}
}
_getEdmxForUri(model, appFolder, uri) {
const uriSegments = this._strippedUrlPath(uri).split('/')
// one segment of the URI has to match a service name
// NOTE: assumption is that the service definition can be resolved - either by
// - defining corresponding using statement in annotations model or
// - adding the service module folder to the model option
let service = cds.reflect(model).find(service => uriSegments.find(segment => service.name === segment))
if (service) {
const allServices = this.context.for[BUILD_TASK_FIORI].appEdmx.get(appFolder)
if (allServices) {
return allServices.get(service.name + ".xml")
}
}
return null
}
_strippedUrlPath(urlString) {
const url = URL.parse(urlString)
return url.pathname.replace(/^(\/|\\)/, '').replace(/(\/|\\)$/, '') // strip leading and trailing slash or backslash)
}
}
module.exports = FioriBuildPlugin