UNPKG

@sap/cds-dk

Version:

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

322 lines (264 loc) 10.6 kB
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()