import chokidar from 'chokidar'
import fs from 'fs'
import micromatch from 'micromatch'
import normalizePath from 'normalize-path'
import path from 'path'
import { readFileWithRetries } from './utils.js'
export function createWatcher(args, { state, rebuild }) {
let shouldPoll = args['--poll']
let shouldCoalesceWriteEvents = shouldPoll || process.platform === 'win32'
let pollInterval = 10
let watcher = chokidar.watch([], {
atomic: true,
usePolling: shouldPoll,
interval: shouldPoll ? pollInterval : undefined,
ignoreInitial: true,
awaitWriteFinish: shouldCoalesceWriteEvents
? {
stabilityThreshold: 50,
pollInterval: pollInterval,
}
: false,
})
let chain = Promise.resolve()
let changedContent = []
let pendingRebuilds = new Set()
let _timer
let _reject
async function rebuildAndContinue() {
let changes = changedContent.splice(0)
if (changes.length === 0) {
return Promise.resolve()
}
changes.forEach((change) => pendingRebuilds.delete(change.file))
return rebuild(changes).then(
() => {},
(e) => {
console.error(e.toString())
}
)
}
function recordChangedFile(file, content = null, skipPendingCheck = false) {
file = path.resolve(file)
if (pendingRebuilds.has(file) && !skipPendingCheck) {
return Promise.resolve()
}
pendingRebuilds.add(file)
changedContent.push({
file,
content: content ?? (() => fs.promises.readFile(file, 'utf8')),
extension: path.extname(file).slice(1),
})
if (_timer) {
clearTimeout(_timer)
_reject()
}
chain = chain.then(
() =>
new Promise((resolve, reject) => {
_timer = setTimeout(resolve, 10)
_reject = reject
})
)
chain = chain.then(rebuildAndContinue, rebuildAndContinue)
return chain
}
watcher.on('change', (file) => recordChangedFile(file))
watcher.on('add', (file) => recordChangedFile(file))
watcher.on('unlink', (file) => {
file = normalizePath(file)
if (!micromatch.some([file], state.contentPatterns.dynamic)) {
watcher.add(file)
}
})
watcher.on('raw', (evt, filePath, meta) => {
if (evt !== 'rename' || filePath === null) {
return
}
let watchedPath = meta.watchedPath
filePath = watchedPath.endsWith(filePath) ? watchedPath : path.join(watchedPath, filePath)
if (!micromatch.some([filePath], state.contentPatterns.all)) {
return
}
if (pendingRebuilds.has(filePath)) {
return
}
pendingRebuilds.add(filePath)
async function enqueue() {
try {
let content = await readFileWithRetries(path.resolve(filePath))
if (content === undefined) {
return
}
await recordChangedFile(filePath, () => content, true)
} catch {
}
}
enqueue().then(() => {
pendingRebuilds.delete(filePath)
})
})
return {
fswatcher: watcher,
refreshWatchedFiles() {
watcher.add(Array.from(state.contextDependencies))
watcher.add(Array.from(state.configBag.dependencies))
watcher.add(state.contentPatterns.all)
},
}
}