Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/heavy-bottles-like.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/start": patch
---

Fixed changes in route files resulting in a reload instead of hot module replace. Reloads now only are triggered when adding or removing routes.
4 changes: 2 additions & 2 deletions packages/start/src/config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@ export const VIRTUAL_MODULES = {
} as const;

export const VITE_ENVIRONMENTS = {
client: "client",
server: "ssr",
client: "client" as const,
server: "ssr" as const,
};
76 changes: 36 additions & 40 deletions packages/start/src/config/fs-routes/fs-watcher.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,52 @@
import type {
EnvironmentModuleNode,
FSWatcher,
PluginOption,
ViteDevServer,
} from "vite";
import type { EnvironmentModuleNode, FSWatcher, PluginOption, ViteDevServer } from "vite";
import { debounce } from "../../utils/debounce.ts";
import { VITE_ENVIRONMENTS } from "../constants.ts";
import { moduleId } from "./index.ts";
import type { BaseFileSystemRouter } from "./router.ts";

interface CompiledRouter {
removeRoute(path: string): void;
addRoute(path: string): void;
updateRoute(path: string): void;
addEventListener(event: "reload", handler: () => void): void;
removeEventListener(event: "reload", handler: () => void): void;
}

function setupWatcher(watcher: FSWatcher, routes: CompiledRouter): void {
function setupWatcher(watcher: FSWatcher, routes: BaseFileSystemRouter): void {
watcher.on("unlink", path => routes.removeRoute(path));
watcher.on("add", path => routes.addRoute(path));
watcher.on("change", path => routes.updateRoute(path));
}

function createRoutesReloader(
server: ViteDevServer,
routes: CompiledRouter,
routes: BaseFileSystemRouter,
environment: "client" | "ssr",
): () => void {
routes.addEventListener("reload", handleRoutesReload);
return () => routes.removeEventListener("reload", handleRoutesReload);
) {
const devEnv = server.environments[environment]!;
if (!devEnv?.moduleGraph) return;

/**
* Debounce catches multiple route changes in a row
* Short timeout for inexpensive invalidations
*/
const invalidateModule = debounce((mod: EnvironmentModuleNode) => {
devEnv.moduleGraph.invalidateModule(mod);
}, 0);

function handleRoutesReload(): void {
const envName =
environment === "ssr" ? VITE_ENVIRONMENTS.server : VITE_ENVIRONMENTS.client;
const devEnv = server.environments[envName];
if (!devEnv?.moduleGraph) return;
/**
* Long debounce timeout for expensive reloads
*/
const reloadModule = debounce((mod: EnvironmentModuleNode) => {
devEnv.reloadModule(mod);
}, 200);

const mod: EnvironmentModuleNode | undefined =
devEnv.moduleGraph.getModuleById(moduleId);
if (mod) {
const seen = new Set<EnvironmentModuleNode>();
devEnv.moduleGraph.invalidateModule(mod, seen);
return routes.on("reload", function handleRoutesReload(evt): void {
const mod = devEnv.moduleGraph.getModuleById(moduleId)!;
if (!mod) {
devEnv.hot.send({ type: "full-reload" });
return;
}

if (environment !== "ssr") {
if (mod) {
devEnv.reloadModule(mod);
} else if (devEnv.hot) {
devEnv.hot.send({ type: "full-reload" });
}
if (environment === VITE_ENVIRONMENTS.client && evt.detail.type !== "update") {
// Client has to be reloaded when routes are added or removed
reloadModule(mod);
} else {
invalidateModule(mod);
}
}
});
}

export const fileSystemWatcher = (
Expand All @@ -59,11 +55,11 @@ export const fileSystemWatcher = (
const plugin: PluginOption = {
name: "fs-watcher",
async configureServer(server: ViteDevServer) {
Object.keys(routers).forEach(environment => {
const router = (globalThis as any).ROUTERS[environment];
for (const environment of [VITE_ENVIRONMENTS.server, VITE_ENVIRONMENTS.client]) {
const router = routers[environment];
setupWatcher(server.watcher, router);
createRoutesReloader(server, router, environment as keyof typeof routers);
});
createRoutesReloader(server, router, environment);
}
},
};
return plugin;
Expand Down
38 changes: 27 additions & 11 deletions packages/start/src/config/fs-routes/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ export function analyzeModule(src: string) {
);
}

type RouterEvent = CustomEvent<{ route: string; type: "update" | "remove" | "add" }>;

export class BaseFileSystemRouter extends EventTarget {
routes: Route[];

Expand Down Expand Up @@ -98,8 +100,11 @@ export class BaseFileSystemRouter extends EventTarget {
update = undefined;

_addRoute(route: Route) {
this.routes = this.routes.filter(r => r.path !== route.path);
const idx = this.routes.findIndex(r => r.path === route.path);
if (idx >= 0) this.routes.splice(idx, 1);
this.routes.push(route);

return idx >= 0;
}

async addRoute(src: string) {
Expand All @@ -109,33 +114,35 @@ export class BaseFileSystemRouter extends EventTarget {
const route = this.toRoute(src);
if (route) {
this._addRoute(route);
this.reload(route.path);
this.reload(route.path, "add");
}
} catch (e) {
console.error(e);
}
}
}

reload(route: string) {
reload(route: string, type: "update" | "remove" | "add") {
this.dispatchEvent(
new Event("reload", {
// @ts-ignore
new CustomEvent("reload", {
detail: {
route,
type,
},
}),
);
}

async updateRoute(src: string) {
src = normalizePath(src);
async updateRoute(src_: string) {
const src = normalizePath(src_);
if (this.isRoute(src)) {
try {
const route = this.toRoute(src);
if (route) {
this._addRoute(route);
this.reload(route.path);
const updated = this._addRoute(route);
this.reload(route.path, updated ? "update" : "add");
} else {
this.removeRoute(src_);
}
} catch (e) {
console.error(e);
Expand All @@ -151,11 +158,20 @@ export class BaseFileSystemRouter extends EventTarget {
if (path === undefined) {
return;
}
this.routes = this.routes.filter(r => r.path !== path);
this.dispatchEvent(new Event("reload", {}));

const idx = this.routes.findIndex(r => r.path === path);
if (idx === -1) return;

this.routes.splice(idx, 1);
this.reload(path, "remove");
}
}

on(type: string, cb: (evt: RouterEvent) => void) {
this.addEventListener(type, cb as any);
return () => this.removeEventListener(type, cb as any);
}

buildRoutesPromise?: Promise<any[]>;

async getRoutes() {
Expand Down
10 changes: 10 additions & 0 deletions packages/start/src/utils/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Creates a debounced function that delays the invocation of a function
*/
export const debounce = <T extends (...args: any[]) => void>(cb: T, debounceMs: number) => {
let timeout: NodeJS.Timeout | undefined;
return (...args: Parameters<T>) => {
clearTimeout(timeout);
timeout = setTimeout(() => cb(...args), debounceMs)
}
}
Loading