Files
zeekay 6c65e3e941 @hanzo/app 0.2.0: the app SDK, renamed from @hanzo/rooms
Every import, the catalog pin and the docs name @hanzo/app and
github.com/hanzoai/app. lib/rooms.ts is lib/app.ts and
components/rooms.tsx is components/app.tsx.
2026-10-22 19:07:20 -07:00

58 lines
2.8 KiB
TypeScript

import { test, expect } from '@playwright/test'
import { readdirSync, readFileSync } from 'node:fs'
import { join, relative } from 'node:path'
import { gzipSync } from 'node:zlib'
import { OUT, ROOT, read } from './export'
/**
* What the home page makes a browser download before it can run.
*
* The first load of `/` was 635 KB of gzipped script, and most of it drew
* nothing on the page: a server component importing the `@hanzo/ui` root barrel
* registered its ~150 client components — charts, 3D, editors — as client
* entries of every route, and the root not-found put framer in the first load
* of every page. Both are invisible in a screenshot, so the bytes are asserted.
*/
/** Gzipped size of the module scripts the page's HTML names (the noModule polyfill is never fetched). */
function firstLoad(page: string): number {
const html = read(page)
const chunks = new Set([...html.matchAll(/<script src="\/_next\/(static\/chunks\/[^"]+?\.js)"(?![^>]*noModule)/g)].map((m) => decodeURIComponent(m[1])))
let total = 0
for (const chunk of chunks) total += gzipSync(readFileSync(join(OUT, '_next', chunk)), { level: 6 }).length
return total
}
test('the home page first load stays under its budget', () => {
// Measured at 415 KB when this was written; the budget is that plus room for
// a section, not for a library. 445 since @hanzo/app 0.1.39, whose lib/host
// (which the landing's plans reach through lib/destination) reads the app's
// paths with @hanzo/build's route grammar: some 3.5 KB.
expect(firstLoad('index.html') / 1024).toBeLessThan(445)
})
test('a server component reaches @hanzo/ui through its client boundary', () => {
// `components/ui` is the one module that may name the root barrel without
// being a client component: it IS the boundary. A file with no 'use client'
// can render as a server component, and then the barrel ships whole.
const offenders: string[] = []
const walk = (dir: string) => {
for (const entry of readdirSync(dir, { withFileTypes: true })) {
const path = join(dir, entry.name)
if (entry.isDirectory()) walk(path)
else if (/\.tsx?$/.test(entry.name)) {
const code = readFileSync(path, 'utf8')
// The directive may follow comments, never code.
const head = code.replace(/^(?:\s+|\/\/[^\n]*|\/\*[\s\S]*?\*\/)*/, '')
if (/^['"]use client['"]/.test(head)) continue
// A code sample is a template string, and an import written in one ships
// as text (the server pages that highlight samples, since 11265a21).
const source = code.replace(/`(?:\\[\s\S]|[^`\\])*`/g, '``')
if (/from\s+['"]@hanzo\/ui['"]/.test(source)) offenders.push(relative(ROOT, path))
}
}
}
for (const dir of ['app', 'components', 'hooks', 'lib']) walk(join(ROOT, dir))
expect(offenders).toEqual([])
})