@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
153 lines (128 loc) • 6.31 kB
JavaScript
const path = require('node:path')
const cds = require('../../../cds')
const { relativePaths, normalizePath } = require('../../util')
const { BUILD_TASK_FIORI, CDS_CONFIG_PATH_SEP } = 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 cap/issues#8673
*/
module.exports = class FioriBuildPlugin extends require('../edmx') {
_cachedEdmx = new Map()
static get taskDefaults() { return { src: normalizePath(cds.env.folders.app) } }
static hasTask() { return false } // never auto-pulled, only if configured in build tasks
async _ensureCachesInitialized() {
this.context.for ??= {}
this.context.for[BUILD_TASK_FIORI] ??= {}
// cache for later use across multiple FioriBuildPlugin instances
const fioriBuildOptions = this.context.for[BUILD_TASK_FIORI]
if (fioriBuildOptions._initialized) {
return
}
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)
}
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)
// cache edmx per fiori app root folder
this._cachedEdmx = new Map()
await this.compileToEdmx(model)
fioriBuildOptions.appEdmx.set(appFolder, this._cachedEdmx)
}
} finally {
fioriBuildOptions._initialized = true
}
}
/**
* 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() {
await this._ensureCachesInitialized()
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 {
console.log(`UI module does not contain a manifest.json [${relativePaths(cds.root, manifestPath)}], skipping build`)
return
}
const mainService = 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 = 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
}
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) {
return urlString.replace(/^(\/|\\)/, '').replace(/(\/|\\)$/, '') // strip leading and trailing slash or backslash
}
}