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