UNPKG

@sap/cds-dk

Version:

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

273 lines (259 loc) 14.7 kB
const cds = require('../cds') const { join, resolve, basename } = require('path') const { fs, exists, isfile, isdir, path } = cds.utils const { parseXml } = require('./xml') const { REGEX_JAVA_VERSION, JAVA_LTS_VERSIONS } = require('./constants') const TRACE = cds.debug('trace') module.exports = new class ProjectReader { constructor() { this.readProject = this.readProject.bind(this) } #setPackageInfo() { this.isJava = (exists('pom.xml') || cds.cli.options?.add?.has('java')) && !cds.env.for('cds').profiles.includes('node') // opt-out for hybrid projects like sflight // 1. Use project name and static default values this.appVersion = this.isJava ? '1.0.0-SNAPSHOT' : '1.0.0' this.appName = this.appId = basename(cds.root) this.appDescription = 'A simple CAP project.' if (this.isJava) { let artifactId, description, version, properties try { ({ artifactId, description, version, properties } = parseXml(resolve(cds.root, 'pom.xml')) ?? {}) } catch (e) { if (e.code !== 'ENOENT') throw e } const v = version?.[0] === '${revision}' ? properties?.[0]?.revision[0] : version?.[0] ?? '1.0.0-SNAPSHOT' if (v && REGEX_JAVA_VERSION.test(v)) this.appVersion = v this.jdkVersion = properties?.[0]?.['jdk.version']?.[0]?._text?.[0] ?? JAVA_LTS_VERSIONS[JAVA_LTS_VERSIONS.length - 1] if (artifactId?.[0]) this.appName = this.appId = artifactId?.[0]?.split(/-parent/)[0] if (description?.[0]) this.appDescription = description[0] } else if (exists('package.json')) { const { name, version, description } = JSON.parse(fs.readFileSync(join(cds.root, 'package.json'))) this.appVersion = version ?? this.appVersion const segments = (name ?? this.appName).trim().replace(/@/g, '').split('/').map(encodeURIComponent) this.appName = segments[segments.length - 1] this.appId = segments.join('.') this.appDescription = description ?? this.appDescription } } /** * Returns cds.env using 'production' profile by default as mta deployment is executed with having production profile set. */ env4(profile = 'production') { const cdsEnv = process.env.CDS_ENV process.env.CDS_ENV = profile try { return cds.env.for('cds') } finally { cdsEnv ? process.env.CDS_ENV = cdsEnv : delete process.env.CDS_ENV } } /** * @typedef {Object} ProjectConfig * @property {string} appVersion The version of the application. * @property {string} appName The name of the application. * @property {string} appId The ID of the application. * @property {string} appDescription The description of the application. * @property {'java' | 'nodejs'} language The programming language of the project. * @property {string} srvPath The path to the server module. * @property {string} archiveName The name of the archive. * @property {Object} db Database configuration. * @property {string} configFile The name of the configuration file. This is typically the `package.json` for Node.js and `.cdsrc.json` for Java. * @property {string} appPath The path to the app folder. * @property {string} approuterPath The path to the approuter folder. * @property {boolean} hasUIEmbedded Indicates if the project uses an embedded UI. * @property {boolean} hasUIModule Indicates if the project is using a modular UI. * @property {boolean} hasUI Indicates if the project has a UI. * @property {boolean} hasUI5 Indicates if the project is a UI5 project. * @property {boolean} isUI5 Indicates if the project is a UI5 project. * @property {boolean} isJava Indicates if the project is using Java. * @property {boolean} isNodejs Indicates if the project is using Node.js. * @property {boolean} isESM Indicates if the project is an ESM project (type 'module') * @property {boolean} hasEsmStyle Indicates if code supports ESM-style imports and exports. This is true for ESM and Typescript projects. * @property {boolean} isTypescript Indicates if the project is a Typescript project. * @property {boolean} hasTyper Indicates if code supports CDS typer. * @property {boolean} hasHelmUnifiedRuntime Indicates if the project uses Unified Runtime Helm deployment. * @property {boolean} hasHelm Indicates if the project uses CAP Helm deployment. * @property {boolean} hasMta Indicates if the project uses MTA deployment. * @property {boolean} hasConnectivity Indicates if the project uses connectivity. * @property {boolean} hasMultitenancy Indicates if the project is multitenant. * @property {boolean} hasToggles Indicates if the project uses feature toggles. * @property {boolean} hasExtensibility Indicates if the project is extensible. * @property {boolean} hasXsuaa Indicates if the project uses XSUAA. * @property {boolean} hasHana Indicates if the project uses HANA. * @property {boolean} hasPostgres Indicates if the project uses PostgreSQL. * @property {boolean} hasEnterpriseMessaging Indicates if the project uses SAP BTP Event Mesh. * @property {boolean} hasAttachments Indicates if the project uses SAP BTP Object Store Service. * @property {boolean} hasApprouter Indicates if the project uses the SAP Application Router. * @property {boolean} hasHtml5Repo Indicates if the project uses the SAP BTP HTML5 Application Repository. * @property {boolean} hasPortal Indicates if the project uses the SAP Cloud Portal service. * @property {boolean} hasWorkzoneStandard Indicates if the project uses the SAP Build Work Zone, Standard Edition service. * @property {boolean} hasDestination Indicates if the project uses the SAP BTP Destination service. * @property {boolean} hasMTXRoute Indicates if the approuter should have a multitenant route. * @property {boolean} hasMTXRouteJava Indicates if the approuter should have a multitenant subscription route (java). * @property {boolean} hasMalwareScanner Indicates if the project uses the SAP Malware Scanning service. * @property {boolean} hasDynatrace Indicates if the project uses Dynatrace. * @property {boolean} hasCloudLogging Indicates if the project uses Cloud Logging. * @property {String} extendsApp Indicates which project is extended if any. */ /** * Reads the project configuration and constructs a project object. * This function dynamically resolves project properties and returns an object with these properties. * * @returns {ProjectConfig} An object representing the project configuration. */ readProject() { if (!this.appName) this.#setPackageInfo() const env = this.env4('production') const { appVersion, appName, appId, appDescription, jdkVersion } = this TRACE?.({ env }) const pkgJson = () => exists('package.json') ? JSON.parse(fs.readFileSync(join(cds.root, 'package.json'))) : {} const _isESM = () => pkgJson().type === 'module' const _isTypescript = () => !!pkgJson().dependencies?.typescript || !!pkgJson().devDependencies?.typescript const _hasTyper = () => !!pkgJson().dependencies?.['@cap-js/cds-typer'] || !!pkgJson().devDependencies?.['@cap-js/cds-typer'] const _inProd = plugin => require(`./template/${plugin}`).hasInProduction(env) const _ui5 = () => pkgJson().sapux const _uiEmbedded = () => exists(join(env.folders.app, 'xs-app.json')) const _uiModule = () => !_uiEmbedded() && isdir(env.folders.app) && fs.readdirSync(resolve(cds.root, env.folders.app)).length > 0 const _isJava = () => this.isJava const _extends = () => pkgJson().extends const isApp = name => { return name !== 'appconfig' && !name.match(/^[._]/) && name !== 'router' && name !== 'portal' && name !== 'workzone' } const reserved = { dependencies: () => pkgJson().dependencies, has: x => _inProd(x), profile: () => cds.cli.options?.for, language: () => _isJava() ? 'java' : 'nodejs', isJava: () => _isJava(), isNodejs: () => !_isJava(), isTypescript: () => _isTypescript(), isESM: () => _isESM, hasEsmStyle: () => _isTypescript() || _isESM(), hasTyper: () => _hasTyper(), srvPath: () => join(env.build.target, env.folders.srv.replace(/\/$/, '')).replace(/\\/g, '/'), archiveName: () => { const pomXmlPath = resolve(cds.root, env.folders.srv, 'pom.xml') const pom = exists(pomXmlPath) ? parseXml(pomXmlPath) : {} const { artifactId = [basename(cds.root)], packaging = ['jar'] } = pom return artifactId[0] + '-exec.' + packaging[0] }, db: () => { const folder = env.folders.db const name = folder.replace(/\/$/, '') const path = join(env.build.target, name).replace(/\\/g, '/') return { folder, name, path } }, configFile: () => _isJava() ? '.cdsrc.json' : 'package.json', apps: () => { const appPath = path.resolve(cds.root, cds.env.folders.app) if (!exists(appPath)) return [] const apps = fs.readdirSync(appPath).filter(e => isdir(path.join(cds.root, cds.env.folders.app, e)) && isApp(e) ) return apps.map((app, i) => { const manifestPath = path.resolve(cds.root, cds.env.folders.app, app, 'webapp/manifest.json') const manifest = exists(manifestPath) ? JSON.parse(fs.readFileSync(manifestPath, 'utf-8')) : {} const inbounds = manifest?.['sap.app']?.crossNavigation?.inbounds const [firstInbound] = Object.values(inbounds ?? { intent: { semanticObject: 'Books', action: 'display' }}) const vizId = firstInbound.semanticObject + '-' + firstInbound.action return { app, strippedApp: app.replace(/-/g, ''), vizId, isNotLast: i < apps.length - 1, manifestAppId: manifest?.['sap.app']?.id ?? app} }) }, appUIPaths: () => { const appPath = path.resolve(cds.root, cds.env.folders.app) if (!exists(appPath)) return [] return fs.readdirSync(appPath).filter(e => isdir(path.join(cds.root, cds.env.folders.app, e)) && isApp(e) ) }, hasIndexHtml: () => {}, appPath: () => env.folders.app, appVersion: () => appVersion, jdkVersion: () => jdkVersion, approuterPath: () => (_uiEmbedded() ? join(env.folders.app) : join(env.folders.app, 'router')).replace(/\\/g, '/'), appName: () => appName, cleanedAppName: () => appName.replaceAll('_', '-'), strippedAppName: () => appName.replace(/-/g, ''), appId: () => appId, appDescription: () => appDescription, hasUIEmbedded: _uiEmbedded, hasUIModule: _uiModule, hasUI: () => _uiEmbedded() || _uiModule(), isUI5: _ui5, hasUI5: _ui5, hasMTXRoute: () => _inProd('extensibility') || (_inProd('multitenancy') && !_isJava()), hasMTXRouteJava: () => (_inProd('helm-unified-runtime') || _inProd('helm')) && _isJava() && _inProd('multitenancy'), imageRegistry: () => exists('chart/values.yaml') ? cds.load.yaml(join(cds.root, 'chart/values.yaml')).global?.image?.registry ?? '<your-container-registry>' : '<your-container-registry>', extendsApp: _extends, } // Automatically creates availability checks for `cds add` commands. // Maps to the `hasInProduction` implemented in the command. // E.g. `cds add hana` can be checked using `hasHana` or `isHana` const project = (() => { const defined = {} const _get = property => { const p = property if (p in reserved) return reserved[p]() for (const prefix of ['has', 'is']) if (p.startsWith(prefix) && p.length > prefix.length) { return _inProd(p.slice(prefix.length).replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()) } } return new Proxy({}, { get(_, p) { return _get(p) ?? defined[p] }, set(_, p, value) { defined[p] = value; return true }, has(_, p) { return _get(p) ?? p in defined } }) })() return project } } /** * Finds and reads SpringBoot configuration in `cds.root/srv` dir. * This is done in a best-effort way. In no way can we replicate all of SpringBoot's * config variants. * @param {string[]?} roots array of directories to search for application.yaml, like `srv` * @returns {Promise<object[]>} array of objects with `cds` property */ module.exports._readSpringBootConfig = async function (roots=[cds.env.folders.srv]) { const configNodes = [] const rootDirs = roots.map(d => join(cds.root, d)) for (let r of rootDirs) { const file = isfile (join (r,'./src/main/resources/application.yaml')) if (file) { const yaml = cds.load.yaml(file) for (let yamlDoc of Array.isArray(yaml) ? yaml : [yaml]) { let cds = yamlDoc?.cds; if (!cds) continue cds = _normalizeSpringBootCfg(cds) configNodes.push({cds}) } } } return configNodes } /** * Normalize SpringBoot's dots in keys to express nested objects * * @example: `cds.foo.bar = 1` --> `cds: { foo: { bar: 1 } }` * @param {object} obj * @returns {object} */ function _normalizeSpringBootCfg(obj) { if (typeof obj !== 'object') return obj Object.keys(obj).forEach(k => { const prop = k.split('.') const last = prop.pop() // and define the object if not already defined const res = prop.reduce((o, key) => { // define the object if not defined and return return o[key] = o[key] ?? {} }, obj) res[last] = obj[k] // recursively normalize _normalizeSpringBootCfg(obj[k]) // delete the original property from object if it was rewritten if (prop.length) delete obj[k] }) return obj }