Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 20 additions & 6 deletions packages/cli/bin/flue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1228,7 +1228,7 @@ function superviseDevCommand(args: DevArgs) {
configFilesByDirectory.set(directory, basenames);
}

const watchers: fs.FSWatcher[] = [];
const watchers: { close(): void }[] = [];
let child: ChildProcess | undefined;
let restartTimer: NodeJS.Timeout | undefined;
let restartRequested = false;
Expand Down Expand Up @@ -1286,8 +1286,8 @@ function superviseDevCommand(args: DevArgs) {
child?.kill('SIGTERM');
};

try {
for (const [directory, basenames] of configFilesByDirectory) {
for (const [directory, basenames] of configFilesByDirectory) {
try {
const watcher = fs.watch(directory, (_event, filename) => {
const basename = filename?.toString();
if (basename !== undefined) {
Expand All @@ -1307,10 +1307,24 @@ function superviseDevCommand(args: DevArgs) {
exit(1);
});
watchers.push(watcher);
} catch (err) {
if (err instanceof Error && 'code' in err && ((err as NodeJS.ErrnoException).code === 'EMFILE' || (err as NodeJS.ErrnoException).code === 'ENOSPC')) {
console.error(`${dim('config')} ${(err as NodeJS.ErrnoException).code}: ${(err as Error).message}; falling back to polling`);
const interval = setInterval(() => {
for (const basename of basenames) {
const configFile = path.join(directory, basename);
if (configFileStates.get(configFile) !== readConfigFileState(configFile)) {
restart(configFile);
return;
}
}
}, 1000);
watchers.push({ close: () => clearInterval(interval) });
} else {
cliError(`Config watcher failed: ${err instanceof Error ? err.message : String(err)}`);
exit(1);
}
}
} catch (err) {
cliError(`Config watcher failed: ${err instanceof Error ? err.message : String(err)}`);
exit(1);
}

const shutdown = (signal: NodeJS.Signals) => {
Expand Down
63 changes: 60 additions & 3 deletions packages/cli/src/lib/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ interface WatcherHandle {
*/
function createWatcher(options: WatcherOptions): WatcherHandle {
const { root, sourceRoot, output, envFile, configFiles, onChange } = options;
const watchers: fs.FSWatcher[] = [];
const watchers: { close(): void }[] = [];
const watchesDotFlue = sourceRoot === path.join(root, '.flue');
const ignoredConfigFiles = new Set(configFiles.map((file) => path.resolve(file)));

Expand Down Expand Up @@ -387,7 +387,43 @@ function createWatcher(options: WatcherOptions): WatcherHandle {
});
watchers.push(w);
} catch (err) {
error(`Failed to watch ${root}: ${err instanceof Error ? err.message : String(err)}`);
if (err instanceof Error && 'code' in err && ((err as NodeJS.ErrnoException).code === 'EMFILE' || (err as NodeJS.ErrnoException).code === 'ENOSPC')) {
console.warn(`${dim('dev')} ${(err as NodeJS.ErrnoException).code}: file watching unavailable; polling ${root}`);
const cache = new Map<string, number>();
const interval = setInterval(() => {
const walk = (dir: string, baseRel: string) => {
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const relPath = baseRel ? `${baseRel}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
if (isIgnoredPath(relPath)) continue;
walk(path.join(dir, entry.name), relPath);
} else if (entry.isFile()) {
if (isIgnoredPath(relPath)) continue;
try {
const stat = fs.statSync(path.join(dir, entry.name));
const cached = cache.get(relPath);
if (cached !== undefined && cached !== stat.mtimeMs) {
onChange(relPath);
}
cache.set(relPath, stat.mtimeMs);
} catch {
// file removed between readdir and stat
}
}
}
};
walk(root, '');
}, 1000);
watchers.push({ close: () => clearInterval(interval) });
} else {
error(`Failed to watch ${root}: ${err instanceof Error ? err.message : String(err)}`);
}
}

try {
Expand All @@ -397,7 +433,28 @@ function createWatcher(options: WatcherOptions): WatcherHandle {
if (filename?.toString() === envBasename) onChange(envFile);
});
watchers.push(w);
} catch {}
} catch (err) {
if (err instanceof Error && 'code' in err && ((err as NodeJS.ErrnoException).code === 'EMFILE' || (err as NodeJS.ErrnoException).code === 'ENOSPC')) {
let cached: string | undefined;
try {
const stat = fs.statSync(envFile);
cached = `${stat.mtimeMs}:${stat.size}`;
} catch {}
const interval = setInterval(() => {
try {
const stat = fs.statSync(envFile);
const state = `${stat.mtimeMs}:${stat.size}`;
if (cached !== undefined && cached !== state) {
onChange(envFile);
}
cached = state;
} catch {
// file removed
}
}, 1000);
watchers.push({ close: () => clearInterval(interval) });
}
}

return {
close() {
Expand Down
Loading