UNPKG

@sap/cds-dk

Version:

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

265 lines (213 loc) 9.36 kB
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 } }