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