@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
322 lines (264 loc) • 10.6 kB
JavaScript
module.exports = Object.assign (repl, {
options: [ '--run', '--use', '--ql' ],
shortcuts: [ '-r', '-u' ],
help: `
# SYNOPSIS
*cds repl* [ <options> ]
Launches into a read-eval-print-loop, an interactive playground to
experiment with cds' JavaScript APIs. See documentation of Node.js'
REPL for details at _http://nodejs.org/api/repl.html_
# OPTIONS
*-r | --run* <project>
Runs a cds server from a given CAP project folder, or module name.
You can then access the entities and services of the running server.
It's the same as using the repl's builtin _.run_ command.
*-u | --use* <cds feature>
Loads the given cds feature into the repl's global context. For example,
if you specify _xl_ it makes the _cds.xl_ module's methods available.
It's the same as doing _{ref,val,xpr,...} = cds.xl_ within the repl.
*--ql*
Starts the repl in cql evaluation mode.
It's the same as using the _.ql_ command within the repl.
# EXAMPLES
*cds repl* --run bookshop
*cds repl* --run .
*cds repl* --use ql
# SEE ALSO
*cds eval* to evaluate and execute JavaScript.
`})
async function repl ([project], options={}) {
const isTerminal = Boolean(process.stdin.isTTY && process.stdout.isTTY);
const cds = require('../lib/cds')
const { GREEN, PINK, BLUE, CYAN, YELLOW, GRAY, BOLD, RESET } = cds.utils.colors
console.log(`${GREEN}${BOLD}Welcome to cds repl v${cds.version}${RESET}`)
const { inspect } = require('util')
inspect.defaultOptions.colors = true
inspect.defaultOptions.depth = 11
Object.defineProperty (cds,'repl',{value:true})
let context={}, repl = { context, displayPrompt(){} }
if ((options.run ??= project)) await _run (options.run)
if (options.use) _export (...options.use.split(','))
if (options.ql) _ql()
else _js()
process.on('uncaughtException', console.error)
process.on('unhandledRejection', console.error)
// Ugly hack to prevent "[ERR_INVALID_REPL_INPUT]: Listeners for `uncaughtException` cannot be used in the REPL" errors caused by winston when connecting to remote services
process.on = (event, listener) => event !== 'uncaughtException' && event !== 'unhandledRejection' && process.addListener(event, listener)
function _js() {
repl = require('repl').start ({
...options,
ignoreUndefined: true,
useGlobal: true,
terminal: isTerminal,
writer: o => {
const options = {}
if (!o || typeof o !== 'object') return o
if ('init' in o && 'on' in o && 'run' in o) options.depth = 0
return inspect(o,options).replace(/\[Object: null prototype\] /g, '')
}
})
_initHistory(repl, '.cds-repl-history')
repl.on('exit', () => options.context = repl.context)
repl.on('exit', () => { if (!transition.started) cds.shutdown() })
if (Object.keys(context).length) Object.assign (repl.context, context)
if (!Object.hasOwn(repl.context, 'Foo')) cds.extend (repl.context) .with ( class {
get Foo() { return ctx.Foo ??= new cds.entity({ name: 'Foo' }) }; set Foo(v) { ctx.Foo = v }
get Bar() { return ctx.Bar ??= new cds.entity({ name: 'Bar' }) }; set Bar(v) { ctx.Bar = v }
get expect() { return ctx.expect ??= cds.test.expect }; set expect(v) { ctx.expect = v }
} .prototype )
const ctx={}
repl.defineCommand ('run', { action: _run,
help: 'Runs a cds server from a given CAP project folder, or module name like @capire/bookshop.',
})
repl.defineCommand ('inspect', { action: _inspect,
help: 'Sets options for util.inspect, e.g. `.inspect .depth=1`.',
})
repl.defineCommand ('ql', { action: transition(_ql),
help: 'Switch to cql repl mode, evaluating cql queries'
})
repl.displayPrompt()
}
async function _run (project) {
await require('../bin/serve') ([project], { project, port: 0,
'with-bindings': true,
'with-mocks': true,
'in-memory?': true,
})
_export ('entities', 'services')
}
function _export (...cds_features) {
console.log()
console.log ('------------------------------------------------------------------------')
console.log ('Following variables are made available in your repl\'s global context:')
console.log()
for (let each of cds_features) {
let module = cds[each]
console.log (`from cds.${each}: {`)
for (let p in module) {
if (p in global || p.startsWith('_') || p.includes('.')) continue
console.log (` ${GREEN}${p}${RESET},`)
repl.context[p] = module [p]
}
console.log (`}`)
console.log()
}
console.log('Simply type e.g.', GREEN+ Object.keys(repl.context).at(-1) +RESET, 'in the prompt to use the respective objects.')
console.log()
repl.displayPrompt()
}
async function _inspect (_args) {
const args = _args.split(' '), subjects={}, options = { depth:0 }, defaults = inspect.defaultOptions
for (let each of args) {
// args of shape .<option>=<value> set inspect options
if (each[0] === '.') each = eval(`options${each}`)
// others are subjects to inpsect with given options
else subjects[each ||= 'defaults'] = each === 'defaults' ? defaults : function _recurse (o,slug) {
const [ head,...tail ] = slug, x = o[head]
if (tail.length && x && typeof x === 'object') return _recurse (x,tail)
else return x
} (repl.context, each.split('.'))
}
// if no subjects were given, just set the options globally
if (!Object.keys(subjects).length) {
Object.assign (defaults, options)
console.log('\n', 'updated node:util.inspect.defaultOptions with:', options)
} else for (let [each,subject] of Object.entries(subjects))
console.log ('\n'+each+':', inspect (subject, options))
console.log()
repl.displayPrompt()
}
function _initHistory(repl, file) {
repl.history = []
const home = process.env.HOME || process.env.USERPROFILE
const history = require('path').join(home,file)
const fs = require('fs')
fs.readFile(history, 'utf-8', (e, txt) => e || Object.assign(repl.history, txt.split('\n')))
repl.on('exit', () => fs.writeFile(history, repl.history.join('\n'), () => {}))
}
function transition(start) {
return function () {
transition.started = true
this.clearBufferedCommand()
this.close()
setImmediate(() => {
start()
transition.started = false
})
}
}
// CQL Mode
function _ql() {
const cqlColors = {
keyword: BLUE,
entity: CYAN,
string: GREEN,
number: YELLOW,
token: PINK,
suggestion: GRAY,
reset: RESET,
}
const highlight = createHighlighter(cds, cqlColors)
const prompt = `cql${PINK}>${RESET} `
repl = require('repl').start ({
prompt,
eval: _cql,
terminal: isTerminal,
completer: (line) => {
if (line.startsWith('.'))
return [Object.keys(repl.commands).map(key => `.${key}`).filter(c => c.startsWith(line)), line]
return [[], line]
},
})
_initHistory(repl, '.cds-repl-ql-history')
repl.on('exit', () => { if (!transition.started) cds.shutdown() })
repl.defineCommand ('target', { action: _target,
help: 'Specify the service to evaluate against'
})
repl.defineCommand ('namespace', { action: _namespace,
help: 'Specify a default namespace'
})
repl.defineCommand ('js', { action: transition(_js),
help: 'Switch to js repl mode, evaluating javascript'
})
// Syntax highlighting for input line
let terminated = false
function refresh() { setTimeout(() => {if (terminated) return; repl._refreshLine(); }, 0) }
repl._writeToOutput = function _writeToOutput(str) {
if (terminated) return
if (!str.startsWith(prompt)) return repl.output.write(str)
repl.output.write(prompt + highlight(str.slice(prompt.length)));
};
process.stdin.on('keypress', refresh);
repl.on('exit', () => {
terminated = true
process.stdin.off('keypress', refresh)
})
repl.displayPrompt()
}
async function _cql(code, context, replResourceName, callback) {
const { service = cds.db, user } = repl.context
if (code.trim() === '') return callback()
cds.context = { user }
const query = cds.ql(code)
const result = await service.run(query)
callback(null, result)
}
function _target(args) {
if (!args) return console.info(
`Specify a service. Available services: ${Object.keys(cds.services).join(', ')}
Examples:
.target db
.target AdminService --roles admin`)
const [s] = args.split(' ')
const service = cds.services[s]
if (!service) throw new Error(`No service with name ${s}. Available services: ${Object.keys(cds.services).join(', ')}`)
repl.context.service = service
const roles = args.match(/(-r|--roles) ([^\s]+)/)?.[2]?.split(',')
repl.context.user = roles? {roles} : undefined
}
function _namespace(ns) {
cds.model.namespace = ns
}
}
function createHighlighter(cds, colors) {
const { compiler } = cds
const keywords = new Set(['as', 'and', 'or', 'not', 'null'])
function category(token) {
const type = token.type?.toLowerCase()
if (['number', 'string'].includes(type)) return type;
let cat = token.parsedAs?.toLowerCase()
.replace('ext','').replace('implicit','')
.replace(/^.*alias/,'alias')
.replace('from', 'entity').replace('artref', 'entity')
.replace('item', 'element')
if (cat === 'keyword' && !keywords.has(token.text?.trim())) return '_keyword'
return cat
}
return input => {
const prefix = 'entity __query as '
const options = { attachTokens: true, messages: [] }
const { tokenStream: { tokens } } = compiler.$lsp.parse(prefix + input, '<query>.cds', options)
const highlighted = []
let lastToken
for (const token of tokens) {
if (lastToken) {
const start = lastToken.start - prefix.length
const stop = token.start - prefix.length
if (start >= 0) {
const value = input.slice(start, stop)
let cat = category(lastToken)
const colored = _color(cat, value)
highlighted.push(colored)
}
}
lastToken = token
}
return highlighted.join('')
}
function _color(type, value) {
const start = colors[type] || ''
const reset = colors.reset || ''
return start ? `${start}${value}${reset}` : value
}
}
if (!module.parent) repl()