Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
125 commits
Select commit Hold shift + click to select a range
ec38f2e
feat(react): add Tailwind, Markdown, code, fonts, async, Link, Head a…
ivoIturrieta Oct 15, 2026
f8a2dc2
docs(react): document the new entries, parity rules and changelog
ivoIturrieta Oct 15, 2026
8618967
test: raise the bundle budget and check the Provider in consumer apps
ivoIturrieta Oct 15, 2026
a44dba4
test(from-react-email): keep the gaps around a dark header as row pad…
ivoIturrieta Oct 15, 2026
f2a062a
fix(react): render what the editor exports for each block's design
ivoIturrieta Oct 15, 2026
8b5134f
fix(from-react-email): leave the root-font warning out of the design …
ivoIturrieta Oct 15, 2026
493d0e4
docs(react): document editor-parity rendering, root fonts and display…
ivoIturrieta Oct 15, 2026
54fdb24
fix(react): keep each async template's <Head> and <Font> in its own r…
ivoIturrieta Oct 15, 2026
8f7084e
fix(react): keep a component's linked image or styled rule as HTML
ivoIturrieta Oct 15, 2026
7c60a1f
fix(react): read an out-of-range numeric entity as U+FFFD in Markdown
ivoIturrieta Oct 15, 2026
6334f75
docs(react): note where a Row's style and className go in 0.3
ivoIturrieta Oct 15, 2026
aaf5d49
fix(react): keep a Column's link or rule as HTML when its block can't…
ivoIturrieta Oct 15, 2026
d9742b3
chore(release): release Elements as 0.3.0 and raise migrate's range t…
ivoIturrieta Oct 15, 2026
81fd71a
docs(react): list everything new in 0.3 in the changelog
ivoIturrieta Oct 16, 2026
66de45c
docs(react): add the changelog's history from 0.1.1 to 0.2.1
ivoIturrieta Oct 16, 2026
d7f0f2d
fix(react): keep the original error as the cause of render errors
ivoIturrieta Oct 16, 2026
a0fd69d
fix(react): collect async renders where Elements' React isn't react-d…
ivoIturrieta Oct 16, 2026
a10b343
test(react): render async emails in the Next.js integration apps
ivoIturrieta Oct 16, 2026
9cf682f
feat(react): add RenderError and the onError option
ivoIturrieta Oct 16, 2026
288be88
feat(react): add the walker engine
ivoIturrieta Oct 16, 2026
52b552f
feat(react)!: render templates with the walker, not react-dom
ivoIturrieta Oct 16, 2026
7e125e0
test(react): async walk, error policy and react-server build
ivoIturrieta Oct 16, 2026
6c73974
test(react): pass the suite on each React version, in either build
ivoIturrieta Oct 16, 2026
2d9d66d
ci: run the react suite on each React version, and on its canary
ivoIturrieta Oct 16, 2026
deb8e56
docs(react): how the renderer works, and errors
ivoIturrieta Oct 16, 2026
1a797a5
test(react): a Server Component's hook after Elements' renders, an em…
ivoIturrieta Oct 16, 2026
1667fb5
fix(from-react-email): check a migrated template whose parts fail
ivoIturrieta Oct 16, 2026
42687e4
test(react): a Server Component's hook after Elements' renders, on Ne…
ivoIturrieta Oct 16, 2026
05d8396
docs(react): comments that said React renders the template
ivoIturrieta Oct 16, 2026
2eb0f2f
test(react): pin the Page and Document HTML of Divider, Html, Menu an…
ivoIturrieta Oct 16, 2026
35ad770
feat(react): write Divider, Html, Menu and Social HTML without react-dom
ivoIturrieta Oct 16, 2026
931d1ea
test(react): compare the string exporters with the pinned exporters
ivoIturrieta Oct 16, 2026
effb831
test(react): render Divider, Html, Menu and Social with react-dom/ser…
ivoIturrieta Oct 16, 2026
aca96e2
refactor(react): write host markup and the string exporters' with one…
ivoIturrieta Oct 17, 2026
ad2b821
perf(react): make the design without rendering the HTML
ivoIturrieta Oct 17, 2026
af3e77e
perf(react): map props and write the design's px zeros in fewer steps
ivoIturrieta Oct 17, 2026
d80ce17
refactor(react): collect a template as a tree of its records, without…
ivoIturrieta Oct 17, 2026
ce25dab
perf(react): make the model once a render, and its values once
ivoIturrieta Oct 17, 2026
35ee685
perf(react): map a component's props with fewer copies
ivoIturrieta Oct 17, 2026
fefece4
perf(react): make the design's values in one object each, and read pl…
ivoIturrieta Oct 17, 2026
f3bdc71
perf(react): fewer objects in the walk's output, the model and the head
ivoIturrieta Oct 17, 2026
2ad9982
perf(react): give the document a root's HTML as Body writes it
ivoIturrieta Oct 17, 2026
4ac5228
perf(react): make the model's elements without copying their props again
ivoIturrieta Oct 17, 2026
15ceff6
refactor(react): keep the async walk's holes in the async entry
ivoIturrieta Oct 17, 2026
be05cf2
feat(react): add idPrefix, React's identifierPrefix for a template's ids
ivoIturrieta Oct 17, 2026
5921fda
fix(react): compile a <Tailwind config> with its own copy's compiler
ivoIturrieta Oct 17, 2026
fe60a17
test(react): keys in a list of lists are each list's own
ivoIturrieta Oct 17, 2026
5e73b54
perf(react): write a Row's and a root's HTML with one function
ivoIturrieta Oct 17, 2026
d2c66f8
docs(react): the engine's entry points and tests as they are now
ivoIturrieta Oct 17, 2026
794b126
test(react): pin the pages' HTML as React 19 writes it too
ivoIturrieta Oct 17, 2026
fb4e5da
fix(react): render what isn't Elements' own from the element as it is
ivoIturrieta Oct 17, 2026
f52b3fe
docs(react): say how fast the renderers are now
ivoIturrieta Oct 17, 2026
774b869
fix(react): import nothing by name that React's react-server build lacks
ivoIturrieta Oct 17, 2026
aa32639
test(react): build the Next.js 15 app with webpack too, every entry i…
ivoIturrieta Oct 17, 2026
6a9f305
fix(react): leave out a Row or Column whose exporter fails with onErr…
ivoIturrieta Oct 17, 2026
fa1ed40
test(react): install tailwindcss in the Next.js 15 app for its Tailwi…
ivoIturrieta Oct 17, 2026
a617287
docs(react): say in the README which React Elements takes, and how to…
ivoIturrieta Oct 17, 2026
87ee78e
docs(react): say the async renderers load Tailwind for any <Tailwind>
ivoIturrieta Oct 17, 2026
47bfcdb
fix(react): wait for a promise the template, a wrapper or a class throws
ivoIturrieta Oct 17, 2026
335c881
fix(react): render another copy's root as this copy's
ivoIturrieta Oct 17, 2026
4a44699
ci(react): raise the main entry's bundle budget to 190KB
ivoIturrieta Oct 17, 2026
935aeb4
fix(react): rewrite another copy's Columns in a <Tailwind>
ivoIturrieta Oct 17, 2026
3ef214c
fix(react): call a component of yours named like a root
ivoIturrieta Oct 17, 2026
c08d234
fix(react): render a <Suspense>'s fallback whatever onError says
ivoIturrieta Oct 17, 2026
3e7b7e5
fix(react): render an async template with no root once, as the synchr…
ivoIturrieta Oct 17, 2026
74225b4
ci(react): raise the main entry's bundle budget to 191KB
ivoIturrieta Oct 17, 2026
1168620
fix(react): render a template with no root once, as a root's content
ivoIturrieta Oct 17, 2026
61addc5
fix(react): wait for what's async in a template with no root
ivoIturrieta Oct 17, 2026
974010b
ci(react): raise the main entry's bundle budget to 192KB
ivoIturrieta Oct 17, 2026
edc342f
fix(react): say <template> for a failure at a rootless template's top
ivoIturrieta Oct 17, 2026
0b2d2bb
fix(react): keep what Tailwind compiles bounded in a long-lived process
ivoIturrieta Oct 17, 2026
dad3e6a
fix(react): fail a template that sets its state every time it renders
ivoIturrieta Oct 17, 2026
27610ee
fix(react): keep a render a template wrapper starts out of the outer one
ivoIturrieta Oct 17, 2026
c62f232
fix(react): know React 18.0 to 18.2, and say an older React is older
ivoIturrieta Oct 17, 2026
4a0f02c
fix(react): keep raw HTML in Markdown to tags that display, read in o…
ivoIturrieta Oct 18, 2026
0bebad6
fix(react): render a <Head>'s components as the template's
ivoIturrieta Oct 18, 2026
ad50d38
fix(react): keep what's said once, and the style names, bounded
ivoIturrieta Oct 18, 2026
331b04d
fix(react): throw a template wrapper's error from renderToJson as it is
ivoIturrieta Oct 18, 2026
c8dd482
fix(react): put a custom tool's failing head in the error policy
ivoIturrieta Oct 18, 2026
cdfde75
ci(react): raise the main entry's bundle budget to 193KB
ivoIturrieta Oct 18, 2026
1486293
test(react): keep the sanitizer's fuzz quick in CI
ivoIturrieta Oct 18, 2026
51e71c3
fix(react): keep a `$` in a component's style or class as it is
ivoIturrieta Oct 18, 2026
c7c0647
fix(react): keep a block's padding where its margin class can't be added
ivoIturrieta Oct 18, 2026
5d7fcd8
fix(react): make Tailwind's logical sides follow the text direction
ivoIturrieta Oct 18, 2026
ddea696
docs(react): give the bundle budget and size as they are
ivoIturrieta Oct 18, 2026
49eac31
fix(react): give a render the user's code starts a scope of its own
ivoIturrieta Oct 18, 2026
e95b172
fix(react): keep what unwrapping a template found in its render's scope
ivoIturrieta Oct 18, 2026
bd98c0f
fix(react): resolve a lazy template in the async renderers
ivoIturrieta Oct 18, 2026
8e5d517
ci(react): raise the main entry's bundle budget to 193.5KB
ivoIturrieta Oct 18, 2026
c21f361
test(react): start a render from a Tailwind plugin too
ivoIturrieta Oct 18, 2026
155e214
test(react): read the reviewed snapshots through Vite
ivoIturrieta Oct 18, 2026
4e8f24b
fix(react): unwrap a Suspense around the template
ivoIturrieta Oct 18, 2026
6cbe053
fix(react): compile a Tailwind config changed in place as it is now
ivoIturrieta Oct 18, 2026
2ef9f27
refactor(react): keep which Row or Column failed in the render's scope
ivoIturrieta Oct 18, 2026
0b6dc0a
fix(react): keep an async render's compiled Tailwind in its scope
ivoIturrieta Oct 18, 2026
1d4b91a
docs(react): say where a render a component starts begins
ivoIturrieta Oct 18, 2026
8cb22e5
ci(react): raise the main entry's bundle budget to 194.5KB
ivoIturrieta Oct 18, 2026
2f0f76c
fix(react): say why a root's settings go unread in the async renderer…
ivoIturrieta Oct 18, 2026
b3ae5a5
ci(react): give what the main entry grew by as it is
ivoIturrieta Oct 18, 2026
ab3dd9c
docs(react): say what a Suspense around a template with no root is
ivoIturrieta Oct 18, 2026
908ef7e
fix(react): read markup as a browser does when inlining Tailwind classes
ivoIturrieta Oct 18, 2026
7b9a27d
fix(react): know a Tailwind class by its own name in its rule's selector
ivoIturrieta Oct 18, 2026
54d46e2
fix(react): don't hide markup on every screen with Tailwind's screen …
ivoIturrieta Oct 18, 2026
c440cd9
fix(react): mark a select's options and write form controls as react-…
ivoIturrieta Oct 18, 2026
6faa952
fix(react): write a style's and a script's text as react-dom does, an…
ivoIturrieta Oct 18, 2026
c15ecbe
ci(react): raise the main entry's bundle budget to 198KB
ivoIturrieta Oct 18, 2026
e6fb5a9
test(react): say each difference the differential test lists
ivoIturrieta Oct 18, 2026
6a1e01a
fix(react): write a title's innerHTML as React 18.0 and 18.1 do
ivoIturrieta Oct 18, 2026
157e2b9
docs(react): say what Head escapes in a style's or a script's text
ivoIturrieta Oct 18, 2026
72f1cb0
fix(react): keep the element a class component returns in a Column
ivoIturrieta Oct 18, 2026
ad2152c
fix(react): inline the rest of a class a screen variant changes in part
ivoIturrieta Oct 18, 2026
24e31d2
fix(react): inline no class into Markdown's HTML that loads anything
ivoIturrieta Oct 18, 2026
f75eee1
fix(react): refuse old IE filters and image() in Markdown's styles
ivoIturrieta Oct 18, 2026
055057c
fix(react): keep one unreadable Tailwind class from breaking the rest
ivoIturrieta Oct 18, 2026
a75b97a
fix(react): leave out only what shows or hides markup for a screen va…
ivoIturrieta Oct 18, 2026
6786a55
fix(react): keep no class in Markdown's HTML whose value loads anything
ivoIturrieta Oct 18, 2026
c20faf6
fix(shared): keep a heading's links in the plain text
ivoIturrieta Oct 18, 2026
dca7fe2
ci: publish Elements only after every React version passes
ivoIturrieta Oct 18, 2026
932f992
fix(react): ask again for a fresh Tailwind compiler that failed
ivoIturrieta Oct 18, 2026
1029b6a
docs(react): list how the plain text differs from 0.2 without options
ivoIturrieta Oct 18, 2026
f863917
ci(react): raise the main entry's bundle budget to 198.5KB
ivoIturrieta Oct 18, 2026
aba1059
test(react): give the two heaviest tests the time a CI runner needs
ivoIturrieta Oct 18, 2026
92b7f76
fix(react): keep a class on inline elements in text children
ivoIturrieta Oct 18, 2026
5321b2d
fix(shared): write 0px in lengths whose key ends in "margin"
ivoIturrieta Oct 18, 2026
bc81fd5
docs(react): say which rich text a Tailwind inlines
ivoIturrieta Oct 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Release & Publish
#
# Gated publish flow, one package at a time:
# 1. PR merged to main → triggers build & test
# 1. PR merged to main → triggers build & test, and the react package's
# suite on each React it supports (react-versions.yml)
# 2. Each package publishes only when its code changed since its last
# release tag (Elements: v*, migrate: migrate-v*)
# 3. A reviewer approves each publish in the Actions UI (environment gate)
Expand Down Expand Up @@ -160,9 +161,18 @@ jobs:
echo "migrate=$MIGRATE" >> $GITHUB_OUTPUT
echo "Elements changed: $ELEMENTS, migrate changed: $MIGRATE"

