Repository navigation
fix: render without random numbers, so Next.js 16 can prerender emails - #65
Merged
Merged
Conversation
Next.js 16's Cache Components (create-next-app's default) prerender pages and GET route handlers when the app builds, and fail the build if a render reads a random number or the date. 0.2.0 asked crypto.getRandomValues() for each render's nonce, so a new Next.js app couldn't build a page, route handler or Client Component that renders an email; 0.1.22 could. The nonce now comes from a count of the process's renders (shared by every copy of Elements, as the collecting stack is) and performance's high-resolution clock, which Next.js leaves for timing. Data written before a render still can't name it, and it never reaches the output, so rendered HTML, designs and plain text are unchanged. - render-isolation.test.tsx: a render calls no crypto.getRandomValues(), crypto.randomUUID(), Math.random() or Date.now() (fails before the fix). - tests/next16-integration: Next.js 16 with Cache Components, a route handler, Server Component and Client Component prerendered; added to CI. Its build fails with the published 0.2.0. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01F9N7pEyxbxtqQmAw8BgEps
The nonce now comes from performance's clock and a count of renders. Where fake timers freeze the clock, or a runtime has no performance, an email (and one rendered inside a component of it) must render exactly as it does with a clock. Fails if the nonce assumes performance exists. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01F9N7pEyxbxtqQmAw8BgEps
Waku gives server code a react-dom/server that has no renderer, so importing Elements in a Server Component fails (0.1.22 when bundling, 0.2.0 when the page loads). Rendering in a Client Component works, as in its server render. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01F9N7pEyxbxtqQmAw8BgEps
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #65 +/- ##
=======================================
Coverage 96.55% 96.55%
=======================================
Files 40 40
Lines 2787 2789 +2
Branches 795 795
=======================================
+ Hits 2691 2693 +2
Misses 95 95
Partials 1 1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A new Next.js 16 app (
create-next-appturns on Cache Components) can't build a page, GET route handler or Client Component that renders an email with 0.2.0. Next.js prerenders those at build time and fails the build when a render reads a random number. 0.2.0 askedcrypto.getRandomValues()for each render's nonce (the marker that stops markup in a template's data from passing for one of the collecting render's markers). 0.1.22 builds the same pages.The nonce now comes from a count of renders (shared by every copy of Elements in a process, as the collecting stack is) and
performance's high-resolution clock, which Next.js leaves for timing. The nonce never reaches the output, so HTML, designs and plain text are unchanged.Changes
packages/react/src/utils/normalize.tsx:nonce()uses the render count andperformance.timeOrigin/performance.now(). It reads no random numbers or date.packages/react/src/regression/render-isolation.test.tsx: a render calls nocrypto.getRandomValues(),crypto.randomUUID(),Math.random()orDate.now()(fails on 0.2.0). An email renders the same where the clock stands still (fake timers) or there's noperformance, and so does an email rendered inside one of its components.tests/next16-integration(new, in CI): Next.js 16.4 with Cache Components, where a route handler, a Server Component and a Client Component that render an email are prerendered and checked byverify.mjs. Its build fails with the published 0.2.0..github/workflows/test.yml,CLAUDE.md,tests/nextjs-integration/verify.mjs: the new consumer step and its docs.packages/react/README.md: Waku's Server Components can't render emails, and no release of Elements can there.Test Plan
pnpm buildpasses; ESM bundle 117,988 bytes (budget 118,000)pnpm testpasses (react 989, shared 107, convert-core 193, from-react-email 283, migrate 178), and with coveragenext buildwith Turbopack and with--webpack, andnext dev), Jest 30GENERATED_SEEDS=5000)nodejs_compat), Express 5, Node + TypeScript (ESM and CommonJS), Bun, Deno, and Vitest 5 in node/jsdom/happy-dom with real timers, fake timers and a frozenperformance. All renders were right and matched 0.2.0.Notes
timeOrigin0). A guessed nonce lets markup in a template's data rearrange or break that one render; it can't add markup beyond the raw HTML that data already puts there. Using crypto where Next.js hasn't wrapped it would mean detecting its wrapper, which could break with a Next.js update, and would go over the bundle budget.@unlayer/react-elements-previousto it, per CLAUDE.md.🤖 Generated with Claude Code
https://claude.ai/code/session_01F9N7pEyxbxtqQmAw8BgEps
Generated by Claude Code
📖 Storybook Preview: https://unlayer.github.io/elements/pr/65/