Skip to content

Commit fbbfaaf

Browse files
committed
fix: debounce fs route hmr events, avoid reload for existing routes
1 parent ec1b82b commit fbbfaaf

4 files changed

Lines changed: 61 additions & 48 deletions

File tree

packages/start/src/config/constants.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ export const VIRTUAL_MODULES = {
1414
} as const;
1515

1616
export const VITE_ENVIRONMENTS = {
17-
client: "client",
18-
server: "ssr",
17+
client: "client" as const,
18+
server: "ssr" as const,
1919
};

packages/start/src/config/fs-routes/fs-watcher.ts

Lines changed: 36 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,52 @@
1-
import type {
2-
EnvironmentModuleNode,
3-
FSWatcher,
4-
PluginOption,
5-
ViteDevServer,
6-
} from "vite";
1+
import type { EnvironmentModuleNode, FSWatcher, PluginOption, ViteDevServer } from "vite";
2+
import { debounce } from "../../utils/debounce.ts";
73
import { VITE_ENVIRONMENTS } from "../constants.ts";
84
import { moduleId } from "./index.ts";
95
import type { BaseFileSystemRouter } from "./router.ts";
106

11-
interface CompiledRouter {
12-
removeRoute(path: string): void;
13-
addRoute(path: string): void;
14-
updateRoute(path: string): void;
15-
addEventListener(event: "reload", handler: () => void): void;
16-
removeEventListener(event: "reload", handler: () => void): void;
17-
}
18-
19-
function setupWatcher(watcher: FSWatcher, routes: CompiledRouter): void {
7+
function setupWatcher(watcher: FSWatcher, routes: BaseFileSystemRouter): void {
208
watcher.on("unlink", path => routes.removeRoute(path));
219
watcher.on("add", path => routes.addRoute(path));
2210
watcher.on("change", path => routes.updateRoute(path));
2311
}
2412

2513
function createRoutesReloader(
2614
server: ViteDevServer,
27-
routes: CompiledRouter,
15+
routes: BaseFileSystemRouter,
2816
environment: "client" | "ssr",
29-
): () => void {
30-
routes.addEventListener("reload", handleRoutesReload);
31-
return () => routes.removeEventListener("reload", handleRoutesReload);
17+
) {
18+
const devEnv = server.environments[environment]!;
19+
if (!devEnv?.moduleGraph) return;
20+
21+
/**
22+
* Debounce catches multiple route changes in a row
23+
* Short timeout for inexpensive invalidations11
24+
*/
25+
const invalidateModule = debounce((mod: EnvironmentModuleNode) => {
26+
devEnv.moduleGraph.invalidateModule(mod);
27+
}, 0);
3228

33-
function handleRoutesReload(): void {
34-
const envName =
35-
environment === "ssr" ? VITE_ENVIRONMENTS.server : VITE_ENVIRONMENTS.client;
36-
const devEnv = server.environments[envName];
37-
if (!devEnv?.moduleGraph) return;
29+
/**
30+
* Long debounce timeout for expensive reloads
31+
*/
32+
const reloadModule = debounce((mod: EnvironmentModuleNode) => {
33+
devEnv.reloadModule(mod);
34+
}, 200);
3835

39-
const mod: EnvironmentModuleNode | undefined =
40-
devEnv.moduleGraph.getModuleById(moduleId);
41-
if (mod) {
42-
const seen = new Set<EnvironmentModuleNode>();
43-
devEnv.moduleGraph.invalidateModule(mod, seen);
36+
return routes.on("reload", function handleRoutesReload(evt): void {
37+
const mod = devEnv.moduleGraph.getModuleById(moduleId)!;
38+
if (!mod) {
39+
devEnv.hot.send({ type: "full-reload" });
40+
return;
4441
}
4542

46-
if (environment !== "ssr") {
47-
if (mod) {
48-
devEnv.reloadModule(mod);
49-
} else if (devEnv.hot) {
50-
devEnv.hot.send({ type: "full-reload" });
51-
}
43+
if (environment === VITE_ENVIRONMENTS.client && evt.detail.type !== "update") {
44+
// Client has to be reloaded when routes are added or removed
45+
reloadModule(mod);
46+
} else {
47+
invalidateModule(mod);
5248
}
53-
}
49+
});
5450
}
5551

5652
export const fileSystemWatcher = (
@@ -59,11 +55,11 @@ export const fileSystemWatcher = (
5955
const plugin: PluginOption = {
6056
name: "fs-watcher",
6157
async configureServer(server: ViteDevServer) {
62-
Object.keys(routers).forEach(environment => {
63-
const router = (globalThis as any).ROUTERS[environment];
58+
for (const environment of [VITE_ENVIRONMENTS.server, VITE_ENVIRONMENTS.client]) {
59+
const router = routers[environment];
6460
setupWatcher(server.watcher, router);
65-
createRoutesReloader(server, router, environment as keyof typeof routers);
66-
});
61+
createRoutesReloader(server, router, environment);
62+
}
6763
},
6864
};
6965
return plugin;

packages/start/src/config/fs-routes/router.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export function analyzeModule(src: string) {
3232
);
3333
}
3434

35+
type RouterEvent = CustomEvent<{ route: string; type: "update" | "remove" | "add" }>;
36+
3537
export class BaseFileSystemRouter extends EventTarget {
3638
routes: Route[];
3739

@@ -109,20 +111,20 @@ export class BaseFileSystemRouter extends EventTarget {
109111
const route = this.toRoute(src);
110112
if (route) {
111113
this._addRoute(route);
112-
this.reload(route.path);
114+
this.reload(route.path, "add");
113115
}
114116
} catch (e) {
115117
console.error(e);
116118
}
117119
}
118120
}
119121

120-
reload(route: string) {
122+
reload(route: string, type: "update" | "remove" | "add") {
121123
this.dispatchEvent(
122-
new Event("reload", {
123-
// @ts-ignore
124+
new CustomEvent("reload", {
124125
detail: {
125126
route,
127+
type,
126128
},
127129
}),
128130
);
@@ -135,7 +137,7 @@ export class BaseFileSystemRouter extends EventTarget {
135137
const route = this.toRoute(src);
136138
if (route) {
137139
this._addRoute(route);
138-
this.reload(route.path);
140+
this.reload(route.path, "update");
139141
}
140142
} catch (e) {
141143
console.error(e);
@@ -152,10 +154,15 @@ export class BaseFileSystemRouter extends EventTarget {
152154
return;
153155
}
154156
this.routes = this.routes.filter(r => r.path !== path);
155-
this.dispatchEvent(new Event("reload", {}));
157+
this.reload(path, "remove");
156158
}
157159
}
158160

161+
on(type: string, cb: (evt: RouterEvent) => void) {
162+
this.addEventListener(type, cb as any);
163+
return () => this.removeEventListener(type, cb as any);
164+
}
165+
159166
buildRoutesPromise?: Promise<any[]>;
160167

161168
async getRoutes() {
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/**
2+
* Creates a debounced function that delays the invocation of a function
3+
*/
4+
export const debounce = <T extends (...args: any[]) => void>(cb: T, debounceMs: number) => {
5+
let timeout: NodeJS.Timeout | undefined;
6+
return (...args: Parameters<T>) => {
7+
clearTimeout(timeout);
8+
timeout = setTimeout(() => cb(...args), debounceMs)
9+
}
10+
}

0 commit comments

Comments
 (0)