# The react package's suite on each React it supports, for this run's commit: Elements publishes only when every
# one passes (test.yml runs it too, but nothing waits for that run).
react-versions:
name: React versions
needs: changes
if: needs.changes.outputs.elements == 'true'
uses: ./.github/workflows/react-versions.yml
secrets: inherit

publish:
name: Publish @unlayer/react-elements
needs: [build-and-test, changes]
needs: [build-and-test, changes, react-versions]
if: needs.changes.outputs.elements == 'true'
runs-on: ubuntu-latest
outputs:
Expand Down
53 changes: 53 additions & 0 deletions .github/workflows/react-canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: React canary

# Weekly, the react package's renderer (src/engine) against React's canary: what changes in React before it ships.
# It reports and doesn't block: a failure here is a heads-up to update the React version table
# (packages/react/src/engine/react-compat.ts) or the walker before that React is released.
on:
schedule:
- cron: '0 6 * * 1' # Mondays at 6am UTC
workflow_dispatch:

jobs:
canary:
name: React canary (fuzz)
runs-on: ubuntu-latest
continue-on-error: true

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.7.0

- name: Authenticate npm registry
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.TEST_NPM_TOKEN }}" > ~/.npmrc

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install React's canary
run: |
pnpm --filter @unlayer/react-elements add -D react@canary react-dom@canary
node -e "console.log('React', require('./packages/react/node_modules/react/package.json').version)"

