UNPKG

@sap/cds-dk

Version:

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

168 lines (143 loc) 6.83 kB
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