@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
265 lines (213 loc) • 9.36 kB
JavaScript
const watchOnlyOptions = ['--ext', '--livereload', '--experimental-hot-reload', '--open', '--debug', '--inspect', '--inspect-brk', '--include', '--exclude']
module.exports = Object.assign ( watch, {
flags: [ '--in-memory', '--in-memory?', '--with-mocks', '--with-mtx' ],
options: [ '--port', ...watchOnlyOptions ],
help: `
# SYNOPSIS
*cds watch* [<project>]
Tells cds to watch for relevant things to come or change in the specified
project or the current work directory. Compiles and (re-)runs the server
on every change detected.
Actually, cds watch is just a convenient shortcut for:
*cds serve all --with-mocks --in-memory?*
# OPTIONS
*--port* <number>
Specify the port on which the launched server listens.
If you specify '0', the server picks a random free port.
Alternatively, specify the port using env variable _PORT_.
*--ext* <extensions>
Specify file extensions to watch for in a comma-separated list.
*Example:* cds w --ext cds,json,js.
*--include* <paths,...>
Comma-separated list of additional paths to watch.
*--exclude* <paths,...>
Comma-separated list of additional paths to ignore.
*--livereload* <port | false>
Specify the port for the livereload server. Defaults to '35729'.
Disable it with value _false_.
*--experimental-hot-reload* <true | false>
Specify if the experimental hot reload should be used. Changes to the CDS model
in most scenarios no longer restart the server
*--open* <url>
Open the given URL (suffix) in the browser after starting.
If none is given, the default application URL will be opened.
*--profile* <profile,...>
Specify from which profile(s) the binding information is taken.
*Example:* cds w --profile hybrid,production
*--with-mtx*
A shortcut for _--profile with-mtx_ to enable mtx features also in
development, while by default, these are only enabled in production.
*--debug* / *--inspect* <host:port | 127.0.0.1:9229>
Activate debugger on the given host:port.
If port 0 is specified, a random available port will be used.
*--inspect-brk* <host:port | 127.0.0.1:9229>
Activate debugger on the given host:port and break at start of user script.
If port 0 is specified, a random available port will be used.
# SEE ALSO
*cds serve --help* for the different start options.
`})
const fs = require ('node:fs')
const cds = require('../lib/cds')
const DEBUG = /\b(y|all|cli|watch)\b/.test (process.env.DEBUG) ? ((..._) => console.debug ('[cli] -', ..._)) : undefined
const {delimiter,relative,resolve,sep} = require ('path')
const {codes} = require('../lib/util/term'), t = codes
const escapedSep = sep === '\\' ? '\\\\' : sep // escape backslash for windows
const ignoreDefaults = [
/\.git/,
/node_modules/,
/_out/,
/@types/,
/@cds-models/,
new RegExp(`app${escapedSep}.+?${escapedSep}webapp`),
new RegExp(`app${escapedSep}.+?${escapedSep}.*\\.json`),
new RegExp(`app${escapedSep}.+?${escapedSep}dist${escapedSep}`),
new RegExp(`app${escapedSep}.+?${escapedSep}target${escapedSep}`),
new RegExp(`app${escapedSep}.+?${escapedSep}tsconfig\\.json$`),
new RegExp(`app${escapedSep}.+?${escapedSep}.*\\.tsbuildinfo$`),
/package-lock\.json$/,
new RegExp(`${escapedSep}gen${escapedSep}`)
]
async function watch ([cwd], {
args = ['serve', 'all'],
ext = 'cds,csn,csv,ts,mjs,cjs,js,json,properties,edmx,xml,env',
include = [],
exclude = [],
...options
}={}) {
const log = (first,...more) => console.log (t.yellow + (first||''), ...more, t.reset)
const paths4 = p => typeof p === 'string' ? p.split(',').map(p => p.trim()) : (p || [])
const included = paths4(include), excluded = paths4(exclude)
const isJava = fs.existsSync('pom.xml') && process.env.CDS_ENV !== 'node'
if (!Object.keys(options).includes('with-mocks')) args.push ('--with-mocks')
if (!Object.keys(options).find(a => /^in-memory\??$/.test(a))) args.push ('--in-memory?')
args
.push (...(process.argv.slice(3).filter(a => !args.includes(a))) // add deduped command line args
.filter(removeArgsIn(watchOnlyOptions)) // remove all watch-only options, as `cds serve` would not accept them
.filter(a => a !== '--watch') // from `cds serve --watch`
.filter(a => a !== cwd))
if (cwd) {
const ocwd = process.env._original_cwd = process.cwd()
if (fs.existsSync(cwd)) cwd = resolve (cwd)
else try { // try module name like `@capire/bookshop`
cwd = require.resolve (cwd+'/package.json', {paths:[ ocwd ]}).slice(0,-13)
} catch { throw `Error: No such folder or package: '${cwd}'` }
log (`cd ${relative(ocwd,cwd)}`)
}
else cwd = cds.root
if (isJava) {
options.livereload = false
options['experimental-hot-reload'] = false
}
if (!isJava) log (`\n${t.bold}cds ${args.join(' ')}`)
const env = Object.assign (process.env, {
_args: JSON.stringify(args),
_options: JSON.stringify(options)
})
if (process.env.CDS_TYPESCRIPT === 'ts-node' || process.env.CDS_TYPESCRIPT === 'true') {
// Needed by ts-node/tsc to find the tsconfig.json. Same as `--project`.
env.TS_NODE_PROJECT ??= cwd
}
// if sap/cds is not installed locally, run with cds-dk's deps. (sap/cds, sqlite etc)
if (!installed('@sap/cds', cwd)) {
env.NODE_PATH = [...paths(cwd), ...paths(__dirname)].join(delimiter)
}
const delay = parseInt(process.env.CDS_WATCH_DELAY) || 200
const escapedRegex = s => s.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
const resolved = p => resolve(cwd, p)
const excludedPaths = excluded.map(resolved).map(escapedRegex)
const ignore = excluded.length
? new RegExp(ignoreDefaults.map(d => d.source).concat(excludedPaths).join('|'))
: new RegExp(ignoreDefaults.map(d => d.source).join('|'))
const watched = [ cwd, ...included.map(resolved) ]
let liveReload //= ...
if (!/false|no/.test(options.livereload)) {
log ('( live reload enabled for browsers )')
liveReload = new (require ('../lib/watch/livereload-server'))
env.CDS_LIVERELOAD_PATH = require.resolve ('../lib/watch/livereload-connect')
env.CDS_LIVERELOAD_URL = await liveReload.start (options['livereload'], cwd, delay)
.catch(err => { console.log(`Error starting live reload: ${err.message}`); liveReload = null })
}
DEBUG && DEBUG('watching:', {
pid: process.pid,
folders: watched,
extensions: ext,
ignoring: [ ...excludedPaths.map(s => new RegExp(s)), ...ignoreDefaults ]
})
if (process.argv.includes('--profile') || process.env.CDS_ENV) {
const bind = require('../lib/bind/shared')
Object.assign(env, await bind.env({cwd}))
}
// if (isJava) {
// const { spawn } = require('node:child_process')
// const path = require('node:path')
// const cwd = path.join(cds.root, cds.env.folders.srv)
// const child = spawn('mvn', ['cds:watch'], { cwd, stdio: 'inherit' })
// child.on('close', (code) => {
// if (code !== 0) console.error(`mvn process exited with code ${code}`)
// })
// return
// }
const nodemon = require('../lib/watch/node-watch')
const watcher = nodemon ({
script: resolve(__dirname,'../lib/watch/watched'), cwd, env, ext, ignore, watched,
delay,
options
}).on('restart', (files)=>{
log (`\n${t.bold} ___________________________\n`)
if (liveReload) liveReload.markPending(files)
}).on('message', async (msg) => { switch (msg.code) { // messages from child process
case 'listening':
env.PORT = msg.address?.port || msg.port
if (options.open) require('../lib/watch/open').openURL (options.open, env.PORT)
else liveReload?.reload()
delete options.open
break
case 'EADDRINUSE': env.PORT = 0; _addr_in_use(msg); break
case 'model-refreshed':
case 'refreshModel':
liveReload?.reload()
break;
case 'restart':
watcher.restart()
break;
default: console.error (msg)
}})
process.on('exit', ()=> log (`${t.bold+t.green}\n[cds] - my watch has ended. ${t.reset}`))
return watcher
}
const _addr_in_use = (msg) => console.error (`
${t.red+t.bold}[EADDRINUSE]${t.reset} - port ${msg.port} is already in use by another server process.
> Press Return to restart with an arbitrary port.
`)
const FileHandlers = {
edmx: [ (...args) => (FileHandlers._import || (FileHandlers._import = require ('./import'))) (...args) ],
changed (files,cwd) {
if (files) for (let each of files) {
const [,ext] = /\.(\w+)$/.exec(each) || []
for (let handle of this[ext] || []) handle (each,{into:cwd})
}
},
}
function removeArgsIn (toRemove) {
let argRemoved = false
return arg => { // filter function
if (toRemove.includes(arg)) {
argRemoved = true
return false
}
else if (argRemoved) { // if next arg is a value, skip it as well
argRemoved = false
if (!arg.startsWith('-')) return false
}
return true
}
}
// builds paths from dir + its parent node_modules
function paths (dir) {
const parts = resolve(dir).split(sep), n = parts.length, nm = sep+'node_modules'
return parts.map ((_,i,a)=> a.slice(0,n-i).join(sep)+nm).filter(p => fs.existsSync(p))
}
function installed(id, ...paths) {
try { return require.resolve (id, { paths }) }
catch(e) { if (e.code !== 'MODULE_NOT_FOUND') throw e }
}