From 3580c13518d53dff3b975290d91d6d445b3ba1da Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Thu, 9 Apr 2026 18:39:12 -0700 Subject: [PATCH] perf: lazy-load wagmi providers only on interactive pages Previously every MDX page was wrapped in which eagerly loaded wagmi + viem + tanstack-query + accounts SDK (121 KB brotli). Only ~24 pages actually use interactive wallet demos. Pages now opt in via frontmatter `interactive: true`. The _mdx-wrapper uses React.lazy to load Providers only when this flag (or `mipd`) is set. Content pages no longer fetch the wagmi bundle at all. Measured: -36 scripts, -3.9 MB JS on content pages, -25% TBT. Also adds bundle:analyze and lighthouse measurement scripts. --- .gitignore | 5 + e2e/no-raw-mermaid.test.ts | 17 +- package.json | 7 +- scripts/bundle-diff.ts | 376 ++++++++++++++++++ scripts/lighthouse.ts | 251 ++++++++++++ src/pages/_mdx-wrapper.tsx | 36 +- src/pages/accounts/index.mdx | 1 + src/pages/guide/_template.mdx | 1 + .../guide/issuance/create-a-stablecoin.mdx | 1 + .../guide/issuance/distribute-rewards.mdx | 1 + .../guide/issuance/manage-stablecoin.mdx | 1 + src/pages/guide/issuance/mint-stablecoins.mdx | 1 + src/pages/guide/issuance/use-for-fees.mdx | 1 + src/pages/guide/payments/accept-a-payment.mdx | 1 + .../payments/pay-fees-in-any-stablecoin.mdx | 1 + src/pages/guide/payments/send-a-payment.mdx | 1 + .../payments/send-parallel-transactions.mdx | 1 + .../guide/payments/sponsor-user-fees.mdx | 1 + src/pages/guide/payments/transfer-memos.mdx | 4 + .../guide/stablecoin-dex/executing-swaps.mdx | 1 + .../stablecoin-dex/managing-fee-liquidity.mdx | 1 + .../stablecoin-dex/providing-liquidity.mdx | 1 + src/pages/guide/use-accounts/add-funds.mdx | 1 + .../guide/use-accounts/connect-to-wallets.mdx | 1 + .../guide/use-accounts/embed-passkeys.mdx | 1 + .../guide/use-accounts/embed-tempo-wallet.mdx | 1 + src/pages/quickstart/connection-details.mdx | 1 + src/pages/quickstart/faucet.mdx | 1 + src/pages/quickstart/integrate-tempo.mdx | 1 + src/pages/quickstart/tokenlist.mdx | 1 + 30 files changed, 710 insertions(+), 9 deletions(-) create mode 100644 scripts/bundle-diff.ts create mode 100644 scripts/lighthouse.ts diff --git a/.gitignore b/.gitignore index 9db348e7..96a1253c 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,11 @@ src/pages/protocol/tips/tip-* # Test scratch files guides/tmp/ +# Bundle analysis +.bundle-baseline.json +stats.json +lighthouse-*.json + # Playwright playwright-report/ test-results/ diff --git a/e2e/no-raw-mermaid.test.ts b/e2e/no-raw-mermaid.test.ts index 16098859..f66a5e2c 100644 --- a/e2e/no-raw-mermaid.test.ts +++ b/e2e/no-raw-mermaid.test.ts @@ -1,7 +1,7 @@ -import { expect, test } from '@playwright/test' -import { readdirSync, readFileSync, statSync } from 'node:fs' +import { readdirSync, readFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' +import { expect, test } from '@playwright/test' const __dirname = dirname(fileURLToPath(import.meta.url)) @@ -26,13 +26,16 @@ test('no raw ```mermaid code blocks in MDX files', () => { for (const file of mdxFiles) { const content = readFileSync(file, 'utf-8') if (/^```mermaid\s*$/m.test(content)) { - const relative = file.replace(join(__dirname, '..') + '/', '') + const relative = file.replace(`${join(__dirname, '..')}/`, '') violations.push(relative) } } - expect(violations, [ - 'Found raw ```mermaid code blocks. Use instead:', - ...violations.map((f) => ` - ${f}`), - ].join('\n')).toHaveLength(0) + expect( + violations, + [ + 'Found raw ```mermaid code blocks. Use instead:', + ...violations.map((f) => ` - ${f}`), + ].join('\n'), + ).toHaveLength(0) }) diff --git a/package.json b/package.json index a9147403..7ec58121 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,12 @@ "check": "biome check --write --unsafe .", "check:types": "tsgo --project tsconfig.json --noEmit", "preview": "node dist/preview.js", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "bundle:analyze": "node --experimental-strip-types scripts/bundle-diff.ts --skip-build", + "bundle:diff": "node --experimental-strip-types scripts/bundle-diff.ts", + "bundle:save": "node --experimental-strip-types scripts/bundle-diff.ts --save", + "lighthouse": "node --experimental-strip-types scripts/lighthouse.ts", + "lighthouse:mobile": "node --experimental-strip-types scripts/lighthouse.ts --mobile" }, "dependencies": { "@iconify-json/lucide": "^1.2.86", diff --git a/scripts/bundle-diff.ts b/scripts/bundle-diff.ts new file mode 100644 index 00000000..097dfd52 --- /dev/null +++ b/scripts/bundle-diff.ts @@ -0,0 +1,376 @@ +#!/usr/bin/env node +/** + * Bundle size analysis and diff tool for the docs site (Vocs/Waku) + * + * Scans built JS files in dist/public/assets/ and measures raw + brotli sizes. + * Groups chunks into: framework, heavy-deps, app. + * + * Usage: + * node --experimental-strip-types scripts/bundle-diff.ts + * node --experimental-strip-types scripts/bundle-diff.ts --save + * node --experimental-strip-types scripts/bundle-diff.ts --ci + * + * CLI flags: + * --ci - Output markdown for GitHub PR comments + * --save - Save current sizes as baseline + * --baseline - Read baseline from specific file path + * --output - Write current stats to file (for caching) + * --skip-build - Skip build step (use existing dist/) + */ + +import { execSync } from 'node:child_process' +import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { brotliCompressSync, constants } from 'node:zlib' + +const ASSETS_DIR = 'dist/public/assets' +const BASELINE_FILE = '.bundle-baseline.json' + +const BROTLI_WARNING_KB = 10 +const BROTLI_STRONG_WARNING_KB = 25 + +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/, + // mermaid sub-diagram chunks + /Diagram-/, + /^dagre-/, + /^cose-bilkent/, + /^elk-/, + /^arc-/, +] + +const FRAMEWORK_PATTERNS = [ + /^Link-/, + /^_layout-/, + /^_mdx-wrapper-/, + /^client-/, + /^context-/, + /^module-/, + /^facade_vocs/, + /^Head-/, + /^layout-/, + /^MdxPageContext-/, +] + +interface CIOptions { + ci: boolean + baselinePath: string | null + outputPath: string | null + skipBuild: boolean + save: boolean +} + +function parseArgs(args: string[]): CIOptions { + const options: CIOptions = { + ci: false, + baselinePath: null, + outputPath: null, + skipBuild: false, + save: false, + } + + for (let i = 0; i < args.length; i++) { + const arg = args[i] + if (arg === '--ci') { + options.ci = true + } else if (arg === '--baseline' && args[i + 1]) { + options.baselinePath = args[++i] + } else if (arg === '--output' && args[i + 1]) { + options.outputPath = args[++i] + } else if (arg === '--skip-build') { + options.skipBuild = true + } else if (arg === '--save') { + options.save = true + } + } + + return options +} + +interface ChunkInfo { + label: string + size: number + brotliSize: number + group: string +} + +interface SizeAggregate { + size: number + brotli: number +} + +interface GroupStats { + label: string + aggregate: SizeAggregate +} + +interface BundleStats { + timestamp: string + total: SizeAggregate + groups: GroupStats[] + chunks: ChunkInfo[] +} + +function formatBytes( + bytes: number, + signDisplay: Intl.NumberFormatOptions['signDisplay'] = 'auto', +): string { + if (bytes === 0) return '0 B' + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB'] + const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k)) + const value = bytes / k ** i + const formatted = new Intl.NumberFormat('en', { + signDisplay, + maximumFractionDigits: 1, + minimumFractionDigits: 1, + }).format(value) + return `${formatted} ${sizes[i]}` +} + +function formatDelta(current: number, baseline: number): string { + return formatBytes(current - baseline, 'exceptZero') +} + +function categorize(filename: string): string { + if (HEAVY_DEPS_PATTERNS.some((p) => p.test(filename))) return 'heavy-deps' + if (FRAMEWORK_PATTERNS.some((p) => p.test(filename))) return 'framework' + return 'app' +} + +function parseStats(assetsDir: string): BundleStats { + const absDir = resolve(process.cwd(), assetsDir) + const files = readdirSync(absDir).filter((f) => f.endsWith('.js')) + + const chunks: ChunkInfo[] = [] + + for (const file of files) { + const filePath = join(absDir, file) + const raw = readFileSync(filePath) + const rawSize = statSync(filePath).size + const brotli = brotliCompressSync(raw, { + params: { [constants.BROTLI_PARAM_QUALITY]: constants.BROTLI_MAX_QUALITY }, + }) + + chunks.push({ + label: file, + size: rawSize, + brotliSize: brotli.length, + group: categorize(file), + }) + } + + chunks.sort((a, b) => b.brotliSize - a.brotliSize) + + const aggregate = (group: string): SizeAggregate => + chunks + .filter((c) => c.group === group) + .reduce( + (acc, chunk) => ({ + size: acc.size + chunk.size, + brotli: acc.brotli + chunk.brotliSize, + }), + { size: 0, brotli: 0 }, + ) + + const total: SizeAggregate = chunks.reduce( + (acc, chunk) => ({ + size: acc.size + chunk.size, + brotli: acc.brotli + chunk.brotliSize, + }), + { size: 0, brotli: 0 }, + ) + + const groupLabels = ['framework', 'heavy-deps', 'app'] as const + const groups: GroupStats[] = groupLabels.map((label) => ({ + label, + aggregate: aggregate(label), + })) + + return { + timestamp: new Date().toISOString(), + total, + groups, + chunks, + } +} + +function findBaselineGroup(baseline: BundleStats, label: string): SizeAggregate | null { + return baseline.groups.find((g) => g.label === label)?.aggregate ?? null +} + +function printReport(current: BundleStats, baseline: BundleStats | null, options: CIOptions): void { + if (options.ci) { + printMarkdownReport(current, baseline) + } else { + printTerminalReport(current, baseline) + } +} + +function printTerminalReport(current: BundleStats, baseline: BundleStats | null): void { + console.log(`\n${'='.repeat(60)}`) + console.log('Docs Bundle Size Analysis') + console.log('='.repeat(60)) + + console.log('\nAll sizes are brotli-compressed.\n') + + console.log('Current Build:') + console.log(` Total: ${formatBytes(current.total.brotli)}`) + for (const { label, aggregate } of current.groups) { + console.log(` ${label}: ${formatBytes(aggregate.brotli)}`) + } + + if (baseline) { + console.log('\nBaseline:') + console.log(` Total: ${formatBytes(baseline.total.brotli)}`) + for (const { label } of current.groups) { + const bg = findBaselineGroup(baseline, label) + console.log(` ${label}: ${bg ? formatBytes(bg.brotli) : '—'}`) + } + + console.log('\nDelta:') + console.log(` Total: ${formatDelta(current.total.brotli, baseline.total.brotli)}`) + for (const { label, aggregate } of current.groups) { + const bg = findBaselineGroup(baseline, label) + console.log(` ${label}: ${bg ? formatDelta(aggregate.brotli, bg.brotli) : '—'}`) + } + } + + for (const groupLabel of ['framework', 'heavy-deps', 'app'] as const) { + const groupChunks = current.chunks.filter((c) => c.group === groupLabel) + if (groupChunks.length === 0) continue + + console.log(`\n ${groupLabel} chunks (${groupChunks.length}):`) + for (const chunk of groupChunks.slice(0, 10)) { + const name = chunk.label.padEnd(50) + console.log( + ` ${name} ${formatBytes(chunk.brotliSize).padStart(10)} (raw: ${formatBytes(chunk.size)})`, + ) + } + if (groupChunks.length > 10) { + console.log(` ... and ${groupChunks.length - 10} more ${groupLabel} chunks`) + } + } + + if (!baseline) { + console.log('\n No baseline found. Run with --save to save current as baseline.') + } + + console.log(`\n${'='.repeat(60)}\n`) +} + +function printMarkdownReport(current: BundleStats, baseline: BundleStats | null): void { + let output = '' + + output += '> All sizes are brotli-compressed.\n\n' + + if (baseline) { + const totalDelta = current.total.brotli - baseline.total.brotli + const emoji = totalDelta > 0 ? 'šŸ“ˆ' : totalDelta < 0 ? 'šŸ“‰' : 'āž”ļø' + + output += `${emoji} **Total:** ${formatBytes(current.total.brotli)} (${formatBytes(totalDelta, 'exceptZero')})\n\n` + + output += '| | Current | Baseline | Delta |\n' + output += '|--|---------|----------|-------|\n' + output += `| Total | ${formatBytes(current.total.brotli)} | ${formatBytes(baseline.total.brotli)} | ${formatDelta(current.total.brotli, baseline.total.brotli)} |\n` + for (const { label, aggregate } of current.groups) { + const bg = findBaselineGroup(baseline, label) + if (bg) { + output += `| ${label} | ${formatBytes(aggregate.brotli)} | ${formatBytes(bg.brotli)} | ${formatDelta(aggregate.brotli, bg.brotli)} |\n` + } else { + output += `| ${label} | ${formatBytes(aggregate.brotli)} | — | — |\n` + } + } + + const deltaKb = totalDelta / 1024 + if (deltaKb > BROTLI_STRONG_WARNING_KB) { + output += `\n> [!WARNING]\n> Total bundle increased by ${deltaKb.toFixed(1)} KB (exceeds ${BROTLI_STRONG_WARNING_KB} KB)!\n` + } else if (deltaKb > BROTLI_WARNING_KB) { + output += `\n> [!NOTE]\n> Total bundle increased by ${deltaKb.toFixed(1)} KB (exceeds ${BROTLI_WARNING_KB} KB)\n` + } + } else { + output += `**Total:** ${formatBytes(current.total.brotli)}\n\n` + output += '| | Size |\n' + output += '|--|------|\n' + output += `| Total | ${formatBytes(current.total.brotli)} |\n` + for (const { label, aggregate } of current.groups) { + output += `| ${label} | ${formatBytes(aggregate.brotli)} |\n` + } + output += '\n*No baseline available for comparison*\n' + } + + for (const groupLabel of ['framework', 'heavy-deps', 'app'] as const) { + const groupChunks = current.chunks.filter((c) => c.group === groupLabel) + if (groupChunks.length === 0) continue + + output += `\n
\n${groupLabel} chunks (${groupChunks.length})\n\n` + output += '| Chunk | Size | Raw |\n' + output += '|-------|------|-----|\n' + for (const chunk of groupChunks) { + const name = chunk.label.length > 45 ? `${chunk.label.slice(0, 42)}...` : chunk.label + output += `| \`${name}\` | ${formatBytes(chunk.brotliSize)} | ${formatBytes(chunk.size)} |\n` + } + output += '\n
\n' + } + + console.log(output) +} + +async function main() { + const args = process.argv.slice(2) + const options = parseArgs(args) + + if (!options.skipBuild) { + console.log('Building docs site...') + execSync('pnpm build', { stdio: 'inherit' }) + } + + const assetsDir = resolve(process.cwd(), ASSETS_DIR) + + if (!existsSync(assetsDir)) { + console.error(`Assets directory not found: ${assetsDir}`) + console.error('Make sure to build first: pnpm build') + process.exit(1) + } + + const current = parseStats(ASSETS_DIR) + + if (options.outputPath) { + writeFileSync(options.outputPath, JSON.stringify(current, null, 2)) + console.log(`Stats written to ${options.outputPath}`) + return + } + + if (options.save) { + writeFileSync(BASELINE_FILE, JSON.stringify(current, null, 2)) + console.log(`Baseline saved to ${BASELINE_FILE}`) + printReport(current, null, options) + return + } + + let baseline: BundleStats | null = null + + if (options.baselinePath && existsSync(options.baselinePath)) { + baseline = JSON.parse(readFileSync(options.baselinePath, 'utf-8')) as BundleStats + } else if (existsSync(BASELINE_FILE)) { + baseline = JSON.parse(readFileSync(BASELINE_FILE, 'utf-8')) as BundleStats + } + + printReport(current, baseline, options) +} + +main().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/scripts/lighthouse.ts b/scripts/lighthouse.ts new file mode 100644 index 00000000..fd3acb7f --- /dev/null +++ b/scripts/lighthouse.ts @@ -0,0 +1,251 @@ +#!/usr/bin/env node +/** + * Lighthouse performance measurement for the docs site. + * + * Usage: + * # Build + preview, then run against preview server: + * pnpm build && pnpm preview & + * pnpm lighthouse --url http://localhost:4173 + * + * # Or against dev server (may 500 in headless Chrome): + * pnpm lighthouse --url https://localhost:5173 + * + * # Save baseline, make changes, compare: + * pnpm lighthouse --save baseline.json + * pnpm lighthouse --compare baseline.json + * + * # Mobile throttling: + * pnpm lighthouse:mobile + */ +import { execSync } from 'node:child_process' +import { readFileSync, writeFileSync } from 'node:fs' + +const DEFAULT_PAGES = [ + '/', + '/guide/issuance/create-a-stablecoin', + '/guide/payments/send-a-payment', + '/guide/stablecoin-dex/executing-swaps', + '/guide/stablecoin-dex/providing-liquidity', + '/guide/machine-payments/client', +] + +interface PageResult { + page: string + performance: number + fcp: number + lcp: number + tbt: number + cls: number + tti: number +} + +function parseArgs(argv: string[]) { + const args = argv.slice(2) + const flags = { + url: 'https://localhost:5173', + pages: DEFAULT_PAGES, + mobile: false, + json: false, + compare: '', + save: '', + } + + 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(',') + break + case '--mobile': + flags.mobile = true + break + case '--json': + flags.json = true + break + case '--compare': + flags.compare = args[++i] + break + case '--save': + flags.save = args[++i] + break + } + } + + return flags +} + +const LH_OUTPUT = '/tmp/lighthouse-result.json' + +function runLighthouse(url: string, mobile: boolean): any | null { + const preset = mobile ? 'perf' : 'desktop' + // Run npx from /tmp to avoid devEngines conflicts in the docs package.json + // Use --output-path to avoid pipe truncation on large JSON output + const cmd = `npx lighthouse "${url}" --output=json --output-path=${LH_OUTPUT} --chrome-flags="--headless --no-sandbox --ignore-certificate-errors" --preset=${preset} --quiet` + + try { + execSync(cmd, { + encoding: 'utf-8', + maxBuffer: 50 * 1024 * 1024, + stdio: ['pipe', 'pipe', 'pipe'], + cwd: '/tmp', + }) + const raw = readFileSync(LH_OUTPUT, 'utf-8') + const report = JSON.parse(raw) + + // Check for runtime errors (e.g., page returned 500) + if (report.runtimeError?.code) { + console.error(` āœ— ${report.runtimeError.message.split('.')[0]}`) + return null + } + + return report + } catch (err) { + console.error(` āœ— Lighthouse failed for ${url}`) + if (err instanceof Error) { + const stderr = (err as any).stderr?.toString() || '' + const meaningful = stderr + .split('\n') + .filter((l: string) => !l.includes('npm warn') && l.trim()) + .slice(0, 3) + .join('\n ') + if (meaningful) console.error(` ${meaningful}`) + } + return null + } +} + +function extractMetrics(report: any, page: string): PageResult { + const score = Math.round((report.categories?.performance?.score ?? 0) * 100) + const audits = report.audits ?? {} + + return { + page, + performance: score, + 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, + } +} + +function formatMs(ms: number): string { + if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s` + return `${Math.round(ms)}ms` +} + +function formatCls(cls: number): string { + return cls.toFixed(3) +} + +function pad(str: string, len: number, right = false): string { + if (right) return str.padStart(len) + return str.padEnd(len) +} + +function printTable(results: PageResult[]) { + const pageWidth = Math.max(40, ...results.map((r) => r.page.length + 2)) + + const header = `${pad('Page', pageWidth)}${pad('Perf', 7, true)}${pad('FCP', 9, true)}${pad('LCP', 9, true)}${pad('TBT', 9, true)}${pad('CLS', 8, true)}` + console.log(header) + console.log('─'.repeat(header.length)) + + for (const r of results) { + const line = `${pad(r.page, pageWidth)}${pad(String(r.performance), 7, true)}${pad(formatMs(r.fcp), 9, true)}${pad(formatMs(r.lcp), 9, true)}${pad(formatMs(r.tbt), 9, true)}${pad(formatCls(r.cls), 8, true)}` + console.log(line) + } +} + +function colorDelta(value: number, lowerIsBetter: boolean): string { + const sign = value > 0 ? '+' : '' + const formatted = `${sign}${value.toFixed(1)}` + + // Green = improvement, Red = regression + const improved = lowerIsBetter ? value < 0 : value > 0 + const regressed = lowerIsBetter ? value > 0 : value < 0 + + if (improved) return `\x1b[32m${formatted}\x1b[0m` + if (regressed) return `\x1b[31m${formatted}\x1b[0m` + return formatted +} + +function printComparison(current: PageResult[], baseline: PageResult[]) { + const baselineMap = new Map(baseline.map((r) => [r.page, r])) + const pageWidth = Math.max(40, ...current.map((r) => r.page.length + 2)) + + const header = `${pad('Page', pageWidth)}${pad('Perf', 12, true)}${pad('FCP', 16, true)}${pad('LCP', 16, true)}${pad('TBT', 16, true)}${pad('CLS', 14, true)}` + console.log(header) + console.log('─'.repeat(header.length)) + + for (const r of current) { + const b = baselineMap.get(r.page) + if (!b) { + console.log(`${pad(r.page, pageWidth)} (new page, no baseline)`) + continue + } + + const perfDelta = r.performance - b.performance + const fcpDelta = r.fcp - b.fcp + const lcpDelta = r.lcp - b.lcp + const tbtDelta = r.tbt - b.tbt + const clsDelta = r.cls - b.cls + + const line = + `${pad(r.page, pageWidth)}` + + `${pad(`${r.performance} (${colorDelta(perfDelta, false)})`, 12, true)}` + + `${pad(`${formatMs(r.fcp)} (${colorDelta(fcpDelta, true)})`, 16, true)}` + + `${pad(`${formatMs(r.lcp)} (${colorDelta(lcpDelta, true)})`, 16, true)}` + + `${pad(`${formatMs(r.tbt)} (${colorDelta(tbtDelta, true)})`, 16, true)}` + + `${pad(`${formatCls(r.cls)} (${colorDelta(clsDelta, true)})`, 14, true)}` + + console.log(line) + } +} + +function main() { + const flags = parseArgs(process.argv) + + console.log(`\nšŸ”¦ Lighthouse Performance Audit`) + console.log(` Base URL: ${flags.url}`) + console.log(` Mode: ${flags.mobile ? 'mobile' : 'desktop'}`) + console.log(` Pages: ${flags.pages.length}\n`) + + const results: PageResult[] = [] + + for (const page of flags.pages) { + const fullUrl = `${flags.url.replace(/\/$/, '')}${page}` + process.stdout.write(` Testing ${page} ... `) + + const report = runLighthouse(fullUrl, flags.mobile) + if (!report) { + results.push({ page, performance: 0, fcp: 0, lcp: 0, tbt: 0, cls: 0, tti: 0 }) + continue + } + + const metrics = extractMetrics(report, page) + results.push(metrics) + console.log(`āœ“ ${metrics.performance}/100`) + } + + console.log('') + + if (flags.compare) { + const baseline: PageResult[] = JSON.parse(readFileSync(flags.compare, 'utf-8')) + printComparison(results, baseline) + } else { + printTable(results) + } + + if (flags.save) { + writeFileSync(flags.save, JSON.stringify(results, null, 2)) + console.log(`\nšŸ’¾ Results saved to ${flags.save}`) + } + + if (flags.json) { + console.log(`\n${JSON.stringify(results, null, 2)}`) + } +} + +main() diff --git a/src/pages/_mdx-wrapper.tsx b/src/pages/_mdx-wrapper.tsx index cffbff4f..4a28aa90 100644 --- a/src/pages/_mdx-wrapper.tsx +++ b/src/pages/_mdx-wrapper.tsx @@ -1,14 +1,48 @@ 'use client' +/** + * MDX page wrapper — wraps every MDX page rendered by Vocs. + * + * ## Conditional Providers + * + * The Wagmi/QueryClient/DemoContext provider tree is only rendered on pages + * that declare `interactive: true` in their frontmatter. Content-only pages + * skip the provider tree entirely, avoiding wagmi config initialization. + * + * To make a page interactive (wallet connection, on-chain demos, etc.), add + * to its frontmatter: + * + * ```yaml + * --- + * interactive: true + * --- + * ``` + * + * ## Frontmatter flags + * + * - `interactive` — loads the Wagmi/QueryClient provider tree. Required for + * any page that uses wallet hooks, Demo components, or guide steps. + * - `mipd` — enables Multi Injected Provider Discovery (auto-detects browser + * extension wallets like MetaMask). Implies `interactive`. Only needed on + * pages where users connect external wallets. + */ + import type React from 'react' import { Layout, MdxPageContext } from 'vocs' import Providers from '../components/Providers' export default function MDXWrapper({ children }: { children: React.ReactNode }) { const context = MdxPageContext.use() + const frontmatter = context.frontmatter as Record | undefined + const needsProviders = Boolean(frontmatter?.interactive || frontmatter?.mipd) + return ( - {children} + {needsProviders ? ( + {children} + ) : ( + children + )} ) } diff --git a/src/pages/accounts/index.mdx b/src/pages/accounts/index.mdx index 4a0ac7c7..7bde7db6 100644 --- a/src/pages/accounts/index.mdx +++ b/src/pages/accounts/index.mdx @@ -1,6 +1,7 @@ --- title: Accounts SDK – Getting Started description: Set up the Tempo Accounts SDK to create, manage, and interact with accounts on Tempo. +interactive: true --- import { Cards, Card } from 'vocs' diff --git a/src/pages/guide/_template.mdx b/src/pages/guide/_template.mdx index a5ee7e4c..d7b01451 100644 --- a/src/pages/guide/_template.mdx +++ b/src/pages/guide/_template.mdx @@ -1,5 +1,6 @@ --- searchable: false +interactive: true --- import { Cards, Card } from 'vocs' diff --git a/src/pages/guide/issuance/create-a-stablecoin.mdx b/src/pages/guide/issuance/create-a-stablecoin.mdx index a1a8dab4..8cca78c4 100644 --- a/src/pages/guide/issuance/create-a-stablecoin.mdx +++ b/src/pages/guide/issuance/create-a-stablecoin.mdx @@ -1,5 +1,6 @@ --- description: Create your own stablecoin on Tempo using the TIP-20 token standard. Deploy tokens with built-in compliance features and role-based permissions. +interactive: true --- import { Cards, Card } from 'vocs' diff --git a/src/pages/guide/issuance/distribute-rewards.mdx b/src/pages/guide/issuance/distribute-rewards.mdx index f6b5e5fb..470773a5 100644 --- a/src/pages/guide/issuance/distribute-rewards.mdx +++ b/src/pages/guide/issuance/distribute-rewards.mdx @@ -1,5 +1,6 @@ --- description: Distribute rewards to token holders using TIP-20's built-in reward mechanism. Allocate tokens proportionally based on holder balances. +interactive: true --- import * as Demo from '../../../components/guides/Demo.tsx' diff --git a/src/pages/guide/issuance/manage-stablecoin.mdx b/src/pages/guide/issuance/manage-stablecoin.mdx index abbba2e3..83b096ce 100644 --- a/src/pages/guide/issuance/manage-stablecoin.mdx +++ b/src/pages/guide/issuance/manage-stablecoin.mdx @@ -1,5 +1,6 @@ --- description: Configure stablecoin permissions, supply limits, and compliance policies. Grant roles, set transfer policies, and control pause/unpause functionality. +interactive: true --- import * as Demo from '../../../components/guides/Demo.tsx' diff --git a/src/pages/guide/issuance/mint-stablecoins.mdx b/src/pages/guide/issuance/mint-stablecoins.mdx index 96b9e482..f93a704f 100644 --- a/src/pages/guide/issuance/mint-stablecoins.mdx +++ b/src/pages/guide/issuance/mint-stablecoins.mdx @@ -1,5 +1,6 @@ --- description: Mint new tokens to increase your stablecoin's total supply. Grant the issuer role and create tokens with optional memos for tracking. +interactive: true --- import * as Demo from '../../../components/guides/Demo.tsx' diff --git a/src/pages/guide/issuance/use-for-fees.mdx b/src/pages/guide/issuance/use-for-fees.mdx index b0248b9a..5ba49217 100644 --- a/src/pages/guide/issuance/use-for-fees.mdx +++ b/src/pages/guide/issuance/use-for-fees.mdx @@ -1,5 +1,6 @@ --- description: Enable users to pay transaction fees using your stablecoin. Add fee pool liquidity and integrate with Tempo's flexible fee payment system. +interactive: true --- import { Cards, Card } from 'vocs' diff --git a/src/pages/guide/payments/accept-a-payment.mdx b/src/pages/guide/payments/accept-a-payment.mdx index edea9b71..245908d3 100644 --- a/src/pages/guide/payments/accept-a-payment.mdx +++ b/src/pages/guide/payments/accept-a-payment.mdx @@ -1,5 +1,6 @@ --- description: Accept stablecoin payments in your application. Verify transactions, listen for transfer events, and reconcile payments using memos. +interactive: true --- import * as Demo from '../../../components/guides/Demo.tsx' diff --git a/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx b/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx index 6822a151..95e78504 100644 --- a/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx +++ b/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx @@ -1,6 +1,7 @@ --- description: Configure users to pay transaction fees in any supported stablecoin. Eliminate the need for a separate gas token with Tempo's flexible fee system. showOutline: 1 +interactive: true --- import * as Demo from '../../../components/guides/Demo.tsx' diff --git a/src/pages/guide/payments/send-a-payment.mdx b/src/pages/guide/payments/send-a-payment.mdx index d25821e4..d92b7f20 100644 --- a/src/pages/guide/payments/send-a-payment.mdx +++ b/src/pages/guide/payments/send-a-payment.mdx @@ -1,5 +1,6 @@ --- description: Send stablecoin payments between accounts on Tempo. Include optional memos for reconciliation and tracking with TypeScript, Rust, or Solidity. +interactive: true --- import * as Demo from '../../../components/guides/Demo.tsx' diff --git a/src/pages/guide/payments/send-parallel-transactions.mdx b/src/pages/guide/payments/send-parallel-transactions.mdx index 1db8280d..2bd3cf47 100644 --- a/src/pages/guide/payments/send-parallel-transactions.mdx +++ b/src/pages/guide/payments/send-parallel-transactions.mdx @@ -1,5 +1,6 @@ --- description: Submit multiple transactions concurrently using Tempo's expiring nonce system under-the-hood. +interactive: true --- import * as Demo from '../../../components/guides/Demo.tsx' diff --git a/src/pages/guide/payments/sponsor-user-fees.mdx b/src/pages/guide/payments/sponsor-user-fees.mdx index ffc5c159..3cb11cc2 100644 --- a/src/pages/guide/payments/sponsor-user-fees.mdx +++ b/src/pages/guide/payments/sponsor-user-fees.mdx @@ -1,5 +1,6 @@ --- description: Enable gasless transactions by sponsoring fees for your users. Set up a fee payer service and improve UX by removing friction from payment flows. +interactive: true --- import { Cards, Card, Tabs, Tab } from 'vocs' diff --git a/src/pages/guide/payments/transfer-memos.mdx b/src/pages/guide/payments/transfer-memos.mdx index 07db8d90..33ace07a 100644 --- a/src/pages/guide/payments/transfer-memos.mdx +++ b/src/pages/guide/payments/transfer-memos.mdx @@ -1,3 +1,7 @@ +--- +interactive: true +--- + import { Cards, Card } from 'vocs' import * as Demo from '../../../components/guides/Demo.tsx' import * as Step from '../../../components/guides/steps' diff --git a/src/pages/guide/stablecoin-dex/executing-swaps.mdx b/src/pages/guide/stablecoin-dex/executing-swaps.mdx index 5c015802..af2e324d 100644 --- a/src/pages/guide/stablecoin-dex/executing-swaps.mdx +++ b/src/pages/guide/stablecoin-dex/executing-swaps.mdx @@ -1,5 +1,6 @@ --- description: Learn to execute instant stablecoin swaps on Tempo's DEX. Get price quotes, set slippage protection, and batch approvals with swaps. +interactive: true --- import { Cards, Card } from 'vocs' diff --git a/src/pages/guide/stablecoin-dex/managing-fee-liquidity.mdx b/src/pages/guide/stablecoin-dex/managing-fee-liquidity.mdx index f4511fde..a208bfe5 100644 --- a/src/pages/guide/stablecoin-dex/managing-fee-liquidity.mdx +++ b/src/pages/guide/stablecoin-dex/managing-fee-liquidity.mdx @@ -1,5 +1,6 @@ --- description: Add and remove liquidity in the Fee AMM to enable stablecoin fee conversions. Monitor pools, check LP balances, and rebalance reserves. +interactive: true --- import { Cards, Card } from 'vocs' diff --git a/src/pages/guide/stablecoin-dex/providing-liquidity.mdx b/src/pages/guide/stablecoin-dex/providing-liquidity.mdx index fbea5f9a..a7cbd2f8 100644 --- a/src/pages/guide/stablecoin-dex/providing-liquidity.mdx +++ b/src/pages/guide/stablecoin-dex/providing-liquidity.mdx @@ -1,5 +1,6 @@ --- description: Place limit and flip orders to provide liquidity on the Stablecoin DEX orderbook. Learn to manage orders and set prices using ticks. +interactive: true --- import { Cards, Card } from 'vocs' diff --git a/src/pages/guide/use-accounts/add-funds.mdx b/src/pages/guide/use-accounts/add-funds.mdx index f1465435..a38369d1 100644 --- a/src/pages/guide/use-accounts/add-funds.mdx +++ b/src/pages/guide/use-accounts/add-funds.mdx @@ -1,6 +1,7 @@ --- description: Get test stablecoins on Tempo Testnet using the faucet. Request pathUSD, AlphaUSD, BetaUSD, and ThetaUSD tokens for development and testing. mipd: true +interactive: true --- import * as Demo from '../../../components/guides/Demo.tsx' diff --git a/src/pages/guide/use-accounts/connect-to-wallets.mdx b/src/pages/guide/use-accounts/connect-to-wallets.mdx index 64425465..de191c89 100644 --- a/src/pages/guide/use-accounts/connect-to-wallets.mdx +++ b/src/pages/guide/use-accounts/connect-to-wallets.mdx @@ -1,6 +1,7 @@ --- description: Connect your application to EVM-compatible wallets like MetaMask on Tempo. Set up Wagmi connectors and add the Tempo network to user wallets. mipd: true +interactive: true --- import * as Demo from '../../../components/guides/Demo.tsx' diff --git a/src/pages/guide/use-accounts/embed-passkeys.mdx b/src/pages/guide/use-accounts/embed-passkeys.mdx index 59024b85..e5f3d1b6 100644 --- a/src/pages/guide/use-accounts/embed-passkeys.mdx +++ b/src/pages/guide/use-accounts/embed-passkeys.mdx @@ -1,5 +1,6 @@ --- description: Create domain-bound passkey accounts on Tempo using WebAuthn for secure, passwordless authentication with biometrics like Face ID and Touch ID. +interactive: true --- import { Cards, Card } from 'vocs' diff --git a/src/pages/guide/use-accounts/embed-tempo-wallet.mdx b/src/pages/guide/use-accounts/embed-tempo-wallet.mdx index 4a96aa42..d22c472b 100644 --- a/src/pages/guide/use-accounts/embed-tempo-wallet.mdx +++ b/src/pages/guide/use-accounts/embed-tempo-wallet.mdx @@ -1,5 +1,6 @@ --- description: Embed the Tempo Wallet dialog into your application for a universal wallet experience with account management, passkeys, and fee sponsorship. +interactive: true --- import { Cards, Card } from 'vocs' diff --git a/src/pages/quickstart/connection-details.mdx b/src/pages/quickstart/connection-details.mdx index fc808255..5d77c319 100644 --- a/src/pages/quickstart/connection-details.mdx +++ b/src/pages/quickstart/connection-details.mdx @@ -1,6 +1,7 @@ --- description: Connect to Tempo using browser wallets, CLI tools, or direct RPC endpoints. Get chain ID, URLs, and configuration details. mipd: true +interactive: true --- import * as Demo from '../../components/guides/Demo.tsx' diff --git a/src/pages/quickstart/faucet.mdx b/src/pages/quickstart/faucet.mdx index 26e6b5a6..cc1691cc 100644 --- a/src/pages/quickstart/faucet.mdx +++ b/src/pages/quickstart/faucet.mdx @@ -1,6 +1,7 @@ --- description: Get free test stablecoins on Tempo Testnet. Connect your wallet or enter any address to receive pathUSD, AlphaUSD, BetaUSD, and ThetaUSD. mipd: true +interactive: true --- import * as Demo from '../../components/guides/Demo.tsx' diff --git a/src/pages/quickstart/integrate-tempo.mdx b/src/pages/quickstart/integrate-tempo.mdx index 80a58a8e..9db9ca8b 100644 --- a/src/pages/quickstart/integrate-tempo.mdx +++ b/src/pages/quickstart/integrate-tempo.mdx @@ -1,5 +1,6 @@ --- description: Build on Tempo Testnet. Connect to the network, explore SDKs, and follow guides for accounts, payments, and stablecoin issuance. +interactive: true --- import * as Demo from '../../components/guides/Demo.tsx' diff --git a/src/pages/quickstart/tokenlist.mdx b/src/pages/quickstart/tokenlist.mdx index 8b5272b3..ceff1e7e 100644 --- a/src/pages/quickstart/tokenlist.mdx +++ b/src/pages/quickstart/tokenlist.mdx @@ -1,5 +1,6 @@ --- title: Tempo Token List Registry +interactive: true --- import { Callout } from 'vocs'