@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
249 lines (203 loc) • 9.2 kB
JavaScript
const watchOnlyOptions = ['--ext', '--livereload', '--open', '--debug', '--inspect', '--inspect-brk', '--include', '--exclude']
module.exports = Object.assign ( watch, {
flags: [ '--in-memory', '--in-memory?', '--with-mocks' ],
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_.
*--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
*--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 cds = require('../lib/cds')
const DEBUG = cds.log('cli|watch')
const {delimiter,dirname,relative,resolve,sep} = require ('path')
const {codes} = require('../lib/util/term'), t = codes
const fs = require ('fs')
const extDefaults = 'cds,csn,csv,ts,mjs,cjs,js,json,properties,edmx,xml,env'
const escapedSep = sep === '\\' ? '\\\\' : sep // escape backslash for windows
const ignoreDefaults = [
/\.git/,
/node_modules/,
/_out/,
/@types/,
/@cds-models/,
new RegExp(`app${escapedSep}.+?${escapedSep}webapp${escapedSep}`),
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 = extDefaults,
include = cds.env.watch?.include ?? [],
exclude = cds.env.watch?.exclude ?? [],
...options
}={}) {
const paths4 = p => typeof p === 'string' ? p.split(',').map(p => p.trim()) : (p || [])
const included = paths4(include), excluded = paths4(exclude)
const nonexisting = included.filter(p => !fs.existsSync(p))
if (nonexisting.length > 0) throw `${nonexisting.length > 1 ? 'Paths do' : 'Path does'} not exist: ${nonexisting.join(', ')}`
if (cwd && !cds.utils.exists(cwd)) try { // try module name like `@capire/bookshop`
cwd = require.resolve(cwd+'/package.json', {paths:[ process.cwd() ]}).slice(0,-13)
} catch (err) {
throw `Error: No such folder or package: '${cwd}'`
}
const _cwd = cwd || process.cwd()
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))
let 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 = 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)
}
if (typeof ext !== 'string') ext = extDefaults
const delay = parseInt(process.env.CDS_WATCH_DELAY) || 200
let liveReload //= ...
let firstTime = true
if (!/false|no/.test(options['livereload']) && cds.env.for('cds', _cwd).livereload !== false) {
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 })
}
const log = (first,...more) => console.log (t.yellow + (first||''), ...more, t.reset)
if (cwd) {
const ocwd = process.env._original_cwd = process.cwd()
if (fs.existsSync(cwd)) log (`cd ${cwd}`)
else try {
const resolved = dirname (require.resolve(cwd+'/package.json', {paths:[ocwd]}))
log (`cd ${relative(ocwd,cwd = resolved)}`)
} catch(_){ throw new Error(`No such folder or package: '${ocwd}' -> '${cwd}'`) }
}
log ()
log (`${t.bold}cds ${args.join(' ')}`)
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) ]
if (DEBUG._debug) {
console.log(`\nwatching (pid ${process.pid})`, {
extensions:ext, roots:watched, ignoring: [...excludedPaths.map(s => new RegExp(s)), ...ignoreDefaults]
})
}
if (liveReload) console.log (`live reload enabled for browsers`)
const { bindingEnv, updateBindings } = require('../lib/bind/shared')
Object.assign(env, await bindingEnv({cwd}))
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)
if (this.delayed) clearTimeout (this.delayed)
this.delayed = setTimeout(()=>{
updateBindings({
onBeforeUpdate: () => watcher.stop(),
onAfterUpdate: ({env}) => {
Object.keys(env).forEach( name => watcher.setEnv(name, env[name]) )
watcher.restart()
}
})
FileHandlers.changed (files,cwd)
}, 111) .unref()
}).on('message', async (msg) => { switch (msg.code) { // messages from child process
case 'listening':
env.PORT = (msg.address && msg.address.port) || msg.port
liveReload && liveReload.reload()
firstTime && options['open'] && require('../lib/watch/open').openURL (options['open'], env.PORT)
firstTime = false
break
case 'EADDRINUSE': env.PORT = 0; _addr_in_use(msg); 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 }
}