UNPKG

@sap/cds-dk

Version:

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

176 lines (155 loc) 7.52 kB
const path = require('path') const fs = require('fs') const cds = require('../../../cds') const InternalBuildPlugin = require('../internalBuildPlugin') const ResourcesTarBuilder = require('../mtx/resourcesTarBuilder') const ExtensionCompilation = require('./extensionCompilation') const { FOLDER_GEN, EXTENSION_POINT_VALIDATION } = require('../../constants') const { BuildError, BuildMessage } = require('../../util') const DEBUG = cds.debug('cli|build') class MtxExtensionBuildPlugin extends InternalBuildPlugin { 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 const saasPkgPath = MtxExtensionBuildPlugin._getSaasPkgPath() 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 } } } // 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) { // cleanup existing files in case of an error await fs.promises.rm(dest, { recursive: true }) 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 (fs.statSync(res).isDirectory()) { return res.startsWith(folder) } if (path.dirname(res).startsWith(folder) && /\.js$/.test(res)) { return true } }) // add all resources contained in 'gen/ext' folder return new ResourcesTarBuilder(this).writeTarFile(path.join(dest, 'extension.tgz'), destExt) } _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() { let linter try { // Make sure cds-mtxs APIs are loaded linter = require('@sap/cds-mtxs').xt?.linter if (!linter) { this.pushMessage('MTXS linter cannot be loaded. Update of @sap/cds-dk and @sap/cds-mtxs modules required? Skipping extension model lint step.') return null // too old mtxs } } 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.') } return linter } static _getSaasPkgPath() { 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 let saasPkgPath = this._getWorkspacePath(cds.env.extends) if (!saasPkgPath) { // older projects might not have a workspace definition saasPkgPath = this._getNodeModulesPath(saasPkgPath) } return saasPkgPath } static _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'?`) } } } static _getNodeModulesPath() { const saasPkgPath = path.join(cds.root, 'node_modules', cds.env.extends) if (!fs.existsSync(saasPkgPath)) { throw new BuildError(`The SaaS application base model is missing. Did you execute 'cds pull' and 'npm install'?`) } return saasPkgPath } } module.exports = MtxExtensionBuildPlugin