diff --git a/.bumpy/quiet-wrangler-noop-reloads.md b/.bumpy/quiet-wrangler-noop-reloads.md new file mode 100644 index 000000000..3b160df2a --- /dev/null +++ b/.bumpy/quiet-wrangler-noop-reloads.md @@ -0,0 +1,5 @@ +--- +"@varlock/cloudflare-integration": patch +--- + +Quiet `varlock-wrangler dev` on macOS when env files are only opened/inspected: ignore `fs.watch` events with unchanged mtime, and log same-content no-op reloads only under `VARLOCK_DEBUG`. diff --git a/packages/integrations/cloudflare/package.json b/packages/integrations/cloudflare/package.json index 6b5159fc7..98156bf86 100644 --- a/packages/integrations/cloudflare/package.json +++ b/packages/integrations/cloudflare/package.json @@ -38,7 +38,9 @@ "scripts": { "dev": "tsup --watch", "build": "tsup", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest", + "test:ci": "vitest --run" }, "keywords": [ "varlock", diff --git a/packages/integrations/cloudflare/src/varlock-wrangler.ts b/packages/integrations/cloudflare/src/varlock-wrangler.ts index 2993af271..b03667053 100644 --- a/packages/integrations/cloudflare/src/varlock-wrangler.ts +++ b/packages/integrations/cloudflare/src/varlock-wrangler.ts @@ -10,6 +10,7 @@ import { spawn, execSync } from 'node:child_process'; import { execSyncVarlock, VarlockExecError } from 'varlock/exec-sync-varlock'; import { encryptEnvBlobSync, generateEncryptionKeyHex } from 'varlock/encrypt-env'; +import { getFileMtimeMs, shouldIgnoreUnchangedMtime } from './watch-mtime'; const isWindows = process.platform === 'win32'; const debugEnabled = !!process.env.VARLOCK_DEBUG; @@ -595,7 +596,10 @@ async function handleDev(args: Array) { const changedMsg = changedFileList.length ? `change detected in ${changedFileList.length} env source file${changedFileList.length === 1 ? '' : 's'}` : 'change detected in env source files'; - console.log(`[varlock-wrangler] ${changedMsg}; reloaded env, no changes found, skipping restart.`); + // Quiet by default: same-content saves (mtime changed, graph unchanged) + // and any residual no-ops. Spurious mtime-unchanged events are filtered + // before scheduleRestart runs. + debug(`${changedMsg}; reloaded env, no changes found, skipping restart.`); restartTimeout = undefined; return; } @@ -638,12 +642,25 @@ async function handleDev(args: Array) { } // set up watchers on env source files + // Track mtimes so we can ignore macOS/editor fs.watch noise that fires when a + // file is opened or inspected without being rewritten (mtime stays the same). + const sourceMtimes = new Map(); if (loaded.graph.basePath) { for (const source of loaded.graph.sources) { if (!source.enabled || !source.path) continue; const fullPath = join(loaded.graph.basePath, source.path); try { - const w = watch(fullPath, () => scheduleRestart(fullPath)); + sourceMtimes.set(fullPath, getFileMtimeMs(fullPath)); + const w = watch(fullPath, () => { + const nextMtime = getFileMtimeMs(fullPath); + const prevMtime = sourceMtimes.get(fullPath); + if (shouldIgnoreUnchangedMtime(prevMtime, nextMtime)) { + debug('dev: ignoring watch event with unchanged mtime', fullPath); + return; + } + sourceMtimes.set(fullPath, nextMtime); + scheduleRestart(fullPath); + }); watchers.push(w); debug('dev: watching', fullPath); } catch { diff --git a/packages/integrations/cloudflare/src/watch-mtime.test.ts b/packages/integrations/cloudflare/src/watch-mtime.test.ts new file mode 100644 index 000000000..701a465d0 --- /dev/null +++ b/packages/integrations/cloudflare/src/watch-mtime.test.ts @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import { shouldIgnoreUnchangedMtime } from './watch-mtime'; + +describe('shouldIgnoreUnchangedMtime', () => { + it('ignores when both mtimes are defined and equal', () => { + expect(shouldIgnoreUnchangedMtime(1000, 1000)).toBe(true); + }); + + it('does not ignore when mtime advanced', () => { + expect(shouldIgnoreUnchangedMtime(1000, 1001)).toBe(false); + }); + + it('does not ignore when previous mtime is unknown', () => { + expect(shouldIgnoreUnchangedMtime(undefined, 1000)).toBe(false); + }); + + it('does not ignore when next mtime is unknown', () => { + expect(shouldIgnoreUnchangedMtime(1000, undefined)).toBe(false); + }); + + it('does not ignore when both mtimes are unknown', () => { + expect(shouldIgnoreUnchangedMtime(undefined, undefined)).toBe(false); + }); +}); diff --git a/packages/integrations/cloudflare/src/watch-mtime.ts b/packages/integrations/cloudflare/src/watch-mtime.ts new file mode 100644 index 000000000..e6cf0985c --- /dev/null +++ b/packages/integrations/cloudflare/src/watch-mtime.ts @@ -0,0 +1,27 @@ +import { statSync } from 'node:fs'; + +/** Returns file mtime in ms, or undefined if the path cannot be statted. */ +export function getFileMtimeMs(filePath: string): number | undefined { + try { + return statSync(filePath).mtimeMs; + } catch { + return undefined; + } +} + +/** + * True when a watch event should be ignored because the file's mtime did not + * change. macOS (and some editors/agents) emit fs.watch events when a file is + * merely opened or inspected without rewriting it. + * + * If either mtime is unknown, do not ignore — safer to reload than to miss a + * real change (e.g. file was deleted then recreated). + */ +export function shouldIgnoreUnchangedMtime( + previousMtimeMs: number | undefined, + nextMtimeMs: number | undefined, +): boolean { + return previousMtimeMs !== undefined + && nextMtimeMs !== undefined + && previousMtimeMs === nextMtimeMs; +}