also looking at this
fix(dashboard): unshadow the bridged range so site switching works
#54
2 files
+45
-3
| @@ -1731,15 +1731,17 @@ async function importFromFathom(): Promise<void> { | ||
| 1731 | 1731 | else if (first.status === 413 && first.data.range?.from) { |
| 1732 | 1732 | // Too big for one request. Post the SAME files again in date windows, |
| 1733 | 1733 | // 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 | |
| 1735 | 1737 | const total = Number(first.data.estimate?.pageViews ?? 0) |
| 1736 | 1738 | 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] | |
| 1738 | 1740 | const allDays = Math.round((new Date(`${span[1]}T00:00:00Z`).getTime() - new Date(`${span[0]}T00:00:00Z`).getTime()) / 86400000) + 1 |
| 1739 | 1741 | // Two thirds of the cap per window, so an uneven distribution across the |
| 1740 | 1742 | // range does not push one window back over it. |
| 1741 | 1743 | 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) | |
| 1743 | 1745 | |
| 1744 | 1746 | for (let i = 0; i < windows.length; i++) { |
| 1745 | 1747 | const [fromDay, toDay] = windows[i] |
| @@ -0,0 +1,40 @@ | ||
| 1 | import { describe, expect, test } from 'bun:test' | |
| 2 | import { readdirSync, readFileSync, statSync } from 'node:fs' | |
| 3 | import { join, relative } from 'node:path' | |
| 4 | ||
| 5 | const VIEWS = join(import.meta.dir, '../../resources/views') | |
| 6 | ||
| 7 | function 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. | |
| 23 | describe('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 | }) | |