@@ -1700,6 +1700,14 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
17001700 const path = yield * Path . Path ;
17011701 const staticDir = yield * fileSystem . makeTempDirectoryScoped ( { prefix : "t3-static-hashes-" } ) ;
17021702 yield * fileSystem . makeDirectory ( path . join ( staticDir , "assets" ) ) ;
1703+ yield * fileSystem . makeDirectory ( path . join ( staticDir , ".vite" ) ) ;
1704+ yield * fileSystem . writeFileString (
1705+ path . join ( staticDir , ".vite" , "manifest.json" ) ,
1706+ `{
1707+ "index.html": { "file": "assets/index-AbCd0123.js", "isEntry": true },
1708+ "large.js": { "file": "assets/large-aBcD9876.js" }
1709+ }` ,
1710+ ) ;
17031711 yield * fileSystem . writeFileString ( path . join ( staticDir , "index.html" ) , "<html>app</html>" ) ;
17041712 yield * fileSystem . writeFileString (
17051713 path . join ( staticDir , "assets" , "index-AbCd0123.js" ) ,
@@ -1764,6 +1772,185 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
17641772 } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
17651773 ) ;
17661774
1775+ for ( const manifest of [
1776+ { label : "missing" , contents : null } ,
1777+ { label : "nonmatching" , contents : '{"other.js":{"file":"assets/other-AbCd0123.js"}}' } ,
1778+ { label : "malformed" , contents : "{not-json" } ,
1779+ ] ) {
1780+ it . effect ( `revalidates hash-like static filenames with a ${ manifest . label } manifest` , ( ) =>
1781+ Effect . gen ( function * ( ) {
1782+ const fileSystem = yield * FileSystem . FileSystem ;
1783+ const path = yield * Path . Path ;
1784+ const staticDir = yield * fileSystem . makeTempDirectoryScoped ( {
1785+ prefix : "t3-static-mutable-" ,
1786+ } ) ;
1787+ yield * fileSystem . makeDirectory ( path . join ( staticDir , "assets" ) ) ;
1788+ if ( manifest . contents !== null ) {
1789+ yield * fileSystem . makeDirectory ( path . join ( staticDir , ".vite" ) ) ;
1790+ yield * fileSystem . writeFileString (
1791+ path . join ( staticDir , ".vite" , "manifest.json" ) ,
1792+ manifest . contents ,
1793+ ) ;
1794+ }
1795+ const filePath = path . join ( staticDir , "assets" , "config-20260904.js" ) ;
1796+ yield * fileSystem . writeFileString ( filePath , "first config" ) ;
1797+ yield * buildAppUnderTest ( { config : { staticDir } } ) ;
1798+
1799+ const initial = yield * HttpClient . get ( "/assets/config-20260904.js" ) ;
1800+ assert . equal ( initial . headers [ "cache-control" ] , "no-cache" ) ;
1801+ assert . equal ( yield * initial . text , "first config" ) ;
1802+
1803+ yield * fileSystem . writeFileString ( filePath , "replacement config" ) ;
1804+ const changed = yield * HttpClient . get ( "/assets/config-20260904.js" , {
1805+ headers : { "if-none-match" : initial . headers . etag ! } ,
1806+ } ) ;
1807+ assert . equal ( changed . status , 200 ) ;
1808+ assert . equal ( changed . headers [ "cache-control" ] , "no-cache" ) ;
1809+ assert . notEqual ( changed . headers . etag , initial . headers . etag ) ;
1810+ assert . equal ( yield * changed . text , "replacement config" ) ;
1811+ } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
1812+ ) ;
1813+ }
1814+
1815+ it . effect ( "binds static metadata and bytes to one file across atomic replacement" , ( ) =>
1816+ Effect . gen ( function * ( ) {
1817+ const fileSystem = yield * FileSystem . FileSystem ;
1818+ const path = yield * Path . Path ;
1819+ const staticDir = yield * fileSystem . makeTempDirectoryScoped ( { prefix : "t3-static-replace-" } ) ;
1820+ const beforeOpenPath = path . join ( staticDir , "before-open.txt" ) ;
1821+ const afterOpenPath = path . join ( staticDir , "after-open.txt" ) ;
1822+ const original = "original bytes" ;
1823+ const replacement = "replacement bytes with a different size" ;
1824+ for ( const filePath of [ beforeOpenPath , afterOpenPath ] ) {
1825+ yield * fileSystem . writeFileString ( filePath , original ) ;
1826+ yield * fileSystem . writeFileString ( `${ filePath } .next` , replacement ) ;
1827+ }
1828+ const replaced = new Set < string > ( ) ;
1829+ const replaceOnce = Effect . fnUntraced ( function * ( filePath : string ) {
1830+ if ( replaced . has ( filePath ) ) return ;
1831+ replaced . add ( filePath ) ;
1832+ yield * fileSystem . rename ( `${ filePath } .next` , filePath ) ;
1833+ } ) ;
1834+ const replacingFileSystem = FileSystem . FileSystem . of ( {
1835+ ...fileSystem ,
1836+ stat : ( filePath ) =>
1837+ fileSystem
1838+ . stat ( filePath )
1839+ . pipe (
1840+ Effect . tap ( ( ) => ( filePath === beforeOpenPath ? replaceOnce ( filePath ) : Effect . void ) ) ,
1841+ ) ,
1842+ open : ( filePath , options ) =>
1843+ fileSystem
1844+ . open ( filePath , options )
1845+ . pipe (
1846+ Effect . tap ( ( ) => ( filePath === afterOpenPath ? replaceOnce ( filePath ) : Effect . void ) ) ,
1847+ ) ,
1848+ } ) ;
1849+ yield * buildAppUnderTest ( { config : { staticDir } } ) . pipe (
1850+ Effect . provideService ( FileSystem . FileSystem , replacingFileSystem ) ,
1851+ ) ;
1852+
1853+ for ( const [ name , expected ] of [
1854+ [ "before-open.txt" , replacement ] ,
1855+ [ "after-open.txt" , original ] ,
1856+ ] as const ) {
1857+ const response = yield * HttpClient . get ( `/${ name } ` , {
1858+ headers : { "accept-encoding" : "identity" } ,
1859+ } ) ;
1860+ assert . equal ( response . status , 200 ) ;
1861+ assert . equal ( response . headers [ "content-length" ] , String ( expected . length ) ) ;
1862+ assert . isTrue ( response . headers . etag ?. startsWith ( `W/"${ expected . length . toString ( 16 ) } -` ) ) ;
1863+ assert . equal ( yield * response . text , expected ) ;
1864+ assert . isTrue ( replaced . has ( path . join ( staticDir , name ) ) ) ;
1865+ assert . equal ( yield * fileSystem . readFileString ( path . join ( staticDir , name ) ) , replacement ) ;
1866+ }
1867+ } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
1868+ ) ;
1869+
1870+ it . effect ( "closes static file handles after GET, HEAD, 304, and request cancellation" , ( ) =>
1871+ Effect . gen ( function * ( ) {
1872+ const fileSystem = yield * FileSystem . FileSystem ;
1873+ const path = yield * Path . Path ;
1874+ const staticDir = yield * fileSystem . makeTempDirectoryScoped ( { prefix : "t3-static-close-" } ) ;
1875+ const filePath = path . join ( staticDir , "index.html" ) ;
1876+ const body = "<p>file content</p>" . repeat ( 1024 ) ;
1877+ yield * fileSystem . writeFileString ( filePath , body ) ;
1878+ const closed = yield * Queue . unbounded < FileSystem . File > ( ) ;
1879+ const blocked = yield * Deferred . make < void > ( ) ;
1880+ const active = new Set < FileSystem . File > ( ) ;
1881+ let blockAfterOpen = false ;
1882+ let bodyReads = 0 ;
1883+ const trackedFileSystem = FileSystem . FileSystem . of ( {
1884+ ...fileSystem ,
1885+ open : ( candidate , options ) =>
1886+ Effect . gen ( function * ( ) {
1887+ if ( candidate !== filePath ) return yield * fileSystem . open ( candidate , options ) ;
1888+ let opened : FileSystem . File | undefined ;
1889+ // Registered first, so this signal runs after the real descriptor-close finalizer.
1890+ yield * Effect . addFinalizer ( ( ) =>
1891+ Effect . gen ( function * ( ) {
1892+ if ( opened === undefined ) return ;
1893+ active . delete ( opened ) ;
1894+ yield * Queue . offer ( closed , opened ) ;
1895+ } ) ,
1896+ ) ;
1897+ const file = yield * fileSystem . open ( candidate , options ) ;
1898+ opened = file ;
1899+ active . add ( file ) ;
1900+ if ( blockAfterOpen ) {
1901+ yield * Deferred . succeed ( blocked , undefined ) ;
1902+ return yield * Effect . never ;
1903+ }
1904+ return new Proxy ( file , {
1905+ get ( target , key ) {
1906+ if ( key === "readAlloc" ) {
1907+ return ( size : FileSystem . SizeInput ) => {
1908+ bodyReads += 1 ;
1909+ return target . readAlloc ( size ) ;
1910+ } ;
1911+ }
1912+ return Reflect . get ( target , key , target ) ;
1913+ } ,
1914+ } ) ;
1915+ } ) ,
1916+ } ) ;
1917+ yield * buildAppUnderTest ( { config : { staticDir } } ) . pipe (
1918+ Effect . provideService ( FileSystem . FileSystem , trackedFileSystem ) ,
1919+ ) ;
1920+
1921+ const get = yield * HttpClient . get ( "/" ) ;
1922+ assert . equal ( yield * get . text , body ) ;
1923+ yield * Queue . take ( closed ) ;
1924+ assert . equal ( active . size , 0 ) ;
1925+ assert . isAbove ( bodyReads , 0 ) ;
1926+ const readsAfterGet = bodyReads ;
1927+
1928+ const head = yield * HttpClient . head ( "/" , { headers : { "accept-encoding" : "gzip" } } ) ;
1929+ assert . equal ( head . status , 200 ) ;
1930+ assert . equal ( head . headers [ "content-encoding" ] , "gzip" ) ;
1931+ assert . equal ( yield * head . text , "" ) ;
1932+ yield * Queue . take ( closed ) ;
1933+ assert . equal ( active . size , 0 ) ;
1934+ assert . equal ( bodyReads , readsAfterGet ) ;
1935+
1936+ const unchanged = yield * HttpClient . get ( "/" , {
1937+ headers : { "if-none-match" : get . headers . etag ! } ,
1938+ } ) ;
1939+ assert . equal ( unchanged . status , 304 ) ;
1940+ yield * Queue . take ( closed ) ;
1941+ assert . equal ( active . size , 0 ) ;
1942+ assert . equal ( bodyReads , readsAfterGet ) ;
1943+
1944+ blockAfterOpen = true ;
1945+ const cancelled = yield * HttpClient . get ( "/" ) . pipe ( Effect . forkChild ) ;
1946+ yield * Deferred . await ( blocked ) ;
1947+ assert . equal ( active . size , 1 ) ;
1948+ yield * Fiber . interrupt ( cancelled ) ;
1949+ yield * Queue . take ( closed ) ;
1950+ assert . equal ( active . size , 0 ) ;
1951+ } ) . pipe ( Effect . provide ( NodeHttpServer . layerTest ) ) ,
1952+ ) ;
1953+
17671954 it . effect ( "redirects to dev URL when configured" , ( ) =>
17681955 Effect . gen ( function * ( ) {
17691956 yield * buildAppUnderTest ( {
0 commit comments