diff --git a/package.json b/package.json index d8a8b679..57d1e86e 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "build": "tsgo --build && vite build", "check": "biome check --write --unsafe .", "check:types": "tsgo --project tsconfig.json --noEmit", + "perf:harness": "node --experimental-strip-types scripts/perf-harness.ts", "preview": "node dist/preview.js", "test:e2e": "playwright test", "bundle:analyze": "node --experimental-strip-types scripts/bundle-diff.ts --skip-build", @@ -42,7 +43,7 @@ "unplugin-auto-import": "^21.0.0", "unplugin-icons": "^23.0.1", "viem": "^2.47.16", - "vocs": "https://pkg.pr.new/wevm/vocs@2fb25c2", + "vocs": "https://pkg.pr.new/wevm/vocs@420", "wagmi": "^3.6.1", "waku": "1.0.0-alpha.4", "zod": "^4.3.6" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5a767b5..dc99abcd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,8 +89,8 @@ importers: specifier: ^2.47.16 version: 2.47.16(typescript@5.9.3)(zod@4.3.6) vocs: - specifier: https://pkg.pr.new/wevm/vocs@2fb25c2 - version: https://pkg.pr.new/wevm/vocs@2fb25c2(@cfworker/json-schema@4.1.1)(@types/react@19.2.14)(mermaid@11.14.0)(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.104.1))(react@19.2.5)(rollup@4.60.1)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(waku@1.0.0-alpha.4(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.104.1))(react@19.2.5)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + specifier: https://pkg.pr.new/wevm/vocs@420 + version: https://pkg.pr.new/wevm/vocs@420(@cfworker/json-schema@4.1.1)(@types/react@19.2.14)(mermaid@11.14.0)(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.104.1))(react@19.2.5)(rollup@4.60.1)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(waku@1.0.0-alpha.4(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.104.1))(react@19.2.5)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) wagmi: specifier: ^3.6.1 version: 3.6.1(@tanstack/query-core@5.99.0)(@tanstack/react-query@5.99.0(react@19.2.5))(@types/react@19.2.14)(ox@0.14.15(typescript@5.9.3)(zod@4.3.6))(react@19.2.5)(typescript@5.9.3)(viem@2.47.16(typescript@5.9.3)(zod@4.3.6)) @@ -3902,8 +3902,8 @@ packages: vite: optional: true - vocs@https://pkg.pr.new/wevm/vocs@2fb25c2: - resolution: {tarball: https://pkg.pr.new/wevm/vocs@2fb25c2} + vocs@https://pkg.pr.new/wevm/vocs@420: + resolution: {tarball: https://pkg.pr.new/wevm/vocs@420} version: 0.0.0 hasBin: true peerDependencies: @@ -8136,7 +8136,7 @@ snapshots: optionalDependencies: vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vocs@https://pkg.pr.new/wevm/vocs@2fb25c2(@cfworker/json-schema@4.1.1)(@types/react@19.2.14)(mermaid@11.14.0)(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.104.1))(react@19.2.5)(rollup@4.60.1)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(waku@1.0.0-alpha.4(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.104.1))(react@19.2.5)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): + vocs@https://pkg.pr.new/wevm/vocs@420(@cfworker/json-schema@4.1.1)(@types/react@19.2.14)(mermaid@11.14.0)(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.104.1))(react@19.2.5)(rollup@4.60.1)(typescript@5.9.3)(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(waku@1.0.0-alpha.4(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(react-dom@19.2.5(react@19.2.5))(react-server-dom-webpack@19.2.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(webpack@5.104.1))(react@19.2.5)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@base-ui/react': 1.3.0(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) '@codesandbox/sandpack-react': 2.20.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) diff --git a/scripts/perf-harness.ts b/scripts/perf-harness.ts new file mode 100644 index 00000000..4a70c894 --- /dev/null +++ b/scripts/perf-harness.ts @@ -0,0 +1,743 @@ +#!/usr/bin/env node +/** + * Route-level performance harness for the docs site. + * + * Builds and previews the site by default, runs Lighthouse multiple times per + * route, captures the median metrics, and records a no-interaction network + * profile with Playwright to spot eager off-route fetches. + * + * Usage: + * pnpm perf:harness + * pnpm perf:harness --pages /,/quickstart/faucet --runs 5 + * pnpm perf:harness --save perf-baseline.json + * pnpm perf:harness --compare perf-baseline.json + * pnpm perf:harness --url http://localhost:3001 --skip-build + */ + +import { type ChildProcess, execSync, spawn } from 'node:child_process' +import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { brotliCompressSync, constants } from 'node:zlib' +import { chromium } from '@playwright/test' + +const DEFAULT_BASE_URL = 'http://localhost:3001' +const DEFAULT_PAGES = [ + '/', + '/quickstart/faucet', + '/guide/payments/send-a-payment', + '/guide/stablecoin-dex/providing-liquidity', +] +const LIGHTHOUSE_OUTPUT = '/tmp/docs-perf-harness-lighthouse.json' +const ASSETS_DIR = 'dist/public/assets' +const STARTUP_TIMEOUT_MS = 120_000 + +const HEAVY_DEPS_PATTERNS = [ + /^wagmi/i, + /^viem/i, + /^mermaid/i, + /^monaco/i, + /^cytoscape/i, + /^katex/i, + /^accounts/i, + /^tanstack/i, + /^treemap/i, + /^sql-formatter/i, + /^QueryClientProvider/, + /^useQuery/, + /Diagram-/, + /^dagre-/, + /^cose-bilkent/, + /^elk-/, + /^arc-/, +] + +const FRAMEWORK_PATTERNS = [ + /^Link-/, + /^_layout-/, + /^_mdx-wrapper-/, + /^client-/, + /^context-/, + /^module-/, + /^facade_vocs/, + /^Head-/, + /^layout-/, + /^MdxPageContext-/, +] + +interface Flags { + url: string + pages: string[] + mobile: boolean + runs: number + compare: string + save: string + skipBuild: boolean + skipBundle: boolean +} + +interface LighthouseMetrics { + performance: number + fcp: number + lcp: number + tbt: number + cls: number + tti: number +} + +interface LighthouseAudit { + numericValue?: number | undefined +} + +interface LighthouseReport { + audits?: Record | undefined + categories?: + | { + performance?: + | { + score?: number | null | undefined + } + | undefined + } + | undefined + runtimeError?: + | { + code?: string | undefined + } + | undefined +} + +interface NetworkEntry { + bytes: number + category: string + contentType: string + url: string +} + +interface NetworkAggregate { + bytes: number + category: string + requests: number +} + +interface NetworkSummary { + byCategory: NetworkAggregate[] + offRouteBytes: number + offRouteRequests: number + topOffRouteRequests: Array<{ bytes: number; contentType: string; url: string }> + totalBytes: number + totalRequests: number +} + +interface NavigationTarget { + href: string + text: string +} + +interface NavigationMetrics { + coldClick: number + intentClick: number + target?: NavigationTarget | undefined +} + +interface RouteResult { + lighthouse: LighthouseMetrics + navigation?: NavigationMetrics | undefined + network: NetworkSummary + page: string +} + +interface BundleChunk { + bytes: number + group: 'app' | 'framework' | 'heavy-deps' + label: string +} + +interface BundleSummary { + byGroup: Array<{ bytes: number; group: 'app' | 'framework' | 'heavy-deps' }> + totalBytes: number + topChunks: BundleChunk[] +} + +interface Report { + baseUrl: string + bundle?: BundleSummary | undefined + createdAt: string + mobile: boolean + routes: RouteResult[] + runs: number +} + +function parseArgs(argv: string[]): Flags { + const args = argv.slice(2) + const flags: Flags = { + url: '', + pages: DEFAULT_PAGES, + mobile: false, + runs: 3, + compare: '', + save: '', + skipBuild: false, + skipBundle: false, + } + + for (let i = 0; i < args.length; i++) { + switch (args[i]) { + case '--url': + flags.url = args[++i] || '' + break + case '--pages': + flags.pages = (args[++i] || '').split(',').filter(Boolean) + break + case '--mobile': + flags.mobile = true + break + case '--runs': + flags.runs = Math.max(1, Number(args[++i] || '1')) + break + case '--compare': + flags.compare = args[++i] || '' + break + case '--save': + flags.save = args[++i] || '' + break + case '--skip-build': + flags.skipBuild = true + break + case '--skip-bundle': + flags.skipBundle = true + break + } + } + + return flags +} + +function formatMs(ms: number): string { + if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s` + return `${Math.round(ms)}ms` +} + +function formatBytes(bytes: number): string { + if (bytes < 1024) return `${bytes} B` + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(2)} MB` +} + +function formatCls(cls: number): string { + return cls.toFixed(3) +} + +function median(values: number[]): number { + const sorted = [...values].sort((a, b) => a - b) + const middle = Math.floor(sorted.length / 2) + if (sorted.length % 2 === 0) return (sorted[middle - 1] + sorted[middle]) / 2 + return sorted[middle] || 0 +} + +function normalizePath(pathname: string): string { + if (!pathname || pathname === '/') return '/' + return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname +} + +function categoryForChunk(label: string): 'app' | 'framework' | 'heavy-deps' { + if (HEAVY_DEPS_PATTERNS.some((pattern) => pattern.test(label))) return 'heavy-deps' + if (FRAMEWORK_PATTERNS.some((pattern) => pattern.test(label))) return 'framework' + return 'app' +} + +function bundleSummary(): BundleSummary | undefined { + const assetsDir = resolve(process.cwd(), ASSETS_DIR) + if (!existsSync(assetsDir)) return undefined + + const chunks = readdirSync(assetsDir) + .filter((file) => file.endsWith('.js')) + .map((file) => { + const filePath = join(assetsDir, file) + const raw = readFileSync(filePath) + const compressed = brotliCompressSync(raw, { + params: { [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY }, + }) + + return { + bytes: compressed.length, + group: categoryForChunk(file), + label: file, + } satisfies BundleChunk + }) + .sort((a, b) => b.bytes - a.bytes) + + return { + byGroup: (['framework', 'heavy-deps', 'app'] as const).map((group) => ({ + bytes: chunks + .filter((chunk) => chunk.group === group) + .reduce((sum, chunk) => sum + chunk.bytes, 0), + group, + })), + totalBytes: chunks.reduce((sum, chunk) => sum + chunk.bytes, 0), + topChunks: chunks.slice(0, 10), + } +} + +function runLighthouse(url: string, mobile: boolean): LighthouseReport | null { + const preset = mobile ? 'perf' : 'desktop' + const command = + `npx lighthouse "${url}" --output=json --output-path=${LIGHTHOUSE_OUTPUT} ` + + `--chrome-flags="--headless --no-sandbox --ignore-certificate-errors" --preset=${preset} --quiet` + + try { + execSync(command, { + cwd: '/tmp', + encoding: 'utf-8', + maxBuffer: 50 * 1024 * 1024, + stdio: ['pipe', 'pipe', 'pipe'], + }) + + const report = JSON.parse(readFileSync(LIGHTHOUSE_OUTPUT, 'utf-8')) as LighthouseReport + if (report.runtimeError?.code) return null + return report + } catch { + return null + } +} + +function extractLighthouseMetrics(report: LighthouseReport): LighthouseMetrics { + const audits = report.audits ?? {} + return { + performance: Math.round((report.categories?.performance?.score ?? 0) * 100), + fcp: audits['first-contentful-paint']?.numericValue ?? 0, + lcp: audits['largest-contentful-paint']?.numericValue ?? 0, + tbt: audits['total-blocking-time']?.numericValue ?? 0, + cls: audits['cumulative-layout-shift']?.numericValue ?? 0, + tti: audits.interactive?.numericValue ?? 0, + } +} + +async function getSidebarNavigationTarget(page: import('@playwright/test').Page, pagePath: string) { + const currentPath = normalizePath(new URL(pagePath, 'http://localhost').pathname) + + return await page.evaluate((path) => { + const normalize = (pathname: string) => { + if (!pathname || pathname === '/') return '/' + return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname + } + + const links = Array.from(document.querySelectorAll('a[data-v-sidebar-item]')) + .map((element) => { + const href = element.getAttribute('href') + if (!href || !href.startsWith('/')) return null + + return { + active: element.hasAttribute('data-active'), + href, + path: normalize(new URL(href, window.location.origin).pathname), + text: element.textContent?.trim() || href, + } + }) + .filter((value): value is { active: boolean; href: string; path: string; text: string } => + Boolean(value), + ) + + const candidates = links.filter((link) => link.path !== path) + if (candidates.length === 0) return undefined + + // Prefer the next visible sibling around the active item so we measure a realistic + // sidebar navigation instead of jumping to an arbitrary route elsewhere in the tree. + const activeIndex = links.findIndex((link) => link.active || link.path === path) + if (activeIndex >= 0) { + for (let i = activeIndex + 1; i < links.length; i++) { + if (links[i]?.path !== path) + return { href: links[i]?.href || '', text: links[i]?.text || '' } + } + for (let i = activeIndex - 1; i >= 0; i--) { + if (links[i]?.path !== path) + return { href: links[i]?.href || '', text: links[i]?.text || '' } + } + } + + const candidate = candidates[0] + return candidate ? { href: candidate.href, text: candidate.text } : undefined + }, currentPath) +} + +async function waitForPathname(page: import('@playwright/test').Page, targetHref: string) { + const targetPath = normalizePath(new URL(targetHref, 'http://localhost').pathname) + + await page.waitForFunction( + (expectedPath) => { + const normalize = (pathname: string) => { + if (!pathname || pathname === '/') return '/' + return pathname.endsWith('/') ? pathname.slice(0, -1) : pathname + } + + return normalize(window.location.pathname) === expectedPath + }, + targetPath, + { timeout: 30_000 }, + ) +} + +async function measureNavigationScenario(options: { + baseUrl: string + hoverFirst: boolean + pagePath: string + target: NavigationTarget +}) { + const { baseUrl, hoverFirst, pagePath, target } = options + const browser = await chromium.launch({ headless: true }) + const context = await browser.newContext({ ignoreHTTPSErrors: true }) + const page = await context.newPage() + + try { + await page.goto(new URL(pagePath, `${baseUrl}/`).toString(), { waitUntil: 'networkidle' }) + + const selector = `a[data-v-sidebar-item][href="${target.href}"]` + const targetLink = page.locator(selector).first() + await targetLink.waitFor({ state: 'visible' }) + + if (hoverFirst) { + await targetLink.hover() + await page.waitForTimeout(300) + } + + const startedAt = performance.now() + await Promise.all([waitForPathname(page, target.href), targetLink.click()]) + await page.waitForLoadState('networkidle').catch(() => {}) + + return performance.now() - startedAt + } finally { + await context.close() + await browser.close() + } +} + +async function captureNavigationMetrics(baseUrl: string, pagePath: string, runs: number) { + const browser = await chromium.launch({ headless: true }) + const context = await browser.newContext({ ignoreHTTPSErrors: true }) + const page = await context.newPage() + + try { + await page.goto(new URL(pagePath, `${baseUrl}/`).toString(), { waitUntil: 'networkidle' }) + const target = await getSidebarNavigationTarget(page, pagePath) + if (!target) return undefined + + const coldRuns: number[] = [] + const intentRuns: number[] = [] + + for (let i = 0; i < runs; i++) { + coldRuns.push( + await measureNavigationScenario({ baseUrl, hoverFirst: false, pagePath, target }), + ) + intentRuns.push( + await measureNavigationScenario({ baseUrl, hoverFirst: true, pagePath, target }), + ) + } + + return { + coldClick: median(coldRuns), + intentClick: median(intentRuns), + target, + } satisfies NavigationMetrics + } finally { + await context.close() + await browser.close() + } +} + +async function captureNetworkSummary(baseUrl: string, pagePath: string): Promise { + const base = new URL(baseUrl) + const targetUrl = new URL(pagePath, `${base.origin}/`).toString() + const browser = await chromium.launch({ headless: true }) + const context = await browser.newContext({ ignoreHTTPSErrors: true }) + const page = await context.newPage() + const entries: NetworkEntry[] = [] + const pending: Promise[] = [] + + page.on('requestfinished', (request) => { + pending.push( + (async () => { + const response = await request.response() + if (!response) return + + const url = request.url() + if (url.startsWith('data:')) return + + const headers = response.headers() + const contentType = headers['content-type']?.split(';')[0] ?? '' + const requestUrl = new URL(url) + const isExternal = requestUrl.origin !== base.origin + const pathname = normalizePath(requestUrl.pathname) + const currentPage = normalizePath(new URL(pagePath, `${base.origin}/`).pathname) + const resourceType = request.resourceType() + const sizes = await request.sizes().catch(() => null) + const contentLength = Number(headers['content-length'] || '0') + const bytes = + (sizes?.responseBodySize || 0) + + (sizes?.responseHeadersSize || 0) + + (sizes?.requestHeadersSize || 0) || contentLength + + // Bucket requests by their role in the first paint so off-route bytes make eager + // prefetch behavior obvious in the report. + const category = (() => { + if (isExternal) return 'third-party' + if (pathname.startsWith('/assets/') || contentType.includes('javascript')) return 'js' + if (contentType.includes('text/css') || pathname.endsWith('.css')) return 'css' + if (resourceType === 'font' || contentType.startsWith('font/')) return 'font' + if (resourceType === 'image' || contentType.startsWith('image/')) return 'image' + if (pathname.startsWith('/api/')) return 'api' + if (pathname === currentPage) return 'current-route' + return 'off-route' + })() + + entries.push({ bytes, category, contentType, url }) + })(), + ) + }) + + await page.goto(targetUrl, { waitUntil: 'networkidle' }) + await page.waitForTimeout(1000) + await Promise.allSettled(pending) + await context.close() + await browser.close() + + const byCategory = Array.from( + entries.reduce((map, entry) => { + const aggregate = map.get(entry.category) ?? { + bytes: 0, + category: entry.category, + requests: 0, + } + aggregate.bytes += entry.bytes + aggregate.requests += 1 + map.set(entry.category, aggregate) + return map + }, new Map()), + ) + .map(([, value]) => value) + .sort((a, b) => b.bytes - a.bytes) + + const offRoute = entries.filter((entry) => entry.category === 'off-route') + + return { + byCategory, + offRouteBytes: offRoute.reduce((sum, entry) => sum + entry.bytes, 0), + offRouteRequests: offRoute.length, + topOffRouteRequests: offRoute + .sort((a, b) => b.bytes - a.bytes) + .slice(0, 10) + .map((entry) => ({ bytes: entry.bytes, contentType: entry.contentType, url: entry.url })), + totalBytes: entries.reduce((sum, entry) => sum + entry.bytes, 0), + totalRequests: entries.length, + } +} + +async function waitForServer(url: string): Promise { + const startedAt = Date.now() + while (Date.now() - startedAt < STARTUP_TIMEOUT_MS) { + try { + const response = await fetch(url) + if (response.ok) return + } catch {} + await new Promise((resolvePromise) => setTimeout(resolvePromise, 500)) + } + + throw new Error(`timed out waiting for preview server at ${url}`) +} + +function startPreviewServer(): ChildProcess { + return spawn('pnpm', ['preview'], { + cwd: process.cwd(), + env: process.env, + stdio: ['ignore', 'pipe', 'pipe'], + }) +} + +async function detectPreviewUrl(child: ChildProcess): Promise { + const stdout = child.stdout + const stderr = child.stderr + if (!stdout || !stderr) return DEFAULT_BASE_URL + + return await new Promise((resolvePromise, reject) => { + const timeout = setTimeout(() => { + cleanup() + resolvePromise(DEFAULT_BASE_URL) + }, 5_000) + + const onStdout = (chunk: Buffer | string) => { + const output = chunk.toString() + const match = output.match(/Starting preview server at (https?:\/\/[^\s]+)/) + if (!match?.[1]) return + cleanup() + resolvePromise(match[1]) + } + + const onStderr = (chunk: Buffer | string) => { + const output = chunk.toString().trim() + if (!output) return + cleanup() + reject(new Error(output)) + } + + const onExit = (code: number | null) => { + cleanup() + reject(new Error(`preview server exited early with code ${code ?? 'unknown'}`)) + } + + function cleanup() { + clearTimeout(timeout) + stdout.off('data', onStdout) + stderr.off('data', onStderr) + child.off('exit', onExit) + } + + stdout.on('data', onStdout) + stderr.on('data', onStderr) + child.on('exit', onExit) + }) +} + +function stopPreviewServer(child: ChildProcess | undefined) { + if (!child || child.killed) return + child.kill('SIGTERM') +} + +function compareNumber(current: number, baseline: number, lowerIsBetter: boolean): string { + const delta = current - baseline + const sign = delta > 0 ? '+' : '' + const good = lowerIsBetter ? delta < 0 : delta > 0 + const bad = lowerIsBetter ? delta > 0 : delta < 0 + const formatted = `${sign}${delta.toFixed(1)}` + + if (good) return `\x1b[32m${formatted}\x1b[0m` + if (bad) return `\x1b[31m${formatted}\x1b[0m` + return formatted +} + +function printReport(report: Report, baseline?: Report | undefined) { + console.log('\nDocs Performance Harness') + console.log(`Base URL: ${report.baseUrl}`) + console.log(`Mode: ${report.mobile ? 'mobile' : 'desktop'}`) + console.log(`Runs per route: ${report.runs}`) + + for (const route of report.routes) { + console.log(`\n${route.page}`) + console.log( + ` Lighthouse perf ${route.lighthouse.performance} FCP ${formatMs(route.lighthouse.fcp)} LCP ${formatMs(route.lighthouse.lcp)} TBT ${formatMs(route.lighthouse.tbt)} CLS ${formatCls(route.lighthouse.cls)}`, + ) + console.log( + ` Network ${route.network.totalRequests} req ${formatBytes(route.network.totalBytes)} total ${route.network.offRouteRequests} off-route req ${formatBytes(route.network.offRouteBytes)} off-route`, + ) + + const topOffRoute = route.network.topOffRouteRequests.slice(0, 3) + if (topOffRoute.length > 0) { + for (const request of topOffRoute) { + console.log(` off-route ${formatBytes(request.bytes).padStart(8)} ${request.url}`) + } + } + + if (route.navigation) { + console.log( + ` Navigate cold ${formatMs(route.navigation.coldClick)} intent ${formatMs(route.navigation.intentClick)} target ${route.navigation.target?.href ?? 'n/a'}`, + ) + } + + if (!baseline) continue + const baselineRoute = baseline.routes.find((item) => item.page === route.page) + if (!baselineRoute) continue + + console.log( + ` Delta perf ${compareNumber(route.lighthouse.performance, baselineRoute.lighthouse.performance, false)} FCP ${compareNumber(route.lighthouse.fcp, baselineRoute.lighthouse.fcp, true)} LCP ${compareNumber(route.lighthouse.lcp, baselineRoute.lighthouse.lcp, true)} off-route bytes ${compareNumber(route.network.offRouteBytes, baselineRoute.network.offRouteBytes, true)}`, + ) + + if (route.navigation && baselineRoute.navigation) { + console.log( + ` Nav Delta cold ${compareNumber(route.navigation.coldClick, baselineRoute.navigation.coldClick, true)} intent ${compareNumber(route.navigation.intentClick, baselineRoute.navigation.intentClick, true)}`, + ) + } + } + + if (!report.bundle) return + + console.log(`\nBundle JS total (brotli): ${formatBytes(report.bundle.totalBytes)}`) + for (const group of report.bundle.byGroup) { + console.log(` ${group.group}: ${formatBytes(group.bytes)}`) + } + for (const chunk of report.bundle.topChunks.slice(0, 5)) { + console.log(` top chunk ${formatBytes(chunk.bytes).padStart(8)} ${chunk.label}`) + } + + if (!baseline?.bundle) return + console.log( + ` Delta total: ${compareNumber(report.bundle.totalBytes, baseline.bundle.totalBytes, true)}`, + ) +} + +async function main() { + const flags = parseArgs(process.argv) + let previewServer: ChildProcess | undefined + let baseUrl = flags.url || DEFAULT_BASE_URL + + try { + if (!flags.url) { + if (!flags.skipBuild) execSync('pnpm build', { cwd: process.cwd(), stdio: 'inherit' }) + previewServer = startPreviewServer() + baseUrl = await detectPreviewUrl(previewServer) + await waitForServer(baseUrl) + } + + const routes: RouteResult[] = [] + for (const page of flags.pages) { + process.stdout.write(`\nMeasuring ${page} ...`) + + const lighthouseRuns: LighthouseMetrics[] = [] + for (let i = 0; i < flags.runs; i++) { + const report = runLighthouse(new URL(page, `${baseUrl}/`).toString(), flags.mobile) + if (report) lighthouseRuns.push(extractLighthouseMetrics(report)) + } + + if (lighthouseRuns.length === 0) { + throw new Error(`Lighthouse failed for ${page}`) + } + + routes.push({ + lighthouse: { + performance: Math.round(median(lighthouseRuns.map((run) => run.performance))), + fcp: median(lighthouseRuns.map((run) => run.fcp)), + lcp: median(lighthouseRuns.map((run) => run.lcp)), + tbt: median(lighthouseRuns.map((run) => run.tbt)), + cls: median(lighthouseRuns.map((run) => run.cls)), + tti: median(lighthouseRuns.map((run) => run.tti)), + }, + navigation: await captureNavigationMetrics(baseUrl, page, flags.runs), + network: await captureNetworkSummary(baseUrl, page), + page, + }) + process.stdout.write(' done') + } + + const report: Report = { + baseUrl, + bundle: flags.skipBundle ? undefined : bundleSummary(), + createdAt: new Date().toISOString(), + mobile: flags.mobile, + routes, + runs: flags.runs, + } + const baseline = flags.compare + ? (JSON.parse(readFileSync(flags.compare, 'utf-8')) as Report) + : undefined + + printReport(report, baseline) + + if (flags.save) { + writeFileSync(flags.save, JSON.stringify(report, null, 2)) + console.log(`\nSaved report to ${flags.save}`) + } + } finally { + stopPreviewServer(previewServer) + } +} + +main().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/vocs.config.ts b/vocs.config.ts index 81a4baf3..d1bb43cb 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -68,705 +68,709 @@ export default defineConfig({ }, ], sidebar: { - '/': [ - { - text: 'Home', - link: '/', - }, - { - text: 'Using Tempo with AI', - link: '/guide/using-tempo-with-ai', - }, - { - text: 'Build on Tempo', - items: [ - { - text: 'Getting Funds on Tempo', - link: '/guide/getting-funds', - }, - { - text: 'Create & Use Accounts', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/guide/use-accounts', - }, - { - text: 'Embed Tempo Wallet', - link: '/guide/use-accounts/embed-tempo-wallet', - }, - { - text: 'Embed domain-bound Passkeys', - link: '/guide/use-accounts/embed-passkeys', - }, - { - text: 'Connect to other wallets', - link: '/guide/use-accounts/connect-to-wallets', - }, - { - text: 'Add funds to your balance', - link: '/guide/use-accounts/add-funds', - }, - ], - }, - { - text: 'Make Payments', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/guide/payments', - }, - { - text: 'Send a payment', - link: '/guide/payments/send-a-payment', - }, - { - text: 'Accept a payment', - link: '/guide/payments/accept-a-payment', - }, - { - text: 'Attach a transfer memo', - link: '/guide/payments/transfer-memos', - }, - { - text: 'Pay fees in any stablecoin', - link: '/guide/payments/pay-fees-in-any-stablecoin', - }, - { - text: 'Sponsor user fees', - link: '/guide/payments/sponsor-user-fees', - }, - { - text: 'Send parallel transactions', - link: '/guide/payments/send-parallel-transactions', - }, - // { - // text: 'Start a subscription 🚧', - // disabled: true, - // link: '/guide/payments/start-a-subscription', - // }, - // { - // text: 'Private payments 🚧', - // disabled: true, - // link: '/guide/payments/private-payments', - // }, - ], - }, - { - text: 'Issue Stablecoins', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/guide/issuance', - }, - { - text: 'Create a stablecoin', - link: '/guide/issuance/create-a-stablecoin', - }, - { - text: 'Mint stablecoins', - link: '/guide/issuance/mint-stablecoins', - }, - { - text: 'Use your stablecoin for fees', - link: '/guide/issuance/use-for-fees', - }, - { - text: 'Distribute rewards', - link: '/guide/issuance/distribute-rewards', - }, - { - text: 'Manage your stablecoin', - link: '/guide/issuance/manage-stablecoin', - }, - ], - }, - { - text: 'Exchange Stablecoins', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/guide/stablecoin-dex', - }, - { - text: 'Managing fee liquidity', - link: '/guide/stablecoin-dex/managing-fee-liquidity', - }, - { - text: 'Executing swaps', - link: '/guide/stablecoin-dex/executing-swaps', - }, - { - text: 'View the orderbook', - link: '/guide/stablecoin-dex/view-the-orderbook', - }, - { - text: 'Providing liquidity', - link: '/guide/stablecoin-dex/providing-liquidity', - }, - ], - }, - { - text: 'Make Agentic Payments', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/guide/machine-payments', - }, - { - text: 'Client quickstart', - link: '/guide/machine-payments/client', - }, - { - text: 'Agent quickstart', - link: '/guide/machine-payments/agent', - }, - { - text: 'Server quickstart', - link: '/guide/machine-payments/server', - }, - { - text: 'Accept one-time payments', - link: '/guide/machine-payments/one-time-payments', - }, - { - text: 'Accept pay-as-you-go payments', - link: '/guide/machine-payments/pay-as-you-go', - }, - { - text: 'Accept streamed payments', - link: '/guide/machine-payments/streamed-payments', - }, - { - text: 'Use Cases', - collapsed: true, - items: [ - { - text: 'Monetize Your API', - link: '/guide/machine-payments/use-cases/monetize-your-api', - }, - { - text: 'AI Model Access', - link: '/guide/machine-payments/use-cases/ai-model-access', - }, - { - text: 'Web Search & Research', - link: '/guide/machine-payments/use-cases/web-search-and-research', - }, - { - text: 'Image & Media Generation', - link: '/guide/machine-payments/use-cases/image-and-media-generation', - }, - { - text: 'Browser Automation', - link: '/guide/machine-payments/use-cases/browser-automation', - }, - { - text: 'Compute & Code Execution', - link: '/guide/machine-payments/use-cases/compute-and-code-execution', - }, - { - text: 'Storage', - link: '/guide/machine-payments/use-cases/storage', - }, - { - text: 'Blockchain Data & Analytics', - link: '/guide/machine-payments/use-cases/blockchain-data', - }, - { - text: 'Financial & Market Data', - link: '/guide/machine-payments/use-cases/financial-data', - }, - { - text: 'Data Enrichment & Leads', - link: '/guide/machine-payments/use-cases/data-enrichment-and-leads', - }, - { - text: 'Translation & Language', - link: '/guide/machine-payments/use-cases/translation-and-language', - }, - { - text: 'Maps & Location Data', - link: '/guide/machine-payments/use-cases/location-and-maps', - }, - { - text: 'Agent-to-Agent Services', - link: '/guide/machine-payments/use-cases/agent-to-agent', - }, - ], - }, - ], - }, - ], - }, - { - text: 'Integrate Tempo', - items: [ - { - text: 'Overview', - link: '/quickstart/integrate-tempo', - }, - { - text: 'Connect to the Network', - link: '/quickstart/connection-details', - }, - { - text: 'Use Tempo Transactions', - link: '/guide/tempo-transaction', - }, - { - text: 'Get Testnet Faucet Funds', - link: '/quickstart/faucet', - }, - { - text: 'EVM Differences', - link: '/quickstart/evm-compatibility', - }, - { - text: 'Predeployed Contracts', - link: '/quickstart/predeployed-contracts', - }, - { - text: 'Token List Registry', - link: '/quickstart/tokenlist', - }, - { - text: 'Wallet Developers', - link: '/quickstart/wallet-developers', - }, - { - text: 'Contract Verification', - link: '/quickstart/verify-contracts', - }, - { - text: 'Bridging', - collapsed: true, - items: [ - { - text: 'Bridge via LayerZero', - link: '/guide/bridge-layerzero', - }, - { - text: 'Bridge via Relay', - link: '/guide/bridge-relay', - }, - ], - }, - { - text: 'Ecosystem', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/ecosystem', - }, - { - text: 'Bridges & Exchanges', - link: '/ecosystem/bridges', - }, - { - text: 'Data & Analytics', - link: '/ecosystem/data-analytics', - }, - { - text: 'Block Explorers', - link: '/ecosystem/block-explorers', - }, - { - text: 'Wallets', - link: '/ecosystem/wallets', - }, - { - text: 'Smart Contract Libraries', - link: '/ecosystem/smart-contract-libraries', - }, - { - text: 'Node Infrastructure', - link: '/ecosystem/node-infrastructure', - }, - { - text: 'Security & Compliance', - link: '/ecosystem/security-compliance', - }, - { - text: 'Issuance & Orchestration', - link: '/ecosystem/orchestration', - }, - ], - }, - ], - }, - { - text: 'Tempo Protocol Specs', - items: [ - { - text: 'Overview', - link: '/protocol', - }, - { - text: 'TIP-20 Tokens', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/protocol/tip20/overview', - }, - { - text: 'Specification', - link: '/protocol/tip20/spec', - }, - { - text: 'Reference Implementation', - link: 'https://github.com/tempoxyz/tempo/blob/main/tips/ref-impls/src/TIP20.sol', - }, - { - text: 'Rust Implementation', - link: 'https://github.com/tempoxyz/tempo/tree/main/crates/precompiles/src/tip20', - }, - ], - }, - { - text: 'TIP-20 Rewards', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/protocol/tip20-rewards/overview', - }, - { - text: 'Specification', - link: '/protocol/tip20-rewards/spec', - }, - ], - }, - { - text: 'TIP-403 Policies', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/protocol/tip403/overview', - }, - { - text: 'Specification', - link: '/protocol/tip403/spec', - }, - { - text: 'Reference Implementation', - link: 'https://github.com/tempoxyz/tempo/blob/main/tips/ref-impls/src/TIP403Registry.sol', - }, - { - text: 'Rust Implementation', - link: 'https://github.com/tempoxyz/tempo/tree/main/crates/precompiles/src/tip403_registry', - }, - ], - }, - { - text: 'Fees', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/protocol/fees', - }, - { - text: 'Specification', - link: '/protocol/fees/spec-fee', - }, - { - text: 'Fee AMM', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/protocol/fees/fee-amm', - }, - { - text: 'Specification', - link: '/protocol/fees/spec-fee-amm', - }, - { - text: 'Reference Implementation', - link: 'https://github.com/tempoxyz/tempo/blob/main/tips/ref-impls/src/FeeManager.sol', - }, - { - text: 'Rust Implementation', - link: 'https://github.com/tempoxyz/tempo/tree/main/crates/precompiles/src/tip_fee_manager', - }, - ], - }, - ], - }, - { - text: 'Tempo Transactions', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/protocol/transactions', - }, - { - text: 'Specification', - link: '/protocol/transactions/spec-tempo-transaction', - }, - { - text: 'EIP-4337 Comparison', - link: '/protocol/transactions/eip-4337', - }, - { - text: 'EIP-7702 Comparison', - link: '/protocol/transactions/eip-7702', - }, - { - text: 'Account Keychain Precompile Specification', - link: '/protocol/transactions/AccountKeychain', - }, - { - text: 'Rust Implementation', - link: 'https://github.com/tempoxyz/tempo/blob/main/crates/primitives/src/transaction/tempo_transaction.rs', - }, - ], - }, - { - text: 'Blockspace', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/protocol/blockspace/overview', - }, - { - text: 'Payment Lane Specification', - link: '/protocol/blockspace/payment-lane-specification', - }, - { - text: 'Consensus and Finality', - link: '/protocol/blockspace/consensus', - }, - ], - }, - { - text: 'Stablecoin DEX', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/protocol/exchange', - }, - { - text: 'Specification', - link: '/protocol/exchange/spec', - }, - { - text: 'Quote Tokens', - link: '/protocol/exchange/quote-tokens', - }, - { - text: 'Executing Swaps', - link: '/protocol/exchange/executing-swaps', - }, - { - text: 'Providing Liquidity', - link: '/protocol/exchange/providing-liquidity', - }, - { - text: 'DEX Balance', - link: '/protocol/exchange/exchange-balance', - }, - { - text: 'Reference Implementation', - link: 'https://github.com/tempoxyz/tempo/blob/main/tips/ref-impls/src/stablecoinDex.sol', - }, - { - text: 'Rust Implementation', - link: 'https://github.com/tempoxyz/tempo/tree/main/crates/precompiles/src/stablecoin_exchange', - }, - ], - }, - { - text: 'Network Upgrades', - collapsed: true, - items: [ - { - text: 'T3', - link: '/protocol/upgrades/t3', - }, - { - text: 'T2', - link: '/protocol/upgrades/t2', - }, - ], - }, - { - text: 'TIPs', - link: '/protocol/tips', - }, - ], - }, - { - text: 'Tempo Developer Tools', - items: [ - { - text: 'Accounts SDK', - link: '/accounts', - }, - { - text: 'CLI', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/cli', - }, - { - text: 'Wallet', - link: '/cli/wallet', - }, - { - text: 'Request', - link: '/cli/request', - }, - { - text: 'Download', - link: '/cli/download', - }, - { - text: 'Node', - link: '/cli/node', - }, - ], - }, - { - text: 'SDKs', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/sdk', - }, - { - text: 'TypeScript', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/sdk/typescript', - }, - { - text: 'Viem Reference', - link: 'https://viem.sh/tempo', - }, - { - text: 'Wagmi Reference', - link: 'https://wagmi.sh/tempo', - }, - { - text: 'Prool Reference', - items: [ - { - text: 'Setup', - link: '/sdk/typescript/prool/setup', - }, - ], - }, - ], - }, - { - text: 'Go', - link: '/sdk/go', - }, - { - text: 'Foundry', - link: '/sdk/foundry', - }, - { - text: 'Python', - link: '/sdk/python', - }, - { - text: 'Rust', - link: '/sdk/rust', - }, - ], - }, - ], - }, - { - text: 'Run a Tempo Node', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/guide/node', - }, - { - text: 'System Requirements', - link: '/guide/node/system-requirements', - }, - { - text: 'Installation', - link: '/guide/node/installation', - }, - { - text: 'Running an RPC Node', - link: '/guide/node/rpc', - }, - { - text: 'Running a validator', - items: [ - { - text: 'Overview', - link: '/guide/node/validator', - }, - { - text: 'Operation', - link: '/guide/node/operate-validator', - }, - { - text: 'ValidatorConfig V2', - link: '/guide/node/validator-config-v2', - }, - ], - }, - { - text: 'Network Upgrades and Releases', - link: '/guide/node/network-upgrades', - }, - { - text: 'Changelog', - link: '/changelog', - }, - ], - }, - // { - // text: 'Infrastructure & Tooling', - // items: [ - // { - // text: 'Overview', - // link: '/guide/infrastructure', - // }, - // { - // text: 'Data Indexers', - // link: '/guide/infrastructure/data-indexers', - // }, - // { - // text: 'Developer Tools', - // link: '/guide/infrastructure/developer-tools', - // }, - // { - // text: 'Node Providers', - // link: '/guide/infrastructure/node-providers', - // }, - // ], - // }, - ], + '/': { + items: [ + { + text: 'Home', + link: '/', + }, + { + text: 'Using Tempo with AI', + link: '/guide/using-tempo-with-ai', + }, + { + text: 'Build on Tempo', + items: [ + { + text: 'Getting Funds on Tempo', + link: '/guide/getting-funds', + }, + { + text: 'Create & Use Accounts', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/guide/use-accounts', + }, + { + text: 'Embed Tempo Wallet', + link: '/guide/use-accounts/embed-tempo-wallet', + }, + { + text: 'Embed domain-bound Passkeys', + link: '/guide/use-accounts/embed-passkeys', + }, + { + text: 'Connect to other wallets', + link: '/guide/use-accounts/connect-to-wallets', + }, + { + text: 'Add funds to your balance', + link: '/guide/use-accounts/add-funds', + }, + ], + }, + { + text: 'Make Payments', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/guide/payments', + }, + { + text: 'Send a payment', + link: '/guide/payments/send-a-payment', + }, + { + text: 'Accept a payment', + link: '/guide/payments/accept-a-payment', + }, + { + text: 'Attach a transfer memo', + link: '/guide/payments/transfer-memos', + }, + { + text: 'Pay fees in any stablecoin', + link: '/guide/payments/pay-fees-in-any-stablecoin', + }, + { + text: 'Sponsor user fees', + link: '/guide/payments/sponsor-user-fees', + }, + { + text: 'Send parallel transactions', + link: '/guide/payments/send-parallel-transactions', + }, + // { + // text: 'Start a subscription 🚧', + // disabled: true, + // link: '/guide/payments/start-a-subscription', + // }, + // { + // text: 'Private payments 🚧', + // disabled: true, + // link: '/guide/payments/private-payments', + // }, + ], + }, + { + text: 'Issue Stablecoins', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/guide/issuance', + }, + { + text: 'Create a stablecoin', + link: '/guide/issuance/create-a-stablecoin', + }, + { + text: 'Mint stablecoins', + link: '/guide/issuance/mint-stablecoins', + }, + { + text: 'Use your stablecoin for fees', + link: '/guide/issuance/use-for-fees', + }, + { + text: 'Distribute rewards', + link: '/guide/issuance/distribute-rewards', + }, + { + text: 'Manage your stablecoin', + link: '/guide/issuance/manage-stablecoin', + }, + ], + }, + { + text: 'Exchange Stablecoins', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/guide/stablecoin-dex', + }, + { + text: 'Managing fee liquidity', + link: '/guide/stablecoin-dex/managing-fee-liquidity', + }, + { + text: 'Executing swaps', + link: '/guide/stablecoin-dex/executing-swaps', + }, + { + text: 'View the orderbook', + link: '/guide/stablecoin-dex/view-the-orderbook', + }, + { + text: 'Providing liquidity', + link: '/guide/stablecoin-dex/providing-liquidity', + }, + ], + }, + { + text: 'Make Agentic Payments', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/guide/machine-payments', + }, + { + text: 'Client quickstart', + link: '/guide/machine-payments/client', + }, + { + text: 'Agent quickstart', + link: '/guide/machine-payments/agent', + }, + { + text: 'Server quickstart', + link: '/guide/machine-payments/server', + }, + { + text: 'Accept one-time payments', + link: '/guide/machine-payments/one-time-payments', + }, + { + text: 'Accept pay-as-you-go payments', + link: '/guide/machine-payments/pay-as-you-go', + }, + { + text: 'Accept streamed payments', + link: '/guide/machine-payments/streamed-payments', + }, + { + text: 'Use Cases', + collapsed: true, + items: [ + { + text: 'Monetize Your API', + link: '/guide/machine-payments/use-cases/monetize-your-api', + }, + { + text: 'AI Model Access', + link: '/guide/machine-payments/use-cases/ai-model-access', + }, + { + text: 'Web Search & Research', + link: '/guide/machine-payments/use-cases/web-search-and-research', + }, + { + text: 'Image & Media Generation', + link: '/guide/machine-payments/use-cases/image-and-media-generation', + }, + { + text: 'Browser Automation', + link: '/guide/machine-payments/use-cases/browser-automation', + }, + { + text: 'Compute & Code Execution', + link: '/guide/machine-payments/use-cases/compute-and-code-execution', + }, + { + text: 'Storage', + link: '/guide/machine-payments/use-cases/storage', + }, + { + text: 'Blockchain Data & Analytics', + link: '/guide/machine-payments/use-cases/blockchain-data', + }, + { + text: 'Financial & Market Data', + link: '/guide/machine-payments/use-cases/financial-data', + }, + { + text: 'Data Enrichment & Leads', + link: '/guide/machine-payments/use-cases/data-enrichment-and-leads', + }, + { + text: 'Translation & Language', + link: '/guide/machine-payments/use-cases/translation-and-language', + }, + { + text: 'Maps & Location Data', + link: '/guide/machine-payments/use-cases/location-and-maps', + }, + { + text: 'Agent-to-Agent Services', + link: '/guide/machine-payments/use-cases/agent-to-agent', + }, + ], + }, + ], + }, + ], + }, + { + text: 'Integrate Tempo', + items: [ + { + text: 'Overview', + link: '/quickstart/integrate-tempo', + }, + { + text: 'Connect to the Network', + link: '/quickstart/connection-details', + }, + { + text: 'Use Tempo Transactions', + link: '/guide/tempo-transaction', + }, + { + text: 'Get Testnet Faucet Funds', + link: '/quickstart/faucet', + }, + { + text: 'EVM Differences', + link: '/quickstart/evm-compatibility', + }, + { + text: 'Predeployed Contracts', + link: '/quickstart/predeployed-contracts', + }, + { + text: 'Token List Registry', + link: '/quickstart/tokenlist', + }, + { + text: 'Wallet Developers', + link: '/quickstart/wallet-developers', + }, + { + text: 'Contract Verification', + link: '/quickstart/verify-contracts', + }, + { + text: 'Bridging', + collapsed: true, + items: [ + { + text: 'Bridge via LayerZero', + link: '/guide/bridge-layerzero', + }, + { + text: 'Bridge via Relay', + link: '/guide/bridge-relay', + }, + ], + }, + { + text: 'Ecosystem', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/ecosystem', + }, + { + text: 'Bridges & Exchanges', + link: '/ecosystem/bridges', + }, + { + text: 'Data & Analytics', + link: '/ecosystem/data-analytics', + }, + { + text: 'Block Explorers', + link: '/ecosystem/block-explorers', + }, + { + text: 'Wallets', + link: '/ecosystem/wallets', + }, + { + text: 'Smart Contract Libraries', + link: '/ecosystem/smart-contract-libraries', + }, + { + text: 'Node Infrastructure', + link: '/ecosystem/node-infrastructure', + }, + { + text: 'Security & Compliance', + link: '/ecosystem/security-compliance', + }, + { + text: 'Issuance & Orchestration', + link: '/ecosystem/orchestration', + }, + ], + }, + ], + }, + { + text: 'Tempo Protocol Specs', + items: [ + { + text: 'Overview', + link: '/protocol', + }, + { + text: 'TIP-20 Tokens', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/protocol/tip20/overview', + }, + { + text: 'Specification', + link: '/protocol/tip20/spec', + }, + { + text: 'Reference Implementation', + link: 'https://github.com/tempoxyz/tempo/blob/main/tips/ref-impls/src/TIP20.sol', + }, + { + text: 'Rust Implementation', + link: 'https://github.com/tempoxyz/tempo/tree/main/crates/precompiles/src/tip20', + }, + ], + }, + { + text: 'TIP-20 Rewards', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/protocol/tip20-rewards/overview', + }, + { + text: 'Specification', + link: '/protocol/tip20-rewards/spec', + }, + ], + }, + { + text: 'TIP-403 Policies', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/protocol/tip403/overview', + }, + { + text: 'Specification', + link: '/protocol/tip403/spec', + }, + { + text: 'Reference Implementation', + link: 'https://github.com/tempoxyz/tempo/blob/main/tips/ref-impls/src/TIP403Registry.sol', + }, + { + text: 'Rust Implementation', + link: 'https://github.com/tempoxyz/tempo/tree/main/crates/precompiles/src/tip403_registry', + }, + ], + }, + { + text: 'Fees', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/protocol/fees', + }, + { + text: 'Specification', + link: '/protocol/fees/spec-fee', + }, + { + text: 'Fee AMM', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/protocol/fees/fee-amm', + }, + { + text: 'Specification', + link: '/protocol/fees/spec-fee-amm', + }, + { + text: 'Reference Implementation', + link: 'https://github.com/tempoxyz/tempo/blob/main/tips/ref-impls/src/FeeManager.sol', + }, + { + text: 'Rust Implementation', + link: 'https://github.com/tempoxyz/tempo/tree/main/crates/precompiles/src/tip_fee_manager', + }, + ], + }, + ], + }, + { + text: 'Tempo Transactions', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/protocol/transactions', + }, + { + text: 'Specification', + link: '/protocol/transactions/spec-tempo-transaction', + }, + { + text: 'EIP-4337 Comparison', + link: '/protocol/transactions/eip-4337', + }, + { + text: 'EIP-7702 Comparison', + link: '/protocol/transactions/eip-7702', + }, + { + text: 'Account Keychain Precompile Specification', + link: '/protocol/transactions/AccountKeychain', + }, + { + text: 'Rust Implementation', + link: 'https://github.com/tempoxyz/tempo/blob/main/crates/primitives/src/transaction/tempo_transaction.rs', + }, + ], + }, + { + text: 'Blockspace', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/protocol/blockspace/overview', + }, + { + text: 'Payment Lane Specification', + link: '/protocol/blockspace/payment-lane-specification', + }, + { + text: 'Consensus and Finality', + link: '/protocol/blockspace/consensus', + }, + ], + }, + { + text: 'Stablecoin DEX', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/protocol/exchange', + }, + { + text: 'Specification', + link: '/protocol/exchange/spec', + }, + { + text: 'Quote Tokens', + link: '/protocol/exchange/quote-tokens', + }, + { + text: 'Executing Swaps', + link: '/protocol/exchange/executing-swaps', + }, + { + text: 'Providing Liquidity', + link: '/protocol/exchange/providing-liquidity', + }, + { + text: 'DEX Balance', + link: '/protocol/exchange/exchange-balance', + }, + { + text: 'Reference Implementation', + link: 'https://github.com/tempoxyz/tempo/blob/main/tips/ref-impls/src/stablecoinDex.sol', + }, + { + text: 'Rust Implementation', + link: 'https://github.com/tempoxyz/tempo/tree/main/crates/precompiles/src/stablecoin_exchange', + }, + ], + }, + { + text: 'Network Upgrades', + collapsed: true, + items: [ + { + text: 'T3', + link: '/protocol/upgrades/t3', + }, + { + text: 'T2', + link: '/protocol/upgrades/t2', + }, + ], + }, + { + text: 'TIPs', + link: '/protocol/tips', + }, + ], + }, + { + text: 'Tempo Developer Tools', + items: [ + { + text: 'Accounts SDK', + link: '/accounts', + }, + { + text: 'CLI', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/cli', + }, + { + text: 'Wallet', + link: '/cli/wallet', + }, + { + text: 'Request', + link: '/cli/request', + }, + { + text: 'Download', + link: '/cli/download', + }, + { + text: 'Node', + link: '/cli/node', + }, + ], + }, + { + text: 'SDKs', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/sdk', + }, + { + text: 'TypeScript', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/sdk/typescript', + }, + { + text: 'Viem Reference', + link: 'https://viem.sh/tempo', + }, + { + text: 'Wagmi Reference', + link: 'https://wagmi.sh/tempo', + }, + { + text: 'Prool Reference', + items: [ + { + text: 'Setup', + link: '/sdk/typescript/prool/setup', + }, + ], + }, + ], + }, + { + text: 'Go', + link: '/sdk/go', + }, + { + text: 'Foundry', + link: '/sdk/foundry', + }, + { + text: 'Python', + link: '/sdk/python', + }, + { + text: 'Rust', + link: '/sdk/rust', + }, + ], + }, + ], + }, + { + text: 'Run a Tempo Node', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/guide/node', + }, + { + text: 'System Requirements', + link: '/guide/node/system-requirements', + }, + { + text: 'Installation', + link: '/guide/node/installation', + }, + { + text: 'Running an RPC Node', + link: '/guide/node/rpc', + }, + { + text: 'Running a validator', + items: [ + { + text: 'Overview', + link: '/guide/node/validator', + }, + { + text: 'Operation', + link: '/guide/node/operate-validator', + }, + { + text: 'ValidatorConfig V2', + link: '/guide/node/validator-config-v2', + }, + ], + }, + { + text: 'Network Upgrades and Releases', + link: '/guide/node/network-upgrades', + }, + { + text: 'Changelog', + link: '/changelog', + }, + ], + }, + // { + // text: 'Infrastructure & Tooling', + // items: [ + // { + // text: 'Overview', + // link: '/guide/infrastructure', + // }, + // { + // text: 'Data Indexers', + // link: '/guide/infrastructure/data-indexers', + // }, + // { + // text: 'Developer Tools', + // link: '/guide/infrastructure/developer-tools', + // }, + // { + // text: 'Node Providers', + // link: '/guide/infrastructure/node-providers', + // }, + // ], + // }, + ], + prefetch: 'intent', + }, '/accounts': { backLink: true, + prefetch: 'intent', items: [ { text: 'Accounts SDK', @@ -1230,6 +1234,8 @@ export default defineConfig({ twoslash: { twoslashOptions: { compilerOptions: { + // Override the vocs@420 default ('6.0'), which fails under this repo's TS 5.9. + ignoreDeprecations: '5.0', // ModuleResolutionKind.Bundler = 100 moduleResolution: 100, },