UNPKG

@sap/cds-dk

Version:

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

260 lines (219 loc) 9.13 kB
module.exports = exports = async function cds_version (pattern, options) { console.log() const o = canonic_options4 (pattern, options) const all = await Promise.all([ node_versions(o), java_versions(o), ]) print_versions (all.flat(),o) if (!o.json) { if (o.info) console.log ('\n','> Project:', project_info (o)) if (o._elsp) console.log ('\n', `${RED}ELSPROBLEMS${RESET} reported by ${BOLD}npm ls${RESET}. Run ${BOLD}npm install${RESET} to fix dependency problems, then rerun ${BOLD}cds version${RESET}.`) } console.log() } exports.options = [ '--depth', '--omit' ] exports.flags = [ '--info', '--markdown', '--abs', '--all', '--json' ] exports.shortcuts = [ '-d', '-o', '-i', '-m', '-a' ] exports.help = ` # SYNOPSIS *cds version* <options> *cds -v* <option> Uses _npm ls_ to fetch the versions of installed CAP packages, and prints them in a tabular layout. # OPTIONS *-d | --depth* <n> Specifies the depth of the dependency tree to traverse. Passed to _npm ls --depth_. *-o | --omit* <dev/optional/peer> Omits certain types of dependencies when traversing the dependency tree. Passed to _npm ls --omit_. *-m | --markdown* Prints version information in a tabular markdown format, which you can embed into your bug reports. *-i | --info* Same as *--markdown*, plus prints project name and repository URL at the end as a markdown link. ` const DEBUG = process.env.DEBUG ? (...args) => console.debug('[cds.version] -',...args) : undefined const child_process = require ('node:child_process') const $ = (cmd,..._) => { if (cmd.raw) cmd = String.raw (cmd,..._) // the cmd may be a tagged template DEBUG?.(cmd) return new Promise ((resolve, reject) => child_process.exec (cmd, (e,stdout) => e ? reject(e) : resolve (stdout.trim().split('\n')) )) } // Like $, but tolerates npm ls ELSPROBLEMS const $npm_ls = (cmd,..._) => { if (cmd.raw) cmd = String.raw (cmd,..._) // the cmd may be a tagged template DEBUG?.(cmd) return new Promise ((resolve, reject) => child_process.exec (cmd, (e, stdout, stderr) => { const out = (stdout || '').trim() const err = (stderr || '').trim() if (err) DEBUG?.('npm ls stderr:\n' + err) const isELSPROBLEMS = !!(e && /\bELSPROBLEMS\b/.test(err)) if (e && !isELSPROBLEMS) { const code = e.code ?? e.signal ?? 'UNKNOWN' const msg = `Failed to run ${BOLD}npm ls${RESET} to collect Node.js package versions.\n` + `\n` + ` Command: ${cmd}\n` + ` Exit: ${code}\n` + `\n` + `Re-run with DEBUG=1 to see npm output.\n` + `Run ${BOLD}npm install${RESET}, then rerun ${BOLD}cds version${RESET}.` const nice = new Error(msg, {cause: e}) return reject(nice) } resolve({ lines: out ? out.split('\n') : [], elsp: isELSPROBLEMS }) })) } const cds = require('../lib') const home = require('os').homedir() const cap_packages = /@sap\/[\w-]*\bcds\b|@cap-js\// const { GREEN, YELLOW, RED, DIMMED, BOLD, RESET } = cds.utils.colors const { join, dirname } = require ('node:path') function canonic_options4 ([pattern], options) { const o = { root: cds.root, depth: options.all ? 11 : 1, java: cds.env['project-nature'] === 'java', ...options, } if (pattern) o.pattern = pattern if (o.info) o.markdown ??= true DEBUG?.('options:',o) return o } async function node_versions(o) { // Call npm root and npm ls in parallel... const [ [npm_root_g],[npm_root_l], { lines, elsp } ] = await Promise.all ([ $`npm root --global`, $`npm root --local`, $npm_ls`npm ls -lp ${ (o.omit ? o.omit.split(',').map(o => `--omit=${o} `).join('') : '') + (o.depth ? `--depth=${o.depth}` : '--depth=1') }` ]) // Parse information returned by npm ls -lp // Example macOS: /Users/me/projects/bookshop/node_modules/@sap/cds-dk:@sap/cds-dk@1.0.0:/Users/me/projects/bookshop/cds-dk // Example Windows: C:\Users\me\projects\bookshop\node_modules\@sap\cds-dk:@sap/cds-dk@1.0.0:C:\Users\me\projects\bookshop\cds-dk const _path = /(?:\\\\\?\\)?[A-Za-z]:[^:]+|[^:]+/.source const _line = new RegExp(`^(${_path}):(@?.+)@([^:]+):?(.*)?$`) const _pkg = o.pattern ? RegExp(o.pattern) : cap_packages const deps = lines.map (line => { let [, path, name, version, location = path ] = _line.exec(line) || [] if (!location || (location.includes('EXTRANEOUS') && !o.java)) return if (name.match(_pkg)) return [ name, version, location ] }) .filter (x => x) // Sort SAP packages and CAP packages on top const entries = deps.sort (function sap_then_cap_on_top ([a],[b]) { let a_is_cdk = a === '@sap/cds-dk' let b_is_cdk = b === '@sap/cds-dk' if (a_is_cdk && !b_is_cdk) return -1 if (b_is_cdk && !a_is_cdk) return 1 let a_is_sap = a.startsWith('@sap/cds') let b_is_sap = b.startsWith('@sap/cds') if (a_is_sap && !b_is_sap) return -1 if (b_is_sap && !a_is_sap) return 1 let a_is_cap = a.startsWith('@cap-js') let b_is_cap = b.startsWith('@cap-js') if (a_is_cap && !b_is_cap) return -1 if (b_is_cap && !a_is_cap) return 1 return a < b ? -1 : a > b ? 1 : 0 }) // Put global cds-dk at the top try { const loc = require.resolve ('@sap/cds-dk/package.json', { paths: [npm_root_g] }) const gdk = require (loc) entries.unshift ( [ '@sap/cds-dk (global)', gdk.version, dirname(loc) ] ) } catch (e) { if (e.code !== 'MODULE_NOT_FOUND') throw e } // Add some environment info at the end entries.push ( [ 'cds.home', null, cds.home ], [ 'cds.root', null, o.root ], [ 'npm root -l', null, npm_root_l ], [ 'npm root -g', null, npm_root_g ], [ 'Node.js', process.version.slice(1), process.execPath ], ) if (elsp) o._elsp = true return entries } function java_versions (o) { const javaInfo = !cds.env.cli?.version?.skipjava && o.java if (!javaInfo) return [] else if (process.stderr.isTTY && !o.json) console.warn (DIMMED, 'This takes a while. Collecting Java versions...', RESET) const { MAVEN_ARCHETYPE_VERSION:mav } = require ('../lib/init/constants') const cmd = `mvn com.sap.cds:cds-maven-plugin:${mav}:version -B -ntp -Dcds.version.excludeCds -Dcds.version.json` try { const out = child_process.spawnSync (cmd, { cwd: o.root, shell: true }).stdout.toString().trim() const [,match] = out.match (/===\n(.*)\n===/ms) || [] if (match) return Object.entries(JSON.parse(match)).map(([name,v]) => { let versions = Object.entries(v).map(([k,v]) => (k === 'version' || k === 'name') ? v : `${k}:${v}`).join(', ') return [ name, versions ] }) } catch { /* ignored */ } return [[ 'CAP Java SDK', '-- missing --' ]] } function project_info (o) { try { const { name, repository } = require (join (o.root,'package.json')) const repo = repository?.url || repository || '' return `[${GREEN}${name}${RESET}](${DIMMED}${repo}${RESET})` } catch { /* ignored */ } } function print_versions (rows, o, cwd = o.root+'/') { if (o.json) { const json = rows?.reduce((acc, [name, version, location]) => { acc[name] = {} if (version) acc[name].version = version if (location) acc[name].location = location return acc }, {}) return console.log(JSON.stringify(json||{}, null, 2)) } if (!rows || rows.length === 0) return console.error (YELLOW, ' No CAP packages found.', RESET) // cds10: remove and offer --json option instead if (process.env.WS_BASE_URL || o.java || findPom(cwd)) { console.log(rows.map(([name,version,location]) => `${GREEN}${name === 'cds.home'?'home':name}:${RESET} ${version??location}`).join('\n')) return } // compute column widths for tabular printing const local = o.abs ? p => p : p => !p ? '' : p.startsWith(cwd) ? p.replace(cwd,'./') : p.replace(home,'~') const header = o.markdown && [ 'Package', 'Version', 'Location' ] const all = Object.values(rows); if (header) all.push (header) const w1 = Math.max (...all.map(s => s[0]?.length || 0)) const w2 = Math.max (...all.map(s => s[1]?.length || 0)) const w3 = Math.max (...all.map(s => (s[2] = local(s[2]))?.length || 0)) // prepare line printer function for column-aligned output const I = header ? '|' : '' const print = ([ name, version, location ], [c1,c2,c3]) => console.log( RESET,I+c1, (name||'').padEnd(w1), RESET+I+c2, (version||'').padEnd(w2), RESET+I+c3, (location||'').padEnd(w3), RESET+I ) // print table header if (header) { const uncolored = ['','',''] print (header, uncolored) print ([w1,w2,w3].map(n => '-'.repeat(n)), uncolored) } // print table rows const colored = [ GREEN, YELLOW, DIMMED ] for (let each of rows) print (each, colored) } const { existsSync } = require('fs') function findPom (dir) { if (!dir || dir === '/' || dir === '.') return false if (existsSync(join(dir,'pom.xml'))) return true const parent = dirname(dir) if (parent === dir) return false // reached filesystem root return findPom(parent) }