UNPKG

@sap/cds-dk

Version:

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

249 lines (203 loc) 9.2 kB
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 } }