@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
388 lines (304 loc) • 15 kB
JavaScript
module.exports = exports = Object.assign ( serve, {
options: [
'--service', '--from', '--to', '--at', '--with',
'--port', '--workers',
],
flags: [
'--project', '--projects',
'--in-memory', '--in-memory?',
'--mocked', '--with-mocks', '--with-bindings', '--resolve-bindings',
'--watch',
],
shortcuts: [ '-s', undefined, '-2', '-a', '-w', undefined, undefined, '-p' ],
help: `
# SYNOPSIS
*cds serve* [ <filenames> ] [ <options> ]
*cds serve* [ <service> ] [ <options> ]
Starts http servers that load service definitions from cds models and
construct service providers, mounted to respective endpoints to serve
incoming requests.
If the given argument refers to existing files, an effective model
is loaded from these files and *all services*, that are served.
The default is '*', which loads all models from the project.
If the given argument doesn't match an existing file, it's used
as the name of the *single service* to serve.
# OPTIONS
*-s | --service* <name> (default: 'all')
Serve a _single service_ from specified model(s).
EXAMPLE: *cds serve --service CatalogService*
*-f | --from* <model> (default: '*')
Load service definitions from specified folder(s).
EXAMPLE: *cds serve --from srv*
*-w | --with* <impl>
Define which implementation to use (i.e. a _.js_ file).
EXAMPLE: *cds serve --service CatalogService --with srv/cat-service.js*
*-a | --at* <endpoint>
Add endpoint to bind the service to.
EXAMPLE: *cds serve --at localhost:3030*
*-2 | --to* <protocol>
Decide on the protocol (i.e. _fiori_, _odata_, or _rest_) to serve.
EXAMPLE: *cds serve --to odata*
*-p | --project* [<project>]
Runs _cds serve all_ for the specified project; default: cwd.
You can use *cds run* as shortcut.
*--port* <number>
Specify the port on which the launched server shall listen.
If you specify '0', the server picks a random free port.
Alternatively, specify the port using env variable _PORT_.
*--watch* [<project>]
Like *--project* but starts through _nodemon_ to restart the server
upon changes in code or models.
You can use *cds watch* as shortcut, which is equivalent to:
*cds serve --with-mocks --in-memory? --watch --project ...*
*--workers* <number> | true
Spawns <number> of worker processes in a cluster to handle incoming requests.
If set to 'true', the number of workers is determined by the number of CPUs.
If omitted, the server runs in non-cluster mode as a single process.
*--mocked*
Use this option to launch a _single service_ in a mock server, for
a model you imported from an external source, like an S/4 system,.
In addition to constructing the service provider, this will bootstrap
a transient _in-memory_ database, filled with tables corresponding
to the signatures of the service's exposed entities.
*--with-mocks*
Use this in combination with the variants serving _multiple services_.
It starts in-process mock services for all required services configured
in _package.json#cds.requires_, which don't have external bindings
in the current process environment.
Note that by default, this feature is disabled in production and must be
enabled with configuration 'features.mocked_bindings=true'.
*--with-bindings*
Use this option in local tests, to have all services provided by a
process registered with their physical urls in a temporary file.
All required services are bound automatically upon bootstrapping.
Option *--with-mocks* subsumes this option.
*--resolve-bindings* (beta)
Resolve remote service bindings configured via *cds bind*.
*--in-memory[?]*
Automatically adds a transient in-memory database bootstrapped on
each (re-)start in the same way *cds deploy* would do, based on defaults
or configuration in _package.json#cds.requires.db_. Add a question
mark to apply a more defensive variant which respects the configured
database, if any, and only adds an in-memory database if no
persistent one is configured.
Requires an SQLite driver to be installed. For example: _npm i @cap-js/sqlite_.
# EXAMPLES
*cds serve*
*cds serve* all
*cds serve* CatalogService *--from* app/
*cds serve* CatalogService *--from* srv/ *--at* /cats *--to* rest
*cds serve* all --watch --with-mocks --in-memory?
*cds run* some/project
*cds watch* some/project
*cds watch*
`})
const cds = require('../lib'), { exists, isfile, local, redacted, path } = cds.utils
const COLORS = process.stdout.isTTY && !process.env.NO_COLOR || process.env.FORCE_COLOR
/* eslint-disable no-console */
// provisional loggers, see _prepare_logging
let log = console.log
/**
* The main function which dispatches into the respective usage variants.
* @param {string[]} all - project folder, model filenames, or service name
*/
async function serve (all=[], o={}) {
// canonicalize options to ease subsequent tasks...
cds.options = o
const [pms] = all // project folder, model filenames, or service name
if (o.from) o.from = o.from.split(',')
if (o.project||o.projects) { o.project = pms; o.service='all'; o.from='*' }
else if (o.service) { o.from = pms ? pms.split(',') : '*'}
else if (o.from) { o.service = pms }
else if (exists(pms)) { o.service ='all', o.from = all }
else { o.service = pms||'all', o.from = '*' }
// IMPORTANT: never load any @sap/cds modules before the chdir above happened!
// handle --watch and --project
if (o.watch) return _watch.call(this, o.project,o) // cds serve --watch <project>
if (o.project) _chdir_to (o.project) // cds run --project <project>
// let plugins know about the CLI
cds.cli = { command: 'serve', argv: all, options: o }
// Ensure loading plugins before calling cds.env!
await cds.plugins
const TRACE = cds.debug('trace')
TRACE?.time('total startup time'.padEnd(22))
if (TRACE) {
TRACE?.time('require express'.padEnd(22))
require('express')
TRACE?.timeEnd('require express'.padEnd(22))
}
// Load local server.js early in order to allow setting custom cds.log.Loggers
const cds_server = await _local_server_js() || cds.server
if (!o.silent) _prepare_logging ()
// The following things are meant for dev mode, which can be overruled by feature flags...
const {features} = cds.env
{
// handle --with-mocks resp. --mocked
if (features.with_mocks) o.mocked = _with_mocks(o)
// handle --in-memory resp. --in-memory?
if (features.in_memory_db) o.in_memory = _in_memory(o)
// load service bindings when mocking or asked to
if (features.mocked_bindings && o.mocked || o['with-bindings']) await cds.service.bindings
// live reload, in cooperation with cds watch
if (features.live_reload) require('../app/etc/livereload')
}
// bootstrap server from project-local server.js or from @sap/cds/server.js
const server = await cds_server(o)
// increase keep-alive timeout for CF (gorouter wants >90s)
if (process.env.CF_INSTANCE_GUID) server.keepAliveTimeout = 91 * 1000
// return a promise which resolves to the created http server when listening
return cds.server.listening = new Promise ((_resolve,_reject) => {
server.listening ? _started(server) : server.once('listening',_started)
server.on ('error',_reject) // startup errors like EADDRINUSE
// server.on ('close', _shutdown) // IMPORTANT: Don't do that as that would be a very strange loop
// process.on ('exit', _shutdown) // IMPORTANT: Don't do that as that would be a very strange loop
async function _started() {
_assert_no_multi_installs()
const url = cds.server.url = `http://localhost:${server.address().port}`
cds.emit ('listening', {server,url}) //> inform local listeners
_resolve ({ server, url })
}
const LOG = cds.log()
cds.shutdown = _shutdown //> for programmatic invocation
if (cds.env.server.shutdown_on_uncaught_errors && !cds.repl) {
process.on('unhandledRejection', _shutdown) //> using std logger to have it labelled with [cds] - instead of [cli] -
process.on('uncaughtException', _shutdown) //> using std logger to have it labelled with [cds] - instead of [cli] -
}
process.on('SIGINT', cds.watched ? _shutdown : (s,n)=>_shutdown(s,n,console.log())) //> newline after ^C
process.on('SIGHUP', _shutdown)
process.on('SIGHUP2', _shutdown)
process.on('SIGTERM', _shutdown)
async function _shutdown (signal,n) {
if (signal && n) LOG.debug ('⚡️',signal,n, 'received by cds serve')
let err = typeof signal === 'object' ? signal : null
if (err) {
LOG.error('❗️Uncaught', err)
LOG.error('❗️server shutdown ...❗️')
}
await Promise.all(cds.listeners('shutdown').map(fn => fn(err)))
server.close(()=>{/* it's ok if closed already */}) // first, we try stopping server and process the nice way
let { force_exit_timeout: force_exit } = cds.env.server // after ~1 sec, we force-exit it, unless in test mode
if (force_exit && !global.it) setTimeout(process.exit,force_exit).unref()
}
if (LOG._debug) {
cds.on('shutdown', () => LOG.debug ('⚡️','cds serve - cds.shutdown'))
server.on('close', () => LOG.debug ('⚡️','cds serve - server.close(d)'))
process.on('exit', () => LOG.debug ('⚡️','cds serve - process.exit'))
process.on('beforeExit', ()=> LOG.debug ('⚡️','cds serve - process.beforeExit'))
}
if (process.platform === 'win32') {
process.on('message', msg => msg.close && _shutdown()) // by `cds watch` on Windows
}
TRACE?.timeEnd('total startup time'.padEnd(22))
return server
})
}
async function _local_server_js() {
const _local = file => isfile(file) || isfile (path.join(cds.env.folders.srv,file))
let server_js = process.env.CDS_TYPESCRIPT && _local('server.ts') || _local('server.js')
if (server_js) {
console.log ('[cds] - loading server from', { file: local(server_js) })
let fn = await cds.utils._import(server_js)
if (fn && fn.default) fn = fn.default // default ESM export
return typeof fn === 'function' ? fn : cds.server
}
}
function _prepare_logging () { // NOSONAR
const LOG = cds.log('cds.serve|server',{label:'cds'}); if (!LOG._info) return; else log = LOG.info
// print information when model is loaded
cds.on ('loaded', ({$sources:srcs})=>{
LOG.info (`loaded model from ${srcs.length} file(s):\n${COLORS ? '\x1b[2m' : ''}`)
const limit = 30, shown = srcs.length === limit + 1 ? limit + 1 : limit // REVISIT: configurable limit?
for (let each of srcs.slice(0, shown)) console.log (' ', local(each))
if (srcs.length > shown) {
if (LOG._debug) for (let each of srcs.slice(shown)) console.log (' ', local(each))
else console.log (` ...${srcs.length-shown} more. Run with DEBUG=serve to show all files.`)
}
COLORS && console.log ('\x1b[0m')
})
// print information about each connected service
cds.on ('connect', ({name,kind,options:{use,credentials,impl}})=>{
LOG.info (`connect to ${name} > ${use||kind||impl}`, credentials ? redacted(credentials) : '')
})
// print information about each provided service
cds.on ('serving', (srv) => {
const details = {}
if (srv._source && !srv._source.startsWith('@sap'))
details.impl = local(srv._source)
if (srv.endpoints.length === 1)
details.path = srv.endpoints[0].path // for brevity, omit 'kind' if there is only one protocol
else if (srv.endpoints.length > 1)
details.endpoints = srv.endpoints // full endpoint details if more than one
LOG.info (`${srv.mocked ? 'mocking' : 'serving'} ${srv.name}`, details)
})
// print info when we are finally on air
cds.once ('listening', ({url})=>{
console.log()
LOG.info ('server listening on',{url})
LOG.info ('server', 'v'+cds.version, 'launched in', performance.now().toFixed(0),'ms')
if (process.stdin.isTTY) LOG.info (`[ terminate with ^C ]\n`)
})
}
/** handles --watch option */
function _watch (project,o) {
o.args = process.argv.slice(2) .filter (a => a !== '--watch' && a !== '-w')
return this.load('watch')([project],o)
}
/** handles --project option */
function _chdir_to (project) {
// try using the given project as dirname, e.g. './bookshop'
const dir = cds.utils.isdir (project)
if (dir) return cds.root = dir
// try using the given project as a node package name, e.g. '@capire/bookshop'
try { cds.root = path.dirname (require.resolve(project+'/package.json')) }
// both failed
catch { cds.error `No such folder or package: '${process.cwd()}' -> '${project}'` }
}
/** handles --in-memory[?] option */
function _in_memory (o) {
const {env} = cds, db = env.requires.db, sqlite = env.requires.kinds.sqlite
try {
if (o['in-memory'] || (o['in-memory?'] && !db && require.resolve(sqlite.impl, {paths: [cds.root, cds.home]}))) {
env.add ({ requires: { db: {
kind:'sqlite', ...sqlite,
credentials: db?.credentials?.url ? {url:':memory:'} : {database:':memory:'}
}}})
return true
}
} catch (e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }
if (db && db.credentials && (db.credentials.database || db.credentials.url) === ':memory:') {
return true
}
}
/** handles --with-mocks option */
function _with_mocks (o) {
if (o.mocked || (o.mocked = o['with-mocks'])) {
cds.on ('loaded', model => cds.deploy.include_external_entities_in(model))
const mocks = cds.env.features.test_mocks && isfile ('test/mocked.js')
if (mocks) cds.once ('served', ()=> {
log ('adding mock behaviours from', { file: local(mocks) })
require(mocks)
})
return true
}
}
const _assert_no_multi_installs = ()=> { if (global.__cds_loaded_from?.size > 1) {
console.error(`
-----------------------------------------------------------------------
ERROR: Package '@sap/cds' was loaded from different installations:`,
[ ...global.__cds_loaded_from ],
`\nEnsure a single install to avoid hard-to-resolve errors.
-----------------------------------------------------------------------
`);
if (cds.env.server.exit_on_multi_install) process.exit(1)
}}
exports.exec = function cds_serve (...argv) {
try {
const [ args, options ] = require('./args') (serve, argv)
return serve (args, options)
} catch (e) {
console.error(e) // eslint-disable-line no-console
process.exitCode = 1
}
}
if (!module.parent) exports.exec (...process.argv.slice(2))