ReviewOS

also looking at this

stacks/analyticshq

fix(dashboard): unshadow the bridged range so site switching works

#54
Merged glennmichael123 wants to merge fix/switch-site-range into main
2 files +45 -3
resources/views/dashboard.stxmodified+5-3
Changes to resources/views/dashboard.stx
@@ -1731,15 +1731,17 @@ async function importFromFathom(): Promise<void> {
17311731 else if (first.status === 413 && first.data.range?.from) {
17321732 // Too big for one request. Post the SAME files again in date windows,
17331733 // sized from the estimate the server just sent back.
1734 const range = first.data.range
1734 // Not `range`: that name reaches this block from the server for switchSite,
1735 // and the bridge withholds any name the block declares, even nested like this.
1736 const importRange = first.data.range
17351737 const total = Number(first.data.estimate?.pageViews ?? 0)
17361738 const max = Number(first.data.maxRows ?? 50000)
1737 const span = fathomWindows(range.from, range.to, 3650)[0]
1739 const span = fathomWindows(importRange.from, importRange.to, 3650)[0]
17381740 const allDays = Math.round((new Date(`${span[1]}T00:00:00Z`).getTime() - new Date(`${span[0]}T00:00:00Z`).getTime()) / 86400000) + 1
17391741 // Two thirds of the cap per window, so an uneven distribution across the
17401742 // range does not push one window back over it.
17411743 const perWindow = Math.max(1, Math.floor(allDays * ((max * 0.66) / Math.max(total, 1))))
1742 const windows = fathomWindows(range.from, range.to, perWindow)
1744 const windows = fathomWindows(importRange.from, importRange.to, perWindow)
17431745
17441746 for (let i = 0; i < windows.length; i++) {
17451747 const [fromDay, toDay] = windows[i]
tests/unit/client-payload-hygiene.test.tsadded+40-0
Changes to tests/unit/client-payload-hygiene.test.ts
@@ -0,0 +1,40 @@
1import { describe, expect, test } from 'bun:test'
2import { readdirSync, readFileSync, statSync } from 'node:fs'
3import { join, relative } from 'node:path'
4
5const VIEWS = join(import.meta.dir, '../../resources/views')
6
7function walk(dir: string): string[] {
8 return readdirSync(dir).flatMap((name) => {
9 const path = join(dir, name)
10 if (statSync(path).isDirectory())
11 return walk(path)
12 return path.endsWith('.stx') ? [path] : []
13 })
14}
15
16// stx's server->client data bridge inlines a server binding into the compiled
17// <script client> block only when that block does not itself declare the
18// name. The check is textual, not scoped: a `const range = ...` inside a
19// nested function counts, the bridge then leaves the whole block without
20// `range`, and a top-level use such as switchSite's throws ReferenceError at
21// click time. Nothing at compile time says so. Every name handed to
22// defineClientPayload is therefore reserved throughout the client block.
23describe('client payload hygiene', () => {
24 test('no <script client> block re-declares a name it receives through defineClientPayload', () => {
25 const offenders: Record<string, string[]> = {}
26 for (const file of walk(VIEWS)) {
27 const source = readFileSync(file, 'utf8')
28 const server = [...source.matchAll(/<script\s+server[^>]*>([\s\S]*?)<\/script>/g)].map(match => match[1]).join('\n')
29 const payload = server.match(/defineClientPayload\(\s*\{([\s\S]*?)\}\s*\)/)
30 if (!payload)
31 continue
32 const names = payload[1].split(',').map(entry => entry.split(':')[0].trim()).filter(Boolean)
33 const client = [...source.matchAll(/<script\s+client[^>]*>([\s\S]*?)<\/script>/g)].map(match => match[1]).join('\n')
34 const shadowed = names.filter(name => new RegExp(`(?:const|let|var|function|class)\\s+${name}\\b`).test(client))
35 if (shadowed.length)
36 offenders[relative(VIEWS, file)] = shadowed
37 }
38 expect(offenders).toEqual({})
39 })
40})