ReviewOS

also looking at this

stacks/stx

fix(serve): render every request when cache is false

#2106
Open mkantautas wants to merge fix/serve-cache-option into main
2 files +109 -4
packages/stx/test/serve/serve-cache-option.test.tsadded+98-0
Changes to packages/stx/test/serve/serve-cache-option.test.ts
@@ -0,0 +1,98 @@
1/**
2 * `cache: false` and the programmatic `serve()` page cache.
3 *
4 * `serve()` cached every rendered page under the page file's path and mtime
5 * and never read the `cache` option, so `cache: false` in the stx options
6 * (or the project's stx.config.ts) changed nothing: a page whose server script
7 * computes something per request kept returning its first render, and a page
8 * whose partial changed kept serving the old one until the page file itself
9 * changed.
10 */
11import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
12import fs from 'node:fs'
13import path from 'node:path'
14import { serve } from '../../src/serve'
15
16const DIR = path.join(import.meta.dir, 'fixtures-cache-option')
17const PAGES = path.join(DIR, 'pages')
18const PARTIALS = path.join(DIR, 'partials')
19
20const servers: Array<Awaited<ReturnType<typeof serve>>> = []
21
22async 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/** The page's render count, failing on anything that is not one. */
37async 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
43beforeAll(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/88069d3/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/88069d3/resources/views/partials/note.stx' ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx (line 3915, col 34265) Message: Error loading include file note: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/88069d3/resources/views/partials/note.stx' Context:  3913: 3914: > 3915:
packages/stx/test/serve/serve-cache-option.test.tsadded+98-0
Changes to packages/stx/test/serve/serve-cache-option.test.ts
@@ -0,0 +1,98 @@
1/**
2 * `cache: false` and the programmatic `serve()` page cache.
3 *
4 * `serve()` cached every rendered page under the page file's path and mtime
5 * and never read the `cache` option, so `cache: false` in the stx options
6 * (or the project's stx.config.ts) changed nothing: a page whose server script
7 * computes something per request kept returning its first render, and a page
8 * whose partial changed kept serving the old one until the page file itself
9 * changed.
10 */
11import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
12import fs from 'node:fs'
13import path from 'node:path'
14import { serve } from '../../src/serve'
15
16const DIR = path.join(import.meta.dir, 'fixtures-cache-option')
17const PAGES = path.join(DIR, 'pages')
18const PARTIALS = path.join(DIR, 'partials')
19
20const servers: Array<Awaited<ReturnType<typeof serve>>> = []
21
22async 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/** The page's render count, failing on anything that is not one. */
37async 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
43beforeAll(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/88069d3/resources/views/layouts/app.stx:3915:69374: include error: Error loading include file note: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/88069d3/resources/views/partials/note.stx' ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx (line 3915, col 69374) Message: Error loading include file note: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/88069d3/resources/views/partials/note.stx' Context:  3913: 3914: > 3915:
packages/stx/test/serve/serve-cache-option.test.tsadded+98-0
Changes to packages/stx/test/serve/serve-cache-option.test.ts
@@ -0,0 +1,98 @@
1/**
2 * `cache: false` and the programmatic `serve()` page cache.
3 *
4 * `serve()` cached every rendered page under the page file's path and mtime
5 * and never read the `cache` option, so `cache: false` in the stx options
6 * (or the project's stx.config.ts) changed nothing: a page whose server script
7 * computes something per request kept returning its first render, and a page
8 * whose partial changed kept serving the old one until the page file itself
9 * changed.
10 */
11import { afterAll, beforeAll, describe, expect, it } from 'bun:test'
12import fs from 'node:fs'
13import path from 'node:path'
14import { serve } from '../../src/serve'
15
16const DIR = path.join(import.meta.dir, 'fixtures-cache-option')
17const PAGES = path.join(DIR, 'pages')
18const PARTIALS = path.join(DIR, 'partials')
19
20const servers: Array<Awaited<ReturnType<typeof serve>>> = []
21
22async 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/** The page's render count, failing on anything that is not one. */
37async 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
43beforeAll(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> ╭──────────────────────────────────────────────────────────────╮ │  ERROR  Include Error │ ╰──────────────────────────────────────────────────────────────╯ File: app.stx Message: Error loading include file note: ENOENT: no such file or directory, open '/var/www/reviewos-reviewos/releases/88069d3/resources/views/partials/note.stx' </main>`)
48 await Bun.write(path.join(PAGES, 'counter.stx'), `<script server>
49const renders = globalThis.__stxServeCacheOptionRenders = (globalThis.__stxServeCacheOptionRenders ?? 0) + 1
50</script>
51<main>{{ renders }}</main>
52`)
53})
54
55afterAll(async () => {
56 for (const server of servers)
57 server.stop()
58 await fs.promises.rm(DIR, { recursive: true, force: true })
59})
60
61describe('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:  </main>`
)
48 await Bun.write(path.join(PAGES, 'counter.stx'), `<script server>
49const renders = globalThis.__stxServeCacheOptionRenders = (globalThis.__stxServeCacheOptionRenders ?? 0) + 1
50</script>
51<main>{{ renders }}</main>
52`)
53})
54
55afterAll(async () => {
56 for (const server of servers)
57 server.stop()
58 await fs.promises.rm(DIR, { recursive: true, force: true })
59})
60
61describe('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:  </main>`
)
48 await Bun.write(path.join(PAGES, 'counter.stx'), `<script server>
49const renders = globalThis.__stxServeCacheOptionRenders = (globalThis.__stxServeCacheOptionRenders ?? 0) + 1
50</script>
51<main>{{ renders }}</main>
52`)
53})
54
55afterAll(async () => {
56 for (const server of servers)
57 server.stop()
58 await fs.promises.rm(DIR, { recursive: true, force: true })
59})
60
61describe('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})
@endif
@endif