1- import { mkdirSync , mkdtempSync , rmSync , writeFileSync } from "node:fs" ;
1+ import { mkdirSync , mkdtempSync , realpathSync , rmSync , writeFileSync } from "node:fs" ;
22import { tmpdir } from "node:os" ;
33import { join } from "node:path" ;
4- import { afterEach , describe , expect , it } from "vitest" ;
4+ import {
5+ createServer ,
6+ type DevEnvironment ,
7+ type EnvironmentModuleNode ,
8+ type HotUpdateOptions ,
9+ normalizePath ,
10+ type Plugin ,
11+ type ViteDevServer ,
12+ } from "vite" ;
13+ import { afterEach , describe , expect , it , vi } from "vitest" ;
514
6- import { isHtmlResponse , resolvePreviewServerEntry } from "./dev-server.ts" ;
15+ import { VITE_ENVIRONMENTS } from "./constants.ts" ;
16+ import { isHtmlResponse , resolvePreviewServerEntry , serverOnlyReload } from "./dev-server.ts" ;
717
818const temporaryDirectories : string [ ] = [ ] ;
919
@@ -19,7 +29,10 @@ function createServerEntry(extension: "js" | "mjs") {
1929 return { root, serverEntry } ;
2030}
2131
22- afterEach ( ( ) => {
32+ const servers : ViteDevServer [ ] = [ ] ;
33+
34+ afterEach ( async ( ) => {
35+ await Promise . all ( servers . splice ( 0 ) . map ( server => server . close ( ) ) ) ;
2336 for ( const directory of temporaryDirectories . splice ( 0 ) ) {
2437 rmSync ( directory , { recursive : true } ) ;
2538 }
@@ -42,6 +55,81 @@ describe("resolvePreviewServerEntry", () => {
4255 } ) ;
4356} ) ;
4457
58+ describe ( "serverOnlyReload" , ( ) => {
59+ /** An app whose middleware is configured without a file extension */
60+ async function createApp ( ) {
61+ const root = realpathSync . native ( mkdtempSync ( join ( tmpdir ( ) , "solid-start-server-reload-" ) ) ) ;
62+ temporaryDirectories . push ( root ) ;
63+
64+ mkdirSync ( join ( root , "src" ) ) ;
65+ writeFileSync ( join ( root , "package.json" ) , '{ "type": "module" }' ) ;
66+ for ( const file of [ "entry-server.tsx" , "middleware.ts" , "app.tsx" ] ) {
67+ writeFileSync ( join ( root , "src" , file ) , "export default {};" ) ;
68+ }
69+
70+ const plugin = serverOnlyReload ( "./src/entry-server.tsx" , "./src/middleware" ) as Plugin ;
71+ const server = await createServer ( {
72+ configFile : false ,
73+ logLevel : "silent" ,
74+ root,
75+ server : { middlewareMode : true } ,
76+ plugins : [ plugin ] ,
77+ } ) ;
78+ servers . push ( server ) ;
79+
80+ const send = vi . spyOn ( server . environments [ VITE_ENVIRONMENTS . client ] ! . hot , "send" ) ;
81+ const hotUpdate = plugin . hotUpdate as (
82+ this : { environment : DevEnvironment } ,
83+ options : HotUpdateOptions ,
84+ ) => Promise < void > ;
85+
86+ /** Stands in for Vite calling the hook when a watched file changes */
87+ const change = (
88+ file : string ,
89+ { environment = VITE_ENVIRONMENTS . server , inGraph = true } = { } ,
90+ ) =>
91+ hotUpdate . call (
92+ { environment : server . environments [ environment ] ! } ,
93+ {
94+ type : "update" ,
95+ file : normalizePath ( join ( root , file ) ) ,
96+ timestamp : Date . now ( ) ,
97+ modules : inGraph ? [ { } as EnvironmentModuleNode ] : [ ] ,
98+ read : async ( ) => "" ,
99+ server,
100+ } ,
101+ ) ;
102+
103+ return { change, send } ;
104+ }
105+
106+ it . each ( [ "src/entry-server.tsx" , "src/middleware.ts" ] ) (
107+ "reloads the browser when %s changes" ,
108+ async file => {
109+ const { change, send } = await createApp ( ) ;
110+
111+ await change ( file ) ;
112+
113+ await vi . waitFor ( ( ) => expect ( send ) . toHaveBeenCalledWith ( { type : "full-reload" } ) ) ;
114+ } ,
115+ ) ;
116+
117+ it ( "leaves every other change to Vite's own HMR" , async ( ) => {
118+ const { change, send } = await createApp ( ) ;
119+
120+ // Also rendered on the client, so the client environment updates it
121+ await change ( "src/app.tsx" ) ;
122+ // Already handled by the environment the change was reported for
123+ await change ( "src/entry-server.tsx" , { environment : VITE_ENVIRONMENTS . client } ) ;
124+ // Nothing was rendered from it, so there is nothing to see
125+ await change ( "src/entry-server.tsx" , { inGraph : false } ) ;
126+
127+ // Longer than the debounce the reload is coalesced with
128+ await new Promise ( resolve => setTimeout ( resolve , 200 ) ) ;
129+ expect ( send ) . not . toHaveBeenCalled ( ) ;
130+ } ) ;
131+ } ) ;
132+
45133describe ( "isHtmlResponse" , ( ) => {
46134 it ( "recognizes HTML responses with content type parameters" , ( ) => {
47135 const response = new Response ( null , {
0 commit comments