UNPKG

@sap/cds-dk

Version:

Command line client and development toolkit for the SAP Cloud Application Programming Model

153 lines (128 loc) 6.31 kB
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 } }