@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
176 lines (155 loc) • 7.52 kB
JavaScript
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