mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
next dev and next build run Turbopack, Next 16's default, and the webpack() hook and --webpack flags are gone. react-native is react-native-web by turbopack.resolveAlias, and .web files resolve first by turbopack.resolveExtensions, which Turbopack applies to the project and transpilePackages only: react-native-svg joins that list, or its native build is bundled (Flow source, TurboModuleRegistry) and the build fails. The export has the same 1,020 pages and the same 1,463 other files byte for byte. With chunk names set aside, 973 pages are identical and the rest differ as two webpack builds differ from each other (gui style order, a race in the code samples' colors), except /benchmarks, whose bars move by 1e-14 % because Turbopack parses a JSON float one bit off. Every head gains a same-origin preconnect and next-size-adjust. The font preload reads Turbopack's asset hash, the stale-chunk reload keys on ChunkLoadError, which Turbopack names every failed chunk, and the deploy carries turbopack/ in its cache in place of webpack/.
89 lines
3.8 KiB
TypeScript
89 lines
3.8 KiB
TypeScript
import { test, expect } from '@playwright/test'
|
||
import { ONE_SLASH, RELOADED, STALE } from '../../lib/head'
|
||
import { noise } from '../../lib/analytics/noise'
|
||
|
||
/**
|
||
* WHAT THE HEAD SETTLES BEFORE ANY MODULE RUNS, run against a stand-in window:
|
||
* the address has one slash at the front, and a page from before a deploy
|
||
* reloads once when a chunk it names is gone, and not again.
|
||
*/
|
||
|
||
function head(script: string, pathname = '/', search = '', hash = '') {
|
||
const writes: string[] = []
|
||
const listeners: Record<string, ((e: unknown) => void)[]> = {}
|
||
const stored: Record<string, string> = {}
|
||
const win: Record<string, unknown> = {}
|
||
let reloads = 0
|
||
const location = { pathname, search, hash, reload: () => reloads++ }
|
||
const history = { replaceState: (_s: unknown, _t: string, url: string) => writes.push(url) }
|
||
const sessionStorage = { getItem: (k: string) => stored[k] ?? null, setItem: (k: string, v: string) => (stored[k] = v) }
|
||
const addEventListener = (type: string, fn: (e: unknown) => void) => (listeners[type] ??= []).push(fn)
|
||
new Function('location', 'history', 'sessionStorage', 'addEventListener', 'window', script)(
|
||
location,
|
||
history,
|
||
sessionStorage,
|
||
addEventListener,
|
||
win,
|
||
)
|
||
return {
|
||
writes,
|
||
stored,
|
||
win,
|
||
reloads: () => reloads,
|
||
fire: (type: string, e: unknown) => (listeners[type] ?? []).forEach((fn) => fn(e)),
|
||
}
|
||
}
|
||
|
||
test('a doubled slash at the front of the address becomes one, keeping the query and fragment', () => {
|
||
expect(head(ONE_SLASH, '//models/openai/gpt-5-mini', '?q=1', '#a').writes).toEqual(['/models/openai/gpt-5-mini?q=1#a'])
|
||
expect(head(ONE_SLASH, '///pricing').writes).toEqual(['/pricing'])
|
||
expect(head(ONE_SLASH, '/models/zen').writes, 'an ordinary address is left alone').toEqual([])
|
||
expect(head(ONE_SLASH, '/a//b').writes, 'only the front is the host-shaped part').toEqual([])
|
||
})
|
||
|
||
test('a chunk that is gone reloads the page once, and a second failure reports instead', () => {
|
||
const failed = (file: string) =>
|
||
Object.assign(new Error(`Failed to load chunk /_next/static/chunks/${file} from module 83283`), { name: 'ChunkLoadError' })
|
||
const chunk = failed('0abc123xyz.js')
|
||
const page = head(STALE)
|
||
page.fire('unhandledrejection', { reason: chunk })
|
||
expect(page.reloads()).toBe(1)
|
||
expect(page.win.hzStale, 'the reporter is told the failure is being recovered').toBe(true)
|
||
expect(page.stored[RELOADED]).toBeTruthy()
|
||
|
||
// The reloaded page fails the same way: inside the minute, it does not reload again.
|
||
const again = head(STALE)
|
||
Object.assign(again.stored, page.stored)
|
||
again.fire('error', { error: chunk })
|
||
expect(again.reloads()).toBe(0)
|
||
expect(again.win.hzStale).toBeUndefined()
|
||
|
||
const css = head(STALE)
|
||
css.fire('error', { error: failed('0def456uvw.css') })
|
||
expect(css.reloads(), 'a stylesheet chunk is the same failure').toBe(1)
|
||
|
||
const other = head(STALE)
|
||
other.fire('error', { error: new TypeError('x is undefined') })
|
||
other.fire('unhandledrejection', { reason: undefined })
|
||
expect(other.reloads(), 'any other error is not a stale page').toBe(0)
|
||
})
|
||
|
||
test('what is not this site’s error is not reported, and the rest is', () => {
|
||
for (const err of [
|
||
'Object Not Found Matching Id:1, MethodName:update, ParamCount:4',
|
||
new Error('Object Not Found Matching Id:2, MethodName:update, ParamCount:4'),
|
||
'Script error.',
|
||
new TypeError("undefined is not an object (evaluating 'window.ethereum.selectedAddress = undefined')"),
|
||
'ResizeObserver loop completed with undelivered notifications.',
|
||
'ResizeObserver loop limit exceeded',
|
||
])
|
||
expect(noise(err), String(err)).toBe(true)
|
||
for (const err of [
|
||
new Error('Minified React error #185'),
|
||
new TypeError('Cannot read properties of null (reading "input")'),
|
||
"Script error in /chat: the reply did not parse",
|
||
undefined,
|
||
])
|
||
expect(noise(err), String(err)).toBe(false)
|
||
})
|