From 45e6fd3cb9a80929898ef4515865e9b4dec30c7f Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Mon, 20 Jul 2026 16:07:38 +0530 Subject: [PATCH] fix(dev-server): invalidate WRN cache by content --- packages/dev-server/src/pipeline.ts | 14 +++++++++----- packages/dev-server/test/pipeline.test.ts | 23 ++++++++++++++++++++++- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/packages/dev-server/src/pipeline.ts b/packages/dev-server/src/pipeline.ts index 61cce815..f5587765 100644 --- a/packages/dev-server/src/pipeline.ts +++ b/packages/dev-server/src/pipeline.ts @@ -119,17 +119,21 @@ function compileWireToTs(file: string, version = 0): string { const cacheDir = compileCacheDir ?? join(dirname(file), ".wrnexus"); const name = basename(file).replace(/\.wrn$/, ""); const suffix = version ? `-hmr-${version}` : ""; - const out = join(cacheDir, `${name}-${hashPath(file)}${suffix}.wrn.ts`); + const source = readFileSync(file, "utf8"); + // Include the source contents in the cache identity. Package managers, git + // checkouts, archive extraction, and linked dependencies can all replace a + // file while preserving (or moving backwards) its mtime. An mtime-only cache + // then serves an older compiled component even across a clean build. + const out = join(cacheDir, `${name}-${hashPath(file)}-${hashPath(source)}${suffix}.wrn.ts`); - // Skip recompiling when the on-disk cache is already newer than the source - // (e.g. reused across dev restarts) — avoids a read + compile + write. + // The content hash makes this safe even when source timestamps are preserved. try { - if (statSync(out).mtimeMs >= statSync(file).mtimeMs) return out; + if (statSync(out).isFile()) return out; } catch { /* cache missing → compile below */ } - const code = compileWireFile(readFileSync(file, "utf8"), file); + const code = compileWireFile(source, file); mkdirSync(cacheDir, { recursive: true }); writeFileSync(out, code, "utf8"); return out; diff --git a/packages/dev-server/test/pipeline.test.ts b/packages/dev-server/test/pipeline.test.ts index 91253b31..416b2e55 100644 --- a/packages/dev-server/test/pipeline.test.ts +++ b/packages/dev-server/test/pipeline.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "bun:test"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, rmSync, utimesSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { invalidateModule, loadModule, setCompileCacheDir } from "../src/pipeline.ts"; @@ -41,3 +41,24 @@ test("invalidateModule recompiles changed WRN files in-process", async () => { rmSync(root, { recursive: true, force: true }); } }); + +test("WRN cache follows content when a replacement has an older mtime", async () => { + const root = mkdtempSync(join(tmpdir(), "wrnexus-cache-")); + const file = join(root, "component.wrn"); + setCompileCacheDir(join(root, ".wrnexus")); + + try { + writeFileSync(file, "component Example { view {

Old

} }\n"); + const first = (await loadModule(file)).render as () => unknown; + expect(String(first())).toContain("Old"); + + writeFileSync(file, "component Example { view {

Current

} }\n"); + utimesSync(file, new Date(0), new Date(0)); + invalidateModule(file); + + const current = (await loadModule(file)).render as () => unknown; + expect(String(current())).toContain("Current"); + } finally { + rmSync(root, { recursive: true, force: true }); + } +});