@@ -0,0 +1,98 @@ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + import { afterAll , beforeAll , describe , expect , it } from 'bun:test' 12 + import fs from 'node:fs' 13 + import path from 'node:path' 14 + import { serve } from '../../src/serve' 15 + 16 + const DIR = path . join ( import . meta . dir , 'fixtures-cache-option' ) 17 + const PAGES = path . join ( DIR , 'pages' ) 18 + const PARTIALS = path . join ( DIR , 'partials' ) 19 + 20 + const servers : Array < Awaited < ReturnType < typeof serve >>> = [ ] 21 + 22 + async function start ( stxOptions : Record < string , unknown > = { } , configDir = DIR ) : Promise < ( page : string ) => Promise < string >> { 23 + const server = await serve ( { port : 0 , root : PAGES , configDir , watch : false , stxOptions : { partialsDir : PARTIALS , ... stxOptions } } ) 24 + servers . push ( server ) 25 + const base = server . url . replace ( / \ / $ / , '' ) 26 + return async ( page : string ) => { 27 + const response = await fetch ( `${base}/${page}` ) 28 + const html = await response . text ( ) 29 + const main = html . match ( / < main > ( [ \ s \ S ] * ? ) < \ / main > / ) ? . [ 1 ] 30 + if ( response . status !== 200 || main === undefined ) 31 + throw new Error ( `/${page} answered ${response.status}: ${html.slice(0, 200)}` ) 32 + return main 33 + } 34 + } 35 + 36 + 37 + async function renders ( get : ( page : string ) => Promise < string > ) : Promise < number > { 38 + const count = Number ( await get ( 'counter' ) ) 39 + expect ( Number . isInteger ( count ) ) . toBe ( true ) 40 + return count 41 + } 42 + 43 + beforeAll ( async ( ) => { 44 + await fs . promises . mkdir ( PAGES , { recursive : true } ) 45 + await fs . promises . mkdir ( PARTIALS , { recursive : true } ) 46 + await Bun . write ( path . join ( PARTIALS , 'note.stx' ) , '<p>v1</p>' ) 47 + await Bun . write ( path . join ( PAGES , 'with-partial.stx' ) , `<main>
/var/www/reviewos-reviewos/releases/3e057f9/resources/views/layouts/app.stx:3915:34265: include error: Error loading include file note: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/note.stx'
[1m[31m╭──────────────────────────────────────────────────────────────╮[0m
[1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m
[1m[31m╰──────────────────────────────────────────────────────────────╯[0m
[36m[1mFile:[0m [2mapp.stx[0m [90m(line 3915, col 34265)[0m
[33m[1mMessage:[0m Error loading include file note: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/note.stx'
[34m[1mContext:[0m
[2m 3913: packages/stx/src/serve.ts modified +11 -4 Changes to packages/stx/src/serve.ts ⋯ 149 lines Show the 149 lines above this hunk @@ -150,6 +150,11 @@ export async function serve(options: ServeOptions = {}): Promise<ServeResult> { 150 150 * / 151 151 const fileCache = new Map < string , { content : string , mtime : number , status ? : number , headers ? : Record < string , string > } > ( ) 152 152 153 + 154 + function useFileCache ( ) : boolean { 155 + return stxOptions . cache !== false 156 + } 157 + 153 158 154 159 155 160 ⋯ 2 lines Show the 2 lines above this hunk @@ -158,7 +163,7 @@ export async function serve(options: ServeOptions = {}): Promise<ServeResult> { 158 163 const cacheKey = `${filePath}:${stats.mtimeMs}` 159 164 160 165 161 - const cached = fileCache . get ( cacheKey ) 166 + const cached = useFileCache ( ) ? fileCache . get ( cacheKey ) : undefined 162 167 if ( cached ) { 163 168 return { content : cached . content , status : cached . status , headers : cached . headers } 164 169 } ⋯ 97 lines Show the 97 lines above this hunk @@ -262,7 +267,8 @@ export async function serve(options: ServeOptions = {}): Promise<ServeResult> { 262 267 const headers = readResponseHeaders ( context ) 263 268 264 269 265 - fileCache . set ( cacheKey , { content : output , mtime : stats . mtimeMs , status , headers } ) 270 + if ( useFileCache ( ) ) 271 + fileCache . set ( cacheKey , { content : output , mtime : stats . mtimeMs , status , headers } ) 266 272 267 273 return { content : output , status , headers } 268 274 } ⋯ 6 lines Show the 6 lines above this hunk @@ -275,7 +281,7 @@ export async function serve(options: ServeOptions = {}): Promise<ServeResult> { 275 281 const cacheKey = `${filePath}:${stats.mtimeMs}` 276 282 277 283 278 - const cached = fileCache . get ( cacheKey ) 284 + const cached = useFileCache ( ) ? fileCache . get ( cacheKey ) : undefined 279 285 if ( cached ) { 280 286 return cached . content 281 287 } ⋯ 2 lines Show the 2 lines above this hunk @@ -284,7 +290,8 @@ export async function serve(options: ServeOptions = {}): Promise<ServeResult> { 284 290 const { content } = await readMarkdownFile ( filePath , stxOptions ) 285 291 286 292 287 - fileCache . set ( cacheKey , { content , mtime : stats . mtimeMs } ) 293 + if ( useFileCache ( ) ) 294 + fileCache . set ( cacheKey , { content , mtime : stats . mtimeMs } ) 288 295 289 296 return content 290 297 }
3914:
> 3915: packages/stx/test/serve/serve-cache-option.test.ts added +98 -0 Changes to packages/stx/test/serve/serve-cache-option.test.ts @@ -0,0 +1,98 @@ 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + import { afterAll , beforeAll , describe , expect , it } from 'bun:test' 12 + import fs from 'node:fs' 13 + import path from 'node:path' 14 + import { serve } from '../../src/serve' 15 + 16 + const DIR = path . join ( import . meta . dir , 'fixtures-cache-option' ) 17 + const PAGES = path . join ( DIR , 'pages' ) 18 + const PARTIALS = path . join ( DIR , 'partials' ) 19 + 20 + const servers : Array < Awaited < ReturnType < typeof serve >>> = [ ] 21 + 22 + async function start ( stxOptions : Record < string , unknown > = { } , configDir = DIR ) : Promise < ( page : string ) => Promise < string >> { 23 + const server = await serve ( { port : 0 , root : PAGES , configDir , watch : false , stxOptions : { partialsDir : PARTIALS , ... stxOptions } } ) 24 + servers . push ( server ) 25 + const base = server . url . replace ( / \ / $ / , '' ) 26 + return async ( page : string ) => { 27 + const response = await fetch ( `${base}/${page}` ) 28 + const html = await response . text ( ) 29 + const main = html . match ( / < main > ( [ \ s \ S ] * ? ) < \ / main > / ) ? . [ 1 ] 30 + if ( response . status !== 200 || main === undefined ) 31 + throw new Error ( `/${page} answered ${response.status}: ${html.slice(0, 200)}` ) 32 + return main 33 + } 34 + } 35 + 36 + 37 + async function renders ( get : ( page : string ) => Promise < string > ) : Promise < number > { 38 + const count = Number ( await get ( 'counter' ) ) 39 + expect ( Number . isInteger ( count ) ) . toBe ( true ) 40 + return count 41 + } 42 + 43 + beforeAll ( async ( ) => { 44 + await fs . promises . mkdir ( PAGES , { recursive : true } ) 45 + await fs . promises . mkdir ( PARTIALS , { recursive : true } ) 46 + await Bun . write ( path . join ( PARTIALS , 'note.stx' ) , '<p>v1</p>' ) 47 + await Bun . write ( path . join ( PAGES , 'with-partial.stx' ) , `<main>
[1m[31m╭──────────────────────────────────────────────────────────────╮[0m
[1m[31m│[0m [1m[41m ERROR [0m [31mInclude Error[0m [1m[31m│[0m
[1m[31m╰──────────────────────────────────────────────────────────────╯[0m
[36m[1mFile:[0m [2mapp.stx[0m
[33m[1mMessage:[0m Error loading include file note: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/3e057f9/resources/views/partials/note.stx'
</main>` ) 48 + await Bun . write ( path . join ( PAGES , 'counter.stx' ) , `<script server> 49 + const renders = globalThis . __stxServeCacheOptionRenders = ( globalThis . __stxServeCacheOptionRenders ? ? 0 ) + 1 50 + < / script > 51 + < main > { { renders } } < / main > 52 + `) 53 + } ) 54 + 55 + afterAll ( async ( ) => { 56 + for ( const server of servers ) 57 + server . stop ( ) 58 + await fs . promises . rm ( DIR , { recursive : true , force : true } ) 59 + } ) 60 + 61 + describe ( 'serve() with cache: false' , ( ) => { 62 + it ( 'renders every request' , async ( ) => { 63 + const get = await start ( { cache : false } ) 64 + const first = await renders ( get ) 65 + expect ( await renders ( get ) ) . toBe ( first + 1 ) 66 + } ) 67 + 68 + it ( 'serves a changed partial on the next request' , async ( ) => { 69 + const get = await start ( { cache : false } ) 70 + expect ( await get ( 'with-partial' ) ) . toBe ( '<p>v1</p>' ) 71 + await Bun . write ( path . join ( PARTIALS , 'note.stx' ) , '<p>v2</p>' ) 72 + expect ( await get ( 'with-partial' ) ) . toBe ( '<p>v2</p>' ) 73 + } ) 74 + 75 + it ( 'still caches by default' , async ( ) => { 76 + const get = await start ( ) 77 + const first = await renders ( get ) 78 + expect ( await renders ( get ) ) . toBe ( first ) 79 + } ) 80 + 81 + it ( 'follows cache in a config that reloads' , async ( ) => { 82 + const configDir = path . join ( DIR , 'reloading-config' ) 83 + const config = path . join ( configDir , 'stx.config.ts' ) 84 + await fs . promises . mkdir ( configDir , { recursive : true } ) 85 + await Bun . write ( config , 'export default { extends: [], cache: false }\n' ) 86 + const get = await start ( { } , configDir ) 87 + const first = await renders ( get ) 88 + expect ( await renders ( get ) ) . toBe ( first + 1 ) 89 + 90 + await Bun . write ( config , 'export default { extends: [], cache: true }\n' ) 91 + const cached = await renders ( get ) 92 + expect ( await renders ( get ) ) . toBe ( cached ) 93 + 94 + await Bun . write ( config , 'export default { extends: [], cache: false }\n' ) 95 + const next = await renders ( get ) 96 + expect ( await renders ( get ) ) . toBe ( next + 1 ) 97 + } ) 98 + } )
3916:
3917: [0m
</main>` ) 48 + await Bun . write ( path . join ( PAGES , 'counter.stx' ) , `<script server> 49 + const renders = globalThis . __stxServeCacheOptionRenders = ( globalThis . __stxServeCacheOptionRenders ? ? 0 ) + 1 50 + < / script > 51 + < main > { { renders } } < / main > 52 + `) 53 + } ) 54 + 55 + afterAll ( async ( ) => { 56 + for ( const server of servers ) 57 + server . stop ( ) 58 + await fs . promises . rm ( DIR , { recursive : true , force : true } ) 59 + } ) 60 + 61 + describe ( 'serve() with cache: false' , ( ) => { 62 + it ( 'renders every request' , async ( ) => { 63 + const get = await start ( { cache : false } ) 64 + const first = await renders ( get ) 65 + expect ( await renders ( get ) ) . toBe ( first + 1 ) 66 + } ) 67 + 68 + it ( 'serves a changed partial on the next request' , async ( ) => { 69 + const get = await start ( { cache : false } ) 70 + expect ( await get ( 'with-partial' ) ) . toBe ( '<p>v1</p>' ) 71 + await Bun . write ( path . join ( PARTIALS , 'note.stx' ) , '<p>v2</p>' ) 72 + expect ( await get ( 'with-partial' ) ) . toBe ( '<p>v2</p>' ) 73 + } ) 74 + 75 + it ( 'still caches by default' , async ( ) => { 76 + const get = await start ( ) 77 + const first = await renders ( get ) 78 + expect ( await renders ( get ) ) . toBe ( first ) 79 + } ) 80 + 81 + it ( 'follows cache in a config that reloads' , async ( ) => { 82 + const configDir = path . join ( DIR , 'reloading-config' ) 83 + const config = path . join ( configDir , 'stx.config.ts' ) 84 + await fs . promises . mkdir ( configDir , { recursive : true } ) 85 + await Bun . write ( config , 'export default { extends: [], cache: false }\n' ) 86 + const get = await start ( { } , configDir ) 87 + const first = await renders ( get ) 88 + expect ( await renders ( get ) ) . toBe ( first + 1 ) 89 + 90 + await Bun . write ( config , 'export default { extends: [], cache: true }\n' ) 91 + const cached = await renders ( get ) 92 + expect ( await renders ( get ) ) . toBe ( cached ) 93 + 94 + await Bun . write ( config , 'export default { extends: [], cache: false }\n' ) 95 + const next = await renders ( get ) 96 + expect ( await renders ( get ) ) . toBe ( next + 1 ) 97 + } ) 98 + } )