- name: Build
run: pnpm --filter @unlayer-internal/shared-elements build && pnpm --filter @unlayer/react-elements build

# The walker against react-dom/server on random trees (useId()s included) and on random markup where templates
# have it, random templates against the last release, and the rest of the engine's tests (the self-check of an
# unknown React version among them).
- name: Fuzz
env:
WALKER_FUZZ_SEEDS: 5000
TEMPLATE_SEEDS: 1000
GENERATED_SEEDS: 3000
run: pnpm --filter @unlayer/react-elements exec vitest run src/engine src/regression/generated.test.tsx
69 changes: 69 additions & 0 deletions .github/workflows/react-versions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# React versions
#
# Elements renders templates with its own renderer (src/engine), which must
# render as each React it supports does: the react package's suite on each
# one (its react and react-dom swapped in), in React's development build
# (the full suite) and its production build (the engine's, the renderers'
# and the regression tests: React's act(), which the component tests need,
# is in its development build only). It holds the walker against
# react-dom/server on hand-written and random trees, useId()s included
# (src/engine/differential.test.tsx), React's react-server build with a
# hook it lacks (src/regression/react-server-build.test.ts, React 19), and
# a hook the dispatcher doesn't know (src/engine/dispatcher.test.tsx).
#
# A workflow of its own, called by test.yml (pushes and pull requests) and by
# publish.yml, whose publish job waits for it: a release ships only a commit
# every React passed. `github.sha` is the caller's, so it tests the commit the
# caller tests or publishes.
name: React versions

