Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | 3x 3x 3x 6x 6x 6x 6x 2x 1x 6x 6x 6x 9x 9x 9x 6x 6x 6x 6x 6x 6x 6x 9x 9x 9x 9x 9x 9x 9x 9x 9x 9x | /**
* File Watcher for the dev server.
*
* Wraps chokidar with a small projection that maps absolute file paths
* to HMR-protocol payloads (filePath + webPath + updateType) and a per-
* file debounce so rapid editor saves coalesce into one HMR message.
*
* @module @coherent.js/cli/dev-server/file-watcher
*/
import chokidar from 'chokidar';
import { relative, sep } from 'node:path';
/**
* @typedef {Object} FileChangeEvent
* @property {string} filePath - Absolute filesystem path of the changed file.
* @property {string} webPath - URL-relative path (POSIX separators, leading slash).
* @property {'component'|'style'|'asset'} updateType - Coarse classification used by the HMR client to pick a strategy.
*/
/**
* @typedef {Object} FileWatcherOptions
* @property {string} root - Absolute path to the project root being watched.
* @property {(change: FileChangeEvent) => void} onChange - Called per debounced change.
* @property {(err: Error) => void} [onError] - Called on chokidar errors.
* @property {number} [debounceMs=50] - Coalesce rapid writes to the same file within this window.
* @property {Array<string|RegExp>} [ignored] - Additional ignore patterns (merged with defaults).
*/
/**
* @typedef {Object} FileWatcher
* @property {() => Promise<void>} close - Stop watching and release resources.
*/
const DEFAULT_IGNORES = [
/(^|[/\\])\../, // dotfiles + dotted dirs (.git, .DS_Store, etc.)
/(^|[/\\])node_modules([/\\]|$)/,
/(^|[/\\])dist([/\\]|$)/,
/(^|[/\\])coverage([/\\]|$)/,
/(^|[/\\])\.cache([/\\]|$)/,
];
const COMPONENT_EXTS = new Set(['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx']);
const STYLE_EXTS = new Set(['.css', '.scss', '.sass', '.less']);
function classify(path) {
const dot = path.lastIndexOf('.');
Iif (dot < 0) return 'asset';
const ext = path.slice(dot).toLowerCase();
if (COMPONENT_EXTS.has(ext)) return 'component';
if (STYLE_EXTS.has(ext)) return 'style';
return 'asset';
}
function toWebPath(root, absPath) {
const rel = relative(root, absPath);
// Normalize separators to forward slashes for the URL
const posix = sep === '\\' ? rel.split(sep).join('/') : rel;
return posix.startsWith('/') ? posix : `/${posix}`;
}
/**
* Create a debounced chokidar-backed file watcher.
*
* Resolves once chokidar has emitted `ready` so callers know the
* initial scan is finished and subsequent writes are real edits.
*
* @param {FileWatcherOptions} options
* @returns {Promise<FileWatcher>}
*/
export async function createFileWatcher(options) {
const {
root,
onChange,
onError,
debounceMs = 50,
ignored = [],
} = options;
const watcher = chokidar.watch(root, {
ignored: [...DEFAULT_IGNORES, ...ignored],
ignoreInitial: true,
persistent: true,
awaitWriteFinish: false,
});
/** @type {Map<string, NodeJS.Timeout>} */
const pending = new Map();
function schedule(filePath) {
const prev = pending.get(filePath);
Iif (prev) clearTimeout(prev);
const timer = setTimeout(() => {
pending.delete(filePath);
try {
onChange({
filePath,
webPath: toWebPath(root, filePath),
updateType: classify(filePath),
});
} catch (err) {
if (onError) onError(err);
}
}, debounceMs);
pending.set(filePath, timer);
}
watcher.on('add', schedule);
watcher.on('change', schedule);
watcher.on('unlink', schedule);
Eif (onError) {
watcher.on('error', onError);
}
await new Promise((resolve) => watcher.once('ready', resolve));
return {
async close() {
for (const timer of pending.values()) clearTimeout(timer);
pending.clear();
await watcher.close();
},
};
}
|