UNPKG

@sap/cds-dk

Version:

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

175 lines (158 loc) 5.84 kB
const fs = require('node:fs') const { resolve, sep } = require('node:path') const { EventEmitter } = require('node:events') module.exports = function watch(paths, { filter, ignore, hotReload } = {}) { const ee = new EventEmitter() const roots = [...new Set((Array.isArray(paths) ? paths : [paths]).filter(Boolean).map(p => resolve(p)))] const ac = new AbortController() // eslint-disable-line no-undef let closed = false const _allow = p => !filter || (filter instanceof RegExp ? filter.test(p) : filter(p)) const dataFiles = hotReload && [ new RegExp(`test.*data.*\\.(csv|json)`), new RegExp(`db.*data.*\\.(csv|json)`) ] const _hotReload = !hotReload ? ()=>false : fullPath => { if (!hotReload || !_allow(fullPath)) return false; // Hot reload all CDS files, services are if (fullPath.endsWith('.cds')) return true; // .properties files are used for i18n -> hot reload i18n if (fullPath.endsWith('.properties') && fullPath.includes('i18n')) return true; // .json & .csv files used in data folders trigger a hot reload, e.g. delete and insert for the table if (dataFiles.some(regex => regex.test(fullPath))) { return true; } return false; } const reloadRelevantJSONfiles = hotReload ? [ new RegExp(`\\.cdsrc\\.json`), new RegExp(`\\package\\.json`), new RegExp(`\\.cdsrc-private\\.json`), new RegExp(`default-env\\.json`), ] : [] /** * Helper function to in case of hot reload restrict reloads based on .json files to just * .cdsrc.json, cdsrc-private.json and root package.json */ const _narrowJSONreloads = !hotReload ? ()=> true : fullPath => { if (!hotReload) return true; if (reloadRelevantJSONfiles.some(regex => regex.test(fullPath))) { return true; } return false; } const useNativeRecursive = process.platform === 'darwin' || process.platform === 'win32' if (useNativeRecursive) { const _watch = async root => { try { if (!fs.existsSync(root)) return const watcher = fs.promises.watch(root, { recursive: true, signal: ac.signal }) for await (const event of watcher) { if (closed) break const name = event.filename?.toString?.() ?? event.filename ?? '' const fullPath = resolve(root, name) if (_hotReload(fullPath)) { ee.emit('refreshModel', fs.existsSync(fullPath) ? 'update' : 'remove', fullPath) } else if (_allow(fullPath) && _narrowJSONreloads(fullPath)) { ee.emit('change', fs.existsSync(fullPath) ? 'update' : 'remove', fullPath) } } } catch (e) { if (e.name !== 'AbortError') ee.emit('error', e) } } roots.forEach(r => _watch(r).catch(e => ee.emit('error', e))) ee.close = () => { if (closed) return closed = true ac.abort() ee.emit('close') } return ee } else { return _linuxCompat() } // On Linux, recursive watchers allocates OS watchers for the whole tree, // so we avoid it to skip ignored dirs before allocating watchers. function _linuxCompat() { const watching = new Set() const queued = new Set() const queue = [] const active = new Set() const max = 32 const _ignored = p => { if (!ignore) return false const test = ignore instanceof RegExp ? s => ignore.test(s) : ignore return test(p) || test(p + sep) // allow trailing sep } const _enqueue = d => { if (closed) return const dir = resolve(d) if (_ignored(dir)) return if (watching.has(dir) || queued.has(dir)) return queued.add(dir) queue.push(dir) _pump() } const _startWatch = dir => { if (watching.has(dir)) return watching.add(dir) ;(async () => { try { if (!fs.existsSync(dir)) return const watcher = fs.promises.watch(dir, { signal: ac.signal }) // non-recursive for await (const event of watcher) { if (closed) break const name = event.filename?.toString?.() ?? event.filename ?? '' const fullPath = name ? resolve(dir, name) : dir // if a new dir appears, start watching it (unless ignored) if (name && !_ignored(fullPath) && fs.existsSync(fullPath)) { try { if (fs.lstatSync(fullPath).isDirectory()) _enqueue(fullPath) } catch { /* ignore */ } } if (_hotReload(fullPath)) { ee.emit('refreshModel', fs.existsSync(fullPath) ? 'update' : 'remove', fullPath) } else if (_allow(fullPath) && _narrowJSONreloads(fullPath)) { ee.emit('change', fs.existsSync(fullPath) ? 'update' : 'remove', fullPath) } } } catch (e) { if (e.name !== 'AbortError' && !closed) ee.emit('error', e) } })().catch(e => ee.emit('error', e)) } const _scan = async dir => { _startWatch(dir) let entries = [] try { entries = await fs.promises.readdir(dir, { withFileTypes: true }) } catch { return } for (const e of entries) { if (e.isSymbolicLink?.()) continue if (!e.isDirectory?.()) continue const child = resolve(dir, e.name) if (!_ignored(child)) _enqueue(child) } } const _pump = () => { while (!closed && queue.length && active.size < max) { const dir = queue.shift() queued.delete(dir) const p = _scan(dir) .catch(e => ee.emit('error', e)) .finally(() => { active.delete(p) _pump() }) active.add(p) } } roots.forEach(_enqueue) ee.close = () => { if (closed) return closed = true ac.abort() ee.emit('close') } return ee } }