on:
workflow_call:

jobs:
react-versions:
name: React ${{ matrix.react }} (${{ matrix.build }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
react: ["18.0.0", "18.2.0", "18.3.1", "19.1", "19.2", "latest"]
build: [development, production]

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: 20

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 9.7.0

- name: Authenticate npm registry
run: echo "//registry.npmjs.org/:_authToken=${{ secrets.TEST_NPM_TOKEN }}" > ~/.npmrc

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Install React ${{ matrix.react }}
run: |
pnpm --filter @unlayer/react-elements add -D react@${{ matrix.react }} react-dom@${{ matrix.react }}
node -e "console.log('React', require('./packages/react/node_modules/react/package.json').version)"

- name: Build
run: pnpm --filter @unlayer-internal/shared-elements build && pnpm --filter @unlayer/react-elements build

- name: Tests (React's development build)
if: matrix.build == 'development'
run: pnpm --filter @unlayer/react-elements exec vitest run

- name: Tests (React's production build)
if: matrix.build == 'production'
env:
NODE_ENV: production
run: pnpm --filter @unlayer/react-elements exec vitest run src/engine src/async src/regression src/utils/render-errors.test.tsx src/utils/nested-renders.test.tsx --exclude src/regression/client-render.test.tsx
108 changes: 101 additions & 7 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,71 @@ jobs:
run: pnpm --filter @unlayer/from-react-email typecheck && pnpm --filter @unlayer/migrate typecheck

# ── Bundle size budget ──────────────────────────────────────
# Fail if the react-elements ESM bundle exceeds 118KB.
# Current size: about 113.4KB (0.1.22: about 76KB). Most of the growth is
# the render that collects a template's rows and blocks (one React render,
# Fail if the react-elements ESM bundle exceeds 198.5KB.
# Current size: about 198.3KB (0.1.22: about 76KB). Raised from 198KB
# (about 0.3KB): the plain text reads links before headings and
# preformatted text, each attribute whole, with their URLs set aside
# till the end, and doesn't wrap the URLs listed after the text.
# Raised from 194.5KB
# (about 3.4KB): markup is written as react-dom writes it for the
# installed React, a <select>'s value marking its options (the format
# context the walk carries), the form controls' rules and React 19's
# for a style's, script's and title's text, and a <Head>'s style and
# script text written as given in every React. Raised from 193.5KB
# (about 1KB): a <Suspense> around the template is unwrapped, a root
# the walk of a template with no root meets is said, and which Row or
# Column failed and what an async render compiled for its <Tailwind>s
# are kept in the render's scope. Raised from 193KB
# (about 0.6KB): each render function makes a scope of its own, the
# user's code (components, exporters, heads, onError) runs outside it,
# what unwrapping a template finds is kept in its render's scope, and a
# lazy template is unwrapped. Raised from 192KB
# (about 0.7KB): what's in a <Head> is walked with the template (its
# components waited for, in the error policy), a head function that
# fails is in the error policy, renderToJson throws a template
# wrapper's error as it is, and what the walker says once and its style
# names are bounded. Raised from 191KB
# (about 1KB): a template with no root renders as a root's content
# (walked once, its rows rendered with the render's config), and the
# async renderers collect it as they collect a root's. Raised from 190KB
# (about 1.3KB): Elements' roots are known by a marker shared across
# copies (an older copy's by its code), a suspension reaches the
# <Suspense> around it whatever `onError` says, and an async template
# with no root is rendered without calling it again. Raised from 189KB
# (about 1.2KB): a template, a component around its root and a class
# component that throw a promise (React 18's Suspense) are waited for by
# the async renderers and said by the synchronous ones, and a root of
# another copy of Elements renders as this copy's. Raised from 187KB
# (about 2KB): a Row or Column whose own exporter fails is left out with
# `onError: "omit"`, as a block is (a Column with its cell, its Row made
# again without it), instead of failing the render. Raised from 173.5KB
# (about 13KB): Divider, Html, Menu and Social write their web and
# document HTML without react-dom/server, which can't run in an edge
# runtime or a React Server Components layer (src/exporters/, about 7KB,
# writing with the walker's host markup, one table of React 18's and 19's
# rules for both), and what makes the renders faster: the template
# collected as a tree of its records (no markers parsed back), its model,
# values and the design's copies made once (src/model/, the head's reuse,
# pxMerged), and a root's and Row's HTML written without a walk. The
# async walk's holes are the async entry's. Raised from 131.5KB
# (about 41KB) for the walker (src/engine), the renderer Elements renders
# templates with instead of react-dom: the walk itself, its hooks
# dispatcher, React's attribute and style tables for host markup, the
# async walk's holes and tree-order output, the React version table and
# its self-check, and the error policy (RenderError, `onError`). It
# replaces react-dom in Elements' own renders, which was never counted
# here (a peer dependency); the collecting render's react-dom plumbing
# (the second react-dom module, blind mode, the Sentinel, calling a
# failed component again) went with it. Raised from 130.5KB
# (about 1KB) so the async renderers collect where Elements' React isn't
# react-dom's (Next.js's server layers) as the synchronous ones do, and a
# template whose Rows render as HTML instead of being collected fails
# instead of giving a broken email. Raised from 126KB for
# editor parity: the editor's base stylesheet for each mode in the
# document's head (about 1.1KB of CSS), a Paragraph rendered from the text
# its design holds, a root's `fonts` and its `displayMode` in the design,
# and blocks of another copy of Elements rendered as this one's. Most of
# the growth since 0.1.22 is the render that collects a template's rows and blocks (one React render,
# so hooks, context, Fragments and the user's components work as React
# renders them; a component that throws, suspends or is async left out
# with a message naming it; markup and blocks rendered to strings inside
Expand All @@ -70,14 +132,25 @@ jobs:
# rendering where a bundler gives Elements another copy of React than
# react-dom's (Next.js's server layers), and loading a second module of
# react-dom's when Elements is called inside a renderToString it renders with,
# and leaving out Columns inside markup in a Row, as 0.1.22 did.
# and leaving out Columns inside markup in a Row, as 0.1.22 did, and
# Tailwind classes (the hook a <Tailwind> scope uses in the collected
# render; <Tailwind>, Tailwind itself, the code that reads its CSS and each
# component's class table are in the separate `tailwind` entry, not counted),
# rich text children (inline JSX and components in Paragraph, Heading and
# Button text), code kept in plain text, what a <Font> gives the head and
# the root, right-to-left text alignment, and `style`/`className` on the
# element each component renders, <Link>, the plain text's options, and
# an <UnlayerProvider>'s config read by the roots and the render functions,
# and a link or rule in a Column kept as HTML where its block can't hold it.
# The Markdown parser, the code highlighter, <Font>, <Head> and the
# design-to-code converter (`codegen`) are in their own entries.
# The budget tracks the unminified ESM output as a proxy for code volume;
# it still flags accidental dependency bundling (any real dep is 10KB+).
- name: Check bundle size
run: |
BUNDLE="packages/react/dist/index.js"
SIZE=$(wc -c < "$BUNDLE" | tr -d ' ')
MAX_SIZE=118000
MAX_SIZE=198500
echo "Bundle: $BUNDLE"
echo "Size: $SIZE bytes (budget: $MAX_SIZE bytes)"
if [ "$SIZE" -gt "$MAX_SIZE" ]; then
Expand Down Expand Up @@ -178,8 +251,20 @@ jobs:
npm install "$TARBALL"
npm run build
# An email rendered in a route handler and a Server Component comes out as one (tables, a design with
# its rows), with Elements bundled into Next's server layers (no serverExternalPackages); an edge route
# builds (Turbopack) and renders or says to render in the Node.js runtime.
# its rows), with the synchronous and the async renderers, with Elements bundled into Next's server
# layers (no serverExternalPackages); an App Router
# edge route builds (Turbopack) and renders or says to render in the Node.js runtime, and Pages Router
# API routes (Node.js and edge) render. Each route renders with every entry of Elements too.
npm run verify

# ── Next.js integration build with webpack ──────────────────
# The same app built with webpack, Next.js 15's default bundler (`next build` without --turbopack). It fails
# the build of a module that imports by name what React's react-server build doesn't export (createContext,
# useContext), in an edge route, where Turbopack builds it.
- name: Next.js integration build (webpack)
run: |
cd tests/nextjs-integration
npm run build:webpack
npm run verify

# ── More consumers ──────────────────────────────────────────
Expand All @@ -202,6 +287,7 @@ jobs:
# create-next-app's setup since Next.js 16: a route handler, a Server Component and a Client Component that
# render an email are prerendered when the app builds, and the build fails if a render reads a random number
# or the date. 0.2.0's did (crypto.getRandomValues() for each render's nonce), so no new app could build.
# The async renderers render one in a route handler and a Server Component when it's requested.
- name: Next.js 16 consumer (Cache Components)
run: |
cd tests/next16-integration
Expand All @@ -225,3 +311,11 @@ jobs:
packages/*/coverage/
coverage/
retention-days: 30

# ── React versions ──────────────────────────────────────────
# The react package's suite on each React it supports: see
# react-versions.yml, which publish.yml runs before a release too.
react-versions:
name: React versions
uses: ./.github/workflows/react-versions.yml
secrets: inherit
Loading
Loading