tarballs: redesign preview tarballs index page - #1911

Merged
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements
May 4, 2026
Merged

tarballs: redesign preview tarballs index page#1911
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Summary

Reworks the static index page generated by tarballs/scripts/pack.ts (deployed at https://workflow-tarballs.labs.vercel.dev/) to be more useful and pleasant to look at.

What's new

  • Featured workflow package at the top with a large install command, copy button, and direct tarball download — the rest of the packages are secondary.
  • Top-of-page metadata chips: short SHA (linked to commit on GitHub), branch (linked), PR number (linked), build timestamp, package count + total size.
  • Collapsible "What is this?" explainer describing how the version + dep rewriting works.
  • Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the install command shown for every row in place.
  • Live filter input over the package list, with / keyboard shortcut to focus.
  • Per-row install command, copy button, and direct download link for every package.
  • Geist-inspired theme with proper dark/light support driven by system preference.

Implementation notes

  • Pack step now records the produced tarball size via fs.stat after pnpm pack so the page can show human-readable sizes.
  • The page reads VERCEL_GIT_* env vars for commit / branch / PR links and falls back to local git rev-parse for branch when running off-Vercel.
  • All packages still get an entry, but workflow is filtered out of the bottom list and rendered separately.
  • No changes to the deployment shape — public/<escaped-name>.tgz URLs are unchanged, so existing usage keeps working.

The Vercel preview for this PR will deploy and serve the new page so you can see it live.

Test plan

  • Open the Vercel preview deployment and verify the page renders correctly in light and dark mode
  • Confirm the workflow package appears as the featured card at the top
  • Confirm SHA / branch / PR chips are present and linked
  • Toggle pnpm / npm / yarn / bun and confirm install commands update everywhere
  • Filter the package list (and try the / shortcut)
  • Click copy on a row and on the featured card
  • Click download on a row and verify the tarball downloads
  • Smoke-install the featured tarball into a fresh project and confirm it still works (pnpm i https://<deployment>/workflow.tgz)

🤖 Generated with Claude Code

Rebuild the static index page produced by `tarballs/scripts/pack.ts`:
- Featured `workflow` package up top with prominent install command,
copy button, and direct tarball download
- Top-of-page metadata chips: short SHA (linked to commit), branch,
PR number, build timestamp, package count + total size
- Collapsible "What is this?" explainer
- Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the
install command for every row in place
- Live filter input over the rest of the package list (with `/` shortcut)
- Per-row install command, copy button, and direct download
- Modern dark/light theme with system preference, Geist-inspired styling
Also captures tarball size during pack and renders human-readable byte counts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented May 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1c48345

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-nextjs-workflow-webpackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-astro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-express-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-fastify-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-hono-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nitro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nuxt-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-sveltekit-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-tanstack-start-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-vite-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-swc-playgroundReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-tarballsReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-webReadyReadyPreview, CommentMay 4, 2026 11:41am
1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
workflow-docsSkippedSkippedMay 4, 2026 11:41am

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other5100176686
Total4338010525390

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025
✅ e2e-vercel-prod-tanstack-start72026

📋 View full workflow run

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.031s (-30.2% 🟢)1.005s (~)0.974s101.00x
💻 LocalNitro0.032s (-24.8% 🟢)1.005s (~)0.973s101.05x
🐘 PostgresExpress0.043s (-26.4% 🟢)1.010s (~)0.967s101.38x
💻 LocalNext.js (Turbopack)0.047s1.005s0.958s101.51x
🐘 PostgresNitro0.052s (-45.6% 🟢)1.012s (-3.0%)0.960s101.68x
🌐 RedisNext.js (Turbopack)0.059s1.005s0.947s101.89x
🐘 PostgresNext.js (Turbopack)0.059s1.011s0.952s101.91x
🌐 MongoDBNext.js (Turbopack)0.100s1.009s0.910s103.23x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.068s (-5.6% 🟢)2.006s (~)0.938s101.00x
💻 LocalExpress1.074s (-4.6%)2.006s (~)0.932s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.927s101.01x
🐘 PostgresNitro1.088s (-4.6%)2.010s (~)0.922s101.02x
💻 LocalNext.js (Turbopack)1.108s2.006s0.898s101.04x
🌐 RedisNext.js (Turbopack)1.112s2.007s0.895s101.04x
🐘 PostgresNext.js (Turbopack)1.117s2.009s0.892s101.05x
🌐 MongoDBNext.js (Turbopack)1.166s2.009s0.843s101.09x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.387s (-5.1% 🟢)11.022s (~)0.634s31.00x
💻 LocalExpress10.405s (-4.7%)11.023s (~)0.618s31.00x
🐘 PostgresExpress10.415s (-5.0% 🟢)11.012s (~)0.597s31.00x
🐘 PostgresNitro10.432s (-4.0%)11.018s (~)0.585s31.00x
🌐 RedisNext.js (Turbopack)10.634s11.023s0.389s31.02x
💻 LocalNext.js (Turbopack)10.690s11.023s0.333s31.03x
🐘 PostgresNext.js (Turbopack)10.716s11.017s0.301s31.03x
🌐 MongoDBNext.js (Turbopack)10.822s11.017s0.195s31.04x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.426s (-10.9% 🟢)14.027s (-12.5% 🟢)0.600s51.00x
🐘 PostgresExpress13.463s (-7.7% 🟢)14.021s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.480s (-10.0% 🟢)14.028s (-6.7% 🟢)0.548s51.00x
🐘 PostgresNitro13.544s (-7.2% 🟢)14.019s (-6.7% 🟢)0.475s51.01x
💻 LocalNext.js (Turbopack)14.031s15.028s0.997s41.05x
🌐 RedisNext.js (Turbopack)14.036s14.628s0.592s51.05x
🐘 PostgresNext.js (Turbopack)14.130s15.022s0.892s41.05x
🌐 MongoDBNext.js (Turbopack)14.245s15.023s0.778s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.809s (-29.6% 🟢)12.022s (-29.4% 🟢)0.214s81.00x
🐘 PostgresNitro11.928s (-14.6% 🟢)12.017s (-16.0% 🟢)0.089s81.01x
🐘 PostgresExpress11.967s (-14.6% 🟢)12.265s (-16.0% 🟢)0.298s81.01x
💻 LocalExpress12.248s (-26.2% 🟢)12.774s (-25.0% 🟢)0.526s81.04x
💻 LocalNext.js (Turbopack)13.061s13.737s0.677s71.11x
🌐 RedisNext.js (Turbopack)13.217s13.883s0.666s71.12x
🌐 MongoDBNext.js (Turbopack)13.255s14.019s0.764s71.12x
🐘 PostgresNext.js (Turbopack)13.304s14.021s0.718s71.13x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.141s (-9.4% 🟢)2.006s (~)0.865s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.007s (~)0.861s151.00x
💻 LocalNitro1.171s (-28.2% 🟢)2.006s (-3.3%)0.835s151.03x
💻 LocalExpress1.174s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.217s2.007s0.790s151.07x
🌐 RedisNext.js (Turbopack)1.240s2.006s0.766s151.09x
💻 LocalNext.js (Turbopack)1.289s2.006s0.717s151.13x
🌐 MongoDBNext.js (Turbopack)2.035s2.826s0.791s111.78x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-49.1% 🟢)2.007s (-33.3% 🟢)0.805s151.00x
🐘 PostgresNitro1.229s (-47.7% 🟢)2.007s (-33.3% 🟢)0.778s151.02x
🐘 PostgresNext.js (Turbopack)1.358s2.008s0.650s151.13x
💻 LocalNitro1.685s (-46.4% 🟢)2.006s (-48.4% 🟢)0.320s151.40x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.299s151.42x
💻 LocalNext.js (Turbopack)1.858s2.292s0.434s141.55x
🌐 RedisNext.js (Turbopack)2.337s3.008s0.671s101.95x
🌐 MongoDBNext.js (Turbopack)3.568s4.009s0.441s82.97x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.305s (-62.6% 🟢)2.007s (-50.0% 🟢)0.702s151.00x
🐘 PostgresNitro1.383s (-60.3% 🟢)2.007s (-49.9% 🟢)0.624s151.06x
🐘 PostgresNext.js (Turbopack)1.629s2.007s0.378s151.25x
🌐 RedisNext.js (Turbopack)3.602s4.010s0.408s82.76x
💻 LocalNext.js (Turbopack)4.383s5.011s0.627s63.36x
💻 LocalExpress4.397s (-47.3% 🟢)5.013s (-44.5% 🟢)0.616s63.37x
💻 LocalNitro4.571s (-45.2% 🟢)5.011s (-44.4% 🟢)0.440s63.50x
🌐 MongoDBNext.js (Turbopack)6.273s7.013s0.739s54.81x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.139s (-9.4% 🟢)2.009s (~)0.869s151.00x
🐘 PostgresNitro1.168s (-7.1% 🟢)2.009s (~)0.840s151.03x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.773s151.08x
🐘 PostgresNext.js (Turbopack)1.248s2.007s0.760s151.10x
💻 LocalNext.js (Turbopack)1.321s2.005s0.684s151.16x
💻 LocalExpress1.352s (-28.6% 🟢)2.006s (-15.1% 🟢)0.654s151.19x
💻 LocalNitro1.379s (-26.1% 🟢)2.006s (-14.3% 🟢)0.627s151.21x
🌐 MongoDBNext.js (Turbopack)2.026s2.735s0.709s111.78x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-48.3% 🟢)2.008s (-33.3% 🟢)0.797s151.00x
🐘 PostgresNitro1.232s (-47.3% 🟢)2.008s (-33.3% 🟢)0.776s151.02x
🐘 PostgresNext.js (Turbopack)1.368s2.008s0.640s151.13x
💻 LocalExpress1.838s (-41.3% 🟢)2.075s (-44.8% 🟢)0.238s151.52x
💻 LocalNitro1.954s (-36.3% 🟢)2.469s (-36.5% 🟢)0.515s131.61x
💻 LocalNext.js (Turbopack)2.083s2.826s0.743s111.72x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.94x
🌐 MongoDBNext.js (Turbopack)3.547s4.008s0.461s82.93x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.318s (-62.3% 🟢)2.007s (-50.0% 🟢)0.690s151.00x
🐘 PostgresNitro1.379s (-60.4% 🟢)2.007s (-49.9% 🟢)0.628s151.05x
🐘 PostgresNext.js (Turbopack)1.718s2.075s0.357s151.30x
🌐 RedisNext.js (Turbopack)3.595s4.010s0.415s82.73x
💻 LocalNitro4.877s (-46.7% 🟢)5.516s (-45.0% 🟢)0.639s63.70x
💻 LocalExpress5.136s (-41.6% 🟢)5.515s (-40.5% 🟢)0.379s63.90x
💻 LocalNext.js (Turbopack)5.496s6.011s0.515s54.17x
🌐 MongoDBNext.js (Turbopack)6.286s7.013s0.727s54.77x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.430s (-48.7% 🟢)1.006s (-1.7%)0.575s601.00x
💻 LocalNitro0.457s (-53.4% 🟢)1.004s (-8.3% 🟢)0.547s601.06x
🐘 PostgresNitro0.483s (-41.1% 🟢)1.023s (+1.7%)0.540s591.12x
💻 LocalExpress0.509s (-48.3% 🟢)1.004s (-6.7% 🟢)0.495s601.18x
🌐 RedisNext.js (Turbopack)0.617s1.004s0.388s601.43x
🐘 PostgresNext.js (Turbopack)0.697s1.007s0.310s601.62x
💻 LocalNext.js (Turbopack)0.712s1.004s0.292s601.66x
🌐 MongoDBNext.js (Turbopack)0.739s1.006s0.266s601.72x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.027s (-48.0% 🟢)1.586s (-29.8% 🟢)0.559s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.741s (-17.1% 🟢)0.648s521.06x
💻 LocalNitro1.156s (-61.9% 🟢)2.006s (-46.6% 🟢)0.849s451.13x
💻 LocalExpress1.191s (-60.5% 🟢)2.005s (-44.1% 🟢)0.814s451.16x
🌐 RedisNext.js (Turbopack)1.479s2.006s0.527s451.44x
🐘 PostgresNext.js (Turbopack)1.638s2.007s0.369s451.60x
💻 LocalNext.js (Turbopack)1.780s2.027s0.247s451.73x
🌐 MongoDBNext.js (Turbopack)1.811s2.007s0.196s451.76x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.012s (-49.6% 🟢)2.427s (-44.5% 🟢)0.415s501.00x
🐘 PostgresNitro2.035s (-50.4% 🟢)2.457s (-46.6% 🟢)0.422s491.01x
💻 LocalNitro2.638s (-71.6% 🟢)3.007s (-70.0% 🟢)0.369s401.31x
💻 LocalExpress2.802s (-69.6% 🟢)3.084s (-69.2% 🟢)0.283s391.39x
🌐 RedisNext.js (Turbopack)3.003s3.277s0.275s371.49x
🐘 PostgresNext.js (Turbopack)3.192s4.010s0.817s301.59x
💻 LocalNext.js (Turbopack)3.864s4.145s0.282s291.92x
🌐 MongoDBNext.js (Turbopack)4.155s5.011s0.856s242.07x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.172s (-39.1% 🟢)1.006s (~)0.834s601.00x
🐘 PostgresNitro0.192s (-32.2% 🟢)1.006s (~)0.814s601.12x
🐘 PostgresNext.js (Turbopack)0.241s1.006s0.764s601.40x
🌐 RedisNext.js (Turbopack)0.251s1.004s0.753s601.46x
💻 LocalNitro0.432s (-28.6% 🟢)1.004s (-1.7%)0.572s602.51x
💻 LocalExpress0.466s (-16.9% 🟢)1.004s (~)0.539s602.71x
💻 LocalNext.js (Turbopack)0.567s1.004s0.437s603.30x
🌐 MongoDBNext.js (Turbopack)1.041s1.882s0.842s326.05x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.296s (-41.8% 🟢)1.006s (~)0.710s901.00x
🐘 PostgresNitro0.314s (-36.8% 🟢)1.006s (~)0.693s901.06x
🌐 RedisNext.js (Turbopack)0.414s1.004s0.590s901.40x
🐘 PostgresNext.js (Turbopack)0.461s1.006s0.545s901.55x
💻 LocalNitro2.179s (-14.1% 🟢)2.852s (-5.2% 🟢)0.672s327.35x
💻 LocalNext.js (Turbopack)2.182s2.944s0.762s317.36x
💻 LocalExpress2.223s (-11.6% 🟢)2.821s (-6.3% 🟢)0.598s327.50x
🌐 MongoDBNext.js (Turbopack)2.618s3.007s0.389s308.83x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.578s (-29.3% 🟢)1.014s (~)0.436s1191.00x
🐘 PostgresNitro0.641s (-18.9% 🟢)1.006s (~)0.365s1201.11x
🌐 RedisNext.js (Turbopack)0.770s1.004s0.234s1201.33x
🐘 PostgresNext.js (Turbopack)0.957s1.352s0.394s901.65x
🌐 MongoDBNext.js (Turbopack)5.396s6.011s0.615s209.33x
💻 LocalExpress9.835s (-12.1% 🟢)10.444s (-12.5% 🟢)0.609s1217.00x
💻 LocalNitro9.941s (-11.2% 🟢)10.365s (-11.1% 🟢)0.424s1217.19x
💻 LocalNext.js (Turbopack)11.011s11.573s0.562s1119.03x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.127s (+449.7% 🔺)2.001s (+100.3% 🔺)0.001s (-37.5% 🟢)2.009s (+98.6% 🔺)0.882s101.00x
💻 LocalNitro1.128s (+428.0% 🔺)2.005s (+99.6% 🔺)0.011s (-15.2% 🟢)2.018s (+98.1% 🔺)0.890s101.00x
🐘 PostgresNitro1.135s (+453.5% 🔺)2.000s (+100.1% 🔺)0.001s (-6.7% 🟢)2.010s (+98.7% 🔺)0.875s101.01x
💻 LocalExpress1.140s (+472.4% 🔺)2.005s (+99.6% 🔺)0.013s (+3.3%)2.020s (+98.4% 🔺)0.881s101.01x
💻 LocalNext.js (Turbopack)1.190s2.003s0.010s2.017s0.826s101.06x
🐘 PostgresNext.js (Turbopack)1.197s2.001s0.002s2.011s0.815s101.06x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.482s (+135.3% 🔺)2.006s (+99.3% 🔺)0.003s (-10.4% 🟢)2.022s (+97.7% 🔺)0.540s301.00x
🐘 PostgresNitro1.525s (+144.4% 🔺)2.002s (+98.9% 🔺)0.004s (+0.8%)2.026s (+98.2% 🔺)0.501s301.03x
💻 LocalNext.js (Turbopack)1.670s2.009s0.011s2.023s0.352s301.13x
🐘 PostgresNext.js (Turbopack)1.682s2.010s0.004s2.025s0.343s301.13x
💻 LocalExpress1.705s (+125.2% 🔺)2.011s (+95.4% 🔺)0.010s (+8.5% 🔺)2.202s (+111.8% 🔺)0.498s281.15x
💻 LocalNitro1.713s (+104.2% 🔺)2.010s (+98.6% 🔺)0.010s (+4.8%)2.201s (+97.2% 🔺)0.488s281.16x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.622s (-35.2% 🟢)1.013s (-20.7% 🟢)0.000s (+16.9% 🔺)1.028s (-21.3% 🟢)0.406s591.00x
🐘 PostgresNitro0.681s (-29.7% 🟢)1.047s (-16.1% 🟢)0.000s (-58.6% 🟢)1.059s (-15.8% 🟢)0.378s581.09x
🐘 PostgresNext.js (Turbopack)0.817s1.053s0.000s1.068s0.251s571.31x
💻 LocalNitro1.331s (+8.8% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.686s302.14x
💻 LocalExpress1.394s (+13.8% 🔺)2.015s (~)0.000s (+10.0% 🔺)2.017s (~)0.623s302.24x
💻 LocalNext.js (Turbopack)1.437s2.013s0.000s2.016s0.579s302.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.313s (-26.7% 🟢)2.099s (-2.0%)0.000s (-3.4%)2.131s (-2.0%)0.818s291.00x
🐘 PostgresExpress1.349s (-23.9% 🟢)2.145s (-1.5%)0.000s (+Infinity% 🔺)2.161s (-1.7%)0.812s281.03x
🐘 PostgresNext.js (Turbopack)1.689s2.225s0.000s2.273s0.584s271.29x
💻 LocalNext.js (Turbopack)2.563s3.079s0.001s3.084s0.521s201.95x
💻 LocalNitro3.092s (-8.7% 🟢)3.840s (-4.8%)0.000s (-76.6% 🟢)3.844s (-4.8%)0.752s162.35x
💻 LocalExpress3.227s (-6.9% 🟢)4.031s (~)0.000s (-66.7% 🟢)4.033s (~)0.806s152.46x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro14/21
🐘 PostgresExpress19/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres8/21
Nitro🐘 Postgres14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

Comment threadtarballs/scripts/pack.ts Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visual design is nice — featured workflow card, package-manager toggle, live filter, copy buttons, geist-inspired theme. Going to approve since the deployment shape doesn't change and this only affects the static index page.

That said: I'd push back on the hand-rolled HTML-in-JS approach for anything beyond a one-page diff. The new file is 857 lines, and the implementation has some smells that follow naturally from the medium:

  • The inline <script> block (~80 lines) isn't TypeScript-checked. It's inside a template literal in writeIndexHtml, so the compiler treats it as a string. Refactoring (rename a data- attribute, change a property name) won't surface type errors. The TS half references DOM via data- attributes that have to match by convention.
  • Two parallel "render the page" concerns — the pack.ts script handles both bundling-time logic (scanning packages, rewriting deps, packing tarballs) and presentation logic (HTML composition, CSS, client-side JS for PM toggle/filter/copy). Those were independent before this PR; conflating them makes both harder to evolve.
  • renderFeatured/renderRow and the inline applyPm/applyFilter script must stay in sync on attribute names, class names, and the catalog JSON shape. Right now there's a hand-rolled escapeHtml helper and the contract is "data-install-cmd must equal escapedName." Easy to break in a refactor; no tests catch the drift.
  • Hand-rolled escape helpers (escapeHtml) are correct here but would be unnecessary with JSX or a templating library.

A lightweight Vite + React (or Preact for smaller bundle) SPA would clean this up substantially. The shape I'd suggest:

tarballs/
├── package.json # adds vite, react, @vitejs/plugin-react
├── vite.config.ts # static build → public/
├── index.html # entry HTML
├── src/
│ ├── main.tsx # mount React
│ ├── App.tsx # the page (~80 lines instead of ~600 of HTML+CSS+JS)
│ ├── catalog.ts # types for the JSON catalog
│ └── styles.css # the geist-inspired theme
└── scripts/
├── pack.ts # data-only: scans packages, packs tarballs, writes
│ # public/catalog.json with the build context
└── check-tarballs-smoke.mjs

pack.ts becomes ~140 lines (just the bundling logic), vite build produces the static page that fetches catalog.json, the React component reads it on mount and renders. State (active PM, search query) is useState. Build pipeline is pnpm pack → vite build, and Vercel's existing outputDirectory: "public" works unchanged.

DX wins: HMR while iterating on the design, TypeScript checks the JSX and event handlers, can pull in actual geist (or lucide-react) icons via npm instead of inlined SVG, hot-swap component libraries if the design evolves.

If you don't want to take that on now, this is fine as-is — the page works, the design looks good, and the deployment shape is preserved. But suggest filing a follow-up to migrate before adding the next 200 lines of HTML to this file.

Smaller things I noticed

  • getBuildContext uses process.env.VERCEL_GIT_* for SHA/branch/PR. Local builds get only the SHA/branch fallback via git rev-parse. Worth double-checking the labels on chips degrade gracefully when those env vars are missing — looks like they do (early-return paths with if (ctx.commitUrl)), but a quick local pnpm --filter tarballs build test before merge would confirm.
  • The catalog JSON is embedded via <script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>. JSON-inside-HTML-inside-template-literal is two layers of escape (the escapeHtml handles the outer, but the JSON itself can contain </script> if a package description has one — unlikely but possible). Safer to use JSON.stringify(catalog).replace(/</g, '\\u003c') for the embedded JSON, which escapes the dangerous bytes without affecting parseability.
  • formatBytes uses base-2 units (KB = 1024) but labels them as 'KB'/'MB' which by convention suggest base-10. Minor but KiB/MiB would be more precise (or use base-10 with 1000).
  • applyFilter runs on every keystroke against a re-querying Array.from(document.querySelectorAll('.pkg-row')). With ~25 packages this is fine; if the catalog grows substantially, debounce or pre-compute.

Test plan note

The PR description has 8 unchecked test-plan items. They're all manual visual checks (page renders, filter works, copy fires, etc.). Since the Vercel preview URL is the canonical way to validate this, they probably won't be checked off in advance — but worth at least running through them on the preview before merge.

Comment threadtarballs/scripts/pack.ts Outdated
Built ${ctx.commitUrl ? `from <a href="${escapeHtml(ctx.commitUrl)}" target="_blank" rel="noopener">${escapeHtml(ctx.shortSha)}</a>` : `from <code>${escapeHtml(ctx.shortSha)}</code>`} · ${escapeHtml(packages.length.toString())} packages totaling ${escapeHtml(formatBytes(totalSize))}
</footer>
</div>
<script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this line breaks all client-side interactivity.

Verified on the preview deployment with browser automation — every interactive feature is non-functional:

  • ❌ Package-manager toggle (clicking npm/yarn/bun does nothing)
  • ❌ Live filter (typing in search shows all rows)
  • ❌ Copy buttons (no clipboard write, no Copied state)
  • / keyboard shortcut (body keeps focus)

Root cause: escapeHtml(JSON.stringify(catalog)) HTML-encodes the JSON. The serialized catalog ends up in the page as:

<scriptid="catalog" type="application/json">[{&quot;name&quot;:&quot;@workflow/ai&quot;,&quot;escapedName&quot;:&quot;workflow-ai&quot;...

JSON.parse(textContent) then throws on the very first &quot; because &quot; isn't valid JSON syntax. The IIFE bails on its first line and none of the event listeners get attached. Confirmed live:

>JSON.parse(document.getElementById('catalog').textContent)Uncaught SyntaxError: Expectedpropertynameor'}'inJSONatposition2(line1column3)

<script type="application/json"> content is treated as text by the HTML parser — it does NOT need HTML entity escaping. The only sequence that can break out of it is </script> (or </ in some legacy parsers). The narrowest correct fix is:

<scriptid="catalog"type="application/json">${JSON.stringify(catalog).replace(/</g,'\u003c')}</script>

That escapes < bytes to \u003c in the JSON (legal per the spec — JSON allows \uXXXX escapes for any character), so </script> can't appear in the text and the JSON parses cleanly.

This was the third bullet in my "smaller things I noticed" section above — turns out it's not theoretical, it's the live bug breaking everything client-side. Should not merge until the page works in a real browser.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3df48a7. Went with your replace(/</g, '\u003c') over the bot's </-only escape since it also covers a stray < inside, e.g., a package description. Verified locally that the embedded JSON parses cleanly out of the rendered HTML; preview deploy will validate the four interactive features end-to-end.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating my prior review to REQUEST_CHANGES. Ran the visual test plan against the preview deployment with browser automation and discovered that all client-side interactivity is broken — see the inline comment on pack.ts:696 for the root cause and fix.

The escapeHtml(JSON.stringify(catalog)) call HTML-encodes every " in the JSON to &quot;, so JSON.parse(textContent) throws on its first character. The IIFE bails immediately and none of the event listeners (PM toggle, search filter, copy buttons, / shortcut) get attached.

Test plan results from the preview at https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/:

  • ✅ Page renders correctly in light mode (visual layout, chips, featured card, package list)
  • workflow package appears as the featured card at the top
  • ✅ SHA / branch / timestamp / package count chips are present
  • Package-manager toggle doesn't update install commandsaria-selected stays on pnpm regardless of clicks
  • Live filter doesn't filter — typing in the search input shows all rows unchanged
  • Copy buttons don't fire — no Copied state, no clipboard write
  • / keyboard shortcut doesn't focus search — body keeps focus
  • (couldn't test direct download because earlier broken state interfered, but the <a download> is plain HTML and probably works)

So the page that ships looks correct visually but is functionally a static catalog — the toggle, filter, and copy buttons are all dead UI.

Quick fix at the call site (one line). After fix, all four interactive features should work. Worth re-testing on the next preview deployment.

The other points from my prior review (Vite + React refactor suggestion, the smaller TS-checkable concerns, KB vs KiB nit) all still stand and are non-blocking. Just this one issue blocks merging.

`escapeHtml(JSON.stringify(catalog))` was HTML-encoding every quote in
the embedded catalog JSON to `&quot;`, so `JSON.parse(textContent)` threw
on the first character and the IIFE bailed before attaching any event
listeners — package-manager toggle, search filter, copy buttons, and the
`/` shortcut were all dead UI on the deployed page.
`<script type="application/json">` content is treated as text by the HTML
parser; the only sequence that can break out is `</script>` (or `</`
in legacy parsers). Replace `<` with the JSON `<` escape, which is
legal per the JSON spec and prevents the breakout without needing entity
encoding.
Also switch `formatBytes` from `KB`/`MB` to `KiB`/`MiB` since the
divisor is 1024.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment threadtarballs/scripts/pack.ts Outdated
The smoke check broke in CI: `'workflow' tarball only has 0 files`.
Root cause is that `tar -tvzf` emits a different verbose layout on GNU
tar (Linux, what CI runs) vs BSD tar (macOS, where I tested locally) —
the parser only matched the BSD column ordering, so on Linux every line
was rejected and `fileCount` came out as 0.
Replace the shell-out with a small in-process tar reader using
`zlib.gunzipSync` + manual 512-byte block walk. ustar headers are
trivially structured (name at offset 0, octal size at 124, typeflag at
156, ustar prefix at 345). We emit regular files only (`typeflag` `0`
or NUL) and consume but skip pax extended headers (`x`/`g`) and GNU
long-name entries (`L`). Result is identical on every platform.
Verified locally: 206 files / 998413 bytes for `workflow.tgz` matches
`tar -tvzf` exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pranaygpand others added 3 commits May 4, 2026 19:31
The previous "What's inside?" view crammed nested directory groups,
proportional bars, and per-group file lists into a `<details>` inside
an already-narrow row. It was hard to read and harder to compare.
Replace it with the layout packagephobia uses on its result page:
- Two large headline metric tiles (Publish size / Unpacked size)
with a big bold value, smaller unit, and small uppercase label.
Modeled directly on packagephobia's `Stats` component but using
our existing CSS variables so it tracks light/dark theme.
- A single sortable file table beneath. Default is size-descending so
the contributors to package size are immediately visible. Click a
header to flip direction or switch sort key. Sticky header keeps
the columns visible inside the scrollable region.
Drop the `groupByTopLevel`, `ContentsGroup`, and bar-chart styles —
they were the source of the "hard to use" feedback and don't add
information that the flat sortable table doesn't already convey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the old static tarballs index with a Vite/Preact single-page UI that reads a generated catalog.json, surfaces richer build/package metadata, and adds interactive install/copy/filter/download affordances for preview tarballs.

Changes:

  • Adds a Vite + Preact frontend for the tarballs index, including featured package rendering, filtering, package-manager toggles, and package contents breakdowns.
  • Extends scripts/pack.ts to emit catalog.json with build metadata, tarball sizes, unpacked sizes, and per-file listings.
  • Updates build/smoke-test plumbing so the tarballs deployment now publishes bundled SPA assets plus the generated catalog.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tarballs/vite.config.tsConfigures Vite to build the SPA into public/ without deleting tarballs.
tarballs/turbo.jsonExpands build outputs to include SPA assets and catalog.json.
tarballs/tsconfig.jsonAdds TS config for the new frontend/build files.
tarballs/src/styles.cssAdds the full styling/theme for the redesigned index UI.
tarballs/src/main.tsxBoots the SPA and loads catalog.json.
tarballs/src/icons.tsxAdds inline SVG icons used across the new UI.
tarballs/src/catalog.tsDefines shared catalog/install-command helpers for the frontend.
tarballs/src/app.tsxImplements the main tarballs UI, interactions, and package contents tables.
tarballs/scripts/pack.tsGenerates tarballs plus the new metadata-rich catalog.json.
tarballs/scripts/check-tarballs-smoke.mjsAdds smoke coverage for catalog.json.
tarballs/package.jsonSwitches build to pack.ts && vite build and adds frontend deps/scripts.
tarballs/index.htmlAdds the SPA entry HTML shell.
pnpm-lock.yamlLocks newly added frontend/tooling dependencies.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadtarballs/package.json Outdated
"scripts": {
"build": "node scripts/pack.ts",
"build": "node scripts/pack.ts && vite build",
"dev": "vite",

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. The new dev chains node scripts/pack.ts && vite, and I restructured the build layout to vite's conventional shape so it actually works in dev: public/ is a real vite public dir (pack writes tarballs + catalog.json there, vite serves them at root in dev), and dist/ is the production build output (set as Vercel's outputDirectory).

Comment threadtarballs/src/app.tsx
<SearchIcon />
<input
ref={searchRef}
type="search"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added aria-label="Filter packages" to the input.

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +178 to +185
<div class="pm-tabs" role="tablist" aria-label="Package manager">
{options.map((opt) => (
<button
key={opt}
type="button"
class="pm-tab"
role="tab"
aria-selected={value === opt}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Dropped role="tablist"/role="tab"/aria-selected and made these plain buttons with aria-pressed. Toggle buttons are the honest representation since we never implemented arrow-key roving focus. Each button also has an explicit aria-label (Show install commands for pnpm etc.).

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +428 to +440
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} finally {
ta.remove();
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Refactored to a writeToClipboard helper that returns whether the write actually succeeded; the button only flips to Copied on success. If both navigator.clipboard.writeText and the execCommand fallback fail, it briefly shows a red Failed state instead.

Comment threadtarballs/src/app.tsx Outdated
<code class="pkg-cmd">{cmd}</code>
<div class="pkg-actions">
<CopyButton text={cmd} variant="icon" />
<a class="icon-btn" href={pkg.url} download aria-label="Download">

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added an accessibleName prop to CopyButton and pass Copy install command for <pkg.name> from each row + the featured card. Same treatment on the download <a> link (Download <pkg.name> tarball).

Comment threadtarballs/src/main.tsx Outdated
if (!root) throw new Error('No #app root element');

try {
const res = await fetch('/catalog.json', { cache: 'no-store' });

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345 — dropped cache: 'no-store'. Each preview's URL is unique so the browser cache is the right thing to use.

- main.tsx: drop `cache: 'no-store'` from the catalog fetch. Each
tarballs deployment is immutable per commit, so HTTP caching is
appropriate; forcing no-store made every visit re-download the full
catalog (which now includes per-package file lists).
- app.tsx (search input): add `aria-label="Filter packages"`. The
visible label only contained an icon and placeholder, so screen
readers had no name for the control.
- app.tsx (PmTabs): replace `role="tablist"` / `role="tab"` /
`aria-selected` with plain buttons that use `aria-pressed`. The
ARIA tab pattern requires arrow-key roving focus we never wired
up; toggle buttons are the honest representation. Each button
also gets an explicit `aria-label`.
- app.tsx (row buttons): include the package name in the accessible
label of every per-row copy/download button (and on the featured
card too), so the screen reader buttons/links list distinguishes
them. Added an `accessibleName` prop to `CopyButton`.
- app.tsx (CopyButton): only flip to the "Copied" state when the
write actually succeeded. Both the modern `navigator.clipboard`
path and the `execCommand` fallback can fail; the new
`writeToClipboard` helper returns success and the button shows a
short "Failed" state if both paths fail.
# Make `pnpm dev` work from a clean checkout
The previous `dev: vite` couldn't actually serve the page because
`/catalog.json` 404s and the SPA boots into the error fallback.
Restructure the build layout to vite's conventional shape:
- `public/` is now a true vite public dir — pack writes tarballs and
catalog.json there. In dev, vite serves these at the root.
- `dist/` is the production build output (vite copies public/ into it
and adds index.html + assets/).
- `vercel.json#outputDirectory` switches from `public` → `dist`.
- `turbo.json` outputs updated to match.
- `dev` chains pack before vite so the catalog exists when the dev
server starts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​preact/​preset-vite@​2.10.59910010090100
Addednpm/​preact@​10.29.110010010094100

View full report

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pranay took the Vite + Preact refactor recommendation from my prior review and ran with it. The new shape matches what I sketched almost exactly:

tarballs/
├── index.html # 20-line entry
├── vite.config.ts # 16-line config
├── tsconfig.json # strict, jsxImportSource: preact
├── scripts/
│ ├── pack.ts # 317 lines, data-only — packs tarballs, writes catalog.json
│ └── check-tarballs-smoke.mjs # now also validates catalog.json shape
└── src/
├── main.tsx # 26 lines — fetch + mount
├── app.tsx # 550 lines — typed Preact JSX
├── catalog.ts # types + small helpers
├── icons.tsx # 8 inline SVG icons
└── styles.css # 794 lines, geist-inspired theme

Bundle ships as dist/assets/index-*.js at 26.25 kB (9.69 kB gzipped) — small for a Preact SPA with sortable tables, copy-buttons, search, and a PM toggle. CSS is 11.19 kB / 2.73 kB gzipped.

What I verified

  • Local build works: pnpm turbo build --filter=tarballs runs pack.ts then vite build, producing 26 tarballs in public/, a catalog.json matching the typed shape in src/catalog.ts, and a dist/ ready to deploy.
  • TypeScript is clean: pnpm exec tsc --noEmit from tarballs/ passes with the strict config (strict, noUnusedLocals, noUnusedParameters, isolatedModules).
  • Deployed preview serves correctly: https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/catalog.json returns the expected payload with build context (sha, branch, PR number, GitHub-linked commit/branch/PR URLs) and 26 packages with full file listings + sizes. Each package's url is the immutable per-deployment hash URL (e.g. https://workflow-tarballs-ch8zhxmen.labs.vercel.dev/workflow-ai.tgz), so the catalog is stable even if visited via the git-branch URL.
  • JS bundle contains real Preact: read the bundle, confirmed the App tree renders with hooks (useState, useEffect, useMemo, useRef), addEventListener('keydown', …) for the / shortcut, navigator.clipboard.writeText + execCommand('copy') fallback for CopyButton, and the data-copied / data-failed state machine.
  • Smoke check is stronger now: in addition to verifying tarballs are gzip-magic-prefixed and the index page responds with text/html, it now fetches catalog.json and asserts the workflow package has a non-trivial fileCount — a useful diagnostic for "packages weren't built before pack."

Concerns from my prior review — all addressed

  1. The hand-rolled HTML-in-JS approach — gone. JSX is TypeScript-checked end to end (event handlers, prop types, catalog shape).
  2. Two parallel render concerns merged into one file — split clean. pack.ts is now data-only (~317 lines, all bundling/tar-reading). The presentation layer is in src/.
  3. renderRow / applyPm / applyFilter had to stay in sync via string conventions — gone. State lives in React-style hooks, props are typed.
  4. Hand-rolled escapeHtml — gone. JSX handles escaping.
  5. escapeHtml(JSON.stringify(catalog)) breaking client interactivity — moot. The catalog is a separate /catalog.json static file fetched at runtime, so there's no inline-JSON encoding question.
  6. KB/MB labels with base-2 math — fixed. formatBytes now emits B/KiB/MiB.

Copilot review items — all addressed in 1c48345e6

  • a11y: aria-label="Filter packages" on search input; PM tabs use plain buttons with aria-pressed (no half-implemented role="tablist"); per-row copy buttons get accessibleName="Copy install command for ${pkg.name}" instead of a generic label.
  • Robustness: writeToClipboard returns a boolean so failed writes show "Failed" rather than a misleading "Copied" success state.
  • DX: dev script chains pack.ts && vite; dropped cache: 'no-store' since the catalog URL is per-deployment-immutable.

Smaller improvements landed along the way

  • Tar reader rewrite (6ae7736): replaced tar -tvzf shell-out with an in-process gunzip + 512-byte block walker. Handles GNU long-name (L) entries and pax headers, and works identically on macOS BSD tar and Linux GNU tar. Sorting files by descending size before emitting is a nice touch for the per-package "what's inside" view.
  • PackagePhobia-style SizeStats widget: two big tiles (publish size + unpacked size) with split value/unit rendering. Looks good and reads better than a one-liner.
  • FileTable with sortable columns: ARIA aria-sort on <th>, indicator arrows, click-to-toggle direction. Strips the package/ prefix from displayed paths.

Tiny things — non-blocking

  • pack.ts:117: updateDeps builds rewrite URLs as https://${process.env.VERCEL_URL}/${escapedName}.tgz directly, while baseUrl (defined at line 97) uses the VERCEL_URL ? https://… : '' fallback. Local builds without VERCEL_URL produce package.json deps like https://undefined/.... Pre-existing on main, not introduced by this PR. Not a real-world issue (nobody installs from a local pack output) but noting for a follow-up cleanup if you want consistency.
  • catalog.ts and pack.ts duplicate the same TS interfaces (TarballFile, PackedPackage, BuildContext, Catalog). Drift risk is low since they're both in the same package, but if you want to eliminate it, catalog.ts could be the single source of truth and pack.ts could import type from it. Fine to leave.
  • SizeStats is rendered for every package in the list, not just the featured one — but it's behind a <details> so it doesn't blow up initial render. The 26 expanded <details> plus the FileTable for each would render a lot of DOM if expanded all at once; in practice, users open one or two. Not blocking.

Verdict: approve

Approving and clearing my prior CHANGES_REQUESTED. The redesign hit every concern from my last two reviews plus the Copilot feedback, the build pipeline is cleaner than what was there before, and the deployment shape is preserved (public/<escaped-name>.tgz URLs unchanged, smoke checks pass). Nice work on the followups.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

@v0@vercel can you make a PR to address the remaining follow ups from @TooTallNate's review comments that we didn't get to before merge of this one

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-stableCherry-pick this PR to the stable branch when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

tarballs: redesign preview tarballs index page - #1911

Merged
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements
May 4, 2026
Merged

tarballs: redesign preview tarballs index page#1911
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Summary

Reworks the static index page generated by tarballs/scripts/pack.ts (deployed at https://workflow-tarballs.labs.vercel.dev/) to be more useful and pleasant to look at.

What's new

  • Featured workflow package at the top with a large install command, copy button, and direct tarball download — the rest of the packages are secondary.
  • Top-of-page metadata chips: short SHA (linked to commit on GitHub), branch (linked), PR number (linked), build timestamp, package count + total size.
  • Collapsible "What is this?" explainer describing how the version + dep rewriting works.
  • Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the install command shown for every row in place.
  • Live filter input over the package list, with / keyboard shortcut to focus.
  • Per-row install command, copy button, and direct download link for every package.
  • Geist-inspired theme with proper dark/light support driven by system preference.

Implementation notes

  • Pack step now records the produced tarball size via fs.stat after pnpm pack so the page can show human-readable sizes.
  • The page reads VERCEL_GIT_* env vars for commit / branch / PR links and falls back to local git rev-parse for branch when running off-Vercel.
  • All packages still get an entry, but workflow is filtered out of the bottom list and rendered separately.
  • No changes to the deployment shape — public/<escaped-name>.tgz URLs are unchanged, so existing usage keeps working.

The Vercel preview for this PR will deploy and serve the new page so you can see it live.

Test plan

  • Open the Vercel preview deployment and verify the page renders correctly in light and dark mode
  • Confirm the workflow package appears as the featured card at the top
  • Confirm SHA / branch / PR chips are present and linked
  • Toggle pnpm / npm / yarn / bun and confirm install commands update everywhere
  • Filter the package list (and try the / shortcut)
  • Click copy on a row and on the featured card
  • Click download on a row and verify the tarball downloads
  • Smoke-install the featured tarball into a fresh project and confirm it still works (pnpm i https://<deployment>/workflow.tgz)

🤖 Generated with Claude Code

Rebuild the static index page produced by `tarballs/scripts/pack.ts`:
- Featured `workflow` package up top with prominent install command,
copy button, and direct tarball download
- Top-of-page metadata chips: short SHA (linked to commit), branch,
PR number, build timestamp, package count + total size
- Collapsible "What is this?" explainer
- Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the
install command for every row in place
- Live filter input over the rest of the package list (with `/` shortcut)
- Per-row install command, copy button, and direct download
- Modern dark/light theme with system preference, Geist-inspired styling
Also captures tarball size during pack and renders human-readable byte counts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented May 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1c48345

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-nextjs-workflow-webpackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-astro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-express-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-fastify-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-hono-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nitro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nuxt-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-sveltekit-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-tanstack-start-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-vite-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-swc-playgroundReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-tarballsReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-webReadyReadyPreview, CommentMay 4, 2026 11:41am
1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
workflow-docsSkippedSkippedMay 4, 2026 11:41am

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other5100176686
Total4338010525390

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025
✅ e2e-vercel-prod-tanstack-start72026

📋 View full workflow run

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.031s (-30.2% 🟢)1.005s (~)0.974s101.00x
💻 LocalNitro0.032s (-24.8% 🟢)1.005s (~)0.973s101.05x
🐘 PostgresExpress0.043s (-26.4% 🟢)1.010s (~)0.967s101.38x
💻 LocalNext.js (Turbopack)0.047s1.005s0.958s101.51x
🐘 PostgresNitro0.052s (-45.6% 🟢)1.012s (-3.0%)0.960s101.68x
🌐 RedisNext.js (Turbopack)0.059s1.005s0.947s101.89x
🐘 PostgresNext.js (Turbopack)0.059s1.011s0.952s101.91x
🌐 MongoDBNext.js (Turbopack)0.100s1.009s0.910s103.23x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.068s (-5.6% 🟢)2.006s (~)0.938s101.00x
💻 LocalExpress1.074s (-4.6%)2.006s (~)0.932s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.927s101.01x
🐘 PostgresNitro1.088s (-4.6%)2.010s (~)0.922s101.02x
💻 LocalNext.js (Turbopack)1.108s2.006s0.898s101.04x
🌐 RedisNext.js (Turbopack)1.112s2.007s0.895s101.04x
🐘 PostgresNext.js (Turbopack)1.117s2.009s0.892s101.05x
🌐 MongoDBNext.js (Turbopack)1.166s2.009s0.843s101.09x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.387s (-5.1% 🟢)11.022s (~)0.634s31.00x
💻 LocalExpress10.405s (-4.7%)11.023s (~)0.618s31.00x
🐘 PostgresExpress10.415s (-5.0% 🟢)11.012s (~)0.597s31.00x
🐘 PostgresNitro10.432s (-4.0%)11.018s (~)0.585s31.00x
🌐 RedisNext.js (Turbopack)10.634s11.023s0.389s31.02x
💻 LocalNext.js (Turbopack)10.690s11.023s0.333s31.03x
🐘 PostgresNext.js (Turbopack)10.716s11.017s0.301s31.03x
🌐 MongoDBNext.js (Turbopack)10.822s11.017s0.195s31.04x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.426s (-10.9% 🟢)14.027s (-12.5% 🟢)0.600s51.00x
🐘 PostgresExpress13.463s (-7.7% 🟢)14.021s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.480s (-10.0% 🟢)14.028s (-6.7% 🟢)0.548s51.00x
🐘 PostgresNitro13.544s (-7.2% 🟢)14.019s (-6.7% 🟢)0.475s51.01x
💻 LocalNext.js (Turbopack)14.031s15.028s0.997s41.05x
🌐 RedisNext.js (Turbopack)14.036s14.628s0.592s51.05x
🐘 PostgresNext.js (Turbopack)14.130s15.022s0.892s41.05x
🌐 MongoDBNext.js (Turbopack)14.245s15.023s0.778s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.809s (-29.6% 🟢)12.022s (-29.4% 🟢)0.214s81.00x
🐘 PostgresNitro11.928s (-14.6% 🟢)12.017s (-16.0% 🟢)0.089s81.01x
🐘 PostgresExpress11.967s (-14.6% 🟢)12.265s (-16.0% 🟢)0.298s81.01x
💻 LocalExpress12.248s (-26.2% 🟢)12.774s (-25.0% 🟢)0.526s81.04x
💻 LocalNext.js (Turbopack)13.061s13.737s0.677s71.11x
🌐 RedisNext.js (Turbopack)13.217s13.883s0.666s71.12x
🌐 MongoDBNext.js (Turbopack)13.255s14.019s0.764s71.12x
🐘 PostgresNext.js (Turbopack)13.304s14.021s0.718s71.13x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.141s (-9.4% 🟢)2.006s (~)0.865s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.007s (~)0.861s151.00x
💻 LocalNitro1.171s (-28.2% 🟢)2.006s (-3.3%)0.835s151.03x
💻 LocalExpress1.174s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.217s2.007s0.790s151.07x
🌐 RedisNext.js (Turbopack)1.240s2.006s0.766s151.09x
💻 LocalNext.js (Turbopack)1.289s2.006s0.717s151.13x
🌐 MongoDBNext.js (Turbopack)2.035s2.826s0.791s111.78x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-49.1% 🟢)2.007s (-33.3% 🟢)0.805s151.00x
🐘 PostgresNitro1.229s (-47.7% 🟢)2.007s (-33.3% 🟢)0.778s151.02x
🐘 PostgresNext.js (Turbopack)1.358s2.008s0.650s151.13x
💻 LocalNitro1.685s (-46.4% 🟢)2.006s (-48.4% 🟢)0.320s151.40x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.299s151.42x
💻 LocalNext.js (Turbopack)1.858s2.292s0.434s141.55x
🌐 RedisNext.js (Turbopack)2.337s3.008s0.671s101.95x
🌐 MongoDBNext.js (Turbopack)3.568s4.009s0.441s82.97x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.305s (-62.6% 🟢)2.007s (-50.0% 🟢)0.702s151.00x
🐘 PostgresNitro1.383s (-60.3% 🟢)2.007s (-49.9% 🟢)0.624s151.06x
🐘 PostgresNext.js (Turbopack)1.629s2.007s0.378s151.25x
🌐 RedisNext.js (Turbopack)3.602s4.010s0.408s82.76x
💻 LocalNext.js (Turbopack)4.383s5.011s0.627s63.36x
💻 LocalExpress4.397s (-47.3% 🟢)5.013s (-44.5% 🟢)0.616s63.37x
💻 LocalNitro4.571s (-45.2% 🟢)5.011s (-44.4% 🟢)0.440s63.50x
🌐 MongoDBNext.js (Turbopack)6.273s7.013s0.739s54.81x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.139s (-9.4% 🟢)2.009s (~)0.869s151.00x
🐘 PostgresNitro1.168s (-7.1% 🟢)2.009s (~)0.840s151.03x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.773s151.08x
🐘 PostgresNext.js (Turbopack)1.248s2.007s0.760s151.10x
💻 LocalNext.js (Turbopack)1.321s2.005s0.684s151.16x
💻 LocalExpress1.352s (-28.6% 🟢)2.006s (-15.1% 🟢)0.654s151.19x
💻 LocalNitro1.379s (-26.1% 🟢)2.006s (-14.3% 🟢)0.627s151.21x
🌐 MongoDBNext.js (Turbopack)2.026s2.735s0.709s111.78x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-48.3% 🟢)2.008s (-33.3% 🟢)0.797s151.00x
🐘 PostgresNitro1.232s (-47.3% 🟢)2.008s (-33.3% 🟢)0.776s151.02x
🐘 PostgresNext.js (Turbopack)1.368s2.008s0.640s151.13x
💻 LocalExpress1.838s (-41.3% 🟢)2.075s (-44.8% 🟢)0.238s151.52x
💻 LocalNitro1.954s (-36.3% 🟢)2.469s (-36.5% 🟢)0.515s131.61x
💻 LocalNext.js (Turbopack)2.083s2.826s0.743s111.72x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.94x
🌐 MongoDBNext.js (Turbopack)3.547s4.008s0.461s82.93x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.318s (-62.3% 🟢)2.007s (-50.0% 🟢)0.690s151.00x
🐘 PostgresNitro1.379s (-60.4% 🟢)2.007s (-49.9% 🟢)0.628s151.05x
🐘 PostgresNext.js (Turbopack)1.718s2.075s0.357s151.30x
🌐 RedisNext.js (Turbopack)3.595s4.010s0.415s82.73x
💻 LocalNitro4.877s (-46.7% 🟢)5.516s (-45.0% 🟢)0.639s63.70x
💻 LocalExpress5.136s (-41.6% 🟢)5.515s (-40.5% 🟢)0.379s63.90x
💻 LocalNext.js (Turbopack)5.496s6.011s0.515s54.17x
🌐 MongoDBNext.js (Turbopack)6.286s7.013s0.727s54.77x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.430s (-48.7% 🟢)1.006s (-1.7%)0.575s601.00x
💻 LocalNitro0.457s (-53.4% 🟢)1.004s (-8.3% 🟢)0.547s601.06x
🐘 PostgresNitro0.483s (-41.1% 🟢)1.023s (+1.7%)0.540s591.12x
💻 LocalExpress0.509s (-48.3% 🟢)1.004s (-6.7% 🟢)0.495s601.18x
🌐 RedisNext.js (Turbopack)0.617s1.004s0.388s601.43x
🐘 PostgresNext.js (Turbopack)0.697s1.007s0.310s601.62x
💻 LocalNext.js (Turbopack)0.712s1.004s0.292s601.66x
🌐 MongoDBNext.js (Turbopack)0.739s1.006s0.266s601.72x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.027s (-48.0% 🟢)1.586s (-29.8% 🟢)0.559s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.741s (-17.1% 🟢)0.648s521.06x
💻 LocalNitro1.156s (-61.9% 🟢)2.006s (-46.6% 🟢)0.849s451.13x
💻 LocalExpress1.191s (-60.5% 🟢)2.005s (-44.1% 🟢)0.814s451.16x
🌐 RedisNext.js (Turbopack)1.479s2.006s0.527s451.44x
🐘 PostgresNext.js (Turbopack)1.638s2.007s0.369s451.60x
💻 LocalNext.js (Turbopack)1.780s2.027s0.247s451.73x
🌐 MongoDBNext.js (Turbopack)1.811s2.007s0.196s451.76x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.012s (-49.6% 🟢)2.427s (-44.5% 🟢)0.415s501.00x
🐘 PostgresNitro2.035s (-50.4% 🟢)2.457s (-46.6% 🟢)0.422s491.01x
💻 LocalNitro2.638s (-71.6% 🟢)3.007s (-70.0% 🟢)0.369s401.31x
💻 LocalExpress2.802s (-69.6% 🟢)3.084s (-69.2% 🟢)0.283s391.39x
🌐 RedisNext.js (Turbopack)3.003s3.277s0.275s371.49x
🐘 PostgresNext.js (Turbopack)3.192s4.010s0.817s301.59x
💻 LocalNext.js (Turbopack)3.864s4.145s0.282s291.92x
🌐 MongoDBNext.js (Turbopack)4.155s5.011s0.856s242.07x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.172s (-39.1% 🟢)1.006s (~)0.834s601.00x
🐘 PostgresNitro0.192s (-32.2% 🟢)1.006s (~)0.814s601.12x
🐘 PostgresNext.js (Turbopack)0.241s1.006s0.764s601.40x
🌐 RedisNext.js (Turbopack)0.251s1.004s0.753s601.46x
💻 LocalNitro0.432s (-28.6% 🟢)1.004s (-1.7%)0.572s602.51x
💻 LocalExpress0.466s (-16.9% 🟢)1.004s (~)0.539s602.71x
💻 LocalNext.js (Turbopack)0.567s1.004s0.437s603.30x
🌐 MongoDBNext.js (Turbopack)1.041s1.882s0.842s326.05x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.296s (-41.8% 🟢)1.006s (~)0.710s901.00x
🐘 PostgresNitro0.314s (-36.8% 🟢)1.006s (~)0.693s901.06x
🌐 RedisNext.js (Turbopack)0.414s1.004s0.590s901.40x
🐘 PostgresNext.js (Turbopack)0.461s1.006s0.545s901.55x
💻 LocalNitro2.179s (-14.1% 🟢)2.852s (-5.2% 🟢)0.672s327.35x
💻 LocalNext.js (Turbopack)2.182s2.944s0.762s317.36x
💻 LocalExpress2.223s (-11.6% 🟢)2.821s (-6.3% 🟢)0.598s327.50x
🌐 MongoDBNext.js (Turbopack)2.618s3.007s0.389s308.83x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.578s (-29.3% 🟢)1.014s (~)0.436s1191.00x
🐘 PostgresNitro0.641s (-18.9% 🟢)1.006s (~)0.365s1201.11x
🌐 RedisNext.js (Turbopack)0.770s1.004s0.234s1201.33x
🐘 PostgresNext.js (Turbopack)0.957s1.352s0.394s901.65x
🌐 MongoDBNext.js (Turbopack)5.396s6.011s0.615s209.33x
💻 LocalExpress9.835s (-12.1% 🟢)10.444s (-12.5% 🟢)0.609s1217.00x
💻 LocalNitro9.941s (-11.2% 🟢)10.365s (-11.1% 🟢)0.424s1217.19x
💻 LocalNext.js (Turbopack)11.011s11.573s0.562s1119.03x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.127s (+449.7% 🔺)2.001s (+100.3% 🔺)0.001s (-37.5% 🟢)2.009s (+98.6% 🔺)0.882s101.00x
💻 LocalNitro1.128s (+428.0% 🔺)2.005s (+99.6% 🔺)0.011s (-15.2% 🟢)2.018s (+98.1% 🔺)0.890s101.00x
🐘 PostgresNitro1.135s (+453.5% 🔺)2.000s (+100.1% 🔺)0.001s (-6.7% 🟢)2.010s (+98.7% 🔺)0.875s101.01x
💻 LocalExpress1.140s (+472.4% 🔺)2.005s (+99.6% 🔺)0.013s (+3.3%)2.020s (+98.4% 🔺)0.881s101.01x
💻 LocalNext.js (Turbopack)1.190s2.003s0.010s2.017s0.826s101.06x
🐘 PostgresNext.js (Turbopack)1.197s2.001s0.002s2.011s0.815s101.06x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.482s (+135.3% 🔺)2.006s (+99.3% 🔺)0.003s (-10.4% 🟢)2.022s (+97.7% 🔺)0.540s301.00x
🐘 PostgresNitro1.525s (+144.4% 🔺)2.002s (+98.9% 🔺)0.004s (+0.8%)2.026s (+98.2% 🔺)0.501s301.03x
💻 LocalNext.js (Turbopack)1.670s2.009s0.011s2.023s0.352s301.13x
🐘 PostgresNext.js (Turbopack)1.682s2.010s0.004s2.025s0.343s301.13x
💻 LocalExpress1.705s (+125.2% 🔺)2.011s (+95.4% 🔺)0.010s (+8.5% 🔺)2.202s (+111.8% 🔺)0.498s281.15x
💻 LocalNitro1.713s (+104.2% 🔺)2.010s (+98.6% 🔺)0.010s (+4.8%)2.201s (+97.2% 🔺)0.488s281.16x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.622s (-35.2% 🟢)1.013s (-20.7% 🟢)0.000s (+16.9% 🔺)1.028s (-21.3% 🟢)0.406s591.00x
🐘 PostgresNitro0.681s (-29.7% 🟢)1.047s (-16.1% 🟢)0.000s (-58.6% 🟢)1.059s (-15.8% 🟢)0.378s581.09x
🐘 PostgresNext.js (Turbopack)0.817s1.053s0.000s1.068s0.251s571.31x
💻 LocalNitro1.331s (+8.8% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.686s302.14x
💻 LocalExpress1.394s (+13.8% 🔺)2.015s (~)0.000s (+10.0% 🔺)2.017s (~)0.623s302.24x
💻 LocalNext.js (Turbopack)1.437s2.013s0.000s2.016s0.579s302.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.313s (-26.7% 🟢)2.099s (-2.0%)0.000s (-3.4%)2.131s (-2.0%)0.818s291.00x
🐘 PostgresExpress1.349s (-23.9% 🟢)2.145s (-1.5%)0.000s (+Infinity% 🔺)2.161s (-1.7%)0.812s281.03x
🐘 PostgresNext.js (Turbopack)1.689s2.225s0.000s2.273s0.584s271.29x
💻 LocalNext.js (Turbopack)2.563s3.079s0.001s3.084s0.521s201.95x
💻 LocalNitro3.092s (-8.7% 🟢)3.840s (-4.8%)0.000s (-76.6% 🟢)3.844s (-4.8%)0.752s162.35x
💻 LocalExpress3.227s (-6.9% 🟢)4.031s (~)0.000s (-66.7% 🟢)4.033s (~)0.806s152.46x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro14/21
🐘 PostgresExpress19/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres8/21
Nitro🐘 Postgres14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

Comment threadtarballs/scripts/pack.ts Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visual design is nice — featured workflow card, package-manager toggle, live filter, copy buttons, geist-inspired theme. Going to approve since the deployment shape doesn't change and this only affects the static index page.

That said: I'd push back on the hand-rolled HTML-in-JS approach for anything beyond a one-page diff. The new file is 857 lines, and the implementation has some smells that follow naturally from the medium:

  • The inline <script> block (~80 lines) isn't TypeScript-checked. It's inside a template literal in writeIndexHtml, so the compiler treats it as a string. Refactoring (rename a data- attribute, change a property name) won't surface type errors. The TS half references DOM via data- attributes that have to match by convention.
  • Two parallel "render the page" concerns — the pack.ts script handles both bundling-time logic (scanning packages, rewriting deps, packing tarballs) and presentation logic (HTML composition, CSS, client-side JS for PM toggle/filter/copy). Those were independent before this PR; conflating them makes both harder to evolve.
  • renderFeatured/renderRow and the inline applyPm/applyFilter script must stay in sync on attribute names, class names, and the catalog JSON shape. Right now there's a hand-rolled escapeHtml helper and the contract is "data-install-cmd must equal escapedName." Easy to break in a refactor; no tests catch the drift.
  • Hand-rolled escape helpers (escapeHtml) are correct here but would be unnecessary with JSX or a templating library.

A lightweight Vite + React (or Preact for smaller bundle) SPA would clean this up substantially. The shape I'd suggest:

tarballs/
├── package.json # adds vite, react, @vitejs/plugin-react
├── vite.config.ts # static build → public/
├── index.html # entry HTML
├── src/
│ ├── main.tsx # mount React
│ ├── App.tsx # the page (~80 lines instead of ~600 of HTML+CSS+JS)
│ ├── catalog.ts # types for the JSON catalog
│ └── styles.css # the geist-inspired theme
└── scripts/
├── pack.ts # data-only: scans packages, packs tarballs, writes
│ # public/catalog.json with the build context
└── check-tarballs-smoke.mjs

pack.ts becomes ~140 lines (just the bundling logic), vite build produces the static page that fetches catalog.json, the React component reads it on mount and renders. State (active PM, search query) is useState. Build pipeline is pnpm pack → vite build, and Vercel's existing outputDirectory: "public" works unchanged.

DX wins: HMR while iterating on the design, TypeScript checks the JSX and event handlers, can pull in actual geist (or lucide-react) icons via npm instead of inlined SVG, hot-swap component libraries if the design evolves.

If you don't want to take that on now, this is fine as-is — the page works, the design looks good, and the deployment shape is preserved. But suggest filing a follow-up to migrate before adding the next 200 lines of HTML to this file.

Smaller things I noticed

  • getBuildContext uses process.env.VERCEL_GIT_* for SHA/branch/PR. Local builds get only the SHA/branch fallback via git rev-parse. Worth double-checking the labels on chips degrade gracefully when those env vars are missing — looks like they do (early-return paths with if (ctx.commitUrl)), but a quick local pnpm --filter tarballs build test before merge would confirm.
  • The catalog JSON is embedded via <script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>. JSON-inside-HTML-inside-template-literal is two layers of escape (the escapeHtml handles the outer, but the JSON itself can contain </script> if a package description has one — unlikely but possible). Safer to use JSON.stringify(catalog).replace(/</g, '\\u003c') for the embedded JSON, which escapes the dangerous bytes without affecting parseability.
  • formatBytes uses base-2 units (KB = 1024) but labels them as 'KB'/'MB' which by convention suggest base-10. Minor but KiB/MiB would be more precise (or use base-10 with 1000).
  • applyFilter runs on every keystroke against a re-querying Array.from(document.querySelectorAll('.pkg-row')). With ~25 packages this is fine; if the catalog grows substantially, debounce or pre-compute.

Test plan note

The PR description has 8 unchecked test-plan items. They're all manual visual checks (page renders, filter works, copy fires, etc.). Since the Vercel preview URL is the canonical way to validate this, they probably won't be checked off in advance — but worth at least running through them on the preview before merge.

Comment threadtarballs/scripts/pack.ts Outdated
Built ${ctx.commitUrl ? `from <a href="${escapeHtml(ctx.commitUrl)}" target="_blank" rel="noopener">${escapeHtml(ctx.shortSha)}</a>` : `from <code>${escapeHtml(ctx.shortSha)}</code>`} · ${escapeHtml(packages.length.toString())} packages totaling ${escapeHtml(formatBytes(totalSize))}
</footer>
</div>
<script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this line breaks all client-side interactivity.

Verified on the preview deployment with browser automation — every interactive feature is non-functional:

  • ❌ Package-manager toggle (clicking npm/yarn/bun does nothing)
  • ❌ Live filter (typing in search shows all rows)
  • ❌ Copy buttons (no clipboard write, no Copied state)
  • / keyboard shortcut (body keeps focus)

Root cause: escapeHtml(JSON.stringify(catalog)) HTML-encodes the JSON. The serialized catalog ends up in the page as:

<scriptid="catalog" type="application/json">[{&quot;name&quot;:&quot;@workflow/ai&quot;,&quot;escapedName&quot;:&quot;workflow-ai&quot;...

JSON.parse(textContent) then throws on the very first &quot; because &quot; isn't valid JSON syntax. The IIFE bails on its first line and none of the event listeners get attached. Confirmed live:

>JSON.parse(document.getElementById('catalog').textContent)Uncaught SyntaxError: Expectedpropertynameor'}'inJSONatposition2(line1column3)

<script type="application/json"> content is treated as text by the HTML parser — it does NOT need HTML entity escaping. The only sequence that can break out of it is </script> (or </ in some legacy parsers). The narrowest correct fix is:

<scriptid="catalog"type="application/json">${JSON.stringify(catalog).replace(/</g,'\u003c')}</script>

That escapes < bytes to \u003c in the JSON (legal per the spec — JSON allows \uXXXX escapes for any character), so </script> can't appear in the text and the JSON parses cleanly.

This was the third bullet in my "smaller things I noticed" section above — turns out it's not theoretical, it's the live bug breaking everything client-side. Should not merge until the page works in a real browser.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3df48a7. Went with your replace(/</g, '\u003c') over the bot's </-only escape since it also covers a stray < inside, e.g., a package description. Verified locally that the embedded JSON parses cleanly out of the rendered HTML; preview deploy will validate the four interactive features end-to-end.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating my prior review to REQUEST_CHANGES. Ran the visual test plan against the preview deployment with browser automation and discovered that all client-side interactivity is broken — see the inline comment on pack.ts:696 for the root cause and fix.

The escapeHtml(JSON.stringify(catalog)) call HTML-encodes every " in the JSON to &quot;, so JSON.parse(textContent) throws on its first character. The IIFE bails immediately and none of the event listeners (PM toggle, search filter, copy buttons, / shortcut) get attached.

Test plan results from the preview at https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/:

  • ✅ Page renders correctly in light mode (visual layout, chips, featured card, package list)
  • workflow package appears as the featured card at the top
  • ✅ SHA / branch / timestamp / package count chips are present
  • Package-manager toggle doesn't update install commandsaria-selected stays on pnpm regardless of clicks
  • Live filter doesn't filter — typing in the search input shows all rows unchanged
  • Copy buttons don't fire — no Copied state, no clipboard write
  • / keyboard shortcut doesn't focus search — body keeps focus
  • (couldn't test direct download because earlier broken state interfered, but the <a download> is plain HTML and probably works)

So the page that ships looks correct visually but is functionally a static catalog — the toggle, filter, and copy buttons are all dead UI.

Quick fix at the call site (one line). After fix, all four interactive features should work. Worth re-testing on the next preview deployment.

The other points from my prior review (Vite + React refactor suggestion, the smaller TS-checkable concerns, KB vs KiB nit) all still stand and are non-blocking. Just this one issue blocks merging.

`escapeHtml(JSON.stringify(catalog))` was HTML-encoding every quote in
the embedded catalog JSON to `&quot;`, so `JSON.parse(textContent)` threw
on the first character and the IIFE bailed before attaching any event
listeners — package-manager toggle, search filter, copy buttons, and the
`/` shortcut were all dead UI on the deployed page.
`<script type="application/json">` content is treated as text by the HTML
parser; the only sequence that can break out is `</script>` (or `</`
in legacy parsers). Replace `<` with the JSON `<` escape, which is
legal per the JSON spec and prevents the breakout without needing entity
encoding.
Also switch `formatBytes` from `KB`/`MB` to `KiB`/`MiB` since the
divisor is 1024.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment threadtarballs/scripts/pack.ts Outdated
The smoke check broke in CI: `'workflow' tarball only has 0 files`.
Root cause is that `tar -tvzf` emits a different verbose layout on GNU
tar (Linux, what CI runs) vs BSD tar (macOS, where I tested locally) —
the parser only matched the BSD column ordering, so on Linux every line
was rejected and `fileCount` came out as 0.
Replace the shell-out with a small in-process tar reader using
`zlib.gunzipSync` + manual 512-byte block walk. ustar headers are
trivially structured (name at offset 0, octal size at 124, typeflag at
156, ustar prefix at 345). We emit regular files only (`typeflag` `0`
or NUL) and consume but skip pax extended headers (`x`/`g`) and GNU
long-name entries (`L`). Result is identical on every platform.
Verified locally: 206 files / 998413 bytes for `workflow.tgz` matches
`tar -tvzf` exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pranaygpand others added 3 commits May 4, 2026 19:31
The previous "What's inside?" view crammed nested directory groups,
proportional bars, and per-group file lists into a `<details>` inside
an already-narrow row. It was hard to read and harder to compare.
Replace it with the layout packagephobia uses on its result page:
- Two large headline metric tiles (Publish size / Unpacked size)
with a big bold value, smaller unit, and small uppercase label.
Modeled directly on packagephobia's `Stats` component but using
our existing CSS variables so it tracks light/dark theme.
- A single sortable file table beneath. Default is size-descending so
the contributors to package size are immediately visible. Click a
header to flip direction or switch sort key. Sticky header keeps
the columns visible inside the scrollable region.
Drop the `groupByTopLevel`, `ContentsGroup`, and bar-chart styles —
they were the source of the "hard to use" feedback and don't add
information that the flat sortable table doesn't already convey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the old static tarballs index with a Vite/Preact single-page UI that reads a generated catalog.json, surfaces richer build/package metadata, and adds interactive install/copy/filter/download affordances for preview tarballs.

Changes:

  • Adds a Vite + Preact frontend for the tarballs index, including featured package rendering, filtering, package-manager toggles, and package contents breakdowns.
  • Extends scripts/pack.ts to emit catalog.json with build metadata, tarball sizes, unpacked sizes, and per-file listings.
  • Updates build/smoke-test plumbing so the tarballs deployment now publishes bundled SPA assets plus the generated catalog.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tarballs/vite.config.tsConfigures Vite to build the SPA into public/ without deleting tarballs.
tarballs/turbo.jsonExpands build outputs to include SPA assets and catalog.json.
tarballs/tsconfig.jsonAdds TS config for the new frontend/build files.
tarballs/src/styles.cssAdds the full styling/theme for the redesigned index UI.
tarballs/src/main.tsxBoots the SPA and loads catalog.json.
tarballs/src/icons.tsxAdds inline SVG icons used across the new UI.
tarballs/src/catalog.tsDefines shared catalog/install-command helpers for the frontend.
tarballs/src/app.tsxImplements the main tarballs UI, interactions, and package contents tables.
tarballs/scripts/pack.tsGenerates tarballs plus the new metadata-rich catalog.json.
tarballs/scripts/check-tarballs-smoke.mjsAdds smoke coverage for catalog.json.
tarballs/package.jsonSwitches build to pack.ts && vite build and adds frontend deps/scripts.
tarballs/index.htmlAdds the SPA entry HTML shell.
pnpm-lock.yamlLocks newly added frontend/tooling dependencies.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadtarballs/package.json Outdated
"scripts": {
"build": "node scripts/pack.ts",
"build": "node scripts/pack.ts && vite build",
"dev": "vite",

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. The new dev chains node scripts/pack.ts && vite, and I restructured the build layout to vite's conventional shape so it actually works in dev: public/ is a real vite public dir (pack writes tarballs + catalog.json there, vite serves them at root in dev), and dist/ is the production build output (set as Vercel's outputDirectory).

Comment threadtarballs/src/app.tsx
<SearchIcon />
<input
ref={searchRef}
type="search"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added aria-label="Filter packages" to the input.

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +178 to +185
<div class="pm-tabs" role="tablist" aria-label="Package manager">
{options.map((opt) => (
<button
key={opt}
type="button"
class="pm-tab"
role="tab"
aria-selected={value === opt}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Dropped role="tablist"/role="tab"/aria-selected and made these plain buttons with aria-pressed. Toggle buttons are the honest representation since we never implemented arrow-key roving focus. Each button also has an explicit aria-label (Show install commands for pnpm etc.).

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +428 to +440
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} finally {
ta.remove();
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Refactored to a writeToClipboard helper that returns whether the write actually succeeded; the button only flips to Copied on success. If both navigator.clipboard.writeText and the execCommand fallback fail, it briefly shows a red Failed state instead.

Comment threadtarballs/src/app.tsx Outdated
<code class="pkg-cmd">{cmd}</code>
<div class="pkg-actions">
<CopyButton text={cmd} variant="icon" />
<a class="icon-btn" href={pkg.url} download aria-label="Download">

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added an accessibleName prop to CopyButton and pass Copy install command for <pkg.name> from each row + the featured card. Same treatment on the download <a> link (Download <pkg.name> tarball).

Comment threadtarballs/src/main.tsx Outdated
if (!root) throw new Error('No #app root element');

try {
const res = await fetch('/catalog.json', { cache: 'no-store' });

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345 — dropped cache: 'no-store'. Each preview's URL is unique so the browser cache is the right thing to use.

- main.tsx: drop `cache: 'no-store'` from the catalog fetch. Each
tarballs deployment is immutable per commit, so HTTP caching is
appropriate; forcing no-store made every visit re-download the full
catalog (which now includes per-package file lists).
- app.tsx (search input): add `aria-label="Filter packages"`. The
visible label only contained an icon and placeholder, so screen
readers had no name for the control.
- app.tsx (PmTabs): replace `role="tablist"` / `role="tab"` /
`aria-selected` with plain buttons that use `aria-pressed`. The
ARIA tab pattern requires arrow-key roving focus we never wired
up; toggle buttons are the honest representation. Each button
also gets an explicit `aria-label`.
- app.tsx (row buttons): include the package name in the accessible
label of every per-row copy/download button (and on the featured
card too), so the screen reader buttons/links list distinguishes
them. Added an `accessibleName` prop to `CopyButton`.
- app.tsx (CopyButton): only flip to the "Copied" state when the
write actually succeeded. Both the modern `navigator.clipboard`
path and the `execCommand` fallback can fail; the new
`writeToClipboard` helper returns success and the button shows a
short "Failed" state if both paths fail.
# Make `pnpm dev` work from a clean checkout
The previous `dev: vite` couldn't actually serve the page because
`/catalog.json` 404s and the SPA boots into the error fallback.
Restructure the build layout to vite's conventional shape:
- `public/` is now a true vite public dir — pack writes tarballs and
catalog.json there. In dev, vite serves these at the root.
- `dist/` is the production build output (vite copies public/ into it
and adds index.html + assets/).
- `vercel.json#outputDirectory` switches from `public` → `dist`.
- `turbo.json` outputs updated to match.
- `dev` chains pack before vite so the catalog exists when the dev
server starts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​preact/​preset-vite@​2.10.59910010090100
Addednpm/​preact@​10.29.110010010094100

View full report

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pranay took the Vite + Preact refactor recommendation from my prior review and ran with it. The new shape matches what I sketched almost exactly:

tarballs/
├── index.html # 20-line entry
├── vite.config.ts # 16-line config
├── tsconfig.json # strict, jsxImportSource: preact
├── scripts/
│ ├── pack.ts # 317 lines, data-only — packs tarballs, writes catalog.json
│ └── check-tarballs-smoke.mjs # now also validates catalog.json shape
└── src/
├── main.tsx # 26 lines — fetch + mount
├── app.tsx # 550 lines — typed Preact JSX
├── catalog.ts # types + small helpers
├── icons.tsx # 8 inline SVG icons
└── styles.css # 794 lines, geist-inspired theme

Bundle ships as dist/assets/index-*.js at 26.25 kB (9.69 kB gzipped) — small for a Preact SPA with sortable tables, copy-buttons, search, and a PM toggle. CSS is 11.19 kB / 2.73 kB gzipped.

What I verified

  • Local build works: pnpm turbo build --filter=tarballs runs pack.ts then vite build, producing 26 tarballs in public/, a catalog.json matching the typed shape in src/catalog.ts, and a dist/ ready to deploy.
  • TypeScript is clean: pnpm exec tsc --noEmit from tarballs/ passes with the strict config (strict, noUnusedLocals, noUnusedParameters, isolatedModules).
  • Deployed preview serves correctly: https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/catalog.json returns the expected payload with build context (sha, branch, PR number, GitHub-linked commit/branch/PR URLs) and 26 packages with full file listings + sizes. Each package's url is the immutable per-deployment hash URL (e.g. https://workflow-tarballs-ch8zhxmen.labs.vercel.dev/workflow-ai.tgz), so the catalog is stable even if visited via the git-branch URL.
  • JS bundle contains real Preact: read the bundle, confirmed the App tree renders with hooks (useState, useEffect, useMemo, useRef), addEventListener('keydown', …) for the / shortcut, navigator.clipboard.writeText + execCommand('copy') fallback for CopyButton, and the data-copied / data-failed state machine.
  • Smoke check is stronger now: in addition to verifying tarballs are gzip-magic-prefixed and the index page responds with text/html, it now fetches catalog.json and asserts the workflow package has a non-trivial fileCount — a useful diagnostic for "packages weren't built before pack."

Concerns from my prior review — all addressed

  1. The hand-rolled HTML-in-JS approach — gone. JSX is TypeScript-checked end to end (event handlers, prop types, catalog shape).
  2. Two parallel render concerns merged into one file — split clean. pack.ts is now data-only (~317 lines, all bundling/tar-reading). The presentation layer is in src/.
  3. renderRow / applyPm / applyFilter had to stay in sync via string conventions — gone. State lives in React-style hooks, props are typed.
  4. Hand-rolled escapeHtml — gone. JSX handles escaping.
  5. escapeHtml(JSON.stringify(catalog)) breaking client interactivity — moot. The catalog is a separate /catalog.json static file fetched at runtime, so there's no inline-JSON encoding question.
  6. KB/MB labels with base-2 math — fixed. formatBytes now emits B/KiB/MiB.

Copilot review items — all addressed in 1c48345e6

  • a11y: aria-label="Filter packages" on search input; PM tabs use plain buttons with aria-pressed (no half-implemented role="tablist"); per-row copy buttons get accessibleName="Copy install command for ${pkg.name}" instead of a generic label.
  • Robustness: writeToClipboard returns a boolean so failed writes show "Failed" rather than a misleading "Copied" success state.
  • DX: dev script chains pack.ts && vite; dropped cache: 'no-store' since the catalog URL is per-deployment-immutable.

Smaller improvements landed along the way

  • Tar reader rewrite (6ae7736): replaced tar -tvzf shell-out with an in-process gunzip + 512-byte block walker. Handles GNU long-name (L) entries and pax headers, and works identically on macOS BSD tar and Linux GNU tar. Sorting files by descending size before emitting is a nice touch for the per-package "what's inside" view.
  • PackagePhobia-style SizeStats widget: two big tiles (publish size + unpacked size) with split value/unit rendering. Looks good and reads better than a one-liner.
  • FileTable with sortable columns: ARIA aria-sort on <th>, indicator arrows, click-to-toggle direction. Strips the package/ prefix from displayed paths.

Tiny things — non-blocking

  • pack.ts:117: updateDeps builds rewrite URLs as https://${process.env.VERCEL_URL}/${escapedName}.tgz directly, while baseUrl (defined at line 97) uses the VERCEL_URL ? https://… : '' fallback. Local builds without VERCEL_URL produce package.json deps like https://undefined/.... Pre-existing on main, not introduced by this PR. Not a real-world issue (nobody installs from a local pack output) but noting for a follow-up cleanup if you want consistency.
  • catalog.ts and pack.ts duplicate the same TS interfaces (TarballFile, PackedPackage, BuildContext, Catalog). Drift risk is low since they're both in the same package, but if you want to eliminate it, catalog.ts could be the single source of truth and pack.ts could import type from it. Fine to leave.
  • SizeStats is rendered for every package in the list, not just the featured one — but it's behind a <details> so it doesn't blow up initial render. The 26 expanded <details> plus the FileTable for each would render a lot of DOM if expanded all at once; in practice, users open one or two. Not blocking.

Verdict: approve

Approving and clearing my prior CHANGES_REQUESTED. The redesign hit every concern from my last two reviews plus the Copilot feedback, the build pipeline is cleaner than what was there before, and the deployment shape is preserved (public/<escaped-name>.tgz URLs unchanged, smoke checks pass). Nice work on the followups.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

@v0@vercel can you make a PR to address the remaining follow ups from @TooTallNate's review comments that we didn't get to before merge of this one

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-stableCherry-pick this PR to the stable branch when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

tarballs: redesign preview tarballs index page - #1911

Merged
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements
May 4, 2026
Merged

tarballs: redesign preview tarballs index page#1911
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Summary

Reworks the static index page generated by tarballs/scripts/pack.ts (deployed at https://workflow-tarballs.labs.vercel.dev/) to be more useful and pleasant to look at.

What's new

  • Featured workflow package at the top with a large install command, copy button, and direct tarball download — the rest of the packages are secondary.
  • Top-of-page metadata chips: short SHA (linked to commit on GitHub), branch (linked), PR number (linked), build timestamp, package count + total size.
  • Collapsible "What is this?" explainer describing how the version + dep rewriting works.
  • Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the install command shown for every row in place.
  • Live filter input over the package list, with / keyboard shortcut to focus.
  • Per-row install command, copy button, and direct download link for every package.
  • Geist-inspired theme with proper dark/light support driven by system preference.

Implementation notes

  • Pack step now records the produced tarball size via fs.stat after pnpm pack so the page can show human-readable sizes.
  • The page reads VERCEL_GIT_* env vars for commit / branch / PR links and falls back to local git rev-parse for branch when running off-Vercel.
  • All packages still get an entry, but workflow is filtered out of the bottom list and rendered separately.
  • No changes to the deployment shape — public/<escaped-name>.tgz URLs are unchanged, so existing usage keeps working.

The Vercel preview for this PR will deploy and serve the new page so you can see it live.

Test plan

  • Open the Vercel preview deployment and verify the page renders correctly in light and dark mode
  • Confirm the workflow package appears as the featured card at the top
  • Confirm SHA / branch / PR chips are present and linked
  • Toggle pnpm / npm / yarn / bun and confirm install commands update everywhere
  • Filter the package list (and try the / shortcut)
  • Click copy on a row and on the featured card
  • Click download on a row and verify the tarball downloads
  • Smoke-install the featured tarball into a fresh project and confirm it still works (pnpm i https://<deployment>/workflow.tgz)

🤖 Generated with Claude Code

Rebuild the static index page produced by `tarballs/scripts/pack.ts`:
- Featured `workflow` package up top with prominent install command,
copy button, and direct tarball download
- Top-of-page metadata chips: short SHA (linked to commit), branch,
PR number, build timestamp, package count + total size
- Collapsible "What is this?" explainer
- Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the
install command for every row in place
- Live filter input over the rest of the package list (with `/` shortcut)
- Per-row install command, copy button, and direct download
- Modern dark/light theme with system preference, Geist-inspired styling
Also captures tarball size during pack and renders human-readable byte counts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented May 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1c48345

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-nextjs-workflow-webpackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-astro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-express-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-fastify-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-hono-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nitro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nuxt-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-sveltekit-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-tanstack-start-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-vite-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-swc-playgroundReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-tarballsReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-webReadyReadyPreview, CommentMay 4, 2026 11:41am
1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
workflow-docsSkippedSkippedMay 4, 2026 11:41am

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other5100176686
Total4338010525390

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025
✅ e2e-vercel-prod-tanstack-start72026

📋 View full workflow run

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.031s (-30.2% 🟢)1.005s (~)0.974s101.00x
💻 LocalNitro0.032s (-24.8% 🟢)1.005s (~)0.973s101.05x
🐘 PostgresExpress0.043s (-26.4% 🟢)1.010s (~)0.967s101.38x
💻 LocalNext.js (Turbopack)0.047s1.005s0.958s101.51x
🐘 PostgresNitro0.052s (-45.6% 🟢)1.012s (-3.0%)0.960s101.68x
🌐 RedisNext.js (Turbopack)0.059s1.005s0.947s101.89x
🐘 PostgresNext.js (Turbopack)0.059s1.011s0.952s101.91x
🌐 MongoDBNext.js (Turbopack)0.100s1.009s0.910s103.23x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.068s (-5.6% 🟢)2.006s (~)0.938s101.00x
💻 LocalExpress1.074s (-4.6%)2.006s (~)0.932s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.927s101.01x
🐘 PostgresNitro1.088s (-4.6%)2.010s (~)0.922s101.02x
💻 LocalNext.js (Turbopack)1.108s2.006s0.898s101.04x
🌐 RedisNext.js (Turbopack)1.112s2.007s0.895s101.04x
🐘 PostgresNext.js (Turbopack)1.117s2.009s0.892s101.05x
🌐 MongoDBNext.js (Turbopack)1.166s2.009s0.843s101.09x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.387s (-5.1% 🟢)11.022s (~)0.634s31.00x
💻 LocalExpress10.405s (-4.7%)11.023s (~)0.618s31.00x
🐘 PostgresExpress10.415s (-5.0% 🟢)11.012s (~)0.597s31.00x
🐘 PostgresNitro10.432s (-4.0%)11.018s (~)0.585s31.00x
🌐 RedisNext.js (Turbopack)10.634s11.023s0.389s31.02x
💻 LocalNext.js (Turbopack)10.690s11.023s0.333s31.03x
🐘 PostgresNext.js (Turbopack)10.716s11.017s0.301s31.03x
🌐 MongoDBNext.js (Turbopack)10.822s11.017s0.195s31.04x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.426s (-10.9% 🟢)14.027s (-12.5% 🟢)0.600s51.00x
🐘 PostgresExpress13.463s (-7.7% 🟢)14.021s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.480s (-10.0% 🟢)14.028s (-6.7% 🟢)0.548s51.00x
🐘 PostgresNitro13.544s (-7.2% 🟢)14.019s (-6.7% 🟢)0.475s51.01x
💻 LocalNext.js (Turbopack)14.031s15.028s0.997s41.05x
🌐 RedisNext.js (Turbopack)14.036s14.628s0.592s51.05x
🐘 PostgresNext.js (Turbopack)14.130s15.022s0.892s41.05x
🌐 MongoDBNext.js (Turbopack)14.245s15.023s0.778s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.809s (-29.6% 🟢)12.022s (-29.4% 🟢)0.214s81.00x
🐘 PostgresNitro11.928s (-14.6% 🟢)12.017s (-16.0% 🟢)0.089s81.01x
🐘 PostgresExpress11.967s (-14.6% 🟢)12.265s (-16.0% 🟢)0.298s81.01x
💻 LocalExpress12.248s (-26.2% 🟢)12.774s (-25.0% 🟢)0.526s81.04x
💻 LocalNext.js (Turbopack)13.061s13.737s0.677s71.11x
🌐 RedisNext.js (Turbopack)13.217s13.883s0.666s71.12x
🌐 MongoDBNext.js (Turbopack)13.255s14.019s0.764s71.12x
🐘 PostgresNext.js (Turbopack)13.304s14.021s0.718s71.13x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.141s (-9.4% 🟢)2.006s (~)0.865s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.007s (~)0.861s151.00x
💻 LocalNitro1.171s (-28.2% 🟢)2.006s (-3.3%)0.835s151.03x
💻 LocalExpress1.174s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.217s2.007s0.790s151.07x
🌐 RedisNext.js (Turbopack)1.240s2.006s0.766s151.09x
💻 LocalNext.js (Turbopack)1.289s2.006s0.717s151.13x
🌐 MongoDBNext.js (Turbopack)2.035s2.826s0.791s111.78x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-49.1% 🟢)2.007s (-33.3% 🟢)0.805s151.00x
🐘 PostgresNitro1.229s (-47.7% 🟢)2.007s (-33.3% 🟢)0.778s151.02x
🐘 PostgresNext.js (Turbopack)1.358s2.008s0.650s151.13x
💻 LocalNitro1.685s (-46.4% 🟢)2.006s (-48.4% 🟢)0.320s151.40x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.299s151.42x
💻 LocalNext.js (Turbopack)1.858s2.292s0.434s141.55x
🌐 RedisNext.js (Turbopack)2.337s3.008s0.671s101.95x
🌐 MongoDBNext.js (Turbopack)3.568s4.009s0.441s82.97x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.305s (-62.6% 🟢)2.007s (-50.0% 🟢)0.702s151.00x
🐘 PostgresNitro1.383s (-60.3% 🟢)2.007s (-49.9% 🟢)0.624s151.06x
🐘 PostgresNext.js (Turbopack)1.629s2.007s0.378s151.25x
🌐 RedisNext.js (Turbopack)3.602s4.010s0.408s82.76x
💻 LocalNext.js (Turbopack)4.383s5.011s0.627s63.36x
💻 LocalExpress4.397s (-47.3% 🟢)5.013s (-44.5% 🟢)0.616s63.37x
💻 LocalNitro4.571s (-45.2% 🟢)5.011s (-44.4% 🟢)0.440s63.50x
🌐 MongoDBNext.js (Turbopack)6.273s7.013s0.739s54.81x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.139s (-9.4% 🟢)2.009s (~)0.869s151.00x
🐘 PostgresNitro1.168s (-7.1% 🟢)2.009s (~)0.840s151.03x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.773s151.08x
🐘 PostgresNext.js (Turbopack)1.248s2.007s0.760s151.10x
💻 LocalNext.js (Turbopack)1.321s2.005s0.684s151.16x
💻 LocalExpress1.352s (-28.6% 🟢)2.006s (-15.1% 🟢)0.654s151.19x
💻 LocalNitro1.379s (-26.1% 🟢)2.006s (-14.3% 🟢)0.627s151.21x
🌐 MongoDBNext.js (Turbopack)2.026s2.735s0.709s111.78x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-48.3% 🟢)2.008s (-33.3% 🟢)0.797s151.00x
🐘 PostgresNitro1.232s (-47.3% 🟢)2.008s (-33.3% 🟢)0.776s151.02x
🐘 PostgresNext.js (Turbopack)1.368s2.008s0.640s151.13x
💻 LocalExpress1.838s (-41.3% 🟢)2.075s (-44.8% 🟢)0.238s151.52x
💻 LocalNitro1.954s (-36.3% 🟢)2.469s (-36.5% 🟢)0.515s131.61x
💻 LocalNext.js (Turbopack)2.083s2.826s0.743s111.72x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.94x
🌐 MongoDBNext.js (Turbopack)3.547s4.008s0.461s82.93x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.318s (-62.3% 🟢)2.007s (-50.0% 🟢)0.690s151.00x
🐘 PostgresNitro1.379s (-60.4% 🟢)2.007s (-49.9% 🟢)0.628s151.05x
🐘 PostgresNext.js (Turbopack)1.718s2.075s0.357s151.30x
🌐 RedisNext.js (Turbopack)3.595s4.010s0.415s82.73x
💻 LocalNitro4.877s (-46.7% 🟢)5.516s (-45.0% 🟢)0.639s63.70x
💻 LocalExpress5.136s (-41.6% 🟢)5.515s (-40.5% 🟢)0.379s63.90x
💻 LocalNext.js (Turbopack)5.496s6.011s0.515s54.17x
🌐 MongoDBNext.js (Turbopack)6.286s7.013s0.727s54.77x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.430s (-48.7% 🟢)1.006s (-1.7%)0.575s601.00x
💻 LocalNitro0.457s (-53.4% 🟢)1.004s (-8.3% 🟢)0.547s601.06x
🐘 PostgresNitro0.483s (-41.1% 🟢)1.023s (+1.7%)0.540s591.12x
💻 LocalExpress0.509s (-48.3% 🟢)1.004s (-6.7% 🟢)0.495s601.18x
🌐 RedisNext.js (Turbopack)0.617s1.004s0.388s601.43x
🐘 PostgresNext.js (Turbopack)0.697s1.007s0.310s601.62x
💻 LocalNext.js (Turbopack)0.712s1.004s0.292s601.66x
🌐 MongoDBNext.js (Turbopack)0.739s1.006s0.266s601.72x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.027s (-48.0% 🟢)1.586s (-29.8% 🟢)0.559s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.741s (-17.1% 🟢)0.648s521.06x
💻 LocalNitro1.156s (-61.9% 🟢)2.006s (-46.6% 🟢)0.849s451.13x
💻 LocalExpress1.191s (-60.5% 🟢)2.005s (-44.1% 🟢)0.814s451.16x
🌐 RedisNext.js (Turbopack)1.479s2.006s0.527s451.44x
🐘 PostgresNext.js (Turbopack)1.638s2.007s0.369s451.60x
💻 LocalNext.js (Turbopack)1.780s2.027s0.247s451.73x
🌐 MongoDBNext.js (Turbopack)1.811s2.007s0.196s451.76x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.012s (-49.6% 🟢)2.427s (-44.5% 🟢)0.415s501.00x
🐘 PostgresNitro2.035s (-50.4% 🟢)2.457s (-46.6% 🟢)0.422s491.01x
💻 LocalNitro2.638s (-71.6% 🟢)3.007s (-70.0% 🟢)0.369s401.31x
💻 LocalExpress2.802s (-69.6% 🟢)3.084s (-69.2% 🟢)0.283s391.39x
🌐 RedisNext.js (Turbopack)3.003s3.277s0.275s371.49x
🐘 PostgresNext.js (Turbopack)3.192s4.010s0.817s301.59x
💻 LocalNext.js (Turbopack)3.864s4.145s0.282s291.92x
🌐 MongoDBNext.js (Turbopack)4.155s5.011s0.856s242.07x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.172s (-39.1% 🟢)1.006s (~)0.834s601.00x
🐘 PostgresNitro0.192s (-32.2% 🟢)1.006s (~)0.814s601.12x
🐘 PostgresNext.js (Turbopack)0.241s1.006s0.764s601.40x
🌐 RedisNext.js (Turbopack)0.251s1.004s0.753s601.46x
💻 LocalNitro0.432s (-28.6% 🟢)1.004s (-1.7%)0.572s602.51x
💻 LocalExpress0.466s (-16.9% 🟢)1.004s (~)0.539s602.71x
💻 LocalNext.js (Turbopack)0.567s1.004s0.437s603.30x
🌐 MongoDBNext.js (Turbopack)1.041s1.882s0.842s326.05x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.296s (-41.8% 🟢)1.006s (~)0.710s901.00x
🐘 PostgresNitro0.314s (-36.8% 🟢)1.006s (~)0.693s901.06x
🌐 RedisNext.js (Turbopack)0.414s1.004s0.590s901.40x
🐘 PostgresNext.js (Turbopack)0.461s1.006s0.545s901.55x
💻 LocalNitro2.179s (-14.1% 🟢)2.852s (-5.2% 🟢)0.672s327.35x
💻 LocalNext.js (Turbopack)2.182s2.944s0.762s317.36x
💻 LocalExpress2.223s (-11.6% 🟢)2.821s (-6.3% 🟢)0.598s327.50x
🌐 MongoDBNext.js (Turbopack)2.618s3.007s0.389s308.83x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.578s (-29.3% 🟢)1.014s (~)0.436s1191.00x
🐘 PostgresNitro0.641s (-18.9% 🟢)1.006s (~)0.365s1201.11x
🌐 RedisNext.js (Turbopack)0.770s1.004s0.234s1201.33x
🐘 PostgresNext.js (Turbopack)0.957s1.352s0.394s901.65x
🌐 MongoDBNext.js (Turbopack)5.396s6.011s0.615s209.33x
💻 LocalExpress9.835s (-12.1% 🟢)10.444s (-12.5% 🟢)0.609s1217.00x
💻 LocalNitro9.941s (-11.2% 🟢)10.365s (-11.1% 🟢)0.424s1217.19x
💻 LocalNext.js (Turbopack)11.011s11.573s0.562s1119.03x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.127s (+449.7% 🔺)2.001s (+100.3% 🔺)0.001s (-37.5% 🟢)2.009s (+98.6% 🔺)0.882s101.00x
💻 LocalNitro1.128s (+428.0% 🔺)2.005s (+99.6% 🔺)0.011s (-15.2% 🟢)2.018s (+98.1% 🔺)0.890s101.00x
🐘 PostgresNitro1.135s (+453.5% 🔺)2.000s (+100.1% 🔺)0.001s (-6.7% 🟢)2.010s (+98.7% 🔺)0.875s101.01x
💻 LocalExpress1.140s (+472.4% 🔺)2.005s (+99.6% 🔺)0.013s (+3.3%)2.020s (+98.4% 🔺)0.881s101.01x
💻 LocalNext.js (Turbopack)1.190s2.003s0.010s2.017s0.826s101.06x
🐘 PostgresNext.js (Turbopack)1.197s2.001s0.002s2.011s0.815s101.06x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.482s (+135.3% 🔺)2.006s (+99.3% 🔺)0.003s (-10.4% 🟢)2.022s (+97.7% 🔺)0.540s301.00x
🐘 PostgresNitro1.525s (+144.4% 🔺)2.002s (+98.9% 🔺)0.004s (+0.8%)2.026s (+98.2% 🔺)0.501s301.03x
💻 LocalNext.js (Turbopack)1.670s2.009s0.011s2.023s0.352s301.13x
🐘 PostgresNext.js (Turbopack)1.682s2.010s0.004s2.025s0.343s301.13x
💻 LocalExpress1.705s (+125.2% 🔺)2.011s (+95.4% 🔺)0.010s (+8.5% 🔺)2.202s (+111.8% 🔺)0.498s281.15x
💻 LocalNitro1.713s (+104.2% 🔺)2.010s (+98.6% 🔺)0.010s (+4.8%)2.201s (+97.2% 🔺)0.488s281.16x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.622s (-35.2% 🟢)1.013s (-20.7% 🟢)0.000s (+16.9% 🔺)1.028s (-21.3% 🟢)0.406s591.00x
🐘 PostgresNitro0.681s (-29.7% 🟢)1.047s (-16.1% 🟢)0.000s (-58.6% 🟢)1.059s (-15.8% 🟢)0.378s581.09x
🐘 PostgresNext.js (Turbopack)0.817s1.053s0.000s1.068s0.251s571.31x
💻 LocalNitro1.331s (+8.8% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.686s302.14x
💻 LocalExpress1.394s (+13.8% 🔺)2.015s (~)0.000s (+10.0% 🔺)2.017s (~)0.623s302.24x
💻 LocalNext.js (Turbopack)1.437s2.013s0.000s2.016s0.579s302.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.313s (-26.7% 🟢)2.099s (-2.0%)0.000s (-3.4%)2.131s (-2.0%)0.818s291.00x
🐘 PostgresExpress1.349s (-23.9% 🟢)2.145s (-1.5%)0.000s (+Infinity% 🔺)2.161s (-1.7%)0.812s281.03x
🐘 PostgresNext.js (Turbopack)1.689s2.225s0.000s2.273s0.584s271.29x
💻 LocalNext.js (Turbopack)2.563s3.079s0.001s3.084s0.521s201.95x
💻 LocalNitro3.092s (-8.7% 🟢)3.840s (-4.8%)0.000s (-76.6% 🟢)3.844s (-4.8%)0.752s162.35x
💻 LocalExpress3.227s (-6.9% 🟢)4.031s (~)0.000s (-66.7% 🟢)4.033s (~)0.806s152.46x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro14/21
🐘 PostgresExpress19/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres8/21
Nitro🐘 Postgres14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

Comment threadtarballs/scripts/pack.ts Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visual design is nice — featured workflow card, package-manager toggle, live filter, copy buttons, geist-inspired theme. Going to approve since the deployment shape doesn't change and this only affects the static index page.

That said: I'd push back on the hand-rolled HTML-in-JS approach for anything beyond a one-page diff. The new file is 857 lines, and the implementation has some smells that follow naturally from the medium:

  • The inline <script> block (~80 lines) isn't TypeScript-checked. It's inside a template literal in writeIndexHtml, so the compiler treats it as a string. Refactoring (rename a data- attribute, change a property name) won't surface type errors. The TS half references DOM via data- attributes that have to match by convention.
  • Two parallel "render the page" concerns — the pack.ts script handles both bundling-time logic (scanning packages, rewriting deps, packing tarballs) and presentation logic (HTML composition, CSS, client-side JS for PM toggle/filter/copy). Those were independent before this PR; conflating them makes both harder to evolve.
  • renderFeatured/renderRow and the inline applyPm/applyFilter script must stay in sync on attribute names, class names, and the catalog JSON shape. Right now there's a hand-rolled escapeHtml helper and the contract is "data-install-cmd must equal escapedName." Easy to break in a refactor; no tests catch the drift.
  • Hand-rolled escape helpers (escapeHtml) are correct here but would be unnecessary with JSX or a templating library.

A lightweight Vite + React (or Preact for smaller bundle) SPA would clean this up substantially. The shape I'd suggest:

tarballs/
├── package.json # adds vite, react, @vitejs/plugin-react
├── vite.config.ts # static build → public/
├── index.html # entry HTML
├── src/
│ ├── main.tsx # mount React
│ ├── App.tsx # the page (~80 lines instead of ~600 of HTML+CSS+JS)
│ ├── catalog.ts # types for the JSON catalog
│ └── styles.css # the geist-inspired theme
└── scripts/
├── pack.ts # data-only: scans packages, packs tarballs, writes
│ # public/catalog.json with the build context
└── check-tarballs-smoke.mjs

pack.ts becomes ~140 lines (just the bundling logic), vite build produces the static page that fetches catalog.json, the React component reads it on mount and renders. State (active PM, search query) is useState. Build pipeline is pnpm pack → vite build, and Vercel's existing outputDirectory: "public" works unchanged.

DX wins: HMR while iterating on the design, TypeScript checks the JSX and event handlers, can pull in actual geist (or lucide-react) icons via npm instead of inlined SVG, hot-swap component libraries if the design evolves.

If you don't want to take that on now, this is fine as-is — the page works, the design looks good, and the deployment shape is preserved. But suggest filing a follow-up to migrate before adding the next 200 lines of HTML to this file.

Smaller things I noticed

  • getBuildContext uses process.env.VERCEL_GIT_* for SHA/branch/PR. Local builds get only the SHA/branch fallback via git rev-parse. Worth double-checking the labels on chips degrade gracefully when those env vars are missing — looks like they do (early-return paths with if (ctx.commitUrl)), but a quick local pnpm --filter tarballs build test before merge would confirm.
  • The catalog JSON is embedded via <script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>. JSON-inside-HTML-inside-template-literal is two layers of escape (the escapeHtml handles the outer, but the JSON itself can contain </script> if a package description has one — unlikely but possible). Safer to use JSON.stringify(catalog).replace(/</g, '\\u003c') for the embedded JSON, which escapes the dangerous bytes without affecting parseability.
  • formatBytes uses base-2 units (KB = 1024) but labels them as 'KB'/'MB' which by convention suggest base-10. Minor but KiB/MiB would be more precise (or use base-10 with 1000).
  • applyFilter runs on every keystroke against a re-querying Array.from(document.querySelectorAll('.pkg-row')). With ~25 packages this is fine; if the catalog grows substantially, debounce or pre-compute.

Test plan note

The PR description has 8 unchecked test-plan items. They're all manual visual checks (page renders, filter works, copy fires, etc.). Since the Vercel preview URL is the canonical way to validate this, they probably won't be checked off in advance — but worth at least running through them on the preview before merge.

Comment threadtarballs/scripts/pack.ts Outdated
Built ${ctx.commitUrl ? `from <a href="${escapeHtml(ctx.commitUrl)}" target="_blank" rel="noopener">${escapeHtml(ctx.shortSha)}</a>` : `from <code>${escapeHtml(ctx.shortSha)}</code>`} · ${escapeHtml(packages.length.toString())} packages totaling ${escapeHtml(formatBytes(totalSize))}
</footer>
</div>
<script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this line breaks all client-side interactivity.

Verified on the preview deployment with browser automation — every interactive feature is non-functional:

  • ❌ Package-manager toggle (clicking npm/yarn/bun does nothing)
  • ❌ Live filter (typing in search shows all rows)
  • ❌ Copy buttons (no clipboard write, no Copied state)
  • / keyboard shortcut (body keeps focus)

Root cause: escapeHtml(JSON.stringify(catalog)) HTML-encodes the JSON. The serialized catalog ends up in the page as:

<scriptid="catalog" type="application/json">[{&quot;name&quot;:&quot;@workflow/ai&quot;,&quot;escapedName&quot;:&quot;workflow-ai&quot;...

JSON.parse(textContent) then throws on the very first &quot; because &quot; isn't valid JSON syntax. The IIFE bails on its first line and none of the event listeners get attached. Confirmed live:

>JSON.parse(document.getElementById('catalog').textContent)Uncaught SyntaxError: Expectedpropertynameor'}'inJSONatposition2(line1column3)

<script type="application/json"> content is treated as text by the HTML parser — it does NOT need HTML entity escaping. The only sequence that can break out of it is </script> (or </ in some legacy parsers). The narrowest correct fix is:

<scriptid="catalog"type="application/json">${JSON.stringify(catalog).replace(/</g,'\u003c')}</script>

That escapes < bytes to \u003c in the JSON (legal per the spec — JSON allows \uXXXX escapes for any character), so </script> can't appear in the text and the JSON parses cleanly.

This was the third bullet in my "smaller things I noticed" section above — turns out it's not theoretical, it's the live bug breaking everything client-side. Should not merge until the page works in a real browser.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3df48a7. Went with your replace(/</g, '\u003c') over the bot's </-only escape since it also covers a stray < inside, e.g., a package description. Verified locally that the embedded JSON parses cleanly out of the rendered HTML; preview deploy will validate the four interactive features end-to-end.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating my prior review to REQUEST_CHANGES. Ran the visual test plan against the preview deployment with browser automation and discovered that all client-side interactivity is broken — see the inline comment on pack.ts:696 for the root cause and fix.

The escapeHtml(JSON.stringify(catalog)) call HTML-encodes every " in the JSON to &quot;, so JSON.parse(textContent) throws on its first character. The IIFE bails immediately and none of the event listeners (PM toggle, search filter, copy buttons, / shortcut) get attached.

Test plan results from the preview at https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/:

  • ✅ Page renders correctly in light mode (visual layout, chips, featured card, package list)
  • workflow package appears as the featured card at the top
  • ✅ SHA / branch / timestamp / package count chips are present
  • Package-manager toggle doesn't update install commandsaria-selected stays on pnpm regardless of clicks
  • Live filter doesn't filter — typing in the search input shows all rows unchanged
  • Copy buttons don't fire — no Copied state, no clipboard write
  • / keyboard shortcut doesn't focus search — body keeps focus
  • (couldn't test direct download because earlier broken state interfered, but the <a download> is plain HTML and probably works)

So the page that ships looks correct visually but is functionally a static catalog — the toggle, filter, and copy buttons are all dead UI.

Quick fix at the call site (one line). After fix, all four interactive features should work. Worth re-testing on the next preview deployment.

The other points from my prior review (Vite + React refactor suggestion, the smaller TS-checkable concerns, KB vs KiB nit) all still stand and are non-blocking. Just this one issue blocks merging.

`escapeHtml(JSON.stringify(catalog))` was HTML-encoding every quote in
the embedded catalog JSON to `&quot;`, so `JSON.parse(textContent)` threw
on the first character and the IIFE bailed before attaching any event
listeners — package-manager toggle, search filter, copy buttons, and the
`/` shortcut were all dead UI on the deployed page.
`<script type="application/json">` content is treated as text by the HTML
parser; the only sequence that can break out is `</script>` (or `</`
in legacy parsers). Replace `<` with the JSON `<` escape, which is
legal per the JSON spec and prevents the breakout without needing entity
encoding.
Also switch `formatBytes` from `KB`/`MB` to `KiB`/`MiB` since the
divisor is 1024.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment threadtarballs/scripts/pack.ts Outdated
The smoke check broke in CI: `'workflow' tarball only has 0 files`.
Root cause is that `tar -tvzf` emits a different verbose layout on GNU
tar (Linux, what CI runs) vs BSD tar (macOS, where I tested locally) —
the parser only matched the BSD column ordering, so on Linux every line
was rejected and `fileCount` came out as 0.
Replace the shell-out with a small in-process tar reader using
`zlib.gunzipSync` + manual 512-byte block walk. ustar headers are
trivially structured (name at offset 0, octal size at 124, typeflag at
156, ustar prefix at 345). We emit regular files only (`typeflag` `0`
or NUL) and consume but skip pax extended headers (`x`/`g`) and GNU
long-name entries (`L`). Result is identical on every platform.
Verified locally: 206 files / 998413 bytes for `workflow.tgz` matches
`tar -tvzf` exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pranaygpand others added 3 commits May 4, 2026 19:31
The previous "What's inside?" view crammed nested directory groups,
proportional bars, and per-group file lists into a `<details>` inside
an already-narrow row. It was hard to read and harder to compare.
Replace it with the layout packagephobia uses on its result page:
- Two large headline metric tiles (Publish size / Unpacked size)
with a big bold value, smaller unit, and small uppercase label.
Modeled directly on packagephobia's `Stats` component but using
our existing CSS variables so it tracks light/dark theme.
- A single sortable file table beneath. Default is size-descending so
the contributors to package size are immediately visible. Click a
header to flip direction or switch sort key. Sticky header keeps
the columns visible inside the scrollable region.
Drop the `groupByTopLevel`, `ContentsGroup`, and bar-chart styles —
they were the source of the "hard to use" feedback and don't add
information that the flat sortable table doesn't already convey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the old static tarballs index with a Vite/Preact single-page UI that reads a generated catalog.json, surfaces richer build/package metadata, and adds interactive install/copy/filter/download affordances for preview tarballs.

Changes:

  • Adds a Vite + Preact frontend for the tarballs index, including featured package rendering, filtering, package-manager toggles, and package contents breakdowns.
  • Extends scripts/pack.ts to emit catalog.json with build metadata, tarball sizes, unpacked sizes, and per-file listings.
  • Updates build/smoke-test plumbing so the tarballs deployment now publishes bundled SPA assets plus the generated catalog.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tarballs/vite.config.tsConfigures Vite to build the SPA into public/ without deleting tarballs.
tarballs/turbo.jsonExpands build outputs to include SPA assets and catalog.json.
tarballs/tsconfig.jsonAdds TS config for the new frontend/build files.
tarballs/src/styles.cssAdds the full styling/theme for the redesigned index UI.
tarballs/src/main.tsxBoots the SPA and loads catalog.json.
tarballs/src/icons.tsxAdds inline SVG icons used across the new UI.
tarballs/src/catalog.tsDefines shared catalog/install-command helpers for the frontend.
tarballs/src/app.tsxImplements the main tarballs UI, interactions, and package contents tables.
tarballs/scripts/pack.tsGenerates tarballs plus the new metadata-rich catalog.json.
tarballs/scripts/check-tarballs-smoke.mjsAdds smoke coverage for catalog.json.
tarballs/package.jsonSwitches build to pack.ts && vite build and adds frontend deps/scripts.
tarballs/index.htmlAdds the SPA entry HTML shell.
pnpm-lock.yamlLocks newly added frontend/tooling dependencies.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadtarballs/package.json Outdated
"scripts": {
"build": "node scripts/pack.ts",
"build": "node scripts/pack.ts && vite build",
"dev": "vite",

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. The new dev chains node scripts/pack.ts && vite, and I restructured the build layout to vite's conventional shape so it actually works in dev: public/ is a real vite public dir (pack writes tarballs + catalog.json there, vite serves them at root in dev), and dist/ is the production build output (set as Vercel's outputDirectory).

Comment threadtarballs/src/app.tsx
<SearchIcon />
<input
ref={searchRef}
type="search"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added aria-label="Filter packages" to the input.

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +178 to +185
<div class="pm-tabs" role="tablist" aria-label="Package manager">
{options.map((opt) => (
<button
key={opt}
type="button"
class="pm-tab"
role="tab"
aria-selected={value === opt}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Dropped role="tablist"/role="tab"/aria-selected and made these plain buttons with aria-pressed. Toggle buttons are the honest representation since we never implemented arrow-key roving focus. Each button also has an explicit aria-label (Show install commands for pnpm etc.).

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +428 to +440
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} finally {
ta.remove();
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Refactored to a writeToClipboard helper that returns whether the write actually succeeded; the button only flips to Copied on success. If both navigator.clipboard.writeText and the execCommand fallback fail, it briefly shows a red Failed state instead.

Comment threadtarballs/src/app.tsx Outdated
<code class="pkg-cmd">{cmd}</code>
<div class="pkg-actions">
<CopyButton text={cmd} variant="icon" />
<a class="icon-btn" href={pkg.url} download aria-label="Download">

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added an accessibleName prop to CopyButton and pass Copy install command for <pkg.name> from each row + the featured card. Same treatment on the download <a> link (Download <pkg.name> tarball).

Comment threadtarballs/src/main.tsx Outdated
if (!root) throw new Error('No #app root element');

try {
const res = await fetch('/catalog.json', { cache: 'no-store' });

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345 — dropped cache: 'no-store'. Each preview's URL is unique so the browser cache is the right thing to use.

- main.tsx: drop `cache: 'no-store'` from the catalog fetch. Each
tarballs deployment is immutable per commit, so HTTP caching is
appropriate; forcing no-store made every visit re-download the full
catalog (which now includes per-package file lists).
- app.tsx (search input): add `aria-label="Filter packages"`. The
visible label only contained an icon and placeholder, so screen
readers had no name for the control.
- app.tsx (PmTabs): replace `role="tablist"` / `role="tab"` /
`aria-selected` with plain buttons that use `aria-pressed`. The
ARIA tab pattern requires arrow-key roving focus we never wired
up; toggle buttons are the honest representation. Each button
also gets an explicit `aria-label`.
- app.tsx (row buttons): include the package name in the accessible
label of every per-row copy/download button (and on the featured
card too), so the screen reader buttons/links list distinguishes
them. Added an `accessibleName` prop to `CopyButton`.
- app.tsx (CopyButton): only flip to the "Copied" state when the
write actually succeeded. Both the modern `navigator.clipboard`
path and the `execCommand` fallback can fail; the new
`writeToClipboard` helper returns success and the button shows a
short "Failed" state if both paths fail.
# Make `pnpm dev` work from a clean checkout
The previous `dev: vite` couldn't actually serve the page because
`/catalog.json` 404s and the SPA boots into the error fallback.
Restructure the build layout to vite's conventional shape:
- `public/` is now a true vite public dir — pack writes tarballs and
catalog.json there. In dev, vite serves these at the root.
- `dist/` is the production build output (vite copies public/ into it
and adds index.html + assets/).
- `vercel.json#outputDirectory` switches from `public` → `dist`.
- `turbo.json` outputs updated to match.
- `dev` chains pack before vite so the catalog exists when the dev
server starts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​preact/​preset-vite@​2.10.59910010090100
Addednpm/​preact@​10.29.110010010094100

View full report

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pranay took the Vite + Preact refactor recommendation from my prior review and ran with it. The new shape matches what I sketched almost exactly:

tarballs/
├── index.html # 20-line entry
├── vite.config.ts # 16-line config
├── tsconfig.json # strict, jsxImportSource: preact
├── scripts/
│ ├── pack.ts # 317 lines, data-only — packs tarballs, writes catalog.json
│ └── check-tarballs-smoke.mjs # now also validates catalog.json shape
└── src/
├── main.tsx # 26 lines — fetch + mount
├── app.tsx # 550 lines — typed Preact JSX
├── catalog.ts # types + small helpers
├── icons.tsx # 8 inline SVG icons
└── styles.css # 794 lines, geist-inspired theme

Bundle ships as dist/assets/index-*.js at 26.25 kB (9.69 kB gzipped) — small for a Preact SPA with sortable tables, copy-buttons, search, and a PM toggle. CSS is 11.19 kB / 2.73 kB gzipped.

What I verified

  • Local build works: pnpm turbo build --filter=tarballs runs pack.ts then vite build, producing 26 tarballs in public/, a catalog.json matching the typed shape in src/catalog.ts, and a dist/ ready to deploy.
  • TypeScript is clean: pnpm exec tsc --noEmit from tarballs/ passes with the strict config (strict, noUnusedLocals, noUnusedParameters, isolatedModules).
  • Deployed preview serves correctly: https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/catalog.json returns the expected payload with build context (sha, branch, PR number, GitHub-linked commit/branch/PR URLs) and 26 packages with full file listings + sizes. Each package's url is the immutable per-deployment hash URL (e.g. https://workflow-tarballs-ch8zhxmen.labs.vercel.dev/workflow-ai.tgz), so the catalog is stable even if visited via the git-branch URL.
  • JS bundle contains real Preact: read the bundle, confirmed the App tree renders with hooks (useState, useEffect, useMemo, useRef), addEventListener('keydown', …) for the / shortcut, navigator.clipboard.writeText + execCommand('copy') fallback for CopyButton, and the data-copied / data-failed state machine.
  • Smoke check is stronger now: in addition to verifying tarballs are gzip-magic-prefixed and the index page responds with text/html, it now fetches catalog.json and asserts the workflow package has a non-trivial fileCount — a useful diagnostic for "packages weren't built before pack."

Concerns from my prior review — all addressed

  1. The hand-rolled HTML-in-JS approach — gone. JSX is TypeScript-checked end to end (event handlers, prop types, catalog shape).
  2. Two parallel render concerns merged into one file — split clean. pack.ts is now data-only (~317 lines, all bundling/tar-reading). The presentation layer is in src/.
  3. renderRow / applyPm / applyFilter had to stay in sync via string conventions — gone. State lives in React-style hooks, props are typed.
  4. Hand-rolled escapeHtml — gone. JSX handles escaping.
  5. escapeHtml(JSON.stringify(catalog)) breaking client interactivity — moot. The catalog is a separate /catalog.json static file fetched at runtime, so there's no inline-JSON encoding question.
  6. KB/MB labels with base-2 math — fixed. formatBytes now emits B/KiB/MiB.

Copilot review items — all addressed in 1c48345e6

  • a11y: aria-label="Filter packages" on search input; PM tabs use plain buttons with aria-pressed (no half-implemented role="tablist"); per-row copy buttons get accessibleName="Copy install command for ${pkg.name}" instead of a generic label.
  • Robustness: writeToClipboard returns a boolean so failed writes show "Failed" rather than a misleading "Copied" success state.
  • DX: dev script chains pack.ts && vite; dropped cache: 'no-store' since the catalog URL is per-deployment-immutable.

Smaller improvements landed along the way

  • Tar reader rewrite (6ae7736): replaced tar -tvzf shell-out with an in-process gunzip + 512-byte block walker. Handles GNU long-name (L) entries and pax headers, and works identically on macOS BSD tar and Linux GNU tar. Sorting files by descending size before emitting is a nice touch for the per-package "what's inside" view.
  • PackagePhobia-style SizeStats widget: two big tiles (publish size + unpacked size) with split value/unit rendering. Looks good and reads better than a one-liner.
  • FileTable with sortable columns: ARIA aria-sort on <th>, indicator arrows, click-to-toggle direction. Strips the package/ prefix from displayed paths.

Tiny things — non-blocking

  • pack.ts:117: updateDeps builds rewrite URLs as https://${process.env.VERCEL_URL}/${escapedName}.tgz directly, while baseUrl (defined at line 97) uses the VERCEL_URL ? https://… : '' fallback. Local builds without VERCEL_URL produce package.json deps like https://undefined/.... Pre-existing on main, not introduced by this PR. Not a real-world issue (nobody installs from a local pack output) but noting for a follow-up cleanup if you want consistency.
  • catalog.ts and pack.ts duplicate the same TS interfaces (TarballFile, PackedPackage, BuildContext, Catalog). Drift risk is low since they're both in the same package, but if you want to eliminate it, catalog.ts could be the single source of truth and pack.ts could import type from it. Fine to leave.
  • SizeStats is rendered for every package in the list, not just the featured one — but it's behind a <details> so it doesn't blow up initial render. The 26 expanded <details> plus the FileTable for each would render a lot of DOM if expanded all at once; in practice, users open one or two. Not blocking.

Verdict: approve

Approving and clearing my prior CHANGES_REQUESTED. The redesign hit every concern from my last two reviews plus the Copilot feedback, the build pipeline is cleaner than what was there before, and the deployment shape is preserved (public/<escaped-name>.tgz URLs unchanged, smoke checks pass). Nice work on the followups.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

@v0@vercel can you make a PR to address the remaining follow ups from @TooTallNate's review comments that we didn't get to before merge of this one

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-stableCherry-pick this PR to the stable branch when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

tarballs: redesign preview tarballs index page - #1911

Merged
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements
May 4, 2026
Merged

tarballs: redesign preview tarballs index page#1911
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Summary

Reworks the static index page generated by tarballs/scripts/pack.ts (deployed at https://workflow-tarballs.labs.vercel.dev/) to be more useful and pleasant to look at.

What's new

  • Featured workflow package at the top with a large install command, copy button, and direct tarball download — the rest of the packages are secondary.
  • Top-of-page metadata chips: short SHA (linked to commit on GitHub), branch (linked), PR number (linked), build timestamp, package count + total size.
  • Collapsible "What is this?" explainer describing how the version + dep rewriting works.
  • Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the install command shown for every row in place.
  • Live filter input over the package list, with / keyboard shortcut to focus.
  • Per-row install command, copy button, and direct download link for every package.
  • Geist-inspired theme with proper dark/light support driven by system preference.

Implementation notes

  • Pack step now records the produced tarball size via fs.stat after pnpm pack so the page can show human-readable sizes.
  • The page reads VERCEL_GIT_* env vars for commit / branch / PR links and falls back to local git rev-parse for branch when running off-Vercel.
  • All packages still get an entry, but workflow is filtered out of the bottom list and rendered separately.
  • No changes to the deployment shape — public/<escaped-name>.tgz URLs are unchanged, so existing usage keeps working.

The Vercel preview for this PR will deploy and serve the new page so you can see it live.

Test plan

  • Open the Vercel preview deployment and verify the page renders correctly in light and dark mode
  • Confirm the workflow package appears as the featured card at the top
  • Confirm SHA / branch / PR chips are present and linked
  • Toggle pnpm / npm / yarn / bun and confirm install commands update everywhere
  • Filter the package list (and try the / shortcut)
  • Click copy on a row and on the featured card
  • Click download on a row and verify the tarball downloads
  • Smoke-install the featured tarball into a fresh project and confirm it still works (pnpm i https://<deployment>/workflow.tgz)

🤖 Generated with Claude Code

Rebuild the static index page produced by `tarballs/scripts/pack.ts`:
- Featured `workflow` package up top with prominent install command,
copy button, and direct tarball download
- Top-of-page metadata chips: short SHA (linked to commit), branch,
PR number, build timestamp, package count + total size
- Collapsible "What is this?" explainer
- Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the
install command for every row in place
- Live filter input over the rest of the package list (with `/` shortcut)
- Per-row install command, copy button, and direct download
- Modern dark/light theme with system preference, Geist-inspired styling
Also captures tarball size during pack and renders human-readable byte counts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented May 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1c48345

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-nextjs-workflow-webpackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-astro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-express-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-fastify-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-hono-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nitro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nuxt-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-sveltekit-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-tanstack-start-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-vite-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-swc-playgroundReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-tarballsReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-webReadyReadyPreview, CommentMay 4, 2026 11:41am
1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
workflow-docsSkippedSkippedMay 4, 2026 11:41am

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other5100176686
Total4338010525390

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025
✅ e2e-vercel-prod-tanstack-start72026

📋 View full workflow run

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.031s (-30.2% 🟢)1.005s (~)0.974s101.00x
💻 LocalNitro0.032s (-24.8% 🟢)1.005s (~)0.973s101.05x
🐘 PostgresExpress0.043s (-26.4% 🟢)1.010s (~)0.967s101.38x
💻 LocalNext.js (Turbopack)0.047s1.005s0.958s101.51x
🐘 PostgresNitro0.052s (-45.6% 🟢)1.012s (-3.0%)0.960s101.68x
🌐 RedisNext.js (Turbopack)0.059s1.005s0.947s101.89x
🐘 PostgresNext.js (Turbopack)0.059s1.011s0.952s101.91x
🌐 MongoDBNext.js (Turbopack)0.100s1.009s0.910s103.23x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.068s (-5.6% 🟢)2.006s (~)0.938s101.00x
💻 LocalExpress1.074s (-4.6%)2.006s (~)0.932s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.927s101.01x
🐘 PostgresNitro1.088s (-4.6%)2.010s (~)0.922s101.02x
💻 LocalNext.js (Turbopack)1.108s2.006s0.898s101.04x
🌐 RedisNext.js (Turbopack)1.112s2.007s0.895s101.04x
🐘 PostgresNext.js (Turbopack)1.117s2.009s0.892s101.05x
🌐 MongoDBNext.js (Turbopack)1.166s2.009s0.843s101.09x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.387s (-5.1% 🟢)11.022s (~)0.634s31.00x
💻 LocalExpress10.405s (-4.7%)11.023s (~)0.618s31.00x
🐘 PostgresExpress10.415s (-5.0% 🟢)11.012s (~)0.597s31.00x
🐘 PostgresNitro10.432s (-4.0%)11.018s (~)0.585s31.00x
🌐 RedisNext.js (Turbopack)10.634s11.023s0.389s31.02x
💻 LocalNext.js (Turbopack)10.690s11.023s0.333s31.03x
🐘 PostgresNext.js (Turbopack)10.716s11.017s0.301s31.03x
🌐 MongoDBNext.js (Turbopack)10.822s11.017s0.195s31.04x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.426s (-10.9% 🟢)14.027s (-12.5% 🟢)0.600s51.00x
🐘 PostgresExpress13.463s (-7.7% 🟢)14.021s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.480s (-10.0% 🟢)14.028s (-6.7% 🟢)0.548s51.00x
🐘 PostgresNitro13.544s (-7.2% 🟢)14.019s (-6.7% 🟢)0.475s51.01x
💻 LocalNext.js (Turbopack)14.031s15.028s0.997s41.05x
🌐 RedisNext.js (Turbopack)14.036s14.628s0.592s51.05x
🐘 PostgresNext.js (Turbopack)14.130s15.022s0.892s41.05x
🌐 MongoDBNext.js (Turbopack)14.245s15.023s0.778s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.809s (-29.6% 🟢)12.022s (-29.4% 🟢)0.214s81.00x
🐘 PostgresNitro11.928s (-14.6% 🟢)12.017s (-16.0% 🟢)0.089s81.01x
🐘 PostgresExpress11.967s (-14.6% 🟢)12.265s (-16.0% 🟢)0.298s81.01x
💻 LocalExpress12.248s (-26.2% 🟢)12.774s (-25.0% 🟢)0.526s81.04x
💻 LocalNext.js (Turbopack)13.061s13.737s0.677s71.11x
🌐 RedisNext.js (Turbopack)13.217s13.883s0.666s71.12x
🌐 MongoDBNext.js (Turbopack)13.255s14.019s0.764s71.12x
🐘 PostgresNext.js (Turbopack)13.304s14.021s0.718s71.13x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.141s (-9.4% 🟢)2.006s (~)0.865s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.007s (~)0.861s151.00x
💻 LocalNitro1.171s (-28.2% 🟢)2.006s (-3.3%)0.835s151.03x
💻 LocalExpress1.174s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.217s2.007s0.790s151.07x
🌐 RedisNext.js (Turbopack)1.240s2.006s0.766s151.09x
💻 LocalNext.js (Turbopack)1.289s2.006s0.717s151.13x
🌐 MongoDBNext.js (Turbopack)2.035s2.826s0.791s111.78x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-49.1% 🟢)2.007s (-33.3% 🟢)0.805s151.00x
🐘 PostgresNitro1.229s (-47.7% 🟢)2.007s (-33.3% 🟢)0.778s151.02x
🐘 PostgresNext.js (Turbopack)1.358s2.008s0.650s151.13x
💻 LocalNitro1.685s (-46.4% 🟢)2.006s (-48.4% 🟢)0.320s151.40x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.299s151.42x
💻 LocalNext.js (Turbopack)1.858s2.292s0.434s141.55x
🌐 RedisNext.js (Turbopack)2.337s3.008s0.671s101.95x
🌐 MongoDBNext.js (Turbopack)3.568s4.009s0.441s82.97x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.305s (-62.6% 🟢)2.007s (-50.0% 🟢)0.702s151.00x
🐘 PostgresNitro1.383s (-60.3% 🟢)2.007s (-49.9% 🟢)0.624s151.06x
🐘 PostgresNext.js (Turbopack)1.629s2.007s0.378s151.25x
🌐 RedisNext.js (Turbopack)3.602s4.010s0.408s82.76x
💻 LocalNext.js (Turbopack)4.383s5.011s0.627s63.36x
💻 LocalExpress4.397s (-47.3% 🟢)5.013s (-44.5% 🟢)0.616s63.37x
💻 LocalNitro4.571s (-45.2% 🟢)5.011s (-44.4% 🟢)0.440s63.50x
🌐 MongoDBNext.js (Turbopack)6.273s7.013s0.739s54.81x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.139s (-9.4% 🟢)2.009s (~)0.869s151.00x
🐘 PostgresNitro1.168s (-7.1% 🟢)2.009s (~)0.840s151.03x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.773s151.08x
🐘 PostgresNext.js (Turbopack)1.248s2.007s0.760s151.10x
💻 LocalNext.js (Turbopack)1.321s2.005s0.684s151.16x
💻 LocalExpress1.352s (-28.6% 🟢)2.006s (-15.1% 🟢)0.654s151.19x
💻 LocalNitro1.379s (-26.1% 🟢)2.006s (-14.3% 🟢)0.627s151.21x
🌐 MongoDBNext.js (Turbopack)2.026s2.735s0.709s111.78x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-48.3% 🟢)2.008s (-33.3% 🟢)0.797s151.00x
🐘 PostgresNitro1.232s (-47.3% 🟢)2.008s (-33.3% 🟢)0.776s151.02x
🐘 PostgresNext.js (Turbopack)1.368s2.008s0.640s151.13x
💻 LocalExpress1.838s (-41.3% 🟢)2.075s (-44.8% 🟢)0.238s151.52x
💻 LocalNitro1.954s (-36.3% 🟢)2.469s (-36.5% 🟢)0.515s131.61x
💻 LocalNext.js (Turbopack)2.083s2.826s0.743s111.72x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.94x
🌐 MongoDBNext.js (Turbopack)3.547s4.008s0.461s82.93x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.318s (-62.3% 🟢)2.007s (-50.0% 🟢)0.690s151.00x
🐘 PostgresNitro1.379s (-60.4% 🟢)2.007s (-49.9% 🟢)0.628s151.05x
🐘 PostgresNext.js (Turbopack)1.718s2.075s0.357s151.30x
🌐 RedisNext.js (Turbopack)3.595s4.010s0.415s82.73x
💻 LocalNitro4.877s (-46.7% 🟢)5.516s (-45.0% 🟢)0.639s63.70x
💻 LocalExpress5.136s (-41.6% 🟢)5.515s (-40.5% 🟢)0.379s63.90x
💻 LocalNext.js (Turbopack)5.496s6.011s0.515s54.17x
🌐 MongoDBNext.js (Turbopack)6.286s7.013s0.727s54.77x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.430s (-48.7% 🟢)1.006s (-1.7%)0.575s601.00x
💻 LocalNitro0.457s (-53.4% 🟢)1.004s (-8.3% 🟢)0.547s601.06x
🐘 PostgresNitro0.483s (-41.1% 🟢)1.023s (+1.7%)0.540s591.12x
💻 LocalExpress0.509s (-48.3% 🟢)1.004s (-6.7% 🟢)0.495s601.18x
🌐 RedisNext.js (Turbopack)0.617s1.004s0.388s601.43x
🐘 PostgresNext.js (Turbopack)0.697s1.007s0.310s601.62x
💻 LocalNext.js (Turbopack)0.712s1.004s0.292s601.66x
🌐 MongoDBNext.js (Turbopack)0.739s1.006s0.266s601.72x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.027s (-48.0% 🟢)1.586s (-29.8% 🟢)0.559s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.741s (-17.1% 🟢)0.648s521.06x
💻 LocalNitro1.156s (-61.9% 🟢)2.006s (-46.6% 🟢)0.849s451.13x
💻 LocalExpress1.191s (-60.5% 🟢)2.005s (-44.1% 🟢)0.814s451.16x
🌐 RedisNext.js (Turbopack)1.479s2.006s0.527s451.44x
🐘 PostgresNext.js (Turbopack)1.638s2.007s0.369s451.60x
💻 LocalNext.js (Turbopack)1.780s2.027s0.247s451.73x
🌐 MongoDBNext.js (Turbopack)1.811s2.007s0.196s451.76x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.012s (-49.6% 🟢)2.427s (-44.5% 🟢)0.415s501.00x
🐘 PostgresNitro2.035s (-50.4% 🟢)2.457s (-46.6% 🟢)0.422s491.01x
💻 LocalNitro2.638s (-71.6% 🟢)3.007s (-70.0% 🟢)0.369s401.31x
💻 LocalExpress2.802s (-69.6% 🟢)3.084s (-69.2% 🟢)0.283s391.39x
🌐 RedisNext.js (Turbopack)3.003s3.277s0.275s371.49x
🐘 PostgresNext.js (Turbopack)3.192s4.010s0.817s301.59x
💻 LocalNext.js (Turbopack)3.864s4.145s0.282s291.92x
🌐 MongoDBNext.js (Turbopack)4.155s5.011s0.856s242.07x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.172s (-39.1% 🟢)1.006s (~)0.834s601.00x
🐘 PostgresNitro0.192s (-32.2% 🟢)1.006s (~)0.814s601.12x
🐘 PostgresNext.js (Turbopack)0.241s1.006s0.764s601.40x
🌐 RedisNext.js (Turbopack)0.251s1.004s0.753s601.46x
💻 LocalNitro0.432s (-28.6% 🟢)1.004s (-1.7%)0.572s602.51x
💻 LocalExpress0.466s (-16.9% 🟢)1.004s (~)0.539s602.71x
💻 LocalNext.js (Turbopack)0.567s1.004s0.437s603.30x
🌐 MongoDBNext.js (Turbopack)1.041s1.882s0.842s326.05x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.296s (-41.8% 🟢)1.006s (~)0.710s901.00x
🐘 PostgresNitro0.314s (-36.8% 🟢)1.006s (~)0.693s901.06x
🌐 RedisNext.js (Turbopack)0.414s1.004s0.590s901.40x
🐘 PostgresNext.js (Turbopack)0.461s1.006s0.545s901.55x
💻 LocalNitro2.179s (-14.1% 🟢)2.852s (-5.2% 🟢)0.672s327.35x
💻 LocalNext.js (Turbopack)2.182s2.944s0.762s317.36x
💻 LocalExpress2.223s (-11.6% 🟢)2.821s (-6.3% 🟢)0.598s327.50x
🌐 MongoDBNext.js (Turbopack)2.618s3.007s0.389s308.83x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.578s (-29.3% 🟢)1.014s (~)0.436s1191.00x
🐘 PostgresNitro0.641s (-18.9% 🟢)1.006s (~)0.365s1201.11x
🌐 RedisNext.js (Turbopack)0.770s1.004s0.234s1201.33x
🐘 PostgresNext.js (Turbopack)0.957s1.352s0.394s901.65x
🌐 MongoDBNext.js (Turbopack)5.396s6.011s0.615s209.33x
💻 LocalExpress9.835s (-12.1% 🟢)10.444s (-12.5% 🟢)0.609s1217.00x
💻 LocalNitro9.941s (-11.2% 🟢)10.365s (-11.1% 🟢)0.424s1217.19x
💻 LocalNext.js (Turbopack)11.011s11.573s0.562s1119.03x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.127s (+449.7% 🔺)2.001s (+100.3% 🔺)0.001s (-37.5% 🟢)2.009s (+98.6% 🔺)0.882s101.00x
💻 LocalNitro1.128s (+428.0% 🔺)2.005s (+99.6% 🔺)0.011s (-15.2% 🟢)2.018s (+98.1% 🔺)0.890s101.00x
🐘 PostgresNitro1.135s (+453.5% 🔺)2.000s (+100.1% 🔺)0.001s (-6.7% 🟢)2.010s (+98.7% 🔺)0.875s101.01x
💻 LocalExpress1.140s (+472.4% 🔺)2.005s (+99.6% 🔺)0.013s (+3.3%)2.020s (+98.4% 🔺)0.881s101.01x
💻 LocalNext.js (Turbopack)1.190s2.003s0.010s2.017s0.826s101.06x
🐘 PostgresNext.js (Turbopack)1.197s2.001s0.002s2.011s0.815s101.06x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.482s (+135.3% 🔺)2.006s (+99.3% 🔺)0.003s (-10.4% 🟢)2.022s (+97.7% 🔺)0.540s301.00x
🐘 PostgresNitro1.525s (+144.4% 🔺)2.002s (+98.9% 🔺)0.004s (+0.8%)2.026s (+98.2% 🔺)0.501s301.03x
💻 LocalNext.js (Turbopack)1.670s2.009s0.011s2.023s0.352s301.13x
🐘 PostgresNext.js (Turbopack)1.682s2.010s0.004s2.025s0.343s301.13x
💻 LocalExpress1.705s (+125.2% 🔺)2.011s (+95.4% 🔺)0.010s (+8.5% 🔺)2.202s (+111.8% 🔺)0.498s281.15x
💻 LocalNitro1.713s (+104.2% 🔺)2.010s (+98.6% 🔺)0.010s (+4.8%)2.201s (+97.2% 🔺)0.488s281.16x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.622s (-35.2% 🟢)1.013s (-20.7% 🟢)0.000s (+16.9% 🔺)1.028s (-21.3% 🟢)0.406s591.00x
🐘 PostgresNitro0.681s (-29.7% 🟢)1.047s (-16.1% 🟢)0.000s (-58.6% 🟢)1.059s (-15.8% 🟢)0.378s581.09x
🐘 PostgresNext.js (Turbopack)0.817s1.053s0.000s1.068s0.251s571.31x
💻 LocalNitro1.331s (+8.8% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.686s302.14x
💻 LocalExpress1.394s (+13.8% 🔺)2.015s (~)0.000s (+10.0% 🔺)2.017s (~)0.623s302.24x
💻 LocalNext.js (Turbopack)1.437s2.013s0.000s2.016s0.579s302.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.313s (-26.7% 🟢)2.099s (-2.0%)0.000s (-3.4%)2.131s (-2.0%)0.818s291.00x
🐘 PostgresExpress1.349s (-23.9% 🟢)2.145s (-1.5%)0.000s (+Infinity% 🔺)2.161s (-1.7%)0.812s281.03x
🐘 PostgresNext.js (Turbopack)1.689s2.225s0.000s2.273s0.584s271.29x
💻 LocalNext.js (Turbopack)2.563s3.079s0.001s3.084s0.521s201.95x
💻 LocalNitro3.092s (-8.7% 🟢)3.840s (-4.8%)0.000s (-76.6% 🟢)3.844s (-4.8%)0.752s162.35x
💻 LocalExpress3.227s (-6.9% 🟢)4.031s (~)0.000s (-66.7% 🟢)4.033s (~)0.806s152.46x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro14/21
🐘 PostgresExpress19/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres8/21
Nitro🐘 Postgres14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

Comment threadtarballs/scripts/pack.ts Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visual design is nice — featured workflow card, package-manager toggle, live filter, copy buttons, geist-inspired theme. Going to approve since the deployment shape doesn't change and this only affects the static index page.

That said: I'd push back on the hand-rolled HTML-in-JS approach for anything beyond a one-page diff. The new file is 857 lines, and the implementation has some smells that follow naturally from the medium:

  • The inline <script> block (~80 lines) isn't TypeScript-checked. It's inside a template literal in writeIndexHtml, so the compiler treats it as a string. Refactoring (rename a data- attribute, change a property name) won't surface type errors. The TS half references DOM via data- attributes that have to match by convention.
  • Two parallel "render the page" concerns — the pack.ts script handles both bundling-time logic (scanning packages, rewriting deps, packing tarballs) and presentation logic (HTML composition, CSS, client-side JS for PM toggle/filter/copy). Those were independent before this PR; conflating them makes both harder to evolve.
  • renderFeatured/renderRow and the inline applyPm/applyFilter script must stay in sync on attribute names, class names, and the catalog JSON shape. Right now there's a hand-rolled escapeHtml helper and the contract is "data-install-cmd must equal escapedName." Easy to break in a refactor; no tests catch the drift.
  • Hand-rolled escape helpers (escapeHtml) are correct here but would be unnecessary with JSX or a templating library.

A lightweight Vite + React (or Preact for smaller bundle) SPA would clean this up substantially. The shape I'd suggest:

tarballs/
├── package.json # adds vite, react, @vitejs/plugin-react
├── vite.config.ts # static build → public/
├── index.html # entry HTML
├── src/
│ ├── main.tsx # mount React
│ ├── App.tsx # the page (~80 lines instead of ~600 of HTML+CSS+JS)
│ ├── catalog.ts # types for the JSON catalog
│ └── styles.css # the geist-inspired theme
└── scripts/
├── pack.ts # data-only: scans packages, packs tarballs, writes
│ # public/catalog.json with the build context
└── check-tarballs-smoke.mjs

pack.ts becomes ~140 lines (just the bundling logic), vite build produces the static page that fetches catalog.json, the React component reads it on mount and renders. State (active PM, search query) is useState. Build pipeline is pnpm pack → vite build, and Vercel's existing outputDirectory: "public" works unchanged.

DX wins: HMR while iterating on the design, TypeScript checks the JSX and event handlers, can pull in actual geist (or lucide-react) icons via npm instead of inlined SVG, hot-swap component libraries if the design evolves.

If you don't want to take that on now, this is fine as-is — the page works, the design looks good, and the deployment shape is preserved. But suggest filing a follow-up to migrate before adding the next 200 lines of HTML to this file.

Smaller things I noticed

  • getBuildContext uses process.env.VERCEL_GIT_* for SHA/branch/PR. Local builds get only the SHA/branch fallback via git rev-parse. Worth double-checking the labels on chips degrade gracefully when those env vars are missing — looks like they do (early-return paths with if (ctx.commitUrl)), but a quick local pnpm --filter tarballs build test before merge would confirm.
  • The catalog JSON is embedded via <script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>. JSON-inside-HTML-inside-template-literal is two layers of escape (the escapeHtml handles the outer, but the JSON itself can contain </script> if a package description has one — unlikely but possible). Safer to use JSON.stringify(catalog).replace(/</g, '\\u003c') for the embedded JSON, which escapes the dangerous bytes without affecting parseability.
  • formatBytes uses base-2 units (KB = 1024) but labels them as 'KB'/'MB' which by convention suggest base-10. Minor but KiB/MiB would be more precise (or use base-10 with 1000).
  • applyFilter runs on every keystroke against a re-querying Array.from(document.querySelectorAll('.pkg-row')). With ~25 packages this is fine; if the catalog grows substantially, debounce or pre-compute.

Test plan note

The PR description has 8 unchecked test-plan items. They're all manual visual checks (page renders, filter works, copy fires, etc.). Since the Vercel preview URL is the canonical way to validate this, they probably won't be checked off in advance — but worth at least running through them on the preview before merge.

Comment threadtarballs/scripts/pack.ts Outdated
Built ${ctx.commitUrl ? `from <a href="${escapeHtml(ctx.commitUrl)}" target="_blank" rel="noopener">${escapeHtml(ctx.shortSha)}</a>` : `from <code>${escapeHtml(ctx.shortSha)}</code>`} · ${escapeHtml(packages.length.toString())} packages totaling ${escapeHtml(formatBytes(totalSize))}
</footer>
</div>
<script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this line breaks all client-side interactivity.

Verified on the preview deployment with browser automation — every interactive feature is non-functional:

  • ❌ Package-manager toggle (clicking npm/yarn/bun does nothing)
  • ❌ Live filter (typing in search shows all rows)
  • ❌ Copy buttons (no clipboard write, no Copied state)
  • / keyboard shortcut (body keeps focus)

Root cause: escapeHtml(JSON.stringify(catalog)) HTML-encodes the JSON. The serialized catalog ends up in the page as:

<scriptid="catalog" type="application/json">[{&quot;name&quot;:&quot;@workflow/ai&quot;,&quot;escapedName&quot;:&quot;workflow-ai&quot;...

JSON.parse(textContent) then throws on the very first &quot; because &quot; isn't valid JSON syntax. The IIFE bails on its first line and none of the event listeners get attached. Confirmed live:

>JSON.parse(document.getElementById('catalog').textContent)Uncaught SyntaxError: Expectedpropertynameor'}'inJSONatposition2(line1column3)

<script type="application/json"> content is treated as text by the HTML parser — it does NOT need HTML entity escaping. The only sequence that can break out of it is </script> (or </ in some legacy parsers). The narrowest correct fix is:

<scriptid="catalog"type="application/json">${JSON.stringify(catalog).replace(/</g,'\u003c')}</script>

That escapes < bytes to \u003c in the JSON (legal per the spec — JSON allows \uXXXX escapes for any character), so </script> can't appear in the text and the JSON parses cleanly.

This was the third bullet in my "smaller things I noticed" section above — turns out it's not theoretical, it's the live bug breaking everything client-side. Should not merge until the page works in a real browser.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3df48a7. Went with your replace(/</g, '\u003c') over the bot's </-only escape since it also covers a stray < inside, e.g., a package description. Verified locally that the embedded JSON parses cleanly out of the rendered HTML; preview deploy will validate the four interactive features end-to-end.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating my prior review to REQUEST_CHANGES. Ran the visual test plan against the preview deployment with browser automation and discovered that all client-side interactivity is broken — see the inline comment on pack.ts:696 for the root cause and fix.

The escapeHtml(JSON.stringify(catalog)) call HTML-encodes every " in the JSON to &quot;, so JSON.parse(textContent) throws on its first character. The IIFE bails immediately and none of the event listeners (PM toggle, search filter, copy buttons, / shortcut) get attached.

Test plan results from the preview at https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/:

  • ✅ Page renders correctly in light mode (visual layout, chips, featured card, package list)
  • workflow package appears as the featured card at the top
  • ✅ SHA / branch / timestamp / package count chips are present
  • Package-manager toggle doesn't update install commandsaria-selected stays on pnpm regardless of clicks
  • Live filter doesn't filter — typing in the search input shows all rows unchanged
  • Copy buttons don't fire — no Copied state, no clipboard write
  • / keyboard shortcut doesn't focus search — body keeps focus
  • (couldn't test direct download because earlier broken state interfered, but the <a download> is plain HTML and probably works)

So the page that ships looks correct visually but is functionally a static catalog — the toggle, filter, and copy buttons are all dead UI.

Quick fix at the call site (one line). After fix, all four interactive features should work. Worth re-testing on the next preview deployment.

The other points from my prior review (Vite + React refactor suggestion, the smaller TS-checkable concerns, KB vs KiB nit) all still stand and are non-blocking. Just this one issue blocks merging.

`escapeHtml(JSON.stringify(catalog))` was HTML-encoding every quote in
the embedded catalog JSON to `&quot;`, so `JSON.parse(textContent)` threw
on the first character and the IIFE bailed before attaching any event
listeners — package-manager toggle, search filter, copy buttons, and the
`/` shortcut were all dead UI on the deployed page.
`<script type="application/json">` content is treated as text by the HTML
parser; the only sequence that can break out is `</script>` (or `</`
in legacy parsers). Replace `<` with the JSON `<` escape, which is
legal per the JSON spec and prevents the breakout without needing entity
encoding.
Also switch `formatBytes` from `KB`/`MB` to `KiB`/`MiB` since the
divisor is 1024.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment threadtarballs/scripts/pack.ts Outdated
The smoke check broke in CI: `'workflow' tarball only has 0 files`.
Root cause is that `tar -tvzf` emits a different verbose layout on GNU
tar (Linux, what CI runs) vs BSD tar (macOS, where I tested locally) —
the parser only matched the BSD column ordering, so on Linux every line
was rejected and `fileCount` came out as 0.
Replace the shell-out with a small in-process tar reader using
`zlib.gunzipSync` + manual 512-byte block walk. ustar headers are
trivially structured (name at offset 0, octal size at 124, typeflag at
156, ustar prefix at 345). We emit regular files only (`typeflag` `0`
or NUL) and consume but skip pax extended headers (`x`/`g`) and GNU
long-name entries (`L`). Result is identical on every platform.
Verified locally: 206 files / 998413 bytes for `workflow.tgz` matches
`tar -tvzf` exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pranaygpand others added 3 commits May 4, 2026 19:31
The previous "What's inside?" view crammed nested directory groups,
proportional bars, and per-group file lists into a `<details>` inside
an already-narrow row. It was hard to read and harder to compare.
Replace it with the layout packagephobia uses on its result page:
- Two large headline metric tiles (Publish size / Unpacked size)
with a big bold value, smaller unit, and small uppercase label.
Modeled directly on packagephobia's `Stats` component but using
our existing CSS variables so it tracks light/dark theme.
- A single sortable file table beneath. Default is size-descending so
the contributors to package size are immediately visible. Click a
header to flip direction or switch sort key. Sticky header keeps
the columns visible inside the scrollable region.
Drop the `groupByTopLevel`, `ContentsGroup`, and bar-chart styles —
they were the source of the "hard to use" feedback and don't add
information that the flat sortable table doesn't already convey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the old static tarballs index with a Vite/Preact single-page UI that reads a generated catalog.json, surfaces richer build/package metadata, and adds interactive install/copy/filter/download affordances for preview tarballs.

Changes:

  • Adds a Vite + Preact frontend for the tarballs index, including featured package rendering, filtering, package-manager toggles, and package contents breakdowns.
  • Extends scripts/pack.ts to emit catalog.json with build metadata, tarball sizes, unpacked sizes, and per-file listings.
  • Updates build/smoke-test plumbing so the tarballs deployment now publishes bundled SPA assets plus the generated catalog.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tarballs/vite.config.tsConfigures Vite to build the SPA into public/ without deleting tarballs.
tarballs/turbo.jsonExpands build outputs to include SPA assets and catalog.json.
tarballs/tsconfig.jsonAdds TS config for the new frontend/build files.
tarballs/src/styles.cssAdds the full styling/theme for the redesigned index UI.
tarballs/src/main.tsxBoots the SPA and loads catalog.json.
tarballs/src/icons.tsxAdds inline SVG icons used across the new UI.
tarballs/src/catalog.tsDefines shared catalog/install-command helpers for the frontend.
tarballs/src/app.tsxImplements the main tarballs UI, interactions, and package contents tables.
tarballs/scripts/pack.tsGenerates tarballs plus the new metadata-rich catalog.json.
tarballs/scripts/check-tarballs-smoke.mjsAdds smoke coverage for catalog.json.
tarballs/package.jsonSwitches build to pack.ts && vite build and adds frontend deps/scripts.
tarballs/index.htmlAdds the SPA entry HTML shell.
pnpm-lock.yamlLocks newly added frontend/tooling dependencies.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadtarballs/package.json Outdated
"scripts": {
"build": "node scripts/pack.ts",
"build": "node scripts/pack.ts && vite build",
"dev": "vite",

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. The new dev chains node scripts/pack.ts && vite, and I restructured the build layout to vite's conventional shape so it actually works in dev: public/ is a real vite public dir (pack writes tarballs + catalog.json there, vite serves them at root in dev), and dist/ is the production build output (set as Vercel's outputDirectory).

Comment threadtarballs/src/app.tsx
<SearchIcon />
<input
ref={searchRef}
type="search"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added aria-label="Filter packages" to the input.

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +178 to +185
<div class="pm-tabs" role="tablist" aria-label="Package manager">
{options.map((opt) => (
<button
key={opt}
type="button"
class="pm-tab"
role="tab"
aria-selected={value === opt}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Dropped role="tablist"/role="tab"/aria-selected and made these plain buttons with aria-pressed. Toggle buttons are the honest representation since we never implemented arrow-key roving focus. Each button also has an explicit aria-label (Show install commands for pnpm etc.).

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +428 to +440
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} finally {
ta.remove();
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Refactored to a writeToClipboard helper that returns whether the write actually succeeded; the button only flips to Copied on success. If both navigator.clipboard.writeText and the execCommand fallback fail, it briefly shows a red Failed state instead.

Comment threadtarballs/src/app.tsx Outdated
<code class="pkg-cmd">{cmd}</code>
<div class="pkg-actions">
<CopyButton text={cmd} variant="icon" />
<a class="icon-btn" href={pkg.url} download aria-label="Download">

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added an accessibleName prop to CopyButton and pass Copy install command for <pkg.name> from each row + the featured card. Same treatment on the download <a> link (Download <pkg.name> tarball).

Comment threadtarballs/src/main.tsx Outdated
if (!root) throw new Error('No #app root element');

try {
const res = await fetch('/catalog.json', { cache: 'no-store' });

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345 — dropped cache: 'no-store'. Each preview's URL is unique so the browser cache is the right thing to use.

- main.tsx: drop `cache: 'no-store'` from the catalog fetch. Each
tarballs deployment is immutable per commit, so HTTP caching is
appropriate; forcing no-store made every visit re-download the full
catalog (which now includes per-package file lists).
- app.tsx (search input): add `aria-label="Filter packages"`. The
visible label only contained an icon and placeholder, so screen
readers had no name for the control.
- app.tsx (PmTabs): replace `role="tablist"` / `role="tab"` /
`aria-selected` with plain buttons that use `aria-pressed`. The
ARIA tab pattern requires arrow-key roving focus we never wired
up; toggle buttons are the honest representation. Each button
also gets an explicit `aria-label`.
- app.tsx (row buttons): include the package name in the accessible
label of every per-row copy/download button (and on the featured
card too), so the screen reader buttons/links list distinguishes
them. Added an `accessibleName` prop to `CopyButton`.
- app.tsx (CopyButton): only flip to the "Copied" state when the
write actually succeeded. Both the modern `navigator.clipboard`
path and the `execCommand` fallback can fail; the new
`writeToClipboard` helper returns success and the button shows a
short "Failed" state if both paths fail.
# Make `pnpm dev` work from a clean checkout
The previous `dev: vite` couldn't actually serve the page because
`/catalog.json` 404s and the SPA boots into the error fallback.
Restructure the build layout to vite's conventional shape:
- `public/` is now a true vite public dir — pack writes tarballs and
catalog.json there. In dev, vite serves these at the root.
- `dist/` is the production build output (vite copies public/ into it
and adds index.html + assets/).
- `vercel.json#outputDirectory` switches from `public` → `dist`.
- `turbo.json` outputs updated to match.
- `dev` chains pack before vite so the catalog exists when the dev
server starts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​preact/​preset-vite@​2.10.59910010090100
Addednpm/​preact@​10.29.110010010094100

View full report

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pranay took the Vite + Preact refactor recommendation from my prior review and ran with it. The new shape matches what I sketched almost exactly:

tarballs/
├── index.html # 20-line entry
├── vite.config.ts # 16-line config
├── tsconfig.json # strict, jsxImportSource: preact
├── scripts/
│ ├── pack.ts # 317 lines, data-only — packs tarballs, writes catalog.json
│ └── check-tarballs-smoke.mjs # now also validates catalog.json shape
└── src/
├── main.tsx # 26 lines — fetch + mount
├── app.tsx # 550 lines — typed Preact JSX
├── catalog.ts # types + small helpers
├── icons.tsx # 8 inline SVG icons
└── styles.css # 794 lines, geist-inspired theme

Bundle ships as dist/assets/index-*.js at 26.25 kB (9.69 kB gzipped) — small for a Preact SPA with sortable tables, copy-buttons, search, and a PM toggle. CSS is 11.19 kB / 2.73 kB gzipped.

What I verified

  • Local build works: pnpm turbo build --filter=tarballs runs pack.ts then vite build, producing 26 tarballs in public/, a catalog.json matching the typed shape in src/catalog.ts, and a dist/ ready to deploy.
  • TypeScript is clean: pnpm exec tsc --noEmit from tarballs/ passes with the strict config (strict, noUnusedLocals, noUnusedParameters, isolatedModules).
  • Deployed preview serves correctly: https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/catalog.json returns the expected payload with build context (sha, branch, PR number, GitHub-linked commit/branch/PR URLs) and 26 packages with full file listings + sizes. Each package's url is the immutable per-deployment hash URL (e.g. https://workflow-tarballs-ch8zhxmen.labs.vercel.dev/workflow-ai.tgz), so the catalog is stable even if visited via the git-branch URL.
  • JS bundle contains real Preact: read the bundle, confirmed the App tree renders with hooks (useState, useEffect, useMemo, useRef), addEventListener('keydown', …) for the / shortcut, navigator.clipboard.writeText + execCommand('copy') fallback for CopyButton, and the data-copied / data-failed state machine.
  • Smoke check is stronger now: in addition to verifying tarballs are gzip-magic-prefixed and the index page responds with text/html, it now fetches catalog.json and asserts the workflow package has a non-trivial fileCount — a useful diagnostic for "packages weren't built before pack."

Concerns from my prior review — all addressed

  1. The hand-rolled HTML-in-JS approach — gone. JSX is TypeScript-checked end to end (event handlers, prop types, catalog shape).
  2. Two parallel render concerns merged into one file — split clean. pack.ts is now data-only (~317 lines, all bundling/tar-reading). The presentation layer is in src/.
  3. renderRow / applyPm / applyFilter had to stay in sync via string conventions — gone. State lives in React-style hooks, props are typed.
  4. Hand-rolled escapeHtml — gone. JSX handles escaping.
  5. escapeHtml(JSON.stringify(catalog)) breaking client interactivity — moot. The catalog is a separate /catalog.json static file fetched at runtime, so there's no inline-JSON encoding question.
  6. KB/MB labels with base-2 math — fixed. formatBytes now emits B/KiB/MiB.

Copilot review items — all addressed in 1c48345e6

  • a11y: aria-label="Filter packages" on search input; PM tabs use plain buttons with aria-pressed (no half-implemented role="tablist"); per-row copy buttons get accessibleName="Copy install command for ${pkg.name}" instead of a generic label.
  • Robustness: writeToClipboard returns a boolean so failed writes show "Failed" rather than a misleading "Copied" success state.
  • DX: dev script chains pack.ts && vite; dropped cache: 'no-store' since the catalog URL is per-deployment-immutable.

Smaller improvements landed along the way

  • Tar reader rewrite (6ae7736): replaced tar -tvzf shell-out with an in-process gunzip + 512-byte block walker. Handles GNU long-name (L) entries and pax headers, and works identically on macOS BSD tar and Linux GNU tar. Sorting files by descending size before emitting is a nice touch for the per-package "what's inside" view.
  • PackagePhobia-style SizeStats widget: two big tiles (publish size + unpacked size) with split value/unit rendering. Looks good and reads better than a one-liner.
  • FileTable with sortable columns: ARIA aria-sort on <th>, indicator arrows, click-to-toggle direction. Strips the package/ prefix from displayed paths.

Tiny things — non-blocking

  • pack.ts:117: updateDeps builds rewrite URLs as https://${process.env.VERCEL_URL}/${escapedName}.tgz directly, while baseUrl (defined at line 97) uses the VERCEL_URL ? https://… : '' fallback. Local builds without VERCEL_URL produce package.json deps like https://undefined/.... Pre-existing on main, not introduced by this PR. Not a real-world issue (nobody installs from a local pack output) but noting for a follow-up cleanup if you want consistency.
  • catalog.ts and pack.ts duplicate the same TS interfaces (TarballFile, PackedPackage, BuildContext, Catalog). Drift risk is low since they're both in the same package, but if you want to eliminate it, catalog.ts could be the single source of truth and pack.ts could import type from it. Fine to leave.
  • SizeStats is rendered for every package in the list, not just the featured one — but it's behind a <details> so it doesn't blow up initial render. The 26 expanded <details> plus the FileTable for each would render a lot of DOM if expanded all at once; in practice, users open one or two. Not blocking.

Verdict: approve

Approving and clearing my prior CHANGES_REQUESTED. The redesign hit every concern from my last two reviews plus the Copilot feedback, the build pipeline is cleaner than what was there before, and the deployment shape is preserved (public/<escaped-name>.tgz URLs unchanged, smoke checks pass). Nice work on the followups.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

@v0@vercel can you make a PR to address the remaining follow ups from @TooTallNate's review comments that we didn't get to before merge of this one

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-stableCherry-pick this PR to the stable branch when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

tarballs: redesign preview tarballs index page - #1911

Merged
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements
May 4, 2026
Merged

tarballs: redesign preview tarballs index page#1911
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Summary

Reworks the static index page generated by tarballs/scripts/pack.ts (deployed at https://workflow-tarballs.labs.vercel.dev/) to be more useful and pleasant to look at.

What's new

  • Featured workflow package at the top with a large install command, copy button, and direct tarball download — the rest of the packages are secondary.
  • Top-of-page metadata chips: short SHA (linked to commit on GitHub), branch (linked), PR number (linked), build timestamp, package count + total size.
  • Collapsible "What is this?" explainer describing how the version + dep rewriting works.
  • Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the install command shown for every row in place.
  • Live filter input over the package list, with / keyboard shortcut to focus.
  • Per-row install command, copy button, and direct download link for every package.
  • Geist-inspired theme with proper dark/light support driven by system preference.

Implementation notes

  • Pack step now records the produced tarball size via fs.stat after pnpm pack so the page can show human-readable sizes.
  • The page reads VERCEL_GIT_* env vars for commit / branch / PR links and falls back to local git rev-parse for branch when running off-Vercel.
  • All packages still get an entry, but workflow is filtered out of the bottom list and rendered separately.
  • No changes to the deployment shape — public/<escaped-name>.tgz URLs are unchanged, so existing usage keeps working.

The Vercel preview for this PR will deploy and serve the new page so you can see it live.

Test plan

  • Open the Vercel preview deployment and verify the page renders correctly in light and dark mode
  • Confirm the workflow package appears as the featured card at the top
  • Confirm SHA / branch / PR chips are present and linked
  • Toggle pnpm / npm / yarn / bun and confirm install commands update everywhere
  • Filter the package list (and try the / shortcut)
  • Click copy on a row and on the featured card
  • Click download on a row and verify the tarball downloads
  • Smoke-install the featured tarball into a fresh project and confirm it still works (pnpm i https://<deployment>/workflow.tgz)

🤖 Generated with Claude Code

Rebuild the static index page produced by `tarballs/scripts/pack.ts`:
- Featured `workflow` package up top with prominent install command,
copy button, and direct tarball download
- Top-of-page metadata chips: short SHA (linked to commit), branch,
PR number, build timestamp, package count + total size
- Collapsible "What is this?" explainer
- Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the
install command for every row in place
- Live filter input over the rest of the package list (with `/` shortcut)
- Per-row install command, copy button, and direct download
- Modern dark/light theme with system preference, Geist-inspired styling
Also captures tarball size during pack and renders human-readable byte counts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented May 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1c48345

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-nextjs-workflow-webpackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-astro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-express-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-fastify-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-hono-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nitro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nuxt-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-sveltekit-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-tanstack-start-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-vite-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-swc-playgroundReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-tarballsReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-webReadyReadyPreview, CommentMay 4, 2026 11:41am
1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
workflow-docsSkippedSkippedMay 4, 2026 11:41am

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other5100176686
Total4338010525390

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025
✅ e2e-vercel-prod-tanstack-start72026

📋 View full workflow run

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.031s (-30.2% 🟢)1.005s (~)0.974s101.00x
💻 LocalNitro0.032s (-24.8% 🟢)1.005s (~)0.973s101.05x
🐘 PostgresExpress0.043s (-26.4% 🟢)1.010s (~)0.967s101.38x
💻 LocalNext.js (Turbopack)0.047s1.005s0.958s101.51x
🐘 PostgresNitro0.052s (-45.6% 🟢)1.012s (-3.0%)0.960s101.68x
🌐 RedisNext.js (Turbopack)0.059s1.005s0.947s101.89x
🐘 PostgresNext.js (Turbopack)0.059s1.011s0.952s101.91x
🌐 MongoDBNext.js (Turbopack)0.100s1.009s0.910s103.23x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.068s (-5.6% 🟢)2.006s (~)0.938s101.00x
💻 LocalExpress1.074s (-4.6%)2.006s (~)0.932s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.927s101.01x
🐘 PostgresNitro1.088s (-4.6%)2.010s (~)0.922s101.02x
💻 LocalNext.js (Turbopack)1.108s2.006s0.898s101.04x
🌐 RedisNext.js (Turbopack)1.112s2.007s0.895s101.04x
🐘 PostgresNext.js (Turbopack)1.117s2.009s0.892s101.05x
🌐 MongoDBNext.js (Turbopack)1.166s2.009s0.843s101.09x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.387s (-5.1% 🟢)11.022s (~)0.634s31.00x
💻 LocalExpress10.405s (-4.7%)11.023s (~)0.618s31.00x
🐘 PostgresExpress10.415s (-5.0% 🟢)11.012s (~)0.597s31.00x
🐘 PostgresNitro10.432s (-4.0%)11.018s (~)0.585s31.00x
🌐 RedisNext.js (Turbopack)10.634s11.023s0.389s31.02x
💻 LocalNext.js (Turbopack)10.690s11.023s0.333s31.03x
🐘 PostgresNext.js (Turbopack)10.716s11.017s0.301s31.03x
🌐 MongoDBNext.js (Turbopack)10.822s11.017s0.195s31.04x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.426s (-10.9% 🟢)14.027s (-12.5% 🟢)0.600s51.00x
🐘 PostgresExpress13.463s (-7.7% 🟢)14.021s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.480s (-10.0% 🟢)14.028s (-6.7% 🟢)0.548s51.00x
🐘 PostgresNitro13.544s (-7.2% 🟢)14.019s (-6.7% 🟢)0.475s51.01x
💻 LocalNext.js (Turbopack)14.031s15.028s0.997s41.05x
🌐 RedisNext.js (Turbopack)14.036s14.628s0.592s51.05x
🐘 PostgresNext.js (Turbopack)14.130s15.022s0.892s41.05x
🌐 MongoDBNext.js (Turbopack)14.245s15.023s0.778s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.809s (-29.6% 🟢)12.022s (-29.4% 🟢)0.214s81.00x
🐘 PostgresNitro11.928s (-14.6% 🟢)12.017s (-16.0% 🟢)0.089s81.01x
🐘 PostgresExpress11.967s (-14.6% 🟢)12.265s (-16.0% 🟢)0.298s81.01x
💻 LocalExpress12.248s (-26.2% 🟢)12.774s (-25.0% 🟢)0.526s81.04x
💻 LocalNext.js (Turbopack)13.061s13.737s0.677s71.11x
🌐 RedisNext.js (Turbopack)13.217s13.883s0.666s71.12x
🌐 MongoDBNext.js (Turbopack)13.255s14.019s0.764s71.12x
🐘 PostgresNext.js (Turbopack)13.304s14.021s0.718s71.13x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.141s (-9.4% 🟢)2.006s (~)0.865s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.007s (~)0.861s151.00x
💻 LocalNitro1.171s (-28.2% 🟢)2.006s (-3.3%)0.835s151.03x
💻 LocalExpress1.174s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.217s2.007s0.790s151.07x
🌐 RedisNext.js (Turbopack)1.240s2.006s0.766s151.09x
💻 LocalNext.js (Turbopack)1.289s2.006s0.717s151.13x
🌐 MongoDBNext.js (Turbopack)2.035s2.826s0.791s111.78x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-49.1% 🟢)2.007s (-33.3% 🟢)0.805s151.00x
🐘 PostgresNitro1.229s (-47.7% 🟢)2.007s (-33.3% 🟢)0.778s151.02x
🐘 PostgresNext.js (Turbopack)1.358s2.008s0.650s151.13x
💻 LocalNitro1.685s (-46.4% 🟢)2.006s (-48.4% 🟢)0.320s151.40x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.299s151.42x
💻 LocalNext.js (Turbopack)1.858s2.292s0.434s141.55x
🌐 RedisNext.js (Turbopack)2.337s3.008s0.671s101.95x
🌐 MongoDBNext.js (Turbopack)3.568s4.009s0.441s82.97x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.305s (-62.6% 🟢)2.007s (-50.0% 🟢)0.702s151.00x
🐘 PostgresNitro1.383s (-60.3% 🟢)2.007s (-49.9% 🟢)0.624s151.06x
🐘 PostgresNext.js (Turbopack)1.629s2.007s0.378s151.25x
🌐 RedisNext.js (Turbopack)3.602s4.010s0.408s82.76x
💻 LocalNext.js (Turbopack)4.383s5.011s0.627s63.36x
💻 LocalExpress4.397s (-47.3% 🟢)5.013s (-44.5% 🟢)0.616s63.37x
💻 LocalNitro4.571s (-45.2% 🟢)5.011s (-44.4% 🟢)0.440s63.50x
🌐 MongoDBNext.js (Turbopack)6.273s7.013s0.739s54.81x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.139s (-9.4% 🟢)2.009s (~)0.869s151.00x
🐘 PostgresNitro1.168s (-7.1% 🟢)2.009s (~)0.840s151.03x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.773s151.08x
🐘 PostgresNext.js (Turbopack)1.248s2.007s0.760s151.10x
💻 LocalNext.js (Turbopack)1.321s2.005s0.684s151.16x
💻 LocalExpress1.352s (-28.6% 🟢)2.006s (-15.1% 🟢)0.654s151.19x
💻 LocalNitro1.379s (-26.1% 🟢)2.006s (-14.3% 🟢)0.627s151.21x
🌐 MongoDBNext.js (Turbopack)2.026s2.735s0.709s111.78x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-48.3% 🟢)2.008s (-33.3% 🟢)0.797s151.00x
🐘 PostgresNitro1.232s (-47.3% 🟢)2.008s (-33.3% 🟢)0.776s151.02x
🐘 PostgresNext.js (Turbopack)1.368s2.008s0.640s151.13x
💻 LocalExpress1.838s (-41.3% 🟢)2.075s (-44.8% 🟢)0.238s151.52x
💻 LocalNitro1.954s (-36.3% 🟢)2.469s (-36.5% 🟢)0.515s131.61x
💻 LocalNext.js (Turbopack)2.083s2.826s0.743s111.72x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.94x
🌐 MongoDBNext.js (Turbopack)3.547s4.008s0.461s82.93x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.318s (-62.3% 🟢)2.007s (-50.0% 🟢)0.690s151.00x
🐘 PostgresNitro1.379s (-60.4% 🟢)2.007s (-49.9% 🟢)0.628s151.05x
🐘 PostgresNext.js (Turbopack)1.718s2.075s0.357s151.30x
🌐 RedisNext.js (Turbopack)3.595s4.010s0.415s82.73x
💻 LocalNitro4.877s (-46.7% 🟢)5.516s (-45.0% 🟢)0.639s63.70x
💻 LocalExpress5.136s (-41.6% 🟢)5.515s (-40.5% 🟢)0.379s63.90x
💻 LocalNext.js (Turbopack)5.496s6.011s0.515s54.17x
🌐 MongoDBNext.js (Turbopack)6.286s7.013s0.727s54.77x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.430s (-48.7% 🟢)1.006s (-1.7%)0.575s601.00x
💻 LocalNitro0.457s (-53.4% 🟢)1.004s (-8.3% 🟢)0.547s601.06x
🐘 PostgresNitro0.483s (-41.1% 🟢)1.023s (+1.7%)0.540s591.12x
💻 LocalExpress0.509s (-48.3% 🟢)1.004s (-6.7% 🟢)0.495s601.18x
🌐 RedisNext.js (Turbopack)0.617s1.004s0.388s601.43x
🐘 PostgresNext.js (Turbopack)0.697s1.007s0.310s601.62x
💻 LocalNext.js (Turbopack)0.712s1.004s0.292s601.66x
🌐 MongoDBNext.js (Turbopack)0.739s1.006s0.266s601.72x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.027s (-48.0% 🟢)1.586s (-29.8% 🟢)0.559s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.741s (-17.1% 🟢)0.648s521.06x
💻 LocalNitro1.156s (-61.9% 🟢)2.006s (-46.6% 🟢)0.849s451.13x
💻 LocalExpress1.191s (-60.5% 🟢)2.005s (-44.1% 🟢)0.814s451.16x
🌐 RedisNext.js (Turbopack)1.479s2.006s0.527s451.44x
🐘 PostgresNext.js (Turbopack)1.638s2.007s0.369s451.60x
💻 LocalNext.js (Turbopack)1.780s2.027s0.247s451.73x
🌐 MongoDBNext.js (Turbopack)1.811s2.007s0.196s451.76x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.012s (-49.6% 🟢)2.427s (-44.5% 🟢)0.415s501.00x
🐘 PostgresNitro2.035s (-50.4% 🟢)2.457s (-46.6% 🟢)0.422s491.01x
💻 LocalNitro2.638s (-71.6% 🟢)3.007s (-70.0% 🟢)0.369s401.31x
💻 LocalExpress2.802s (-69.6% 🟢)3.084s (-69.2% 🟢)0.283s391.39x
🌐 RedisNext.js (Turbopack)3.003s3.277s0.275s371.49x
🐘 PostgresNext.js (Turbopack)3.192s4.010s0.817s301.59x
💻 LocalNext.js (Turbopack)3.864s4.145s0.282s291.92x
🌐 MongoDBNext.js (Turbopack)4.155s5.011s0.856s242.07x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.172s (-39.1% 🟢)1.006s (~)0.834s601.00x
🐘 PostgresNitro0.192s (-32.2% 🟢)1.006s (~)0.814s601.12x
🐘 PostgresNext.js (Turbopack)0.241s1.006s0.764s601.40x
🌐 RedisNext.js (Turbopack)0.251s1.004s0.753s601.46x
💻 LocalNitro0.432s (-28.6% 🟢)1.004s (-1.7%)0.572s602.51x
💻 LocalExpress0.466s (-16.9% 🟢)1.004s (~)0.539s602.71x
💻 LocalNext.js (Turbopack)0.567s1.004s0.437s603.30x
🌐 MongoDBNext.js (Turbopack)1.041s1.882s0.842s326.05x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.296s (-41.8% 🟢)1.006s (~)0.710s901.00x
🐘 PostgresNitro0.314s (-36.8% 🟢)1.006s (~)0.693s901.06x
🌐 RedisNext.js (Turbopack)0.414s1.004s0.590s901.40x
🐘 PostgresNext.js (Turbopack)0.461s1.006s0.545s901.55x
💻 LocalNitro2.179s (-14.1% 🟢)2.852s (-5.2% 🟢)0.672s327.35x
💻 LocalNext.js (Turbopack)2.182s2.944s0.762s317.36x
💻 LocalExpress2.223s (-11.6% 🟢)2.821s (-6.3% 🟢)0.598s327.50x
🌐 MongoDBNext.js (Turbopack)2.618s3.007s0.389s308.83x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.578s (-29.3% 🟢)1.014s (~)0.436s1191.00x
🐘 PostgresNitro0.641s (-18.9% 🟢)1.006s (~)0.365s1201.11x
🌐 RedisNext.js (Turbopack)0.770s1.004s0.234s1201.33x
🐘 PostgresNext.js (Turbopack)0.957s1.352s0.394s901.65x
🌐 MongoDBNext.js (Turbopack)5.396s6.011s0.615s209.33x
💻 LocalExpress9.835s (-12.1% 🟢)10.444s (-12.5% 🟢)0.609s1217.00x
💻 LocalNitro9.941s (-11.2% 🟢)10.365s (-11.1% 🟢)0.424s1217.19x
💻 LocalNext.js (Turbopack)11.011s11.573s0.562s1119.03x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.127s (+449.7% 🔺)2.001s (+100.3% 🔺)0.001s (-37.5% 🟢)2.009s (+98.6% 🔺)0.882s101.00x
💻 LocalNitro1.128s (+428.0% 🔺)2.005s (+99.6% 🔺)0.011s (-15.2% 🟢)2.018s (+98.1% 🔺)0.890s101.00x
🐘 PostgresNitro1.135s (+453.5% 🔺)2.000s (+100.1% 🔺)0.001s (-6.7% 🟢)2.010s (+98.7% 🔺)0.875s101.01x
💻 LocalExpress1.140s (+472.4% 🔺)2.005s (+99.6% 🔺)0.013s (+3.3%)2.020s (+98.4% 🔺)0.881s101.01x
💻 LocalNext.js (Turbopack)1.190s2.003s0.010s2.017s0.826s101.06x
🐘 PostgresNext.js (Turbopack)1.197s2.001s0.002s2.011s0.815s101.06x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.482s (+135.3% 🔺)2.006s (+99.3% 🔺)0.003s (-10.4% 🟢)2.022s (+97.7% 🔺)0.540s301.00x
🐘 PostgresNitro1.525s (+144.4% 🔺)2.002s (+98.9% 🔺)0.004s (+0.8%)2.026s (+98.2% 🔺)0.501s301.03x
💻 LocalNext.js (Turbopack)1.670s2.009s0.011s2.023s0.352s301.13x
🐘 PostgresNext.js (Turbopack)1.682s2.010s0.004s2.025s0.343s301.13x
💻 LocalExpress1.705s (+125.2% 🔺)2.011s (+95.4% 🔺)0.010s (+8.5% 🔺)2.202s (+111.8% 🔺)0.498s281.15x
💻 LocalNitro1.713s (+104.2% 🔺)2.010s (+98.6% 🔺)0.010s (+4.8%)2.201s (+97.2% 🔺)0.488s281.16x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.622s (-35.2% 🟢)1.013s (-20.7% 🟢)0.000s (+16.9% 🔺)1.028s (-21.3% 🟢)0.406s591.00x
🐘 PostgresNitro0.681s (-29.7% 🟢)1.047s (-16.1% 🟢)0.000s (-58.6% 🟢)1.059s (-15.8% 🟢)0.378s581.09x
🐘 PostgresNext.js (Turbopack)0.817s1.053s0.000s1.068s0.251s571.31x
💻 LocalNitro1.331s (+8.8% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.686s302.14x
💻 LocalExpress1.394s (+13.8% 🔺)2.015s (~)0.000s (+10.0% 🔺)2.017s (~)0.623s302.24x
💻 LocalNext.js (Turbopack)1.437s2.013s0.000s2.016s0.579s302.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.313s (-26.7% 🟢)2.099s (-2.0%)0.000s (-3.4%)2.131s (-2.0%)0.818s291.00x
🐘 PostgresExpress1.349s (-23.9% 🟢)2.145s (-1.5%)0.000s (+Infinity% 🔺)2.161s (-1.7%)0.812s281.03x
🐘 PostgresNext.js (Turbopack)1.689s2.225s0.000s2.273s0.584s271.29x
💻 LocalNext.js (Turbopack)2.563s3.079s0.001s3.084s0.521s201.95x
💻 LocalNitro3.092s (-8.7% 🟢)3.840s (-4.8%)0.000s (-76.6% 🟢)3.844s (-4.8%)0.752s162.35x
💻 LocalExpress3.227s (-6.9% 🟢)4.031s (~)0.000s (-66.7% 🟢)4.033s (~)0.806s152.46x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro14/21
🐘 PostgresExpress19/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres8/21
Nitro🐘 Postgres14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

Comment threadtarballs/scripts/pack.ts Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visual design is nice — featured workflow card, package-manager toggle, live filter, copy buttons, geist-inspired theme. Going to approve since the deployment shape doesn't change and this only affects the static index page.

That said: I'd push back on the hand-rolled HTML-in-JS approach for anything beyond a one-page diff. The new file is 857 lines, and the implementation has some smells that follow naturally from the medium:

  • The inline <script> block (~80 lines) isn't TypeScript-checked. It's inside a template literal in writeIndexHtml, so the compiler treats it as a string. Refactoring (rename a data- attribute, change a property name) won't surface type errors. The TS half references DOM via data- attributes that have to match by convention.
  • Two parallel "render the page" concerns — the pack.ts script handles both bundling-time logic (scanning packages, rewriting deps, packing tarballs) and presentation logic (HTML composition, CSS, client-side JS for PM toggle/filter/copy). Those were independent before this PR; conflating them makes both harder to evolve.
  • renderFeatured/renderRow and the inline applyPm/applyFilter script must stay in sync on attribute names, class names, and the catalog JSON shape. Right now there's a hand-rolled escapeHtml helper and the contract is "data-install-cmd must equal escapedName." Easy to break in a refactor; no tests catch the drift.
  • Hand-rolled escape helpers (escapeHtml) are correct here but would be unnecessary with JSX or a templating library.

A lightweight Vite + React (or Preact for smaller bundle) SPA would clean this up substantially. The shape I'd suggest:

tarballs/
├── package.json # adds vite, react, @vitejs/plugin-react
├── vite.config.ts # static build → public/
├── index.html # entry HTML
├── src/
│ ├── main.tsx # mount React
│ ├── App.tsx # the page (~80 lines instead of ~600 of HTML+CSS+JS)
│ ├── catalog.ts # types for the JSON catalog
│ └── styles.css # the geist-inspired theme
└── scripts/
├── pack.ts # data-only: scans packages, packs tarballs, writes
│ # public/catalog.json with the build context
└── check-tarballs-smoke.mjs

pack.ts becomes ~140 lines (just the bundling logic), vite build produces the static page that fetches catalog.json, the React component reads it on mount and renders. State (active PM, search query) is useState. Build pipeline is pnpm pack → vite build, and Vercel's existing outputDirectory: "public" works unchanged.

DX wins: HMR while iterating on the design, TypeScript checks the JSX and event handlers, can pull in actual geist (or lucide-react) icons via npm instead of inlined SVG, hot-swap component libraries if the design evolves.

If you don't want to take that on now, this is fine as-is — the page works, the design looks good, and the deployment shape is preserved. But suggest filing a follow-up to migrate before adding the next 200 lines of HTML to this file.

Smaller things I noticed

  • getBuildContext uses process.env.VERCEL_GIT_* for SHA/branch/PR. Local builds get only the SHA/branch fallback via git rev-parse. Worth double-checking the labels on chips degrade gracefully when those env vars are missing — looks like they do (early-return paths with if (ctx.commitUrl)), but a quick local pnpm --filter tarballs build test before merge would confirm.
  • The catalog JSON is embedded via <script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>. JSON-inside-HTML-inside-template-literal is two layers of escape (the escapeHtml handles the outer, but the JSON itself can contain </script> if a package description has one — unlikely but possible). Safer to use JSON.stringify(catalog).replace(/</g, '\\u003c') for the embedded JSON, which escapes the dangerous bytes without affecting parseability.
  • formatBytes uses base-2 units (KB = 1024) but labels them as 'KB'/'MB' which by convention suggest base-10. Minor but KiB/MiB would be more precise (or use base-10 with 1000).
  • applyFilter runs on every keystroke against a re-querying Array.from(document.querySelectorAll('.pkg-row')). With ~25 packages this is fine; if the catalog grows substantially, debounce or pre-compute.

Test plan note

The PR description has 8 unchecked test-plan items. They're all manual visual checks (page renders, filter works, copy fires, etc.). Since the Vercel preview URL is the canonical way to validate this, they probably won't be checked off in advance — but worth at least running through them on the preview before merge.

Comment threadtarballs/scripts/pack.ts Outdated
Built ${ctx.commitUrl ? `from <a href="${escapeHtml(ctx.commitUrl)}" target="_blank" rel="noopener">${escapeHtml(ctx.shortSha)}</a>` : `from <code>${escapeHtml(ctx.shortSha)}</code>`} · ${escapeHtml(packages.length.toString())} packages totaling ${escapeHtml(formatBytes(totalSize))}
</footer>
</div>
<script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this line breaks all client-side interactivity.

Verified on the preview deployment with browser automation — every interactive feature is non-functional:

  • ❌ Package-manager toggle (clicking npm/yarn/bun does nothing)
  • ❌ Live filter (typing in search shows all rows)
  • ❌ Copy buttons (no clipboard write, no Copied state)
  • / keyboard shortcut (body keeps focus)

Root cause: escapeHtml(JSON.stringify(catalog)) HTML-encodes the JSON. The serialized catalog ends up in the page as:

<scriptid="catalog" type="application/json">[{&quot;name&quot;:&quot;@workflow/ai&quot;,&quot;escapedName&quot;:&quot;workflow-ai&quot;...

JSON.parse(textContent) then throws on the very first &quot; because &quot; isn't valid JSON syntax. The IIFE bails on its first line and none of the event listeners get attached. Confirmed live:

>JSON.parse(document.getElementById('catalog').textContent)Uncaught SyntaxError: Expectedpropertynameor'}'inJSONatposition2(line1column3)

<script type="application/json"> content is treated as text by the HTML parser — it does NOT need HTML entity escaping. The only sequence that can break out of it is </script> (or </ in some legacy parsers). The narrowest correct fix is:

<scriptid="catalog"type="application/json">${JSON.stringify(catalog).replace(/</g,'\u003c')}</script>

That escapes < bytes to \u003c in the JSON (legal per the spec — JSON allows \uXXXX escapes for any character), so </script> can't appear in the text and the JSON parses cleanly.

This was the third bullet in my "smaller things I noticed" section above — turns out it's not theoretical, it's the live bug breaking everything client-side. Should not merge until the page works in a real browser.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3df48a7. Went with your replace(/</g, '\u003c') over the bot's </-only escape since it also covers a stray < inside, e.g., a package description. Verified locally that the embedded JSON parses cleanly out of the rendered HTML; preview deploy will validate the four interactive features end-to-end.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating my prior review to REQUEST_CHANGES. Ran the visual test plan against the preview deployment with browser automation and discovered that all client-side interactivity is broken — see the inline comment on pack.ts:696 for the root cause and fix.

The escapeHtml(JSON.stringify(catalog)) call HTML-encodes every " in the JSON to &quot;, so JSON.parse(textContent) throws on its first character. The IIFE bails immediately and none of the event listeners (PM toggle, search filter, copy buttons, / shortcut) get attached.

Test plan results from the preview at https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/:

  • ✅ Page renders correctly in light mode (visual layout, chips, featured card, package list)
  • workflow package appears as the featured card at the top
  • ✅ SHA / branch / timestamp / package count chips are present
  • Package-manager toggle doesn't update install commandsaria-selected stays on pnpm regardless of clicks
  • Live filter doesn't filter — typing in the search input shows all rows unchanged
  • Copy buttons don't fire — no Copied state, no clipboard write
  • / keyboard shortcut doesn't focus search — body keeps focus
  • (couldn't test direct download because earlier broken state interfered, but the <a download> is plain HTML and probably works)

So the page that ships looks correct visually but is functionally a static catalog — the toggle, filter, and copy buttons are all dead UI.

Quick fix at the call site (one line). After fix, all four interactive features should work. Worth re-testing on the next preview deployment.

The other points from my prior review (Vite + React refactor suggestion, the smaller TS-checkable concerns, KB vs KiB nit) all still stand and are non-blocking. Just this one issue blocks merging.

`escapeHtml(JSON.stringify(catalog))` was HTML-encoding every quote in
the embedded catalog JSON to `&quot;`, so `JSON.parse(textContent)` threw
on the first character and the IIFE bailed before attaching any event
listeners — package-manager toggle, search filter, copy buttons, and the
`/` shortcut were all dead UI on the deployed page.
`<script type="application/json">` content is treated as text by the HTML
parser; the only sequence that can break out is `</script>` (or `</`
in legacy parsers). Replace `<` with the JSON `<` escape, which is
legal per the JSON spec and prevents the breakout without needing entity
encoding.
Also switch `formatBytes` from `KB`/`MB` to `KiB`/`MiB` since the
divisor is 1024.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment threadtarballs/scripts/pack.ts Outdated
The smoke check broke in CI: `'workflow' tarball only has 0 files`.
Root cause is that `tar -tvzf` emits a different verbose layout on GNU
tar (Linux, what CI runs) vs BSD tar (macOS, where I tested locally) —
the parser only matched the BSD column ordering, so on Linux every line
was rejected and `fileCount` came out as 0.
Replace the shell-out with a small in-process tar reader using
`zlib.gunzipSync` + manual 512-byte block walk. ustar headers are
trivially structured (name at offset 0, octal size at 124, typeflag at
156, ustar prefix at 345). We emit regular files only (`typeflag` `0`
or NUL) and consume but skip pax extended headers (`x`/`g`) and GNU
long-name entries (`L`). Result is identical on every platform.
Verified locally: 206 files / 998413 bytes for `workflow.tgz` matches
`tar -tvzf` exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pranaygpand others added 3 commits May 4, 2026 19:31
The previous "What's inside?" view crammed nested directory groups,
proportional bars, and per-group file lists into a `<details>` inside
an already-narrow row. It was hard to read and harder to compare.
Replace it with the layout packagephobia uses on its result page:
- Two large headline metric tiles (Publish size / Unpacked size)
with a big bold value, smaller unit, and small uppercase label.
Modeled directly on packagephobia's `Stats` component but using
our existing CSS variables so it tracks light/dark theme.
- A single sortable file table beneath. Default is size-descending so
the contributors to package size are immediately visible. Click a
header to flip direction or switch sort key. Sticky header keeps
the columns visible inside the scrollable region.
Drop the `groupByTopLevel`, `ContentsGroup`, and bar-chart styles —
they were the source of the "hard to use" feedback and don't add
information that the flat sortable table doesn't already convey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the old static tarballs index with a Vite/Preact single-page UI that reads a generated catalog.json, surfaces richer build/package metadata, and adds interactive install/copy/filter/download affordances for preview tarballs.

Changes:

  • Adds a Vite + Preact frontend for the tarballs index, including featured package rendering, filtering, package-manager toggles, and package contents breakdowns.
  • Extends scripts/pack.ts to emit catalog.json with build metadata, tarball sizes, unpacked sizes, and per-file listings.
  • Updates build/smoke-test plumbing so the tarballs deployment now publishes bundled SPA assets plus the generated catalog.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tarballs/vite.config.tsConfigures Vite to build the SPA into public/ without deleting tarballs.
tarballs/turbo.jsonExpands build outputs to include SPA assets and catalog.json.
tarballs/tsconfig.jsonAdds TS config for the new frontend/build files.
tarballs/src/styles.cssAdds the full styling/theme for the redesigned index UI.
tarballs/src/main.tsxBoots the SPA and loads catalog.json.
tarballs/src/icons.tsxAdds inline SVG icons used across the new UI.
tarballs/src/catalog.tsDefines shared catalog/install-command helpers for the frontend.
tarballs/src/app.tsxImplements the main tarballs UI, interactions, and package contents tables.
tarballs/scripts/pack.tsGenerates tarballs plus the new metadata-rich catalog.json.
tarballs/scripts/check-tarballs-smoke.mjsAdds smoke coverage for catalog.json.
tarballs/package.jsonSwitches build to pack.ts && vite build and adds frontend deps/scripts.
tarballs/index.htmlAdds the SPA entry HTML shell.
pnpm-lock.yamlLocks newly added frontend/tooling dependencies.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadtarballs/package.json Outdated
"scripts": {
"build": "node scripts/pack.ts",
"build": "node scripts/pack.ts && vite build",
"dev": "vite",

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. The new dev chains node scripts/pack.ts && vite, and I restructured the build layout to vite's conventional shape so it actually works in dev: public/ is a real vite public dir (pack writes tarballs + catalog.json there, vite serves them at root in dev), and dist/ is the production build output (set as Vercel's outputDirectory).

Comment threadtarballs/src/app.tsx
<SearchIcon />
<input
ref={searchRef}
type="search"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added aria-label="Filter packages" to the input.

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +178 to +185
<div class="pm-tabs" role="tablist" aria-label="Package manager">
{options.map((opt) => (
<button
key={opt}
type="button"
class="pm-tab"
role="tab"
aria-selected={value === opt}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Dropped role="tablist"/role="tab"/aria-selected and made these plain buttons with aria-pressed. Toggle buttons are the honest representation since we never implemented arrow-key roving focus. Each button also has an explicit aria-label (Show install commands for pnpm etc.).

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +428 to +440
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} finally {
ta.remove();
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Refactored to a writeToClipboard helper that returns whether the write actually succeeded; the button only flips to Copied on success. If both navigator.clipboard.writeText and the execCommand fallback fail, it briefly shows a red Failed state instead.

Comment threadtarballs/src/app.tsx Outdated
<code class="pkg-cmd">{cmd}</code>
<div class="pkg-actions">
<CopyButton text={cmd} variant="icon" />
<a class="icon-btn" href={pkg.url} download aria-label="Download">

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added an accessibleName prop to CopyButton and pass Copy install command for <pkg.name> from each row + the featured card. Same treatment on the download <a> link (Download <pkg.name> tarball).

Comment threadtarballs/src/main.tsx Outdated
if (!root) throw new Error('No #app root element');

try {
const res = await fetch('/catalog.json', { cache: 'no-store' });

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345 — dropped cache: 'no-store'. Each preview's URL is unique so the browser cache is the right thing to use.

- main.tsx: drop `cache: 'no-store'` from the catalog fetch. Each
tarballs deployment is immutable per commit, so HTTP caching is
appropriate; forcing no-store made every visit re-download the full
catalog (which now includes per-package file lists).
- app.tsx (search input): add `aria-label="Filter packages"`. The
visible label only contained an icon and placeholder, so screen
readers had no name for the control.
- app.tsx (PmTabs): replace `role="tablist"` / `role="tab"` /
`aria-selected` with plain buttons that use `aria-pressed`. The
ARIA tab pattern requires arrow-key roving focus we never wired
up; toggle buttons are the honest representation. Each button
also gets an explicit `aria-label`.
- app.tsx (row buttons): include the package name in the accessible
label of every per-row copy/download button (and on the featured
card too), so the screen reader buttons/links list distinguishes
them. Added an `accessibleName` prop to `CopyButton`.
- app.tsx (CopyButton): only flip to the "Copied" state when the
write actually succeeded. Both the modern `navigator.clipboard`
path and the `execCommand` fallback can fail; the new
`writeToClipboard` helper returns success and the button shows a
short "Failed" state if both paths fail.
# Make `pnpm dev` work from a clean checkout
The previous `dev: vite` couldn't actually serve the page because
`/catalog.json` 404s and the SPA boots into the error fallback.
Restructure the build layout to vite's conventional shape:
- `public/` is now a true vite public dir — pack writes tarballs and
catalog.json there. In dev, vite serves these at the root.
- `dist/` is the production build output (vite copies public/ into it
and adds index.html + assets/).
- `vercel.json#outputDirectory` switches from `public` → `dist`.
- `turbo.json` outputs updated to match.
- `dev` chains pack before vite so the catalog exists when the dev
server starts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​preact/​preset-vite@​2.10.59910010090100
Addednpm/​preact@​10.29.110010010094100

View full report

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pranay took the Vite + Preact refactor recommendation from my prior review and ran with it. The new shape matches what I sketched almost exactly:

tarballs/
├── index.html # 20-line entry
├── vite.config.ts # 16-line config
├── tsconfig.json # strict, jsxImportSource: preact
├── scripts/
│ ├── pack.ts # 317 lines, data-only — packs tarballs, writes catalog.json
│ └── check-tarballs-smoke.mjs # now also validates catalog.json shape
└── src/
├── main.tsx # 26 lines — fetch + mount
├── app.tsx # 550 lines — typed Preact JSX
├── catalog.ts # types + small helpers
├── icons.tsx # 8 inline SVG icons
└── styles.css # 794 lines, geist-inspired theme

Bundle ships as dist/assets/index-*.js at 26.25 kB (9.69 kB gzipped) — small for a Preact SPA with sortable tables, copy-buttons, search, and a PM toggle. CSS is 11.19 kB / 2.73 kB gzipped.

What I verified

  • Local build works: pnpm turbo build --filter=tarballs runs pack.ts then vite build, producing 26 tarballs in public/, a catalog.json matching the typed shape in src/catalog.ts, and a dist/ ready to deploy.
  • TypeScript is clean: pnpm exec tsc --noEmit from tarballs/ passes with the strict config (strict, noUnusedLocals, noUnusedParameters, isolatedModules).
  • Deployed preview serves correctly: https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/catalog.json returns the expected payload with build context (sha, branch, PR number, GitHub-linked commit/branch/PR URLs) and 26 packages with full file listings + sizes. Each package's url is the immutable per-deployment hash URL (e.g. https://workflow-tarballs-ch8zhxmen.labs.vercel.dev/workflow-ai.tgz), so the catalog is stable even if visited via the git-branch URL.
  • JS bundle contains real Preact: read the bundle, confirmed the App tree renders with hooks (useState, useEffect, useMemo, useRef), addEventListener('keydown', …) for the / shortcut, navigator.clipboard.writeText + execCommand('copy') fallback for CopyButton, and the data-copied / data-failed state machine.
  • Smoke check is stronger now: in addition to verifying tarballs are gzip-magic-prefixed and the index page responds with text/html, it now fetches catalog.json and asserts the workflow package has a non-trivial fileCount — a useful diagnostic for "packages weren't built before pack."

Concerns from my prior review — all addressed

  1. The hand-rolled HTML-in-JS approach — gone. JSX is TypeScript-checked end to end (event handlers, prop types, catalog shape).
  2. Two parallel render concerns merged into one file — split clean. pack.ts is now data-only (~317 lines, all bundling/tar-reading). The presentation layer is in src/.
  3. renderRow / applyPm / applyFilter had to stay in sync via string conventions — gone. State lives in React-style hooks, props are typed.
  4. Hand-rolled escapeHtml — gone. JSX handles escaping.
  5. escapeHtml(JSON.stringify(catalog)) breaking client interactivity — moot. The catalog is a separate /catalog.json static file fetched at runtime, so there's no inline-JSON encoding question.
  6. KB/MB labels with base-2 math — fixed. formatBytes now emits B/KiB/MiB.

Copilot review items — all addressed in 1c48345e6

  • a11y: aria-label="Filter packages" on search input; PM tabs use plain buttons with aria-pressed (no half-implemented role="tablist"); per-row copy buttons get accessibleName="Copy install command for ${pkg.name}" instead of a generic label.
  • Robustness: writeToClipboard returns a boolean so failed writes show "Failed" rather than a misleading "Copied" success state.
  • DX: dev script chains pack.ts && vite; dropped cache: 'no-store' since the catalog URL is per-deployment-immutable.

Smaller improvements landed along the way

  • Tar reader rewrite (6ae7736): replaced tar -tvzf shell-out with an in-process gunzip + 512-byte block walker. Handles GNU long-name (L) entries and pax headers, and works identically on macOS BSD tar and Linux GNU tar. Sorting files by descending size before emitting is a nice touch for the per-package "what's inside" view.
  • PackagePhobia-style SizeStats widget: two big tiles (publish size + unpacked size) with split value/unit rendering. Looks good and reads better than a one-liner.
  • FileTable with sortable columns: ARIA aria-sort on <th>, indicator arrows, click-to-toggle direction. Strips the package/ prefix from displayed paths.

Tiny things — non-blocking

  • pack.ts:117: updateDeps builds rewrite URLs as https://${process.env.VERCEL_URL}/${escapedName}.tgz directly, while baseUrl (defined at line 97) uses the VERCEL_URL ? https://… : '' fallback. Local builds without VERCEL_URL produce package.json deps like https://undefined/.... Pre-existing on main, not introduced by this PR. Not a real-world issue (nobody installs from a local pack output) but noting for a follow-up cleanup if you want consistency.
  • catalog.ts and pack.ts duplicate the same TS interfaces (TarballFile, PackedPackage, BuildContext, Catalog). Drift risk is low since they're both in the same package, but if you want to eliminate it, catalog.ts could be the single source of truth and pack.ts could import type from it. Fine to leave.
  • SizeStats is rendered for every package in the list, not just the featured one — but it's behind a <details> so it doesn't blow up initial render. The 26 expanded <details> plus the FileTable for each would render a lot of DOM if expanded all at once; in practice, users open one or two. Not blocking.

Verdict: approve

Approving and clearing my prior CHANGES_REQUESTED. The redesign hit every concern from my last two reviews plus the Copilot feedback, the build pipeline is cleaner than what was there before, and the deployment shape is preserved (public/<escaped-name>.tgz URLs unchanged, smoke checks pass). Nice work on the followups.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

@v0@vercel can you make a PR to address the remaining follow ups from @TooTallNate's review comments that we didn't get to before merge of this one

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-stableCherry-pick this PR to the stable branch when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

tarballs: redesign preview tarballs index page - #1911

Merged
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements
May 4, 2026
Merged

tarballs: redesign preview tarballs index page#1911
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Summary

Reworks the static index page generated by tarballs/scripts/pack.ts (deployed at https://workflow-tarballs.labs.vercel.dev/) to be more useful and pleasant to look at.

What's new

  • Featured workflow package at the top with a large install command, copy button, and direct tarball download — the rest of the packages are secondary.
  • Top-of-page metadata chips: short SHA (linked to commit on GitHub), branch (linked), PR number (linked), build timestamp, package count + total size.
  • Collapsible "What is this?" explainer describing how the version + dep rewriting works.
  • Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the install command shown for every row in place.
  • Live filter input over the package list, with / keyboard shortcut to focus.
  • Per-row install command, copy button, and direct download link for every package.
  • Geist-inspired theme with proper dark/light support driven by system preference.

Implementation notes

  • Pack step now records the produced tarball size via fs.stat after pnpm pack so the page can show human-readable sizes.
  • The page reads VERCEL_GIT_* env vars for commit / branch / PR links and falls back to local git rev-parse for branch when running off-Vercel.
  • All packages still get an entry, but workflow is filtered out of the bottom list and rendered separately.
  • No changes to the deployment shape — public/<escaped-name>.tgz URLs are unchanged, so existing usage keeps working.

The Vercel preview for this PR will deploy and serve the new page so you can see it live.

Test plan

  • Open the Vercel preview deployment and verify the page renders correctly in light and dark mode
  • Confirm the workflow package appears as the featured card at the top
  • Confirm SHA / branch / PR chips are present and linked
  • Toggle pnpm / npm / yarn / bun and confirm install commands update everywhere
  • Filter the package list (and try the / shortcut)
  • Click copy on a row and on the featured card
  • Click download on a row and verify the tarball downloads
  • Smoke-install the featured tarball into a fresh project and confirm it still works (pnpm i https://<deployment>/workflow.tgz)

🤖 Generated with Claude Code

Rebuild the static index page produced by `tarballs/scripts/pack.ts`:
- Featured `workflow` package up top with prominent install command,
copy button, and direct tarball download
- Top-of-page metadata chips: short SHA (linked to commit), branch,
PR number, build timestamp, package count + total size
- Collapsible "What is this?" explainer
- Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the
install command for every row in place
- Live filter input over the rest of the package list (with `/` shortcut)
- Per-row install command, copy button, and direct download
- Modern dark/light theme with system preference, Geist-inspired styling
Also captures tarball size during pack and renders human-readable byte counts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented May 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1c48345

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-nextjs-workflow-webpackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-astro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-express-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-fastify-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-hono-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nitro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nuxt-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-sveltekit-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-tanstack-start-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-vite-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-swc-playgroundReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-tarballsReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-webReadyReadyPreview, CommentMay 4, 2026 11:41am
1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
workflow-docsSkippedSkippedMay 4, 2026 11:41am

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other5100176686
Total4338010525390

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025
✅ e2e-vercel-prod-tanstack-start72026

📋 View full workflow run

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.031s (-30.2% 🟢)1.005s (~)0.974s101.00x
💻 LocalNitro0.032s (-24.8% 🟢)1.005s (~)0.973s101.05x
🐘 PostgresExpress0.043s (-26.4% 🟢)1.010s (~)0.967s101.38x
💻 LocalNext.js (Turbopack)0.047s1.005s0.958s101.51x
🐘 PostgresNitro0.052s (-45.6% 🟢)1.012s (-3.0%)0.960s101.68x
🌐 RedisNext.js (Turbopack)0.059s1.005s0.947s101.89x
🐘 PostgresNext.js (Turbopack)0.059s1.011s0.952s101.91x
🌐 MongoDBNext.js (Turbopack)0.100s1.009s0.910s103.23x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.068s (-5.6% 🟢)2.006s (~)0.938s101.00x
💻 LocalExpress1.074s (-4.6%)2.006s (~)0.932s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.927s101.01x
🐘 PostgresNitro1.088s (-4.6%)2.010s (~)0.922s101.02x
💻 LocalNext.js (Turbopack)1.108s2.006s0.898s101.04x
🌐 RedisNext.js (Turbopack)1.112s2.007s0.895s101.04x
🐘 PostgresNext.js (Turbopack)1.117s2.009s0.892s101.05x
🌐 MongoDBNext.js (Turbopack)1.166s2.009s0.843s101.09x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.387s (-5.1% 🟢)11.022s (~)0.634s31.00x
💻 LocalExpress10.405s (-4.7%)11.023s (~)0.618s31.00x
🐘 PostgresExpress10.415s (-5.0% 🟢)11.012s (~)0.597s31.00x
🐘 PostgresNitro10.432s (-4.0%)11.018s (~)0.585s31.00x
🌐 RedisNext.js (Turbopack)10.634s11.023s0.389s31.02x
💻 LocalNext.js (Turbopack)10.690s11.023s0.333s31.03x
🐘 PostgresNext.js (Turbopack)10.716s11.017s0.301s31.03x
🌐 MongoDBNext.js (Turbopack)10.822s11.017s0.195s31.04x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.426s (-10.9% 🟢)14.027s (-12.5% 🟢)0.600s51.00x
🐘 PostgresExpress13.463s (-7.7% 🟢)14.021s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.480s (-10.0% 🟢)14.028s (-6.7% 🟢)0.548s51.00x
🐘 PostgresNitro13.544s (-7.2% 🟢)14.019s (-6.7% 🟢)0.475s51.01x
💻 LocalNext.js (Turbopack)14.031s15.028s0.997s41.05x
🌐 RedisNext.js (Turbopack)14.036s14.628s0.592s51.05x
🐘 PostgresNext.js (Turbopack)14.130s15.022s0.892s41.05x
🌐 MongoDBNext.js (Turbopack)14.245s15.023s0.778s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.809s (-29.6% 🟢)12.022s (-29.4% 🟢)0.214s81.00x
🐘 PostgresNitro11.928s (-14.6% 🟢)12.017s (-16.0% 🟢)0.089s81.01x
🐘 PostgresExpress11.967s (-14.6% 🟢)12.265s (-16.0% 🟢)0.298s81.01x
💻 LocalExpress12.248s (-26.2% 🟢)12.774s (-25.0% 🟢)0.526s81.04x
💻 LocalNext.js (Turbopack)13.061s13.737s0.677s71.11x
🌐 RedisNext.js (Turbopack)13.217s13.883s0.666s71.12x
🌐 MongoDBNext.js (Turbopack)13.255s14.019s0.764s71.12x
🐘 PostgresNext.js (Turbopack)13.304s14.021s0.718s71.13x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.141s (-9.4% 🟢)2.006s (~)0.865s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.007s (~)0.861s151.00x
💻 LocalNitro1.171s (-28.2% 🟢)2.006s (-3.3%)0.835s151.03x
💻 LocalExpress1.174s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.217s2.007s0.790s151.07x
🌐 RedisNext.js (Turbopack)1.240s2.006s0.766s151.09x
💻 LocalNext.js (Turbopack)1.289s2.006s0.717s151.13x
🌐 MongoDBNext.js (Turbopack)2.035s2.826s0.791s111.78x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-49.1% 🟢)2.007s (-33.3% 🟢)0.805s151.00x
🐘 PostgresNitro1.229s (-47.7% 🟢)2.007s (-33.3% 🟢)0.778s151.02x
🐘 PostgresNext.js (Turbopack)1.358s2.008s0.650s151.13x
💻 LocalNitro1.685s (-46.4% 🟢)2.006s (-48.4% 🟢)0.320s151.40x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.299s151.42x
💻 LocalNext.js (Turbopack)1.858s2.292s0.434s141.55x
🌐 RedisNext.js (Turbopack)2.337s3.008s0.671s101.95x
🌐 MongoDBNext.js (Turbopack)3.568s4.009s0.441s82.97x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.305s (-62.6% 🟢)2.007s (-50.0% 🟢)0.702s151.00x
🐘 PostgresNitro1.383s (-60.3% 🟢)2.007s (-49.9% 🟢)0.624s151.06x
🐘 PostgresNext.js (Turbopack)1.629s2.007s0.378s151.25x
🌐 RedisNext.js (Turbopack)3.602s4.010s0.408s82.76x
💻 LocalNext.js (Turbopack)4.383s5.011s0.627s63.36x
💻 LocalExpress4.397s (-47.3% 🟢)5.013s (-44.5% 🟢)0.616s63.37x
💻 LocalNitro4.571s (-45.2% 🟢)5.011s (-44.4% 🟢)0.440s63.50x
🌐 MongoDBNext.js (Turbopack)6.273s7.013s0.739s54.81x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.139s (-9.4% 🟢)2.009s (~)0.869s151.00x
🐘 PostgresNitro1.168s (-7.1% 🟢)2.009s (~)0.840s151.03x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.773s151.08x
🐘 PostgresNext.js (Turbopack)1.248s2.007s0.760s151.10x
💻 LocalNext.js (Turbopack)1.321s2.005s0.684s151.16x
💻 LocalExpress1.352s (-28.6% 🟢)2.006s (-15.1% 🟢)0.654s151.19x
💻 LocalNitro1.379s (-26.1% 🟢)2.006s (-14.3% 🟢)0.627s151.21x
🌐 MongoDBNext.js (Turbopack)2.026s2.735s0.709s111.78x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-48.3% 🟢)2.008s (-33.3% 🟢)0.797s151.00x
🐘 PostgresNitro1.232s (-47.3% 🟢)2.008s (-33.3% 🟢)0.776s151.02x
🐘 PostgresNext.js (Turbopack)1.368s2.008s0.640s151.13x
💻 LocalExpress1.838s (-41.3% 🟢)2.075s (-44.8% 🟢)0.238s151.52x
💻 LocalNitro1.954s (-36.3% 🟢)2.469s (-36.5% 🟢)0.515s131.61x
💻 LocalNext.js (Turbopack)2.083s2.826s0.743s111.72x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.94x
🌐 MongoDBNext.js (Turbopack)3.547s4.008s0.461s82.93x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.318s (-62.3% 🟢)2.007s (-50.0% 🟢)0.690s151.00x
🐘 PostgresNitro1.379s (-60.4% 🟢)2.007s (-49.9% 🟢)0.628s151.05x
🐘 PostgresNext.js (Turbopack)1.718s2.075s0.357s151.30x
🌐 RedisNext.js (Turbopack)3.595s4.010s0.415s82.73x
💻 LocalNitro4.877s (-46.7% 🟢)5.516s (-45.0% 🟢)0.639s63.70x
💻 LocalExpress5.136s (-41.6% 🟢)5.515s (-40.5% 🟢)0.379s63.90x
💻 LocalNext.js (Turbopack)5.496s6.011s0.515s54.17x
🌐 MongoDBNext.js (Turbopack)6.286s7.013s0.727s54.77x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.430s (-48.7% 🟢)1.006s (-1.7%)0.575s601.00x
💻 LocalNitro0.457s (-53.4% 🟢)1.004s (-8.3% 🟢)0.547s601.06x
🐘 PostgresNitro0.483s (-41.1% 🟢)1.023s (+1.7%)0.540s591.12x
💻 LocalExpress0.509s (-48.3% 🟢)1.004s (-6.7% 🟢)0.495s601.18x
🌐 RedisNext.js (Turbopack)0.617s1.004s0.388s601.43x
🐘 PostgresNext.js (Turbopack)0.697s1.007s0.310s601.62x
💻 LocalNext.js (Turbopack)0.712s1.004s0.292s601.66x
🌐 MongoDBNext.js (Turbopack)0.739s1.006s0.266s601.72x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.027s (-48.0% 🟢)1.586s (-29.8% 🟢)0.559s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.741s (-17.1% 🟢)0.648s521.06x
💻 LocalNitro1.156s (-61.9% 🟢)2.006s (-46.6% 🟢)0.849s451.13x
💻 LocalExpress1.191s (-60.5% 🟢)2.005s (-44.1% 🟢)0.814s451.16x
🌐 RedisNext.js (Turbopack)1.479s2.006s0.527s451.44x
🐘 PostgresNext.js (Turbopack)1.638s2.007s0.369s451.60x
💻 LocalNext.js (Turbopack)1.780s2.027s0.247s451.73x
🌐 MongoDBNext.js (Turbopack)1.811s2.007s0.196s451.76x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.012s (-49.6% 🟢)2.427s (-44.5% 🟢)0.415s501.00x
🐘 PostgresNitro2.035s (-50.4% 🟢)2.457s (-46.6% 🟢)0.422s491.01x
💻 LocalNitro2.638s (-71.6% 🟢)3.007s (-70.0% 🟢)0.369s401.31x
💻 LocalExpress2.802s (-69.6% 🟢)3.084s (-69.2% 🟢)0.283s391.39x
🌐 RedisNext.js (Turbopack)3.003s3.277s0.275s371.49x
🐘 PostgresNext.js (Turbopack)3.192s4.010s0.817s301.59x
💻 LocalNext.js (Turbopack)3.864s4.145s0.282s291.92x
🌐 MongoDBNext.js (Turbopack)4.155s5.011s0.856s242.07x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.172s (-39.1% 🟢)1.006s (~)0.834s601.00x
🐘 PostgresNitro0.192s (-32.2% 🟢)1.006s (~)0.814s601.12x
🐘 PostgresNext.js (Turbopack)0.241s1.006s0.764s601.40x
🌐 RedisNext.js (Turbopack)0.251s1.004s0.753s601.46x
💻 LocalNitro0.432s (-28.6% 🟢)1.004s (-1.7%)0.572s602.51x
💻 LocalExpress0.466s (-16.9% 🟢)1.004s (~)0.539s602.71x
💻 LocalNext.js (Turbopack)0.567s1.004s0.437s603.30x
🌐 MongoDBNext.js (Turbopack)1.041s1.882s0.842s326.05x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.296s (-41.8% 🟢)1.006s (~)0.710s901.00x
🐘 PostgresNitro0.314s (-36.8% 🟢)1.006s (~)0.693s901.06x
🌐 RedisNext.js (Turbopack)0.414s1.004s0.590s901.40x
🐘 PostgresNext.js (Turbopack)0.461s1.006s0.545s901.55x
💻 LocalNitro2.179s (-14.1% 🟢)2.852s (-5.2% 🟢)0.672s327.35x
💻 LocalNext.js (Turbopack)2.182s2.944s0.762s317.36x
💻 LocalExpress2.223s (-11.6% 🟢)2.821s (-6.3% 🟢)0.598s327.50x
🌐 MongoDBNext.js (Turbopack)2.618s3.007s0.389s308.83x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.578s (-29.3% 🟢)1.014s (~)0.436s1191.00x
🐘 PostgresNitro0.641s (-18.9% 🟢)1.006s (~)0.365s1201.11x
🌐 RedisNext.js (Turbopack)0.770s1.004s0.234s1201.33x
🐘 PostgresNext.js (Turbopack)0.957s1.352s0.394s901.65x
🌐 MongoDBNext.js (Turbopack)5.396s6.011s0.615s209.33x
💻 LocalExpress9.835s (-12.1% 🟢)10.444s (-12.5% 🟢)0.609s1217.00x
💻 LocalNitro9.941s (-11.2% 🟢)10.365s (-11.1% 🟢)0.424s1217.19x
💻 LocalNext.js (Turbopack)11.011s11.573s0.562s1119.03x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.127s (+449.7% 🔺)2.001s (+100.3% 🔺)0.001s (-37.5% 🟢)2.009s (+98.6% 🔺)0.882s101.00x
💻 LocalNitro1.128s (+428.0% 🔺)2.005s (+99.6% 🔺)0.011s (-15.2% 🟢)2.018s (+98.1% 🔺)0.890s101.00x
🐘 PostgresNitro1.135s (+453.5% 🔺)2.000s (+100.1% 🔺)0.001s (-6.7% 🟢)2.010s (+98.7% 🔺)0.875s101.01x
💻 LocalExpress1.140s (+472.4% 🔺)2.005s (+99.6% 🔺)0.013s (+3.3%)2.020s (+98.4% 🔺)0.881s101.01x
💻 LocalNext.js (Turbopack)1.190s2.003s0.010s2.017s0.826s101.06x
🐘 PostgresNext.js (Turbopack)1.197s2.001s0.002s2.011s0.815s101.06x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.482s (+135.3% 🔺)2.006s (+99.3% 🔺)0.003s (-10.4% 🟢)2.022s (+97.7% 🔺)0.540s301.00x
🐘 PostgresNitro1.525s (+144.4% 🔺)2.002s (+98.9% 🔺)0.004s (+0.8%)2.026s (+98.2% 🔺)0.501s301.03x
💻 LocalNext.js (Turbopack)1.670s2.009s0.011s2.023s0.352s301.13x
🐘 PostgresNext.js (Turbopack)1.682s2.010s0.004s2.025s0.343s301.13x
💻 LocalExpress1.705s (+125.2% 🔺)2.011s (+95.4% 🔺)0.010s (+8.5% 🔺)2.202s (+111.8% 🔺)0.498s281.15x
💻 LocalNitro1.713s (+104.2% 🔺)2.010s (+98.6% 🔺)0.010s (+4.8%)2.201s (+97.2% 🔺)0.488s281.16x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.622s (-35.2% 🟢)1.013s (-20.7% 🟢)0.000s (+16.9% 🔺)1.028s (-21.3% 🟢)0.406s591.00x
🐘 PostgresNitro0.681s (-29.7% 🟢)1.047s (-16.1% 🟢)0.000s (-58.6% 🟢)1.059s (-15.8% 🟢)0.378s581.09x
🐘 PostgresNext.js (Turbopack)0.817s1.053s0.000s1.068s0.251s571.31x
💻 LocalNitro1.331s (+8.8% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.686s302.14x
💻 LocalExpress1.394s (+13.8% 🔺)2.015s (~)0.000s (+10.0% 🔺)2.017s (~)0.623s302.24x
💻 LocalNext.js (Turbopack)1.437s2.013s0.000s2.016s0.579s302.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.313s (-26.7% 🟢)2.099s (-2.0%)0.000s (-3.4%)2.131s (-2.0%)0.818s291.00x
🐘 PostgresExpress1.349s (-23.9% 🟢)2.145s (-1.5%)0.000s (+Infinity% 🔺)2.161s (-1.7%)0.812s281.03x
🐘 PostgresNext.js (Turbopack)1.689s2.225s0.000s2.273s0.584s271.29x
💻 LocalNext.js (Turbopack)2.563s3.079s0.001s3.084s0.521s201.95x
💻 LocalNitro3.092s (-8.7% 🟢)3.840s (-4.8%)0.000s (-76.6% 🟢)3.844s (-4.8%)0.752s162.35x
💻 LocalExpress3.227s (-6.9% 🟢)4.031s (~)0.000s (-66.7% 🟢)4.033s (~)0.806s152.46x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro14/21
🐘 PostgresExpress19/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres8/21
Nitro🐘 Postgres14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

Comment threadtarballs/scripts/pack.ts Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visual design is nice — featured workflow card, package-manager toggle, live filter, copy buttons, geist-inspired theme. Going to approve since the deployment shape doesn't change and this only affects the static index page.

That said: I'd push back on the hand-rolled HTML-in-JS approach for anything beyond a one-page diff. The new file is 857 lines, and the implementation has some smells that follow naturally from the medium:

  • The inline <script> block (~80 lines) isn't TypeScript-checked. It's inside a template literal in writeIndexHtml, so the compiler treats it as a string. Refactoring (rename a data- attribute, change a property name) won't surface type errors. The TS half references DOM via data- attributes that have to match by convention.
  • Two parallel "render the page" concerns — the pack.ts script handles both bundling-time logic (scanning packages, rewriting deps, packing tarballs) and presentation logic (HTML composition, CSS, client-side JS for PM toggle/filter/copy). Those were independent before this PR; conflating them makes both harder to evolve.
  • renderFeatured/renderRow and the inline applyPm/applyFilter script must stay in sync on attribute names, class names, and the catalog JSON shape. Right now there's a hand-rolled escapeHtml helper and the contract is "data-install-cmd must equal escapedName." Easy to break in a refactor; no tests catch the drift.
  • Hand-rolled escape helpers (escapeHtml) are correct here but would be unnecessary with JSX or a templating library.

A lightweight Vite + React (or Preact for smaller bundle) SPA would clean this up substantially. The shape I'd suggest:

tarballs/
├── package.json # adds vite, react, @vitejs/plugin-react
├── vite.config.ts # static build → public/
├── index.html # entry HTML
├── src/
│ ├── main.tsx # mount React
│ ├── App.tsx # the page (~80 lines instead of ~600 of HTML+CSS+JS)
│ ├── catalog.ts # types for the JSON catalog
│ └── styles.css # the geist-inspired theme
└── scripts/
├── pack.ts # data-only: scans packages, packs tarballs, writes
│ # public/catalog.json with the build context
└── check-tarballs-smoke.mjs

pack.ts becomes ~140 lines (just the bundling logic), vite build produces the static page that fetches catalog.json, the React component reads it on mount and renders. State (active PM, search query) is useState. Build pipeline is pnpm pack → vite build, and Vercel's existing outputDirectory: "public" works unchanged.

DX wins: HMR while iterating on the design, TypeScript checks the JSX and event handlers, can pull in actual geist (or lucide-react) icons via npm instead of inlined SVG, hot-swap component libraries if the design evolves.

If you don't want to take that on now, this is fine as-is — the page works, the design looks good, and the deployment shape is preserved. But suggest filing a follow-up to migrate before adding the next 200 lines of HTML to this file.

Smaller things I noticed

  • getBuildContext uses process.env.VERCEL_GIT_* for SHA/branch/PR. Local builds get only the SHA/branch fallback via git rev-parse. Worth double-checking the labels on chips degrade gracefully when those env vars are missing — looks like they do (early-return paths with if (ctx.commitUrl)), but a quick local pnpm --filter tarballs build test before merge would confirm.
  • The catalog JSON is embedded via <script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>. JSON-inside-HTML-inside-template-literal is two layers of escape (the escapeHtml handles the outer, but the JSON itself can contain </script> if a package description has one — unlikely but possible). Safer to use JSON.stringify(catalog).replace(/</g, '\\u003c') for the embedded JSON, which escapes the dangerous bytes without affecting parseability.
  • formatBytes uses base-2 units (KB = 1024) but labels them as 'KB'/'MB' which by convention suggest base-10. Minor but KiB/MiB would be more precise (or use base-10 with 1000).
  • applyFilter runs on every keystroke against a re-querying Array.from(document.querySelectorAll('.pkg-row')). With ~25 packages this is fine; if the catalog grows substantially, debounce or pre-compute.

Test plan note

The PR description has 8 unchecked test-plan items. They're all manual visual checks (page renders, filter works, copy fires, etc.). Since the Vercel preview URL is the canonical way to validate this, they probably won't be checked off in advance — but worth at least running through them on the preview before merge.

Comment threadtarballs/scripts/pack.ts Outdated
Built ${ctx.commitUrl ? `from <a href="${escapeHtml(ctx.commitUrl)}" target="_blank" rel="noopener">${escapeHtml(ctx.shortSha)}</a>` : `from <code>${escapeHtml(ctx.shortSha)}</code>`} · ${escapeHtml(packages.length.toString())} packages totaling ${escapeHtml(formatBytes(totalSize))}
</footer>
</div>
<script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this line breaks all client-side interactivity.

Verified on the preview deployment with browser automation — every interactive feature is non-functional:

  • ❌ Package-manager toggle (clicking npm/yarn/bun does nothing)
  • ❌ Live filter (typing in search shows all rows)
  • ❌ Copy buttons (no clipboard write, no Copied state)
  • / keyboard shortcut (body keeps focus)

Root cause: escapeHtml(JSON.stringify(catalog)) HTML-encodes the JSON. The serialized catalog ends up in the page as:

<scriptid="catalog" type="application/json">[{&quot;name&quot;:&quot;@workflow/ai&quot;,&quot;escapedName&quot;:&quot;workflow-ai&quot;...

JSON.parse(textContent) then throws on the very first &quot; because &quot; isn't valid JSON syntax. The IIFE bails on its first line and none of the event listeners get attached. Confirmed live:

>JSON.parse(document.getElementById('catalog').textContent)Uncaught SyntaxError: Expectedpropertynameor'}'inJSONatposition2(line1column3)

<script type="application/json"> content is treated as text by the HTML parser — it does NOT need HTML entity escaping. The only sequence that can break out of it is </script> (or </ in some legacy parsers). The narrowest correct fix is:

<scriptid="catalog"type="application/json">${JSON.stringify(catalog).replace(/</g,'\u003c')}</script>

That escapes < bytes to \u003c in the JSON (legal per the spec — JSON allows \uXXXX escapes for any character), so </script> can't appear in the text and the JSON parses cleanly.

This was the third bullet in my "smaller things I noticed" section above — turns out it's not theoretical, it's the live bug breaking everything client-side. Should not merge until the page works in a real browser.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3df48a7. Went with your replace(/</g, '\u003c') over the bot's </-only escape since it also covers a stray < inside, e.g., a package description. Verified locally that the embedded JSON parses cleanly out of the rendered HTML; preview deploy will validate the four interactive features end-to-end.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating my prior review to REQUEST_CHANGES. Ran the visual test plan against the preview deployment with browser automation and discovered that all client-side interactivity is broken — see the inline comment on pack.ts:696 for the root cause and fix.

The escapeHtml(JSON.stringify(catalog)) call HTML-encodes every " in the JSON to &quot;, so JSON.parse(textContent) throws on its first character. The IIFE bails immediately and none of the event listeners (PM toggle, search filter, copy buttons, / shortcut) get attached.

Test plan results from the preview at https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/:

  • ✅ Page renders correctly in light mode (visual layout, chips, featured card, package list)
  • workflow package appears as the featured card at the top
  • ✅ SHA / branch / timestamp / package count chips are present
  • Package-manager toggle doesn't update install commandsaria-selected stays on pnpm regardless of clicks
  • Live filter doesn't filter — typing in the search input shows all rows unchanged
  • Copy buttons don't fire — no Copied state, no clipboard write
  • / keyboard shortcut doesn't focus search — body keeps focus
  • (couldn't test direct download because earlier broken state interfered, but the <a download> is plain HTML and probably works)

So the page that ships looks correct visually but is functionally a static catalog — the toggle, filter, and copy buttons are all dead UI.

Quick fix at the call site (one line). After fix, all four interactive features should work. Worth re-testing on the next preview deployment.

The other points from my prior review (Vite + React refactor suggestion, the smaller TS-checkable concerns, KB vs KiB nit) all still stand and are non-blocking. Just this one issue blocks merging.

`escapeHtml(JSON.stringify(catalog))` was HTML-encoding every quote in
the embedded catalog JSON to `&quot;`, so `JSON.parse(textContent)` threw
on the first character and the IIFE bailed before attaching any event
listeners — package-manager toggle, search filter, copy buttons, and the
`/` shortcut were all dead UI on the deployed page.
`<script type="application/json">` content is treated as text by the HTML
parser; the only sequence that can break out is `</script>` (or `</`
in legacy parsers). Replace `<` with the JSON `<` escape, which is
legal per the JSON spec and prevents the breakout without needing entity
encoding.
Also switch `formatBytes` from `KB`/`MB` to `KiB`/`MiB` since the
divisor is 1024.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment threadtarballs/scripts/pack.ts Outdated
The smoke check broke in CI: `'workflow' tarball only has 0 files`.
Root cause is that `tar -tvzf` emits a different verbose layout on GNU
tar (Linux, what CI runs) vs BSD tar (macOS, where I tested locally) —
the parser only matched the BSD column ordering, so on Linux every line
was rejected and `fileCount` came out as 0.
Replace the shell-out with a small in-process tar reader using
`zlib.gunzipSync` + manual 512-byte block walk. ustar headers are
trivially structured (name at offset 0, octal size at 124, typeflag at
156, ustar prefix at 345). We emit regular files only (`typeflag` `0`
or NUL) and consume but skip pax extended headers (`x`/`g`) and GNU
long-name entries (`L`). Result is identical on every platform.
Verified locally: 206 files / 998413 bytes for `workflow.tgz` matches
`tar -tvzf` exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pranaygpand others added 3 commits May 4, 2026 19:31
The previous "What's inside?" view crammed nested directory groups,
proportional bars, and per-group file lists into a `<details>` inside
an already-narrow row. It was hard to read and harder to compare.
Replace it with the layout packagephobia uses on its result page:
- Two large headline metric tiles (Publish size / Unpacked size)
with a big bold value, smaller unit, and small uppercase label.
Modeled directly on packagephobia's `Stats` component but using
our existing CSS variables so it tracks light/dark theme.
- A single sortable file table beneath. Default is size-descending so
the contributors to package size are immediately visible. Click a
header to flip direction or switch sort key. Sticky header keeps
the columns visible inside the scrollable region.
Drop the `groupByTopLevel`, `ContentsGroup`, and bar-chart styles —
they were the source of the "hard to use" feedback and don't add
information that the flat sortable table doesn't already convey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the old static tarballs index with a Vite/Preact single-page UI that reads a generated catalog.json, surfaces richer build/package metadata, and adds interactive install/copy/filter/download affordances for preview tarballs.

Changes:

  • Adds a Vite + Preact frontend for the tarballs index, including featured package rendering, filtering, package-manager toggles, and package contents breakdowns.
  • Extends scripts/pack.ts to emit catalog.json with build metadata, tarball sizes, unpacked sizes, and per-file listings.
  • Updates build/smoke-test plumbing so the tarballs deployment now publishes bundled SPA assets plus the generated catalog.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tarballs/vite.config.tsConfigures Vite to build the SPA into public/ without deleting tarballs.
tarballs/turbo.jsonExpands build outputs to include SPA assets and catalog.json.
tarballs/tsconfig.jsonAdds TS config for the new frontend/build files.
tarballs/src/styles.cssAdds the full styling/theme for the redesigned index UI.
tarballs/src/main.tsxBoots the SPA and loads catalog.json.
tarballs/src/icons.tsxAdds inline SVG icons used across the new UI.
tarballs/src/catalog.tsDefines shared catalog/install-command helpers for the frontend.
tarballs/src/app.tsxImplements the main tarballs UI, interactions, and package contents tables.
tarballs/scripts/pack.tsGenerates tarballs plus the new metadata-rich catalog.json.
tarballs/scripts/check-tarballs-smoke.mjsAdds smoke coverage for catalog.json.
tarballs/package.jsonSwitches build to pack.ts && vite build and adds frontend deps/scripts.
tarballs/index.htmlAdds the SPA entry HTML shell.
pnpm-lock.yamlLocks newly added frontend/tooling dependencies.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadtarballs/package.json Outdated
"scripts": {
"build": "node scripts/pack.ts",
"build": "node scripts/pack.ts && vite build",
"dev": "vite",

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. The new dev chains node scripts/pack.ts && vite, and I restructured the build layout to vite's conventional shape so it actually works in dev: public/ is a real vite public dir (pack writes tarballs + catalog.json there, vite serves them at root in dev), and dist/ is the production build output (set as Vercel's outputDirectory).

Comment threadtarballs/src/app.tsx
<SearchIcon />
<input
ref={searchRef}
type="search"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added aria-label="Filter packages" to the input.

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +178 to +185
<div class="pm-tabs" role="tablist" aria-label="Package manager">
{options.map((opt) => (
<button
key={opt}
type="button"
class="pm-tab"
role="tab"
aria-selected={value === opt}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Dropped role="tablist"/role="tab"/aria-selected and made these plain buttons with aria-pressed. Toggle buttons are the honest representation since we never implemented arrow-key roving focus. Each button also has an explicit aria-label (Show install commands for pnpm etc.).

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +428 to +440
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} finally {
ta.remove();
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Refactored to a writeToClipboard helper that returns whether the write actually succeeded; the button only flips to Copied on success. If both navigator.clipboard.writeText and the execCommand fallback fail, it briefly shows a red Failed state instead.

Comment threadtarballs/src/app.tsx Outdated
<code class="pkg-cmd">{cmd}</code>
<div class="pkg-actions">
<CopyButton text={cmd} variant="icon" />
<a class="icon-btn" href={pkg.url} download aria-label="Download">

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added an accessibleName prop to CopyButton and pass Copy install command for <pkg.name> from each row + the featured card. Same treatment on the download <a> link (Download <pkg.name> tarball).

Comment threadtarballs/src/main.tsx Outdated
if (!root) throw new Error('No #app root element');

try {
const res = await fetch('/catalog.json', { cache: 'no-store' });

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345 — dropped cache: 'no-store'. Each preview's URL is unique so the browser cache is the right thing to use.

- main.tsx: drop `cache: 'no-store'` from the catalog fetch. Each
tarballs deployment is immutable per commit, so HTTP caching is
appropriate; forcing no-store made every visit re-download the full
catalog (which now includes per-package file lists).
- app.tsx (search input): add `aria-label="Filter packages"`. The
visible label only contained an icon and placeholder, so screen
readers had no name for the control.
- app.tsx (PmTabs): replace `role="tablist"` / `role="tab"` /
`aria-selected` with plain buttons that use `aria-pressed`. The
ARIA tab pattern requires arrow-key roving focus we never wired
up; toggle buttons are the honest representation. Each button
also gets an explicit `aria-label`.
- app.tsx (row buttons): include the package name in the accessible
label of every per-row copy/download button (and on the featured
card too), so the screen reader buttons/links list distinguishes
them. Added an `accessibleName` prop to `CopyButton`.
- app.tsx (CopyButton): only flip to the "Copied" state when the
write actually succeeded. Both the modern `navigator.clipboard`
path and the `execCommand` fallback can fail; the new
`writeToClipboard` helper returns success and the button shows a
short "Failed" state if both paths fail.
# Make `pnpm dev` work from a clean checkout
The previous `dev: vite` couldn't actually serve the page because
`/catalog.json` 404s and the SPA boots into the error fallback.
Restructure the build layout to vite's conventional shape:
- `public/` is now a true vite public dir — pack writes tarballs and
catalog.json there. In dev, vite serves these at the root.
- `dist/` is the production build output (vite copies public/ into it
and adds index.html + assets/).
- `vercel.json#outputDirectory` switches from `public` → `dist`.
- `turbo.json` outputs updated to match.
- `dev` chains pack before vite so the catalog exists when the dev
server starts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​preact/​preset-vite@​2.10.59910010090100
Addednpm/​preact@​10.29.110010010094100

View full report

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pranay took the Vite + Preact refactor recommendation from my prior review and ran with it. The new shape matches what I sketched almost exactly:

tarballs/
├── index.html # 20-line entry
├── vite.config.ts # 16-line config
├── tsconfig.json # strict, jsxImportSource: preact
├── scripts/
│ ├── pack.ts # 317 lines, data-only — packs tarballs, writes catalog.json
│ └── check-tarballs-smoke.mjs # now also validates catalog.json shape
└── src/
├── main.tsx # 26 lines — fetch + mount
├── app.tsx # 550 lines — typed Preact JSX
├── catalog.ts # types + small helpers
├── icons.tsx # 8 inline SVG icons
└── styles.css # 794 lines, geist-inspired theme

Bundle ships as dist/assets/index-*.js at 26.25 kB (9.69 kB gzipped) — small for a Preact SPA with sortable tables, copy-buttons, search, and a PM toggle. CSS is 11.19 kB / 2.73 kB gzipped.

What I verified

  • Local build works: pnpm turbo build --filter=tarballs runs pack.ts then vite build, producing 26 tarballs in public/, a catalog.json matching the typed shape in src/catalog.ts, and a dist/ ready to deploy.
  • TypeScript is clean: pnpm exec tsc --noEmit from tarballs/ passes with the strict config (strict, noUnusedLocals, noUnusedParameters, isolatedModules).
  • Deployed preview serves correctly: https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/catalog.json returns the expected payload with build context (sha, branch, PR number, GitHub-linked commit/branch/PR URLs) and 26 packages with full file listings + sizes. Each package's url is the immutable per-deployment hash URL (e.g. https://workflow-tarballs-ch8zhxmen.labs.vercel.dev/workflow-ai.tgz), so the catalog is stable even if visited via the git-branch URL.
  • JS bundle contains real Preact: read the bundle, confirmed the App tree renders with hooks (useState, useEffect, useMemo, useRef), addEventListener('keydown', …) for the / shortcut, navigator.clipboard.writeText + execCommand('copy') fallback for CopyButton, and the data-copied / data-failed state machine.
  • Smoke check is stronger now: in addition to verifying tarballs are gzip-magic-prefixed and the index page responds with text/html, it now fetches catalog.json and asserts the workflow package has a non-trivial fileCount — a useful diagnostic for "packages weren't built before pack."

Concerns from my prior review — all addressed

  1. The hand-rolled HTML-in-JS approach — gone. JSX is TypeScript-checked end to end (event handlers, prop types, catalog shape).
  2. Two parallel render concerns merged into one file — split clean. pack.ts is now data-only (~317 lines, all bundling/tar-reading). The presentation layer is in src/.
  3. renderRow / applyPm / applyFilter had to stay in sync via string conventions — gone. State lives in React-style hooks, props are typed.
  4. Hand-rolled escapeHtml — gone. JSX handles escaping.
  5. escapeHtml(JSON.stringify(catalog)) breaking client interactivity — moot. The catalog is a separate /catalog.json static file fetched at runtime, so there's no inline-JSON encoding question.
  6. KB/MB labels with base-2 math — fixed. formatBytes now emits B/KiB/MiB.

Copilot review items — all addressed in 1c48345e6

  • a11y: aria-label="Filter packages" on search input; PM tabs use plain buttons with aria-pressed (no half-implemented role="tablist"); per-row copy buttons get accessibleName="Copy install command for ${pkg.name}" instead of a generic label.
  • Robustness: writeToClipboard returns a boolean so failed writes show "Failed" rather than a misleading "Copied" success state.
  • DX: dev script chains pack.ts && vite; dropped cache: 'no-store' since the catalog URL is per-deployment-immutable.

Smaller improvements landed along the way

  • Tar reader rewrite (6ae7736): replaced tar -tvzf shell-out with an in-process gunzip + 512-byte block walker. Handles GNU long-name (L) entries and pax headers, and works identically on macOS BSD tar and Linux GNU tar. Sorting files by descending size before emitting is a nice touch for the per-package "what's inside" view.
  • PackagePhobia-style SizeStats widget: two big tiles (publish size + unpacked size) with split value/unit rendering. Looks good and reads better than a one-liner.
  • FileTable with sortable columns: ARIA aria-sort on <th>, indicator arrows, click-to-toggle direction. Strips the package/ prefix from displayed paths.

Tiny things — non-blocking

  • pack.ts:117: updateDeps builds rewrite URLs as https://${process.env.VERCEL_URL}/${escapedName}.tgz directly, while baseUrl (defined at line 97) uses the VERCEL_URL ? https://… : '' fallback. Local builds without VERCEL_URL produce package.json deps like https://undefined/.... Pre-existing on main, not introduced by this PR. Not a real-world issue (nobody installs from a local pack output) but noting for a follow-up cleanup if you want consistency.
  • catalog.ts and pack.ts duplicate the same TS interfaces (TarballFile, PackedPackage, BuildContext, Catalog). Drift risk is low since they're both in the same package, but if you want to eliminate it, catalog.ts could be the single source of truth and pack.ts could import type from it. Fine to leave.
  • SizeStats is rendered for every package in the list, not just the featured one — but it's behind a <details> so it doesn't blow up initial render. The 26 expanded <details> plus the FileTable for each would render a lot of DOM if expanded all at once; in practice, users open one or two. Not blocking.

Verdict: approve

Approving and clearing my prior CHANGES_REQUESTED. The redesign hit every concern from my last two reviews plus the Copilot feedback, the build pipeline is cleaner than what was there before, and the deployment shape is preserved (public/<escaped-name>.tgz URLs unchanged, smoke checks pass). Nice work on the followups.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

@v0@vercel can you make a PR to address the remaining follow ups from @TooTallNate's review comments that we didn't get to before merge of this one

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-stableCherry-pick this PR to the stable branch when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

tarballs: redesign preview tarballs index page - #1911

Merged
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements
May 4, 2026
Merged

tarballs: redesign preview tarballs index page#1911
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Summary

Reworks the static index page generated by tarballs/scripts/pack.ts (deployed at https://workflow-tarballs.labs.vercel.dev/) to be more useful and pleasant to look at.

What's new

  • Featured workflow package at the top with a large install command, copy button, and direct tarball download — the rest of the packages are secondary.
  • Top-of-page metadata chips: short SHA (linked to commit on GitHub), branch (linked), PR number (linked), build timestamp, package count + total size.
  • Collapsible "What is this?" explainer describing how the version + dep rewriting works.
  • Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the install command shown for every row in place.
  • Live filter input over the package list, with / keyboard shortcut to focus.
  • Per-row install command, copy button, and direct download link for every package.
  • Geist-inspired theme with proper dark/light support driven by system preference.

Implementation notes

  • Pack step now records the produced tarball size via fs.stat after pnpm pack so the page can show human-readable sizes.
  • The page reads VERCEL_GIT_* env vars for commit / branch / PR links and falls back to local git rev-parse for branch when running off-Vercel.
  • All packages still get an entry, but workflow is filtered out of the bottom list and rendered separately.
  • No changes to the deployment shape — public/<escaped-name>.tgz URLs are unchanged, so existing usage keeps working.

The Vercel preview for this PR will deploy and serve the new page so you can see it live.

Test plan

  • Open the Vercel preview deployment and verify the page renders correctly in light and dark mode
  • Confirm the workflow package appears as the featured card at the top
  • Confirm SHA / branch / PR chips are present and linked
  • Toggle pnpm / npm / yarn / bun and confirm install commands update everywhere
  • Filter the package list (and try the / shortcut)
  • Click copy on a row and on the featured card
  • Click download on a row and verify the tarball downloads
  • Smoke-install the featured tarball into a fresh project and confirm it still works (pnpm i https://<deployment>/workflow.tgz)

🤖 Generated with Claude Code

Rebuild the static index page produced by `tarballs/scripts/pack.ts`:
- Featured `workflow` package up top with prominent install command,
copy button, and direct tarball download
- Top-of-page metadata chips: short SHA (linked to commit), branch,
PR number, build timestamp, package count + total size
- Collapsible "What is this?" explainer
- Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the
install command for every row in place
- Live filter input over the rest of the package list (with `/` shortcut)
- Per-row install command, copy button, and direct download
- Modern dark/light theme with system preference, Geist-inspired styling
Also captures tarball size during pack and renders human-readable byte counts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented May 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1c48345

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-nextjs-workflow-webpackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-astro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-express-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-fastify-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-hono-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nitro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nuxt-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-sveltekit-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-tanstack-start-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-vite-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-swc-playgroundReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-tarballsReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-webReadyReadyPreview, CommentMay 4, 2026 11:41am
1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
workflow-docsSkippedSkippedMay 4, 2026 11:41am

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other5100176686
Total4338010525390

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025
✅ e2e-vercel-prod-tanstack-start72026

📋 View full workflow run

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.031s (-30.2% 🟢)1.005s (~)0.974s101.00x
💻 LocalNitro0.032s (-24.8% 🟢)1.005s (~)0.973s101.05x
🐘 PostgresExpress0.043s (-26.4% 🟢)1.010s (~)0.967s101.38x
💻 LocalNext.js (Turbopack)0.047s1.005s0.958s101.51x
🐘 PostgresNitro0.052s (-45.6% 🟢)1.012s (-3.0%)0.960s101.68x
🌐 RedisNext.js (Turbopack)0.059s1.005s0.947s101.89x
🐘 PostgresNext.js (Turbopack)0.059s1.011s0.952s101.91x
🌐 MongoDBNext.js (Turbopack)0.100s1.009s0.910s103.23x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.068s (-5.6% 🟢)2.006s (~)0.938s101.00x
💻 LocalExpress1.074s (-4.6%)2.006s (~)0.932s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.927s101.01x
🐘 PostgresNitro1.088s (-4.6%)2.010s (~)0.922s101.02x
💻 LocalNext.js (Turbopack)1.108s2.006s0.898s101.04x
🌐 RedisNext.js (Turbopack)1.112s2.007s0.895s101.04x
🐘 PostgresNext.js (Turbopack)1.117s2.009s0.892s101.05x
🌐 MongoDBNext.js (Turbopack)1.166s2.009s0.843s101.09x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.387s (-5.1% 🟢)11.022s (~)0.634s31.00x
💻 LocalExpress10.405s (-4.7%)11.023s (~)0.618s31.00x
🐘 PostgresExpress10.415s (-5.0% 🟢)11.012s (~)0.597s31.00x
🐘 PostgresNitro10.432s (-4.0%)11.018s (~)0.585s31.00x
🌐 RedisNext.js (Turbopack)10.634s11.023s0.389s31.02x
💻 LocalNext.js (Turbopack)10.690s11.023s0.333s31.03x
🐘 PostgresNext.js (Turbopack)10.716s11.017s0.301s31.03x
🌐 MongoDBNext.js (Turbopack)10.822s11.017s0.195s31.04x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.426s (-10.9% 🟢)14.027s (-12.5% 🟢)0.600s51.00x
🐘 PostgresExpress13.463s (-7.7% 🟢)14.021s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.480s (-10.0% 🟢)14.028s (-6.7% 🟢)0.548s51.00x
🐘 PostgresNitro13.544s (-7.2% 🟢)14.019s (-6.7% 🟢)0.475s51.01x
💻 LocalNext.js (Turbopack)14.031s15.028s0.997s41.05x
🌐 RedisNext.js (Turbopack)14.036s14.628s0.592s51.05x
🐘 PostgresNext.js (Turbopack)14.130s15.022s0.892s41.05x
🌐 MongoDBNext.js (Turbopack)14.245s15.023s0.778s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.809s (-29.6% 🟢)12.022s (-29.4% 🟢)0.214s81.00x
🐘 PostgresNitro11.928s (-14.6% 🟢)12.017s (-16.0% 🟢)0.089s81.01x
🐘 PostgresExpress11.967s (-14.6% 🟢)12.265s (-16.0% 🟢)0.298s81.01x
💻 LocalExpress12.248s (-26.2% 🟢)12.774s (-25.0% 🟢)0.526s81.04x
💻 LocalNext.js (Turbopack)13.061s13.737s0.677s71.11x
🌐 RedisNext.js (Turbopack)13.217s13.883s0.666s71.12x
🌐 MongoDBNext.js (Turbopack)13.255s14.019s0.764s71.12x
🐘 PostgresNext.js (Turbopack)13.304s14.021s0.718s71.13x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.141s (-9.4% 🟢)2.006s (~)0.865s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.007s (~)0.861s151.00x
💻 LocalNitro1.171s (-28.2% 🟢)2.006s (-3.3%)0.835s151.03x
💻 LocalExpress1.174s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.217s2.007s0.790s151.07x
🌐 RedisNext.js (Turbopack)1.240s2.006s0.766s151.09x
💻 LocalNext.js (Turbopack)1.289s2.006s0.717s151.13x
🌐 MongoDBNext.js (Turbopack)2.035s2.826s0.791s111.78x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-49.1% 🟢)2.007s (-33.3% 🟢)0.805s151.00x
🐘 PostgresNitro1.229s (-47.7% 🟢)2.007s (-33.3% 🟢)0.778s151.02x
🐘 PostgresNext.js (Turbopack)1.358s2.008s0.650s151.13x
💻 LocalNitro1.685s (-46.4% 🟢)2.006s (-48.4% 🟢)0.320s151.40x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.299s151.42x
💻 LocalNext.js (Turbopack)1.858s2.292s0.434s141.55x
🌐 RedisNext.js (Turbopack)2.337s3.008s0.671s101.95x
🌐 MongoDBNext.js (Turbopack)3.568s4.009s0.441s82.97x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.305s (-62.6% 🟢)2.007s (-50.0% 🟢)0.702s151.00x
🐘 PostgresNitro1.383s (-60.3% 🟢)2.007s (-49.9% 🟢)0.624s151.06x
🐘 PostgresNext.js (Turbopack)1.629s2.007s0.378s151.25x
🌐 RedisNext.js (Turbopack)3.602s4.010s0.408s82.76x
💻 LocalNext.js (Turbopack)4.383s5.011s0.627s63.36x
💻 LocalExpress4.397s (-47.3% 🟢)5.013s (-44.5% 🟢)0.616s63.37x
💻 LocalNitro4.571s (-45.2% 🟢)5.011s (-44.4% 🟢)0.440s63.50x
🌐 MongoDBNext.js (Turbopack)6.273s7.013s0.739s54.81x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.139s (-9.4% 🟢)2.009s (~)0.869s151.00x
🐘 PostgresNitro1.168s (-7.1% 🟢)2.009s (~)0.840s151.03x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.773s151.08x
🐘 PostgresNext.js (Turbopack)1.248s2.007s0.760s151.10x
💻 LocalNext.js (Turbopack)1.321s2.005s0.684s151.16x
💻 LocalExpress1.352s (-28.6% 🟢)2.006s (-15.1% 🟢)0.654s151.19x
💻 LocalNitro1.379s (-26.1% 🟢)2.006s (-14.3% 🟢)0.627s151.21x
🌐 MongoDBNext.js (Turbopack)2.026s2.735s0.709s111.78x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-48.3% 🟢)2.008s (-33.3% 🟢)0.797s151.00x
🐘 PostgresNitro1.232s (-47.3% 🟢)2.008s (-33.3% 🟢)0.776s151.02x
🐘 PostgresNext.js (Turbopack)1.368s2.008s0.640s151.13x
💻 LocalExpress1.838s (-41.3% 🟢)2.075s (-44.8% 🟢)0.238s151.52x
💻 LocalNitro1.954s (-36.3% 🟢)2.469s (-36.5% 🟢)0.515s131.61x
💻 LocalNext.js (Turbopack)2.083s2.826s0.743s111.72x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.94x
🌐 MongoDBNext.js (Turbopack)3.547s4.008s0.461s82.93x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.318s (-62.3% 🟢)2.007s (-50.0% 🟢)0.690s151.00x
🐘 PostgresNitro1.379s (-60.4% 🟢)2.007s (-49.9% 🟢)0.628s151.05x
🐘 PostgresNext.js (Turbopack)1.718s2.075s0.357s151.30x
🌐 RedisNext.js (Turbopack)3.595s4.010s0.415s82.73x
💻 LocalNitro4.877s (-46.7% 🟢)5.516s (-45.0% 🟢)0.639s63.70x
💻 LocalExpress5.136s (-41.6% 🟢)5.515s (-40.5% 🟢)0.379s63.90x
💻 LocalNext.js (Turbopack)5.496s6.011s0.515s54.17x
🌐 MongoDBNext.js (Turbopack)6.286s7.013s0.727s54.77x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.430s (-48.7% 🟢)1.006s (-1.7%)0.575s601.00x
💻 LocalNitro0.457s (-53.4% 🟢)1.004s (-8.3% 🟢)0.547s601.06x
🐘 PostgresNitro0.483s (-41.1% 🟢)1.023s (+1.7%)0.540s591.12x
💻 LocalExpress0.509s (-48.3% 🟢)1.004s (-6.7% 🟢)0.495s601.18x
🌐 RedisNext.js (Turbopack)0.617s1.004s0.388s601.43x
🐘 PostgresNext.js (Turbopack)0.697s1.007s0.310s601.62x
💻 LocalNext.js (Turbopack)0.712s1.004s0.292s601.66x
🌐 MongoDBNext.js (Turbopack)0.739s1.006s0.266s601.72x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.027s (-48.0% 🟢)1.586s (-29.8% 🟢)0.559s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.741s (-17.1% 🟢)0.648s521.06x
💻 LocalNitro1.156s (-61.9% 🟢)2.006s (-46.6% 🟢)0.849s451.13x
💻 LocalExpress1.191s (-60.5% 🟢)2.005s (-44.1% 🟢)0.814s451.16x
🌐 RedisNext.js (Turbopack)1.479s2.006s0.527s451.44x
🐘 PostgresNext.js (Turbopack)1.638s2.007s0.369s451.60x
💻 LocalNext.js (Turbopack)1.780s2.027s0.247s451.73x
🌐 MongoDBNext.js (Turbopack)1.811s2.007s0.196s451.76x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.012s (-49.6% 🟢)2.427s (-44.5% 🟢)0.415s501.00x
🐘 PostgresNitro2.035s (-50.4% 🟢)2.457s (-46.6% 🟢)0.422s491.01x
💻 LocalNitro2.638s (-71.6% 🟢)3.007s (-70.0% 🟢)0.369s401.31x
💻 LocalExpress2.802s (-69.6% 🟢)3.084s (-69.2% 🟢)0.283s391.39x
🌐 RedisNext.js (Turbopack)3.003s3.277s0.275s371.49x
🐘 PostgresNext.js (Turbopack)3.192s4.010s0.817s301.59x
💻 LocalNext.js (Turbopack)3.864s4.145s0.282s291.92x
🌐 MongoDBNext.js (Turbopack)4.155s5.011s0.856s242.07x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.172s (-39.1% 🟢)1.006s (~)0.834s601.00x
🐘 PostgresNitro0.192s (-32.2% 🟢)1.006s (~)0.814s601.12x
🐘 PostgresNext.js (Turbopack)0.241s1.006s0.764s601.40x
🌐 RedisNext.js (Turbopack)0.251s1.004s0.753s601.46x
💻 LocalNitro0.432s (-28.6% 🟢)1.004s (-1.7%)0.572s602.51x
💻 LocalExpress0.466s (-16.9% 🟢)1.004s (~)0.539s602.71x
💻 LocalNext.js (Turbopack)0.567s1.004s0.437s603.30x
🌐 MongoDBNext.js (Turbopack)1.041s1.882s0.842s326.05x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.296s (-41.8% 🟢)1.006s (~)0.710s901.00x
🐘 PostgresNitro0.314s (-36.8% 🟢)1.006s (~)0.693s901.06x
🌐 RedisNext.js (Turbopack)0.414s1.004s0.590s901.40x
🐘 PostgresNext.js (Turbopack)0.461s1.006s0.545s901.55x
💻 LocalNitro2.179s (-14.1% 🟢)2.852s (-5.2% 🟢)0.672s327.35x
💻 LocalNext.js (Turbopack)2.182s2.944s0.762s317.36x
💻 LocalExpress2.223s (-11.6% 🟢)2.821s (-6.3% 🟢)0.598s327.50x
🌐 MongoDBNext.js (Turbopack)2.618s3.007s0.389s308.83x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.578s (-29.3% 🟢)1.014s (~)0.436s1191.00x
🐘 PostgresNitro0.641s (-18.9% 🟢)1.006s (~)0.365s1201.11x
🌐 RedisNext.js (Turbopack)0.770s1.004s0.234s1201.33x
🐘 PostgresNext.js (Turbopack)0.957s1.352s0.394s901.65x
🌐 MongoDBNext.js (Turbopack)5.396s6.011s0.615s209.33x
💻 LocalExpress9.835s (-12.1% 🟢)10.444s (-12.5% 🟢)0.609s1217.00x
💻 LocalNitro9.941s (-11.2% 🟢)10.365s (-11.1% 🟢)0.424s1217.19x
💻 LocalNext.js (Turbopack)11.011s11.573s0.562s1119.03x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.127s (+449.7% 🔺)2.001s (+100.3% 🔺)0.001s (-37.5% 🟢)2.009s (+98.6% 🔺)0.882s101.00x
💻 LocalNitro1.128s (+428.0% 🔺)2.005s (+99.6% 🔺)0.011s (-15.2% 🟢)2.018s (+98.1% 🔺)0.890s101.00x
🐘 PostgresNitro1.135s (+453.5% 🔺)2.000s (+100.1% 🔺)0.001s (-6.7% 🟢)2.010s (+98.7% 🔺)0.875s101.01x
💻 LocalExpress1.140s (+472.4% 🔺)2.005s (+99.6% 🔺)0.013s (+3.3%)2.020s (+98.4% 🔺)0.881s101.01x
💻 LocalNext.js (Turbopack)1.190s2.003s0.010s2.017s0.826s101.06x
🐘 PostgresNext.js (Turbopack)1.197s2.001s0.002s2.011s0.815s101.06x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.482s (+135.3% 🔺)2.006s (+99.3% 🔺)0.003s (-10.4% 🟢)2.022s (+97.7% 🔺)0.540s301.00x
🐘 PostgresNitro1.525s (+144.4% 🔺)2.002s (+98.9% 🔺)0.004s (+0.8%)2.026s (+98.2% 🔺)0.501s301.03x
💻 LocalNext.js (Turbopack)1.670s2.009s0.011s2.023s0.352s301.13x
🐘 PostgresNext.js (Turbopack)1.682s2.010s0.004s2.025s0.343s301.13x
💻 LocalExpress1.705s (+125.2% 🔺)2.011s (+95.4% 🔺)0.010s (+8.5% 🔺)2.202s (+111.8% 🔺)0.498s281.15x
💻 LocalNitro1.713s (+104.2% 🔺)2.010s (+98.6% 🔺)0.010s (+4.8%)2.201s (+97.2% 🔺)0.488s281.16x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.622s (-35.2% 🟢)1.013s (-20.7% 🟢)0.000s (+16.9% 🔺)1.028s (-21.3% 🟢)0.406s591.00x
🐘 PostgresNitro0.681s (-29.7% 🟢)1.047s (-16.1% 🟢)0.000s (-58.6% 🟢)1.059s (-15.8% 🟢)0.378s581.09x
🐘 PostgresNext.js (Turbopack)0.817s1.053s0.000s1.068s0.251s571.31x
💻 LocalNitro1.331s (+8.8% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.686s302.14x
💻 LocalExpress1.394s (+13.8% 🔺)2.015s (~)0.000s (+10.0% 🔺)2.017s (~)0.623s302.24x
💻 LocalNext.js (Turbopack)1.437s2.013s0.000s2.016s0.579s302.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.313s (-26.7% 🟢)2.099s (-2.0%)0.000s (-3.4%)2.131s (-2.0%)0.818s291.00x
🐘 PostgresExpress1.349s (-23.9% 🟢)2.145s (-1.5%)0.000s (+Infinity% 🔺)2.161s (-1.7%)0.812s281.03x
🐘 PostgresNext.js (Turbopack)1.689s2.225s0.000s2.273s0.584s271.29x
💻 LocalNext.js (Turbopack)2.563s3.079s0.001s3.084s0.521s201.95x
💻 LocalNitro3.092s (-8.7% 🟢)3.840s (-4.8%)0.000s (-76.6% 🟢)3.844s (-4.8%)0.752s162.35x
💻 LocalExpress3.227s (-6.9% 🟢)4.031s (~)0.000s (-66.7% 🟢)4.033s (~)0.806s152.46x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro14/21
🐘 PostgresExpress19/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres8/21
Nitro🐘 Postgres14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

Comment threadtarballs/scripts/pack.ts Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visual design is nice — featured workflow card, package-manager toggle, live filter, copy buttons, geist-inspired theme. Going to approve since the deployment shape doesn't change and this only affects the static index page.

That said: I'd push back on the hand-rolled HTML-in-JS approach for anything beyond a one-page diff. The new file is 857 lines, and the implementation has some smells that follow naturally from the medium:

  • The inline <script> block (~80 lines) isn't TypeScript-checked. It's inside a template literal in writeIndexHtml, so the compiler treats it as a string. Refactoring (rename a data- attribute, change a property name) won't surface type errors. The TS half references DOM via data- attributes that have to match by convention.
  • Two parallel "render the page" concerns — the pack.ts script handles both bundling-time logic (scanning packages, rewriting deps, packing tarballs) and presentation logic (HTML composition, CSS, client-side JS for PM toggle/filter/copy). Those were independent before this PR; conflating them makes both harder to evolve.
  • renderFeatured/renderRow and the inline applyPm/applyFilter script must stay in sync on attribute names, class names, and the catalog JSON shape. Right now there's a hand-rolled escapeHtml helper and the contract is "data-install-cmd must equal escapedName." Easy to break in a refactor; no tests catch the drift.
  • Hand-rolled escape helpers (escapeHtml) are correct here but would be unnecessary with JSX or a templating library.

A lightweight Vite + React (or Preact for smaller bundle) SPA would clean this up substantially. The shape I'd suggest:

tarballs/
├── package.json # adds vite, react, @vitejs/plugin-react
├── vite.config.ts # static build → public/
├── index.html # entry HTML
├── src/
│ ├── main.tsx # mount React
│ ├── App.tsx # the page (~80 lines instead of ~600 of HTML+CSS+JS)
│ ├── catalog.ts # types for the JSON catalog
│ └── styles.css # the geist-inspired theme
└── scripts/
├── pack.ts # data-only: scans packages, packs tarballs, writes
│ # public/catalog.json with the build context
└── check-tarballs-smoke.mjs

pack.ts becomes ~140 lines (just the bundling logic), vite build produces the static page that fetches catalog.json, the React component reads it on mount and renders. State (active PM, search query) is useState. Build pipeline is pnpm pack → vite build, and Vercel's existing outputDirectory: "public" works unchanged.

DX wins: HMR while iterating on the design, TypeScript checks the JSX and event handlers, can pull in actual geist (or lucide-react) icons via npm instead of inlined SVG, hot-swap component libraries if the design evolves.

If you don't want to take that on now, this is fine as-is — the page works, the design looks good, and the deployment shape is preserved. But suggest filing a follow-up to migrate before adding the next 200 lines of HTML to this file.

Smaller things I noticed

  • getBuildContext uses process.env.VERCEL_GIT_* for SHA/branch/PR. Local builds get only the SHA/branch fallback via git rev-parse. Worth double-checking the labels on chips degrade gracefully when those env vars are missing — looks like they do (early-return paths with if (ctx.commitUrl)), but a quick local pnpm --filter tarballs build test before merge would confirm.
  • The catalog JSON is embedded via <script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>. JSON-inside-HTML-inside-template-literal is two layers of escape (the escapeHtml handles the outer, but the JSON itself can contain </script> if a package description has one — unlikely but possible). Safer to use JSON.stringify(catalog).replace(/</g, '\\u003c') for the embedded JSON, which escapes the dangerous bytes without affecting parseability.
  • formatBytes uses base-2 units (KB = 1024) but labels them as 'KB'/'MB' which by convention suggest base-10. Minor but KiB/MiB would be more precise (or use base-10 with 1000).
  • applyFilter runs on every keystroke against a re-querying Array.from(document.querySelectorAll('.pkg-row')). With ~25 packages this is fine; if the catalog grows substantially, debounce or pre-compute.

Test plan note

The PR description has 8 unchecked test-plan items. They're all manual visual checks (page renders, filter works, copy fires, etc.). Since the Vercel preview URL is the canonical way to validate this, they probably won't be checked off in advance — but worth at least running through them on the preview before merge.

Comment threadtarballs/scripts/pack.ts Outdated
Built ${ctx.commitUrl ? `from <a href="${escapeHtml(ctx.commitUrl)}" target="_blank" rel="noopener">${escapeHtml(ctx.shortSha)}</a>` : `from <code>${escapeHtml(ctx.shortSha)}</code>`} · ${escapeHtml(packages.length.toString())} packages totaling ${escapeHtml(formatBytes(totalSize))}
</footer>
</div>
<script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this line breaks all client-side interactivity.

Verified on the preview deployment with browser automation — every interactive feature is non-functional:

  • ❌ Package-manager toggle (clicking npm/yarn/bun does nothing)
  • ❌ Live filter (typing in search shows all rows)
  • ❌ Copy buttons (no clipboard write, no Copied state)
  • / keyboard shortcut (body keeps focus)

Root cause: escapeHtml(JSON.stringify(catalog)) HTML-encodes the JSON. The serialized catalog ends up in the page as:

<scriptid="catalog" type="application/json">[{&quot;name&quot;:&quot;@workflow/ai&quot;,&quot;escapedName&quot;:&quot;workflow-ai&quot;...

JSON.parse(textContent) then throws on the very first &quot; because &quot; isn't valid JSON syntax. The IIFE bails on its first line and none of the event listeners get attached. Confirmed live:

>JSON.parse(document.getElementById('catalog').textContent)Uncaught SyntaxError: Expectedpropertynameor'}'inJSONatposition2(line1column3)

<script type="application/json"> content is treated as text by the HTML parser — it does NOT need HTML entity escaping. The only sequence that can break out of it is </script> (or </ in some legacy parsers). The narrowest correct fix is:

<scriptid="catalog"type="application/json">${JSON.stringify(catalog).replace(/</g,'\u003c')}</script>

That escapes < bytes to \u003c in the JSON (legal per the spec — JSON allows \uXXXX escapes for any character), so </script> can't appear in the text and the JSON parses cleanly.

This was the third bullet in my "smaller things I noticed" section above — turns out it's not theoretical, it's the live bug breaking everything client-side. Should not merge until the page works in a real browser.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3df48a7. Went with your replace(/</g, '\u003c') over the bot's </-only escape since it also covers a stray < inside, e.g., a package description. Verified locally that the embedded JSON parses cleanly out of the rendered HTML; preview deploy will validate the four interactive features end-to-end.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating my prior review to REQUEST_CHANGES. Ran the visual test plan against the preview deployment with browser automation and discovered that all client-side interactivity is broken — see the inline comment on pack.ts:696 for the root cause and fix.

The escapeHtml(JSON.stringify(catalog)) call HTML-encodes every " in the JSON to &quot;, so JSON.parse(textContent) throws on its first character. The IIFE bails immediately and none of the event listeners (PM toggle, search filter, copy buttons, / shortcut) get attached.

Test plan results from the preview at https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/:

  • ✅ Page renders correctly in light mode (visual layout, chips, featured card, package list)
  • workflow package appears as the featured card at the top
  • ✅ SHA / branch / timestamp / package count chips are present
  • Package-manager toggle doesn't update install commandsaria-selected stays on pnpm regardless of clicks
  • Live filter doesn't filter — typing in the search input shows all rows unchanged
  • Copy buttons don't fire — no Copied state, no clipboard write
  • / keyboard shortcut doesn't focus search — body keeps focus
  • (couldn't test direct download because earlier broken state interfered, but the <a download> is plain HTML and probably works)

So the page that ships looks correct visually but is functionally a static catalog — the toggle, filter, and copy buttons are all dead UI.

Quick fix at the call site (one line). After fix, all four interactive features should work. Worth re-testing on the next preview deployment.

The other points from my prior review (Vite + React refactor suggestion, the smaller TS-checkable concerns, KB vs KiB nit) all still stand and are non-blocking. Just this one issue blocks merging.

`escapeHtml(JSON.stringify(catalog))` was HTML-encoding every quote in
the embedded catalog JSON to `&quot;`, so `JSON.parse(textContent)` threw
on the first character and the IIFE bailed before attaching any event
listeners — package-manager toggle, search filter, copy buttons, and the
`/` shortcut were all dead UI on the deployed page.
`<script type="application/json">` content is treated as text by the HTML
parser; the only sequence that can break out is `</script>` (or `</`
in legacy parsers). Replace `<` with the JSON `<` escape, which is
legal per the JSON spec and prevents the breakout without needing entity
encoding.
Also switch `formatBytes` from `KB`/`MB` to `KiB`/`MiB` since the
divisor is 1024.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment threadtarballs/scripts/pack.ts Outdated
The smoke check broke in CI: `'workflow' tarball only has 0 files`.
Root cause is that `tar -tvzf` emits a different verbose layout on GNU
tar (Linux, what CI runs) vs BSD tar (macOS, where I tested locally) —
the parser only matched the BSD column ordering, so on Linux every line
was rejected and `fileCount` came out as 0.
Replace the shell-out with a small in-process tar reader using
`zlib.gunzipSync` + manual 512-byte block walk. ustar headers are
trivially structured (name at offset 0, octal size at 124, typeflag at
156, ustar prefix at 345). We emit regular files only (`typeflag` `0`
or NUL) and consume but skip pax extended headers (`x`/`g`) and GNU
long-name entries (`L`). Result is identical on every platform.
Verified locally: 206 files / 998413 bytes for `workflow.tgz` matches
`tar -tvzf` exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pranaygpand others added 3 commits May 4, 2026 19:31
The previous "What's inside?" view crammed nested directory groups,
proportional bars, and per-group file lists into a `<details>` inside
an already-narrow row. It was hard to read and harder to compare.
Replace it with the layout packagephobia uses on its result page:
- Two large headline metric tiles (Publish size / Unpacked size)
with a big bold value, smaller unit, and small uppercase label.
Modeled directly on packagephobia's `Stats` component but using
our existing CSS variables so it tracks light/dark theme.
- A single sortable file table beneath. Default is size-descending so
the contributors to package size are immediately visible. Click a
header to flip direction or switch sort key. Sticky header keeps
the columns visible inside the scrollable region.
Drop the `groupByTopLevel`, `ContentsGroup`, and bar-chart styles —
they were the source of the "hard to use" feedback and don't add
information that the flat sortable table doesn't already convey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the old static tarballs index with a Vite/Preact single-page UI that reads a generated catalog.json, surfaces richer build/package metadata, and adds interactive install/copy/filter/download affordances for preview tarballs.

Changes:

  • Adds a Vite + Preact frontend for the tarballs index, including featured package rendering, filtering, package-manager toggles, and package contents breakdowns.
  • Extends scripts/pack.ts to emit catalog.json with build metadata, tarball sizes, unpacked sizes, and per-file listings.
  • Updates build/smoke-test plumbing so the tarballs deployment now publishes bundled SPA assets plus the generated catalog.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tarballs/vite.config.tsConfigures Vite to build the SPA into public/ without deleting tarballs.
tarballs/turbo.jsonExpands build outputs to include SPA assets and catalog.json.
tarballs/tsconfig.jsonAdds TS config for the new frontend/build files.
tarballs/src/styles.cssAdds the full styling/theme for the redesigned index UI.
tarballs/src/main.tsxBoots the SPA and loads catalog.json.
tarballs/src/icons.tsxAdds inline SVG icons used across the new UI.
tarballs/src/catalog.tsDefines shared catalog/install-command helpers for the frontend.
tarballs/src/app.tsxImplements the main tarballs UI, interactions, and package contents tables.
tarballs/scripts/pack.tsGenerates tarballs plus the new metadata-rich catalog.json.
tarballs/scripts/check-tarballs-smoke.mjsAdds smoke coverage for catalog.json.
tarballs/package.jsonSwitches build to pack.ts && vite build and adds frontend deps/scripts.
tarballs/index.htmlAdds the SPA entry HTML shell.
pnpm-lock.yamlLocks newly added frontend/tooling dependencies.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadtarballs/package.json Outdated
"scripts": {
"build": "node scripts/pack.ts",
"build": "node scripts/pack.ts && vite build",
"dev": "vite",

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. The new dev chains node scripts/pack.ts && vite, and I restructured the build layout to vite's conventional shape so it actually works in dev: public/ is a real vite public dir (pack writes tarballs + catalog.json there, vite serves them at root in dev), and dist/ is the production build output (set as Vercel's outputDirectory).

Comment threadtarballs/src/app.tsx
<SearchIcon />
<input
ref={searchRef}
type="search"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added aria-label="Filter packages" to the input.

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +178 to +185
<div class="pm-tabs" role="tablist" aria-label="Package manager">
{options.map((opt) => (
<button
key={opt}
type="button"
class="pm-tab"
role="tab"
aria-selected={value === opt}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Dropped role="tablist"/role="tab"/aria-selected and made these plain buttons with aria-pressed. Toggle buttons are the honest representation since we never implemented arrow-key roving focus. Each button also has an explicit aria-label (Show install commands for pnpm etc.).

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +428 to +440
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} finally {
ta.remove();
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Refactored to a writeToClipboard helper that returns whether the write actually succeeded; the button only flips to Copied on success. If both navigator.clipboard.writeText and the execCommand fallback fail, it briefly shows a red Failed state instead.

Comment threadtarballs/src/app.tsx Outdated
<code class="pkg-cmd">{cmd}</code>
<div class="pkg-actions">
<CopyButton text={cmd} variant="icon" />
<a class="icon-btn" href={pkg.url} download aria-label="Download">

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added an accessibleName prop to CopyButton and pass Copy install command for <pkg.name> from each row + the featured card. Same treatment on the download <a> link (Download <pkg.name> tarball).

Comment threadtarballs/src/main.tsx Outdated
if (!root) throw new Error('No #app root element');

try {
const res = await fetch('/catalog.json', { cache: 'no-store' });

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345 — dropped cache: 'no-store'. Each preview's URL is unique so the browser cache is the right thing to use.

- main.tsx: drop `cache: 'no-store'` from the catalog fetch. Each
tarballs deployment is immutable per commit, so HTTP caching is
appropriate; forcing no-store made every visit re-download the full
catalog (which now includes per-package file lists).
- app.tsx (search input): add `aria-label="Filter packages"`. The
visible label only contained an icon and placeholder, so screen
readers had no name for the control.
- app.tsx (PmTabs): replace `role="tablist"` / `role="tab"` /
`aria-selected` with plain buttons that use `aria-pressed`. The
ARIA tab pattern requires arrow-key roving focus we never wired
up; toggle buttons are the honest representation. Each button
also gets an explicit `aria-label`.
- app.tsx (row buttons): include the package name in the accessible
label of every per-row copy/download button (and on the featured
card too), so the screen reader buttons/links list distinguishes
them. Added an `accessibleName` prop to `CopyButton`.
- app.tsx (CopyButton): only flip to the "Copied" state when the
write actually succeeded. Both the modern `navigator.clipboard`
path and the `execCommand` fallback can fail; the new
`writeToClipboard` helper returns success and the button shows a
short "Failed" state if both paths fail.
# Make `pnpm dev` work from a clean checkout
The previous `dev: vite` couldn't actually serve the page because
`/catalog.json` 404s and the SPA boots into the error fallback.
Restructure the build layout to vite's conventional shape:
- `public/` is now a true vite public dir — pack writes tarballs and
catalog.json there. In dev, vite serves these at the root.
- `dist/` is the production build output (vite copies public/ into it
and adds index.html + assets/).
- `vercel.json#outputDirectory` switches from `public` → `dist`.
- `turbo.json` outputs updated to match.
- `dev` chains pack before vite so the catalog exists when the dev
server starts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​preact/​preset-vite@​2.10.59910010090100
Addednpm/​preact@​10.29.110010010094100

View full report

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pranay took the Vite + Preact refactor recommendation from my prior review and ran with it. The new shape matches what I sketched almost exactly:

tarballs/
├── index.html # 20-line entry
├── vite.config.ts # 16-line config
├── tsconfig.json # strict, jsxImportSource: preact
├── scripts/
│ ├── pack.ts # 317 lines, data-only — packs tarballs, writes catalog.json
│ └── check-tarballs-smoke.mjs # now also validates catalog.json shape
└── src/
├── main.tsx # 26 lines — fetch + mount
├── app.tsx # 550 lines — typed Preact JSX
├── catalog.ts # types + small helpers
├── icons.tsx # 8 inline SVG icons
└── styles.css # 794 lines, geist-inspired theme

Bundle ships as dist/assets/index-*.js at 26.25 kB (9.69 kB gzipped) — small for a Preact SPA with sortable tables, copy-buttons, search, and a PM toggle. CSS is 11.19 kB / 2.73 kB gzipped.

What I verified

  • Local build works: pnpm turbo build --filter=tarballs runs pack.ts then vite build, producing 26 tarballs in public/, a catalog.json matching the typed shape in src/catalog.ts, and a dist/ ready to deploy.
  • TypeScript is clean: pnpm exec tsc --noEmit from tarballs/ passes with the strict config (strict, noUnusedLocals, noUnusedParameters, isolatedModules).
  • Deployed preview serves correctly: https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/catalog.json returns the expected payload with build context (sha, branch, PR number, GitHub-linked commit/branch/PR URLs) and 26 packages with full file listings + sizes. Each package's url is the immutable per-deployment hash URL (e.g. https://workflow-tarballs-ch8zhxmen.labs.vercel.dev/workflow-ai.tgz), so the catalog is stable even if visited via the git-branch URL.
  • JS bundle contains real Preact: read the bundle, confirmed the App tree renders with hooks (useState, useEffect, useMemo, useRef), addEventListener('keydown', …) for the / shortcut, navigator.clipboard.writeText + execCommand('copy') fallback for CopyButton, and the data-copied / data-failed state machine.
  • Smoke check is stronger now: in addition to verifying tarballs are gzip-magic-prefixed and the index page responds with text/html, it now fetches catalog.json and asserts the workflow package has a non-trivial fileCount — a useful diagnostic for "packages weren't built before pack."

Concerns from my prior review — all addressed

  1. The hand-rolled HTML-in-JS approach — gone. JSX is TypeScript-checked end to end (event handlers, prop types, catalog shape).
  2. Two parallel render concerns merged into one file — split clean. pack.ts is now data-only (~317 lines, all bundling/tar-reading). The presentation layer is in src/.
  3. renderRow / applyPm / applyFilter had to stay in sync via string conventions — gone. State lives in React-style hooks, props are typed.
  4. Hand-rolled escapeHtml — gone. JSX handles escaping.
  5. escapeHtml(JSON.stringify(catalog)) breaking client interactivity — moot. The catalog is a separate /catalog.json static file fetched at runtime, so there's no inline-JSON encoding question.
  6. KB/MB labels with base-2 math — fixed. formatBytes now emits B/KiB/MiB.

Copilot review items — all addressed in 1c48345e6

  • a11y: aria-label="Filter packages" on search input; PM tabs use plain buttons with aria-pressed (no half-implemented role="tablist"); per-row copy buttons get accessibleName="Copy install command for ${pkg.name}" instead of a generic label.
  • Robustness: writeToClipboard returns a boolean so failed writes show "Failed" rather than a misleading "Copied" success state.
  • DX: dev script chains pack.ts && vite; dropped cache: 'no-store' since the catalog URL is per-deployment-immutable.

Smaller improvements landed along the way

  • Tar reader rewrite (6ae7736): replaced tar -tvzf shell-out with an in-process gunzip + 512-byte block walker. Handles GNU long-name (L) entries and pax headers, and works identically on macOS BSD tar and Linux GNU tar. Sorting files by descending size before emitting is a nice touch for the per-package "what's inside" view.
  • PackagePhobia-style SizeStats widget: two big tiles (publish size + unpacked size) with split value/unit rendering. Looks good and reads better than a one-liner.
  • FileTable with sortable columns: ARIA aria-sort on <th>, indicator arrows, click-to-toggle direction. Strips the package/ prefix from displayed paths.

Tiny things — non-blocking

  • pack.ts:117: updateDeps builds rewrite URLs as https://${process.env.VERCEL_URL}/${escapedName}.tgz directly, while baseUrl (defined at line 97) uses the VERCEL_URL ? https://… : '' fallback. Local builds without VERCEL_URL produce package.json deps like https://undefined/.... Pre-existing on main, not introduced by this PR. Not a real-world issue (nobody installs from a local pack output) but noting for a follow-up cleanup if you want consistency.
  • catalog.ts and pack.ts duplicate the same TS interfaces (TarballFile, PackedPackage, BuildContext, Catalog). Drift risk is low since they're both in the same package, but if you want to eliminate it, catalog.ts could be the single source of truth and pack.ts could import type from it. Fine to leave.
  • SizeStats is rendered for every package in the list, not just the featured one — but it's behind a <details> so it doesn't blow up initial render. The 26 expanded <details> plus the FileTable for each would render a lot of DOM if expanded all at once; in practice, users open one or two. Not blocking.

Verdict: approve

Approving and clearing my prior CHANGES_REQUESTED. The redesign hit every concern from my last two reviews plus the Copilot feedback, the build pipeline is cleaner than what was there before, and the deployment shape is preserved (public/<escaped-name>.tgz URLs unchanged, smoke checks pass). Nice work on the followups.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

@v0@vercel can you make a PR to address the remaining follow ups from @TooTallNate's review comments that we didn't get to before merge of this one

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-stableCherry-pick this PR to the stable branch when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@TooTallNate
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

tarballs: redesign preview tarballs index page - #1911

Merged
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements
May 4, 2026
Merged

tarballs: redesign preview tarballs index page#1911
pranaygp merged 9 commits into
mainfrom
pgp/tarball-page-improvements

Conversation

@pranaygp

Copy link
Copy Markdown
Contributor

Summary

Reworks the static index page generated by tarballs/scripts/pack.ts (deployed at https://workflow-tarballs.labs.vercel.dev/) to be more useful and pleasant to look at.

What's new

  • Featured workflow package at the top with a large install command, copy button, and direct tarball download — the rest of the packages are secondary.
  • Top-of-page metadata chips: short SHA (linked to commit on GitHub), branch (linked), PR number (linked), build timestamp, package count + total size.
  • Collapsible "What is this?" explainer describing how the version + dep rewriting works.
  • Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the install command shown for every row in place.
  • Live filter input over the package list, with / keyboard shortcut to focus.
  • Per-row install command, copy button, and direct download link for every package.
  • Geist-inspired theme with proper dark/light support driven by system preference.

Implementation notes

  • Pack step now records the produced tarball size via fs.stat after pnpm pack so the page can show human-readable sizes.
  • The page reads VERCEL_GIT_* env vars for commit / branch / PR links and falls back to local git rev-parse for branch when running off-Vercel.
  • All packages still get an entry, but workflow is filtered out of the bottom list and rendered separately.
  • No changes to the deployment shape — public/<escaped-name>.tgz URLs are unchanged, so existing usage keeps working.

The Vercel preview for this PR will deploy and serve the new page so you can see it live.

Test plan

  • Open the Vercel preview deployment and verify the page renders correctly in light and dark mode
  • Confirm the workflow package appears as the featured card at the top
  • Confirm SHA / branch / PR chips are present and linked
  • Toggle pnpm / npm / yarn / bun and confirm install commands update everywhere
  • Filter the package list (and try the / shortcut)
  • Click copy on a row and on the featured card
  • Click download on a row and verify the tarball downloads
  • Smoke-install the featured tarball into a fresh project and confirm it still works (pnpm i https://<deployment>/workflow.tgz)

🤖 Generated with Claude Code

Rebuild the static index page produced by `tarballs/scripts/pack.ts`:
- Featured `workflow` package up top with prominent install command,
copy button, and direct tarball download
- Top-of-page metadata chips: short SHA (linked to commit), branch,
PR number, build timestamp, package count + total size
- Collapsible "What is this?" explainer
- Package-manager tab toggle (pnpm / npm / yarn / bun) that swaps the
install command for every row in place
- Live filter input over the rest of the package list (with `/` shortcut)
- Per-row install command, copy button, and direct download
- Modern dark/light theme with system preference, Geist-inspired styling
Also captures tarball size during pack and renders human-readable byte counts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-botBot commented May 4, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 1c48345

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercelBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

ProjectDeploymentActionsUpdated (UTC)
example-nextjs-workflow-turbopackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-nextjs-workflow-webpackReadyReadyPreview, CommentMay 4, 2026 11:41am
example-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-astro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-express-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-fastify-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-hono-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nitro-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-nuxt-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-sveltekit-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-tanstack-start-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workbench-vite-workflowReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-swc-playgroundReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-tarballsReadyReadyPreview, CommentMay 4, 2026 11:41am
workflow-webReadyReadyPreview, CommentMay 4, 2026 11:41am
1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
workflow-docsSkippedSkippedMay 4, 2026 11:41am

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production85902191078
✅ 💻 Local Development95702191176
✅ 📦 Local Production95702191176
✅ 🐘 Local Postgres95702191176
✅ 🪟 Windows980098
✅ 📋 Other5100176686
Total4338010525390

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro72026
✅ example72026
✅ express72026
✅ fastify72026
✅ hono72026
✅ nextjs-turbopack9602
✅ nextjs-webpack9602
✅ nitro72026
✅ nuxt72026
✅ sveltekit9107
✅ vite72026
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable73025
✅ express-stable73025
✅ fastify-stable73025
✅ hono-stable73025
✅ nextjs-turbopack-canary79019
✅ nextjs-turbopack-stable9800
✅ nextjs-webpack-canary79019
✅ nextjs-webpack-stable9800
✅ nitro-stable73025
✅ nuxt-stable73025
✅ sveltekit-stable9206
✅ vite-stable73025
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9800
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable73025
✅ e2e-local-dev-tanstack-start-stable73025
✅ e2e-local-postgres-nest-stable73025
✅ e2e-local-postgres-tanstack-start-stable73025
✅ e2e-local-prod-nest-stable73025
✅ e2e-local-prod-tanstack-start-stable73025
✅ e2e-vercel-prod-tanstack-start72026

📋 View full workflow run

@github-actions

github-actionsBot commented May 4, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Express0.031s (-30.2% 🟢)1.005s (~)0.974s101.00x
💻 LocalNitro0.032s (-24.8% 🟢)1.005s (~)0.973s101.05x
🐘 PostgresExpress0.043s (-26.4% 🟢)1.010s (~)0.967s101.38x
💻 LocalNext.js (Turbopack)0.047s1.005s0.958s101.51x
🐘 PostgresNitro0.052s (-45.6% 🟢)1.012s (-3.0%)0.960s101.68x
🌐 RedisNext.js (Turbopack)0.059s1.005s0.947s101.89x
🐘 PostgresNext.js (Turbopack)0.059s1.011s0.952s101.91x
🌐 MongoDBNext.js (Turbopack)0.100s1.009s0.910s103.23x
workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.068s (-5.6% 🟢)2.006s (~)0.938s101.00x
💻 LocalExpress1.074s (-4.6%)2.006s (~)0.932s101.01x
🐘 PostgresExpress1.081s (-5.7% 🟢)2.009s (~)0.927s101.01x
🐘 PostgresNitro1.088s (-4.6%)2.010s (~)0.922s101.02x
💻 LocalNext.js (Turbopack)1.108s2.006s0.898s101.04x
🌐 RedisNext.js (Turbopack)1.112s2.007s0.895s101.04x
🐘 PostgresNext.js (Turbopack)1.117s2.009s0.892s101.05x
🌐 MongoDBNext.js (Turbopack)1.166s2.009s0.843s101.09x
workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.387s (-5.1% 🟢)11.022s (~)0.634s31.00x
💻 LocalExpress10.405s (-4.7%)11.023s (~)0.618s31.00x
🐘 PostgresExpress10.415s (-5.0% 🟢)11.012s (~)0.597s31.00x
🐘 PostgresNitro10.432s (-4.0%)11.018s (~)0.585s31.00x
🌐 RedisNext.js (Turbopack)10.634s11.023s0.389s31.02x
💻 LocalNext.js (Turbopack)10.690s11.023s0.333s31.03x
🐘 PostgresNext.js (Turbopack)10.716s11.017s0.301s31.03x
🌐 MongoDBNext.js (Turbopack)10.822s11.017s0.195s31.04x
workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro13.426s (-10.9% 🟢)14.027s (-12.5% 🟢)0.600s51.00x
🐘 PostgresExpress13.463s (-7.7% 🟢)14.021s (-6.7% 🟢)0.558s51.00x
💻 LocalExpress13.480s (-10.0% 🟢)14.028s (-6.7% 🟢)0.548s51.00x
🐘 PostgresNitro13.544s (-7.2% 🟢)14.019s (-6.7% 🟢)0.475s51.01x
💻 LocalNext.js (Turbopack)14.031s15.028s0.997s41.05x
🌐 RedisNext.js (Turbopack)14.036s14.628s0.592s51.05x
🐘 PostgresNext.js (Turbopack)14.130s15.022s0.892s41.05x
🌐 MongoDBNext.js (Turbopack)14.245s15.023s0.778s41.06x
workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro11.809s (-29.6% 🟢)12.022s (-29.4% 🟢)0.214s81.00x
🐘 PostgresNitro11.928s (-14.6% 🟢)12.017s (-16.0% 🟢)0.089s81.01x
🐘 PostgresExpress11.967s (-14.6% 🟢)12.265s (-16.0% 🟢)0.298s81.01x
💻 LocalExpress12.248s (-26.2% 🟢)12.774s (-25.0% 🟢)0.526s81.04x
💻 LocalNext.js (Turbopack)13.061s13.737s0.677s71.11x
🌐 RedisNext.js (Turbopack)13.217s13.883s0.666s71.12x
🌐 MongoDBNext.js (Turbopack)13.255s14.019s0.764s71.12x
🐘 PostgresNext.js (Turbopack)13.304s14.021s0.718s71.13x
Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.141s (-9.4% 🟢)2.006s (~)0.865s151.00x
🐘 PostgresNitro1.146s (-10.1% 🟢)2.007s (~)0.861s151.00x
💻 LocalNitro1.171s (-28.2% 🟢)2.006s (-3.3%)0.835s151.03x
💻 LocalExpress1.174s (-21.1% 🟢)2.006s (~)0.832s151.03x
🐘 PostgresNext.js (Turbopack)1.217s2.007s0.790s151.07x
🌐 RedisNext.js (Turbopack)1.240s2.006s0.766s151.09x
💻 LocalNext.js (Turbopack)1.289s2.006s0.717s151.13x
🌐 MongoDBNext.js (Turbopack)2.035s2.826s0.791s111.78x
Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-49.1% 🟢)2.007s (-33.3% 🟢)0.805s151.00x
🐘 PostgresNitro1.229s (-47.7% 🟢)2.007s (-33.3% 🟢)0.778s151.02x
🐘 PostgresNext.js (Turbopack)1.358s2.008s0.650s151.13x
💻 LocalNitro1.685s (-46.4% 🟢)2.006s (-48.4% 🟢)0.320s151.40x
💻 LocalExpress1.706s (-42.2% 🟢)2.006s (-41.9% 🟢)0.299s151.42x
💻 LocalNext.js (Turbopack)1.858s2.292s0.434s141.55x
🌐 RedisNext.js (Turbopack)2.337s3.008s0.671s101.95x
🌐 MongoDBNext.js (Turbopack)3.568s4.009s0.441s82.97x
Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.305s (-62.6% 🟢)2.007s (-50.0% 🟢)0.702s151.00x
🐘 PostgresNitro1.383s (-60.3% 🟢)2.007s (-49.9% 🟢)0.624s151.06x
🐘 PostgresNext.js (Turbopack)1.629s2.007s0.378s151.25x
🌐 RedisNext.js (Turbopack)3.602s4.010s0.408s82.76x
💻 LocalNext.js (Turbopack)4.383s5.011s0.627s63.36x
💻 LocalExpress4.397s (-47.3% 🟢)5.013s (-44.5% 🟢)0.616s63.37x
💻 LocalNitro4.571s (-45.2% 🟢)5.011s (-44.4% 🟢)0.440s63.50x
🌐 MongoDBNext.js (Turbopack)6.273s7.013s0.739s54.81x
Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.139s (-9.4% 🟢)2.009s (~)0.869s151.00x
🐘 PostgresNitro1.168s (-7.1% 🟢)2.009s (~)0.840s151.03x
🌐 RedisNext.js (Turbopack)1.233s2.006s0.773s151.08x
🐘 PostgresNext.js (Turbopack)1.248s2.007s0.760s151.10x
💻 LocalNext.js (Turbopack)1.321s2.005s0.684s151.16x
💻 LocalExpress1.352s (-28.6% 🟢)2.006s (-15.1% 🟢)0.654s151.19x
💻 LocalNitro1.379s (-26.1% 🟢)2.006s (-14.3% 🟢)0.627s151.21x
🌐 MongoDBNext.js (Turbopack)2.026s2.735s0.709s111.78x
Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.211s (-48.3% 🟢)2.008s (-33.3% 🟢)0.797s151.00x
🐘 PostgresNitro1.232s (-47.3% 🟢)2.008s (-33.3% 🟢)0.776s151.02x
🐘 PostgresNext.js (Turbopack)1.368s2.008s0.640s151.13x
💻 LocalExpress1.838s (-41.3% 🟢)2.075s (-44.8% 🟢)0.238s151.52x
💻 LocalNitro1.954s (-36.3% 🟢)2.469s (-36.5% 🟢)0.515s131.61x
💻 LocalNext.js (Turbopack)2.083s2.826s0.743s111.72x
🌐 RedisNext.js (Turbopack)2.350s3.008s0.658s101.94x
🌐 MongoDBNext.js (Turbopack)3.547s4.008s0.461s82.93x
Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.318s (-62.3% 🟢)2.007s (-50.0% 🟢)0.690s151.00x
🐘 PostgresNitro1.379s (-60.4% 🟢)2.007s (-49.9% 🟢)0.628s151.05x
🐘 PostgresNext.js (Turbopack)1.718s2.075s0.357s151.30x
🌐 RedisNext.js (Turbopack)3.595s4.010s0.415s82.73x
💻 LocalNitro4.877s (-46.7% 🟢)5.516s (-45.0% 🟢)0.639s63.70x
💻 LocalExpress5.136s (-41.6% 🟢)5.515s (-40.5% 🟢)0.379s63.90x
💻 LocalNext.js (Turbopack)5.496s6.011s0.515s54.17x
🌐 MongoDBNext.js (Turbopack)6.286s7.013s0.727s54.77x
workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.430s (-48.7% 🟢)1.006s (-1.7%)0.575s601.00x
💻 LocalNitro0.457s (-53.4% 🟢)1.004s (-8.3% 🟢)0.547s601.06x
🐘 PostgresNitro0.483s (-41.1% 🟢)1.023s (+1.7%)0.540s591.12x
💻 LocalExpress0.509s (-48.3% 🟢)1.004s (-6.7% 🟢)0.495s601.18x
🌐 RedisNext.js (Turbopack)0.617s1.004s0.388s601.43x
🐘 PostgresNext.js (Turbopack)0.697s1.007s0.310s601.62x
💻 LocalNext.js (Turbopack)0.712s1.004s0.292s601.66x
🌐 MongoDBNext.js (Turbopack)0.739s1.006s0.266s601.72x
workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.027s (-48.0% 🟢)1.586s (-29.8% 🟢)0.559s571.00x
🐘 PostgresNitro1.093s (-43.3% 🟢)1.741s (-17.1% 🟢)0.648s521.06x
💻 LocalNitro1.156s (-61.9% 🟢)2.006s (-46.6% 🟢)0.849s451.13x
💻 LocalExpress1.191s (-60.5% 🟢)2.005s (-44.1% 🟢)0.814s451.16x
🌐 RedisNext.js (Turbopack)1.479s2.006s0.527s451.44x
🐘 PostgresNext.js (Turbopack)1.638s2.007s0.369s451.60x
💻 LocalNext.js (Turbopack)1.780s2.027s0.247s451.73x
🌐 MongoDBNext.js (Turbopack)1.811s2.007s0.196s451.76x
workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.012s (-49.6% 🟢)2.427s (-44.5% 🟢)0.415s501.00x
🐘 PostgresNitro2.035s (-50.4% 🟢)2.457s (-46.6% 🟢)0.422s491.01x
💻 LocalNitro2.638s (-71.6% 🟢)3.007s (-70.0% 🟢)0.369s401.31x
💻 LocalExpress2.802s (-69.6% 🟢)3.084s (-69.2% 🟢)0.283s391.39x
🌐 RedisNext.js (Turbopack)3.003s3.277s0.275s371.49x
🐘 PostgresNext.js (Turbopack)3.192s4.010s0.817s301.59x
💻 LocalNext.js (Turbopack)3.864s4.145s0.282s291.92x
🌐 MongoDBNext.js (Turbopack)4.155s5.011s0.856s242.07x
workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.172s (-39.1% 🟢)1.006s (~)0.834s601.00x
🐘 PostgresNitro0.192s (-32.2% 🟢)1.006s (~)0.814s601.12x
🐘 PostgresNext.js (Turbopack)0.241s1.006s0.764s601.40x
🌐 RedisNext.js (Turbopack)0.251s1.004s0.753s601.46x
💻 LocalNitro0.432s (-28.6% 🟢)1.004s (-1.7%)0.572s602.51x
💻 LocalExpress0.466s (-16.9% 🟢)1.004s (~)0.539s602.71x
💻 LocalNext.js (Turbopack)0.567s1.004s0.437s603.30x
🌐 MongoDBNext.js (Turbopack)1.041s1.882s0.842s326.05x
workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.296s (-41.8% 🟢)1.006s (~)0.710s901.00x
🐘 PostgresNitro0.314s (-36.8% 🟢)1.006s (~)0.693s901.06x
🌐 RedisNext.js (Turbopack)0.414s1.004s0.590s901.40x
🐘 PostgresNext.js (Turbopack)0.461s1.006s0.545s901.55x
💻 LocalNitro2.179s (-14.1% 🟢)2.852s (-5.2% 🟢)0.672s327.35x
💻 LocalNext.js (Turbopack)2.182s2.944s0.762s317.36x
💻 LocalExpress2.223s (-11.6% 🟢)2.821s (-6.3% 🟢)0.598s327.50x
🌐 MongoDBNext.js (Turbopack)2.618s3.007s0.389s308.83x
workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.578s (-29.3% 🟢)1.014s (~)0.436s1191.00x
🐘 PostgresNitro0.641s (-18.9% 🟢)1.006s (~)0.365s1201.11x
🌐 RedisNext.js (Turbopack)0.770s1.004s0.234s1201.33x
🐘 PostgresNext.js (Turbopack)0.957s1.352s0.394s901.65x
🌐 MongoDBNext.js (Turbopack)5.396s6.011s0.615s209.33x
💻 LocalExpress9.835s (-12.1% 🟢)10.444s (-12.5% 🟢)0.609s1217.00x
💻 LocalNitro9.941s (-11.2% 🟢)10.365s (-11.1% 🟢)0.424s1217.19x
💻 LocalNext.js (Turbopack)11.011s11.573s0.562s1119.03x
Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.127s (+449.7% 🔺)2.001s (+100.3% 🔺)0.001s (-37.5% 🟢)2.009s (+98.6% 🔺)0.882s101.00x
💻 LocalNitro1.128s (+428.0% 🔺)2.005s (+99.6% 🔺)0.011s (-15.2% 🟢)2.018s (+98.1% 🔺)0.890s101.00x
🐘 PostgresNitro1.135s (+453.5% 🔺)2.000s (+100.1% 🔺)0.001s (-6.7% 🟢)2.010s (+98.7% 🔺)0.875s101.01x
💻 LocalExpress1.140s (+472.4% 🔺)2.005s (+99.6% 🔺)0.013s (+3.3%)2.020s (+98.4% 🔺)0.881s101.01x
💻 LocalNext.js (Turbopack)1.190s2.003s0.010s2.017s0.826s101.06x
🐘 PostgresNext.js (Turbopack)1.197s2.001s0.002s2.011s0.815s101.06x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.482s (+135.3% 🔺)2.006s (+99.3% 🔺)0.003s (-10.4% 🟢)2.022s (+97.7% 🔺)0.540s301.00x
🐘 PostgresNitro1.525s (+144.4% 🔺)2.002s (+98.9% 🔺)0.004s (+0.8%)2.026s (+98.2% 🔺)0.501s301.03x
💻 LocalNext.js (Turbopack)1.670s2.009s0.011s2.023s0.352s301.13x
🐘 PostgresNext.js (Turbopack)1.682s2.010s0.004s2.025s0.343s301.13x
💻 LocalExpress1.705s (+125.2% 🔺)2.011s (+95.4% 🔺)0.010s (+8.5% 🔺)2.202s (+111.8% 🔺)0.498s281.15x
💻 LocalNitro1.713s (+104.2% 🔺)2.010s (+98.6% 🔺)0.010s (+4.8%)2.201s (+97.2% 🔺)0.488s281.16x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.622s (-35.2% 🟢)1.013s (-20.7% 🟢)0.000s (+16.9% 🔺)1.028s (-21.3% 🟢)0.406s591.00x
🐘 PostgresNitro0.681s (-29.7% 🟢)1.047s (-16.1% 🟢)0.000s (-58.6% 🟢)1.059s (-15.8% 🟢)0.378s581.09x
🐘 PostgresNext.js (Turbopack)0.817s1.053s0.000s1.068s0.251s571.31x
💻 LocalNitro1.331s (+8.8% 🔺)2.015s (~)0.000s (+66.7% 🔺)2.017s (~)0.686s302.14x
💻 LocalExpress1.394s (+13.8% 🔺)2.015s (~)0.000s (+10.0% 🔺)2.017s (~)0.623s302.24x
💻 LocalNext.js (Turbopack)1.437s2.013s0.000s2.016s0.579s302.31x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----
fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.313s (-26.7% 🟢)2.099s (-2.0%)0.000s (-3.4%)2.131s (-2.0%)0.818s291.00x
🐘 PostgresExpress1.349s (-23.9% 🟢)2.145s (-1.5%)0.000s (+Infinity% 🔺)2.161s (-1.7%)0.812s281.03x
🐘 PostgresNext.js (Turbopack)1.689s2.225s0.000s2.273s0.584s271.29x
💻 LocalNext.js (Turbopack)2.563s3.079s0.001s3.084s0.521s201.95x
💻 LocalNitro3.092s (-8.7% 🟢)3.840s (-4.8%)0.000s (-76.6% 🟢)3.844s (-4.8%)0.752s162.35x
💻 LocalExpress3.227s (-6.9% 🟢)4.031s (~)0.000s (-66.7% 🟢)4.033s (~)0.806s152.46x
🌐 MongoDBNext.js (Turbopack)⚠️missing-----
🌐 RedisNext.js (Turbopack)⚠️missing-----

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro14/21
🐘 PostgresExpress19/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres18/21
Next.js (Turbopack)🐘 Postgres8/21
Nitro🐘 Postgres14/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run


Some benchmark jobs failed:

  • Local: success
  • Postgres: success
  • Vercel: failure

Check the workflow run for details.

Comment threadtarballs/scripts/pack.ts Outdated

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The visual design is nice — featured workflow card, package-manager toggle, live filter, copy buttons, geist-inspired theme. Going to approve since the deployment shape doesn't change and this only affects the static index page.

That said: I'd push back on the hand-rolled HTML-in-JS approach for anything beyond a one-page diff. The new file is 857 lines, and the implementation has some smells that follow naturally from the medium:

  • The inline <script> block (~80 lines) isn't TypeScript-checked. It's inside a template literal in writeIndexHtml, so the compiler treats it as a string. Refactoring (rename a data- attribute, change a property name) won't surface type errors. The TS half references DOM via data- attributes that have to match by convention.
  • Two parallel "render the page" concerns — the pack.ts script handles both bundling-time logic (scanning packages, rewriting deps, packing tarballs) and presentation logic (HTML composition, CSS, client-side JS for PM toggle/filter/copy). Those were independent before this PR; conflating them makes both harder to evolve.
  • renderFeatured/renderRow and the inline applyPm/applyFilter script must stay in sync on attribute names, class names, and the catalog JSON shape. Right now there's a hand-rolled escapeHtml helper and the contract is "data-install-cmd must equal escapedName." Easy to break in a refactor; no tests catch the drift.
  • Hand-rolled escape helpers (escapeHtml) are correct here but would be unnecessary with JSX or a templating library.

A lightweight Vite + React (or Preact for smaller bundle) SPA would clean this up substantially. The shape I'd suggest:

tarballs/
├── package.json # adds vite, react, @vitejs/plugin-react
├── vite.config.ts # static build → public/
├── index.html # entry HTML
├── src/
│ ├── main.tsx # mount React
│ ├── App.tsx # the page (~80 lines instead of ~600 of HTML+CSS+JS)
│ ├── catalog.ts # types for the JSON catalog
│ └── styles.css # the geist-inspired theme
└── scripts/
├── pack.ts # data-only: scans packages, packs tarballs, writes
│ # public/catalog.json with the build context
└── check-tarballs-smoke.mjs

pack.ts becomes ~140 lines (just the bundling logic), vite build produces the static page that fetches catalog.json, the React component reads it on mount and renders. State (active PM, search query) is useState. Build pipeline is pnpm pack → vite build, and Vercel's existing outputDirectory: "public" works unchanged.

DX wins: HMR while iterating on the design, TypeScript checks the JSX and event handlers, can pull in actual geist (or lucide-react) icons via npm instead of inlined SVG, hot-swap component libraries if the design evolves.

If you don't want to take that on now, this is fine as-is — the page works, the design looks good, and the deployment shape is preserved. But suggest filing a follow-up to migrate before adding the next 200 lines of HTML to this file.

Smaller things I noticed

  • getBuildContext uses process.env.VERCEL_GIT_* for SHA/branch/PR. Local builds get only the SHA/branch fallback via git rev-parse. Worth double-checking the labels on chips degrade gracefully when those env vars are missing — looks like they do (early-return paths with if (ctx.commitUrl)), but a quick local pnpm --filter tarballs build test before merge would confirm.
  • The catalog JSON is embedded via <script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>. JSON-inside-HTML-inside-template-literal is two layers of escape (the escapeHtml handles the outer, but the JSON itself can contain </script> if a package description has one — unlikely but possible). Safer to use JSON.stringify(catalog).replace(/</g, '\\u003c') for the embedded JSON, which escapes the dangerous bytes without affecting parseability.
  • formatBytes uses base-2 units (KB = 1024) but labels them as 'KB'/'MB' which by convention suggest base-10. Minor but KiB/MiB would be more precise (or use base-10 with 1000).
  • applyFilter runs on every keystroke against a re-querying Array.from(document.querySelectorAll('.pkg-row')). With ~25 packages this is fine; if the catalog grows substantially, debounce or pre-compute.

Test plan note

The PR description has 8 unchecked test-plan items. They're all manual visual checks (page renders, filter works, copy fires, etc.). Since the Vercel preview URL is the canonical way to validate this, they probably won't be checked off in advance — but worth at least running through them on the preview before merge.

Comment threadtarballs/scripts/pack.ts Outdated
Built ${ctx.commitUrl ? `from <a href="${escapeHtml(ctx.commitUrl)}" target="_blank" rel="noopener">${escapeHtml(ctx.shortSha)}</a>` : `from <code>${escapeHtml(ctx.shortSha)}</code>`} · ${escapeHtml(packages.length.toString())} packages totaling ${escapeHtml(formatBytes(totalSize))}
</footer>
</div>
<script id="catalog" type="application/json">${escapeHtml(JSON.stringify(catalog))}</script>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this line breaks all client-side interactivity.

Verified on the preview deployment with browser automation — every interactive feature is non-functional:

  • ❌ Package-manager toggle (clicking npm/yarn/bun does nothing)
  • ❌ Live filter (typing in search shows all rows)
  • ❌ Copy buttons (no clipboard write, no Copied state)
  • / keyboard shortcut (body keeps focus)

Root cause: escapeHtml(JSON.stringify(catalog)) HTML-encodes the JSON. The serialized catalog ends up in the page as:

<scriptid="catalog" type="application/json">[{&quot;name&quot;:&quot;@workflow/ai&quot;,&quot;escapedName&quot;:&quot;workflow-ai&quot;...

JSON.parse(textContent) then throws on the very first &quot; because &quot; isn't valid JSON syntax. The IIFE bails on its first line and none of the event listeners get attached. Confirmed live:

>JSON.parse(document.getElementById('catalog').textContent)Uncaught SyntaxError: Expectedpropertynameor'}'inJSONatposition2(line1column3)

<script type="application/json"> content is treated as text by the HTML parser — it does NOT need HTML entity escaping. The only sequence that can break out of it is </script> (or </ in some legacy parsers). The narrowest correct fix is:

<scriptid="catalog"type="application/json">${JSON.stringify(catalog).replace(/</g,'\u003c')}</script>

That escapes < bytes to \u003c in the JSON (legal per the spec — JSON allows \uXXXX escapes for any character), so </script> can't appear in the text and the JSON parses cleanly.

This was the third bullet in my "smaller things I noticed" section above — turns out it's not theoretical, it's the live bug breaking everything client-side. Should not merge until the page works in a real browser.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 3df48a7. Went with your replace(/</g, '\u003c') over the bot's </-only escape since it also covers a stray < inside, e.g., a package description. Verified locally that the embedded JSON parses cleanly out of the rendered HTML; preview deploy will validate the four interactive features end-to-end.

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updating my prior review to REQUEST_CHANGES. Ran the visual test plan against the preview deployment with browser automation and discovered that all client-side interactivity is broken — see the inline comment on pack.ts:696 for the root cause and fix.

The escapeHtml(JSON.stringify(catalog)) call HTML-encodes every " in the JSON to &quot;, so JSON.parse(textContent) throws on its first character. The IIFE bails immediately and none of the event listeners (PM toggle, search filter, copy buttons, / shortcut) get attached.

Test plan results from the preview at https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/:

  • ✅ Page renders correctly in light mode (visual layout, chips, featured card, package list)
  • workflow package appears as the featured card at the top
  • ✅ SHA / branch / timestamp / package count chips are present
  • Package-manager toggle doesn't update install commandsaria-selected stays on pnpm regardless of clicks
  • Live filter doesn't filter — typing in the search input shows all rows unchanged
  • Copy buttons don't fire — no Copied state, no clipboard write
  • / keyboard shortcut doesn't focus search — body keeps focus
  • (couldn't test direct download because earlier broken state interfered, but the <a download> is plain HTML and probably works)

So the page that ships looks correct visually but is functionally a static catalog — the toggle, filter, and copy buttons are all dead UI.

Quick fix at the call site (one line). After fix, all four interactive features should work. Worth re-testing on the next preview deployment.

The other points from my prior review (Vite + React refactor suggestion, the smaller TS-checkable concerns, KB vs KiB nit) all still stand and are non-blocking. Just this one issue blocks merging.

`escapeHtml(JSON.stringify(catalog))` was HTML-encoding every quote in
the embedded catalog JSON to `&quot;`, so `JSON.parse(textContent)` threw
on the first character and the IIFE bailed before attaching any event
listeners — package-manager toggle, search filter, copy buttons, and the
`/` shortcut were all dead UI on the deployed page.
`<script type="application/json">` content is treated as text by the HTML
parser; the only sequence that can break out is `</script>` (or `</`
in legacy parsers). Replace `<` with the JSON `<` escape, which is
legal per the JSON spec and prevents the breakout without needing entity
encoding.
Also switch `formatBytes` from `KB`/`MB` to `KiB`/`MiB` since the
divisor is 1024.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment threadtarballs/scripts/pack.ts Outdated
The smoke check broke in CI: `'workflow' tarball only has 0 files`.
Root cause is that `tar -tvzf` emits a different verbose layout on GNU
tar (Linux, what CI runs) vs BSD tar (macOS, where I tested locally) —
the parser only matched the BSD column ordering, so on Linux every line
was rejected and `fileCount` came out as 0.
Replace the shell-out with a small in-process tar reader using
`zlib.gunzipSync` + manual 512-byte block walk. ustar headers are
trivially structured (name at offset 0, octal size at 124, typeflag at
156, ustar prefix at 345). We emit regular files only (`typeflag` `0`
or NUL) and consume but skip pax extended headers (`x`/`g`) and GNU
long-name entries (`L`). Result is identical on every platform.
Verified locally: 206 files / 998413 bytes for `workflow.tgz` matches
`tar -tvzf` exactly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
pranaygpand others added 3 commits May 4, 2026 19:31
The previous "What's inside?" view crammed nested directory groups,
proportional bars, and per-group file lists into a `<details>` inside
an already-narrow row. It was hard to read and harder to compare.
Replace it with the layout packagephobia uses on its result page:
- Two large headline metric tiles (Publish size / Unpacked size)
with a big bold value, smaller unit, and small uppercase label.
Modeled directly on packagephobia's `Stats` component but using
our existing CSS variables so it tracks light/dark theme.
- A single sortable file table beneath. Default is size-descending so
the contributors to package size are immediately visible. Click a
header to flip direction or switch sort key. Sticky header keeps
the columns visible inside the scrollable region.
Drop the `groupByTopLevel`, `ContentsGroup`, and bar-chart styles —
they were the source of the "hard to use" feedback and don't add
information that the flat sortable table doesn't already convey.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR replaces the old static tarballs index with a Vite/Preact single-page UI that reads a generated catalog.json, surfaces richer build/package metadata, and adds interactive install/copy/filter/download affordances for preview tarballs.

Changes:

  • Adds a Vite + Preact frontend for the tarballs index, including featured package rendering, filtering, package-manager toggles, and package contents breakdowns.
  • Extends scripts/pack.ts to emit catalog.json with build metadata, tarball sizes, unpacked sizes, and per-file listings.
  • Updates build/smoke-test plumbing so the tarballs deployment now publishes bundled SPA assets plus the generated catalog.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
tarballs/vite.config.tsConfigures Vite to build the SPA into public/ without deleting tarballs.
tarballs/turbo.jsonExpands build outputs to include SPA assets and catalog.json.
tarballs/tsconfig.jsonAdds TS config for the new frontend/build files.
tarballs/src/styles.cssAdds the full styling/theme for the redesigned index UI.
tarballs/src/main.tsxBoots the SPA and loads catalog.json.
tarballs/src/icons.tsxAdds inline SVG icons used across the new UI.
tarballs/src/catalog.tsDefines shared catalog/install-command helpers for the frontend.
tarballs/src/app.tsxImplements the main tarballs UI, interactions, and package contents tables.
tarballs/scripts/pack.tsGenerates tarballs plus the new metadata-rich catalog.json.
tarballs/scripts/check-tarballs-smoke.mjsAdds smoke coverage for catalog.json.
tarballs/package.jsonSwitches build to pack.ts && vite build and adds frontend deps/scripts.
tarballs/index.htmlAdds the SPA entry HTML shell.
pnpm-lock.yamlLocks newly added frontend/tooling dependencies.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment threadtarballs/package.json Outdated
"scripts": {
"build": "node scripts/pack.ts",
"build": "node scripts/pack.ts && vite build",
"dev": "vite",

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. The new dev chains node scripts/pack.ts && vite, and I restructured the build layout to vite's conventional shape so it actually works in dev: public/ is a real vite public dir (pack writes tarballs + catalog.json there, vite serves them at root in dev), and dist/ is the production build output (set as Vercel's outputDirectory).

Comment threadtarballs/src/app.tsx
<SearchIcon />
<input
ref={searchRef}
type="search"

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added aria-label="Filter packages" to the input.

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +178 to +185
<div class="pm-tabs" role="tablist" aria-label="Package manager">
{options.map((opt) => (
<button
key={opt}
type="button"
class="pm-tab"
role="tab"
aria-selected={value === opt}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Dropped role="tablist"/role="tab"/aria-selected and made these plain buttons with aria-pressed. Toggle buttons are the honest representation since we never implemented arrow-key roving focus. Each button also has an explicit aria-label (Show install commands for pnpm etc.).

Comment threadtarballs/src/app.tsx Outdated
Comment on lines +428 to +440
try {
await navigator.clipboard.writeText(text);
} catch {
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
try {
document.execCommand('copy');
} finally {
ta.remove();
}
}

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Refactored to a writeToClipboard helper that returns whether the write actually succeeded; the button only flips to Copied on success. If both navigator.clipboard.writeText and the execCommand fallback fail, it briefly shows a red Failed state instead.

Comment threadtarballs/src/app.tsx Outdated
<code class="pkg-cmd">{cmd}</code>
<div class="pkg-actions">
<CopyButton text={cmd} variant="icon" />
<a class="icon-btn" href={pkg.url} download aria-label="Download">

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345. Added an accessibleName prop to CopyButton and pass Copy install command for <pkg.name> from each row + the featured card. Same treatment on the download <a> link (Download <pkg.name> tarball).

Comment threadtarballs/src/main.tsx Outdated
if (!root) throw new Error('No #app root element');

try {
const res = await fetch('/catalog.json', { cache: 'no-store' });

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 1c48345 — dropped cache: 'no-store'. Each preview's URL is unique so the browser cache is the right thing to use.

- main.tsx: drop `cache: 'no-store'` from the catalog fetch. Each
tarballs deployment is immutable per commit, so HTTP caching is
appropriate; forcing no-store made every visit re-download the full
catalog (which now includes per-package file lists).
- app.tsx (search input): add `aria-label="Filter packages"`. The
visible label only contained an icon and placeholder, so screen
readers had no name for the control.
- app.tsx (PmTabs): replace `role="tablist"` / `role="tab"` /
`aria-selected` with plain buttons that use `aria-pressed`. The
ARIA tab pattern requires arrow-key roving focus we never wired
up; toggle buttons are the honest representation. Each button
also gets an explicit `aria-label`.
- app.tsx (row buttons): include the package name in the accessible
label of every per-row copy/download button (and on the featured
card too), so the screen reader buttons/links list distinguishes
them. Added an `accessibleName` prop to `CopyButton`.
- app.tsx (CopyButton): only flip to the "Copied" state when the
write actually succeeded. Both the modern `navigator.clipboard`
path and the `execCommand` fallback can fail; the new
`writeToClipboard` helper returns success and the button shows a
short "Failed" state if both paths fail.
# Make `pnpm dev` work from a clean checkout
The previous `dev: vite` couldn't actually serve the page because
`/catalog.json` 404s and the SPA boots into the error fallback.
Restructure the build layout to vite's conventional shape:
- `public/` is now a true vite public dir — pack writes tarballs and
catalog.json there. In dev, vite serves these at the root.
- `dist/` is the production build output (vite copies public/ into it
and adds index.html + assets/).
- `vercel.json#outputDirectory` switches from `public` → `dist`.
- `turbo.json` outputs updated to match.
- `dev` chains pack before vite so the catalog exists when the dev
server starts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

DiffPackageSupply Chain
Security
VulnerabilityQualityMaintenanceLicense
Addednpm/​@​preact/​preset-vite@​2.10.59910010090100
Addednpm/​preact@​10.29.110010010094100

View full report

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pranay took the Vite + Preact refactor recommendation from my prior review and ran with it. The new shape matches what I sketched almost exactly:

tarballs/
├── index.html # 20-line entry
├── vite.config.ts # 16-line config
├── tsconfig.json # strict, jsxImportSource: preact
├── scripts/
│ ├── pack.ts # 317 lines, data-only — packs tarballs, writes catalog.json
│ └── check-tarballs-smoke.mjs # now also validates catalog.json shape
└── src/
├── main.tsx # 26 lines — fetch + mount
├── app.tsx # 550 lines — typed Preact JSX
├── catalog.ts # types + small helpers
├── icons.tsx # 8 inline SVG icons
└── styles.css # 794 lines, geist-inspired theme

Bundle ships as dist/assets/index-*.js at 26.25 kB (9.69 kB gzipped) — small for a Preact SPA with sortable tables, copy-buttons, search, and a PM toggle. CSS is 11.19 kB / 2.73 kB gzipped.

What I verified

  • Local build works: pnpm turbo build --filter=tarballs runs pack.ts then vite build, producing 26 tarballs in public/, a catalog.json matching the typed shape in src/catalog.ts, and a dist/ ready to deploy.
  • TypeScript is clean: pnpm exec tsc --noEmit from tarballs/ passes with the strict config (strict, noUnusedLocals, noUnusedParameters, isolatedModules).
  • Deployed preview serves correctly: https://workflow-tarballs-git-pgp-tarball-page-improvements.labs.vercel.dev/catalog.json returns the expected payload with build context (sha, branch, PR number, GitHub-linked commit/branch/PR URLs) and 26 packages with full file listings + sizes. Each package's url is the immutable per-deployment hash URL (e.g. https://workflow-tarballs-ch8zhxmen.labs.vercel.dev/workflow-ai.tgz), so the catalog is stable even if visited via the git-branch URL.
  • JS bundle contains real Preact: read the bundle, confirmed the App tree renders with hooks (useState, useEffect, useMemo, useRef), addEventListener('keydown', …) for the / shortcut, navigator.clipboard.writeText + execCommand('copy') fallback for CopyButton, and the data-copied / data-failed state machine.
  • Smoke check is stronger now: in addition to verifying tarballs are gzip-magic-prefixed and the index page responds with text/html, it now fetches catalog.json and asserts the workflow package has a non-trivial fileCount — a useful diagnostic for "packages weren't built before pack."

Concerns from my prior review — all addressed

  1. The hand-rolled HTML-in-JS approach — gone. JSX is TypeScript-checked end to end (event handlers, prop types, catalog shape).
  2. Two parallel render concerns merged into one file — split clean. pack.ts is now data-only (~317 lines, all bundling/tar-reading). The presentation layer is in src/.
  3. renderRow / applyPm / applyFilter had to stay in sync via string conventions — gone. State lives in React-style hooks, props are typed.
  4. Hand-rolled escapeHtml — gone. JSX handles escaping.
  5. escapeHtml(JSON.stringify(catalog)) breaking client interactivity — moot. The catalog is a separate /catalog.json static file fetched at runtime, so there's no inline-JSON encoding question.
  6. KB/MB labels with base-2 math — fixed. formatBytes now emits B/KiB/MiB.

Copilot review items — all addressed in 1c48345e6

  • a11y: aria-label="Filter packages" on search input; PM tabs use plain buttons with aria-pressed (no half-implemented role="tablist"); per-row copy buttons get accessibleName="Copy install command for ${pkg.name}" instead of a generic label.
  • Robustness: writeToClipboard returns a boolean so failed writes show "Failed" rather than a misleading "Copied" success state.
  • DX: dev script chains pack.ts && vite; dropped cache: 'no-store' since the catalog URL is per-deployment-immutable.

Smaller improvements landed along the way

  • Tar reader rewrite (6ae7736): replaced tar -tvzf shell-out with an in-process gunzip + 512-byte block walker. Handles GNU long-name (L) entries and pax headers, and works identically on macOS BSD tar and Linux GNU tar. Sorting files by descending size before emitting is a nice touch for the per-package "what's inside" view.
  • PackagePhobia-style SizeStats widget: two big tiles (publish size + unpacked size) with split value/unit rendering. Looks good and reads better than a one-liner.
  • FileTable with sortable columns: ARIA aria-sort on <th>, indicator arrows, click-to-toggle direction. Strips the package/ prefix from displayed paths.

Tiny things — non-blocking

  • pack.ts:117: updateDeps builds rewrite URLs as https://${process.env.VERCEL_URL}/${escapedName}.tgz directly, while baseUrl (defined at line 97) uses the VERCEL_URL ? https://… : '' fallback. Local builds without VERCEL_URL produce package.json deps like https://undefined/.... Pre-existing on main, not introduced by this PR. Not a real-world issue (nobody installs from a local pack output) but noting for a follow-up cleanup if you want consistency.
  • catalog.ts and pack.ts duplicate the same TS interfaces (TarballFile, PackedPackage, BuildContext, Catalog). Drift risk is low since they're both in the same package, but if you want to eliminate it, catalog.ts could be the single source of truth and pack.ts could import type from it. Fine to leave.
  • SizeStats is rendered for every package in the list, not just the featured one — but it's behind a <details> so it doesn't blow up initial render. The 26 expanded <details> plus the FileTable for each would render a lot of DOM if expanded all at once; in practice, users open one or two. Not blocking.

Verdict: approve

Approving and clearing my prior CHANGES_REQUESTED. The redesign hit every concern from my last two reviews plus the Copilot feedback, the build pipeline is cleaner than what was there before, and the deployment shape is preserved (public/<escaped-name>.tgz URLs unchanged, smoke checks pass). Nice work on the followups.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

@v0@vercel can you make a PR to address the remaining follow ups from @TooTallNate's review comments that we didn't get to before merge of this one

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-stableCherry-pick this PR to the stable branch when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@pranaygp@TooTallNate