UNPKG

@sap/cds-dk

Version:

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

174 lines (150 loc) 7.41 kB
const cds = require('../../../cds') const { exists, path, isdir, rimraf, tar } = cds.utils const ExtensionCompilation = require('./extensionCompilation') const { FOLDER_GEN, EXTENSION_POINT_VALIDATION } = require('../../constants') const { BuildError, BuildMessage } = require('../../util') const DEBUG = cds.debug('cli|build') const BUILT_IN_NAMESPACES = ['cds.core', 'cds.outbox', 'cds.xt'] module.exports = class MtxExtensionBuildPlugin extends require('../internal') { static get taskDefaults() { return { src: '.' } } static hasTask() { return cds.env.extends } init() { super.init() if (cds.env.build.target === '.') { this.task.dest = path.join(this.task.dest, FOLDER_GEN) } } async build() { const { dest } = this.task const destExt = path.join(dest, 'ext') let model let extensionModel // REVISIT: check existence of SaaS app package folder only // cds.resolve will fail as no index.csn file exists in this folder for the extension migration use case // a compilation error is thrown anyhow if any base model refs cannot be resolved if (!cds.env.extends) throw new BuildError(`Missing configuration 'cds.extends' in package.json`) // cds.env.extends holds the SaaS app package name, also if the extends field is defined on root level // older projects might not have a workspace definition const saasPkgPath = _getWorkspacePath(cds.env.extends) ?? _getNodeModulesPath() const extensionCompilation = new ExtensionCompilation(this) // full model compilation including base model ensures consistency try { model = await this.model() } catch (e) { if (extensionCompilation.isDuplicateDefinitionError(e)) { // clear compilation errors and recompile with removed duplicate resources this.messages.length = 0 model = await extensionCompilation.recompileWithoutDuplicates(e.messages, saasPkgPath) } } // extension model compilation with parsed format if (model) { const options = { ...this.options(), flavor: 'parsed' } const extensionFiles = extensionCompilation.resolveExtensionFiles(model, saasPkgPath) if (extensionFiles.length > 0) { extensionModel = await cds.load(extensionFiles, options) if (extensionModel.requires) { extensionModel.requires.length = 0 } if (extensionModel.definitions) { for (const [name] of Object.entries(extensionModel.definitions)) { for (const namespace of BUILT_IN_NAMESPACES) { if (name.startsWith(`${namespace}.`)) { delete extensionModel.definitions[name] break } } } } } } // reclassify base model warnings as info messages extensionCompilation.reclassifyBaseModelMessages(this.messages, saasPkgPath) // Throw error if messages contain errors if (this.messages.some(msg => msg.severity === 'Error')) { throw new BuildError('CDS compilation failed', this.messages); } if (extensionModel) { // IMPORTANT NOTE: perform all model operations before the linter is called as the linter // will modify the model causing the creation of 'unresolved' associations await this.compileToJson(extensionModel, path.join(destExt, 'extension.csn')) await this.collectLanguageBundles(extensionModel, path.join(destExt, 'i18n')) const files = Object.keys(await cds.deploy.resources(model)) if (files.length > 0) { const dataDest = path.join(destExt, 'data') await Promise.all( files.filter(file => /\.csv$/.test(file)).map(csv => { return this.copy(csv).to(path.join(dataDest, path.basename(csv))) }) ) } if (!this.hasBuildOption(EXTENSION_POINT_VALIDATION, false)) { try { // throws error in case of linting errors this._lintExtModel(extensionModel, model, saasPkgPath) } catch (error) { await rimraf(dest) throw error } } } // existence already checked await this.copy(path.join(cds.root, 'package.json')).to(path.join(destExt, 'package.json')) // copy handlers from srv folder const folder = path.join(cds.root, cds.env.folders.srv).replace(/[/\\]$/, '') await this.copyNativeContent(cds.root, destExt, res => { if (isdir(res)) { return res.startsWith(folder) } if (path.dirname(res).startsWith(folder) && /\.js$/.test(res)) { return true } }) // add all resources contained in 'gen/ext' folder const tarFile = path.join(dest, 'extension.tgz') await tar.czfd(tarFile, destExt) this.pushFile(tarFile) } _lintExtModel(extModel, model, saasPkgPath) { const linter = this._linter() if (!linter) { return } const env = cds.env.for('cds', saasPkgPath) DEBUG?.(`Saas extension point restrictions:\n${env.requires?.['cds.xt.ExtensibilityService']}`) const messages = linter.lint(extModel, model, env) if (messages.length) { // REVISIT: lint messages can be passed as is with cds-mtxs version >= 1.7 throw new BuildError('SaaS extension point restrictions violated. Check the concrete restrictions defined by the SaaS app provider.', messages.map(f => new BuildMessage(f.message, f.severity, f.location || f.element?.$location))) } } _linter() { try { // Make sure cds-mtxs APIs are loaded, from local mtxs preferably return require(require.resolve('@sap/cds-mtxs', { paths: [cds.root, __dirname] })).xt.linter } catch (e) { if (e.code !== 'MODULE_NOT_FOUND') throw e this.pushMessage('MTXS linter cannot be loaded, @sap/cds-mtxs not installed. Skipping extension model lint step.') } } } function _getWorkspacePath(pkg) { const pkgJson = require(path.join(cds.root, 'package.json')) if (pkgJson.workspaces) { try { // loading package.json of SaaS app - might not exist for older projects return path.dirname(require.resolve(path.join(pkg, 'package.json'), { paths: [cds.root] })) } catch (e) { if (e.code !== 'MODULE_NOT_FOUND') throw e throw new BuildError(`The SaaS application base model is missing. Did you execute 'cds pull' and 'npm install'?`) } } } function _getNodeModulesPath() { const saasPkgPath = path.join(cds.root, 'node_modules', cds.env.extends) if (!exists(saasPkgPath)) { throw new BuildError(`The SaaS application base model is missing. Did you execute 'cds pull' and 'npm install'?`) } return saasPkgPath }