diff --git a/.changeset/afraid-sides-agree.md b/.changeset/afraid-sides-agree.md new file mode 100644 index 0000000000..cc82c61116 --- /dev/null +++ b/.changeset/afraid-sides-agree.md @@ -0,0 +1,5 @@ +--- +"@react-email/preview-server": patch +--- + +fix `` not being flagged as incompatible diff --git a/.changeset/better-insects-like.md b/.changeset/better-insects-like.md new file mode 100644 index 0000000000..0bde211273 --- /dev/null +++ b/.changeset/better-insects-like.md @@ -0,0 +1,5 @@ +--- +"@react-email/preview-server": patch +--- + +fix the forced `color-scheme: dark` for the preview diff --git a/.changeset/common-goats-find.md b/.changeset/common-goats-find.md new file mode 100644 index 0000000000..6c7912dc17 --- /dev/null +++ b/.changeset/common-goats-find.md @@ -0,0 +1,6 @@ +--- +"@react-email/preview-server": patch +"react-email": patch +--- + +Use the same version for the preview-server and react-email diff --git a/.changeset/config.json b/.changeset/config.json index d896c865df..9f4df12f06 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -1,15 +1,16 @@ { "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json", - "changelog": "@changesets/cli/changelog", - "commit": false, - "linked": [], "access": "public", "baseBranch": "main", - "updateInternalDependencies": "patch", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [["react-email", "@react-email/preview-server"]], "ignore": [ "@benchmarks/preview-server", "@benchmarks/tailwind-component", "demo", + "email-dev", "web" - ] + ], + "updateInternalDependencies": "patch" } diff --git a/.changeset/crazy-seas-eat.md b/.changeset/crazy-seas-eat.md new file mode 100644 index 0000000000..7761436aee --- /dev/null +++ b/.changeset/crazy-seas-eat.md @@ -0,0 +1,5 @@ +--- +"@react-email/components": minor +--- + +Update `@react-email/tailwind@1.1.0-canary.0` diff --git a/.changeset/deep-clowns-bet.md b/.changeset/deep-clowns-bet.md new file mode 100644 index 0000000000..3c6f842eee --- /dev/null +++ b/.changeset/deep-clowns-bet.md @@ -0,0 +1,5 @@ +--- +"@react-email/render": patch +--- + +fix browser version including errors in the output instead of throwing them diff --git a/.changeset/great-parrots-yell.md b/.changeset/great-parrots-yell.md new file mode 100644 index 0000000000..429c2c034d --- /dev/null +++ b/.changeset/great-parrots-yell.md @@ -0,0 +1,5 @@ +--- +"@react-email/tailwind": minor +--- + +Extract tailwind pseudo classes to stylesheet diff --git a/.changeset/kind-showers-exist.md b/.changeset/kind-showers-exist.md new file mode 100644 index 0000000000..1434a62b2f --- /dev/null +++ b/.changeset/kind-showers-exist.md @@ -0,0 +1,5 @@ +--- +"react-email": patch +--- + +fix static file serving security issue with logging diff --git a/.changeset/mighty-pigs-add.md b/.changeset/mighty-pigs-add.md new file mode 100644 index 0000000000..cf5a202598 --- /dev/null +++ b/.changeset/mighty-pigs-add.md @@ -0,0 +1,5 @@ +--- +"react-email": patch +--- + +Add support for hot reloading with tsconfig path aliases diff --git a/.changeset/petite-boxes-find.md b/.changeset/petite-boxes-find.md new file mode 100644 index 0000000000..a6a6db7d40 --- /dev/null +++ b/.changeset/petite-boxes-find.md @@ -0,0 +1,5 @@ +--- +"@react-email/preview-server": patch +--- + +fix hot reloading with collapsed directories diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 0000000000..d70e1fe323 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,53 @@ +{ + "mode": "pre", + "tag": "canary", + "initialVersions": { + "@benchmarks/preview-server": "0.0.0", + "@benchmarks/tailwind-component": "0.0.0", + "@react-email/body": "0.0.11", + "@react-email/button": "0.1.0", + "@react-email/code-block": "0.1.0", + "@react-email/code-inline": "0.0.5", + "@react-email/column": "0.0.13", + "@react-email/components": "0.1.0", + "@react-email/container": "0.0.15", + "@react-email/font": "0.0.9", + "@react-email/head": "0.0.12", + "@react-email/heading": "0.0.15", + "@react-email/hr": "0.0.11", + "@react-email/html": "0.0.11", + "@react-email/img": "0.0.11", + "@react-email/link": "0.0.12", + "@react-email/markdown": "0.0.15", + "@react-email/preview": "0.0.13", + "@react-email/render": "1.1.2", + "@react-email/row": "0.0.12", + "@react-email/section": "0.0.16", + "@react-email/tailwind": "1.0.5", + "@react-email/text": "0.1.5", + "create-email": "1.2.1", + "demo": "0.0.0", + "docs": "0.0.0", + "react-email": "4.0.16", + "tsconfig": "0.0.0", + "web": "0.0.0", + "@react-email/preview-server": "4.1.0-canary.10", + "email-dev": "0.0.0" + }, + "changesets": [ + "afraid-sides-agree", + "better-insects-like", + "common-goats-find", + "crazy-seas-eat", + "deep-clowns-bet", + "great-parrots-yell", + "kind-showers-exist", + "mighty-pigs-add", + "petite-boxes-find", + "puny-chicken-argue", + "stupid-ghosts-decide", + "tasty-swans-taste", + "tidy-geese-cross", + "tiny-rice-give" + ] +} diff --git a/.changeset/puny-chicken-argue.md b/.changeset/puny-chicken-argue.md new file mode 100644 index 0000000000..cfd78a07a2 --- /dev/null +++ b/.changeset/puny-chicken-argue.md @@ -0,0 +1,5 @@ +--- +"react-email": minor +--- + +use a separate package for storing the preview server (@react-email/preview-server) diff --git a/.changeset/stupid-ghosts-decide.md b/.changeset/stupid-ghosts-decide.md new file mode 100644 index 0000000000..0f831b8f78 --- /dev/null +++ b/.changeset/stupid-ghosts-decide.md @@ -0,0 +1,5 @@ +--- +"react-email": patch +--- + +Fix prettier errors causing NextJS serialization error diff --git a/.changeset/tasty-swans-taste.md b/.changeset/tasty-swans-taste.md new file mode 100644 index 0000000000..fec9f52cfb --- /dev/null +++ b/.changeset/tasty-swans-taste.md @@ -0,0 +1,5 @@ +--- +"@react-email/components": minor +--- + +Updated @react-email/button@0.1.0-canary.0 diff --git a/.changeset/tidy-geese-cross.md b/.changeset/tidy-geese-cross.md new file mode 100644 index 0000000000..2558d752ee --- /dev/null +++ b/.changeset/tidy-geese-cross.md @@ -0,0 +1,5 @@ +--- +"react-email": patch +--- + +Pre-render email templates on hover diff --git a/.changeset/tiny-rice-give.md b/.changeset/tiny-rice-give.md new file mode 100644 index 0000000000..5daf11b7ce --- /dev/null +++ b/.changeset/tiny-rice-give.md @@ -0,0 +1,5 @@ +--- +"@react-email/button": minor +--- + +respect the order in which padding properties are defined diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 5940842a7d..370f054766 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,7 +20,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda - name: pnpm Cache id: pnpm-cache - uses: buildjet/cache@3e70d19e31d6a8030aeddf6ed8dbe601f94d09f4 + uses: buildjet/cache@9347ea1c7c1f331d397aa98b3894420448373372 with: path: | ~/.pnpm-store diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index eda5ee8085..e7749044a1 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -20,7 +20,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda - name: pnpm Cache id: pnpm-cache - uses: buildjet/cache@3e70d19e31d6a8030aeddf6ed8dbe601f94d09f4 + uses: buildjet/cache@9347ea1c7c1f331d397aa98b3894420448373372 with: path: | ~/.pnpm-store diff --git a/.github/workflows/release-canary.yml b/.github/workflows/release-canary.yml index a7ba292919..ef272335d1 100644 --- a/.github/workflows/release-canary.yml +++ b/.github/workflows/release-canary.yml @@ -27,7 +27,7 @@ jobs: corepack prepare pnpm@9.15.0 --activate pnpm config set script-shell "/usr/bin/bash" - name: pnpm Cache - uses: buildjet/cache@3e70d19e31d6a8030aeddf6ed8dbe601f94d09f4 + uses: buildjet/cache@9347ea1c7c1f331d397aa98b3894420448373372 with: path: | ~/.pnpm-store diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index fe39bd5dcb..ed8a3897ff 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda - name: pnpm Cache id: pnpm-cache - uses: buildjet/cache@3e70d19e31d6a8030aeddf6ed8dbe601f94d09f4 + uses: buildjet/cache@9347ea1c7c1f331d397aa98b3894420448373372 with: path: | ~/.pnpm-store diff --git a/.github/workflows/version.yml b/.github/workflows/version.yml index 1c64d39f98..af699de363 100644 --- a/.github/workflows/version.yml +++ b/.github/workflows/version.yml @@ -23,7 +23,7 @@ jobs: uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda - name: pnpm Cache id: pnpm-cache - uses: buildjet/cache@3e70d19e31d6a8030aeddf6ed8dbe601f94d09f4 + uses: buildjet/cache@9347ea1c7c1f331d397aa98b3894420448373372 with: path: | ~/.pnpm-store diff --git a/README.md b/README.md index a68467d544..6b52615bbd 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,13 @@ All components were tested using the most popular email clients. pnpm install ``` -#### Build and run packages +#### Build + +```sh +pnpm build +``` + +#### Run packages ```sh pnpm dev diff --git a/apps/demo/package.json b/apps/demo/package.json index a493d3a061..8529dd4a20 100644 --- a/apps/demo/package.json +++ b/apps/demo/package.json @@ -3,19 +3,20 @@ "version": "0.0.0", "private": true, "scripts": { - "build": "email build", - "dev": "email dev", - "start": "email start", - "export": "email export" + "build": "email-dev build", + "dev": "email-dev dev", + "start": "email-dev start", + "export": "email-dev export" }, "dependencies": { "@react-email/components": "workspace:*", "react": "^19", "react-dom": "^19", - "react-email": "workspace:*" + "email-dev": "workspace:*" }, "devDependencies": { - "next": "15.3.1", + "@react-email/preview-server": "workspace:*", + "next": "^15.3.2", "@types/react": "^19", "@types/react-dom": "^19", "tsx": "4.19.3" diff --git a/apps/web/package.json b/apps/web/package.json index ce813bfc25..59c37b3604 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -21,7 +21,7 @@ "@vercel/analytics": "1.5.0", "framer-motion": "12.7.4", "lucide-react": "^0.492.0", - "next": "15.3.1", + "next": "15.3.2", "prism-react-renderer": "2.4.1", "react": "^19", "react-dom": "^19", diff --git a/apps/web/src/app/api/check-spam/__snapshots__/check-spam.spec.tsx.snap b/apps/web/src/app/api/check-spam/__snapshots__/check-spam.spec.tsx.snap deleted file mode 100644 index 4cde0aa0d6..0000000000 --- a/apps/web/src/app/api/check-spam/__snapshots__/check-spam.spec.tsx.snap +++ /dev/null @@ -1,33 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`checkSpam() > with most spammy email 1`] = ` -{ - "checks": [ - { - "description": "BODY: Money back guarantee", - "name": "MONEY_BACK", - "points": 2.5, - }, - { - "description": "BODY: HTML and text parts are different", - "name": "MPART_ALT_DIFF", - "points": 0.7, - }, - { - "description": "Refers to an erectile drug", - "name": "DRUGS_ERECTILE", - "points": 2.2, - }, - ], - "isSpam": true, - "points": 5.4, -} -`; - -exports[`checkSpam() > with stripe email template using true base url 1`] = ` -{ - "checks": [], - "isSpam": false, - "points": 0, -} -`; diff --git a/apps/web/src/app/api/check-spam/check-spam.spec.tsx b/apps/web/src/app/api/check-spam/check-spam.spec.tsx index 3ea5da5557..22ede559a4 100644 --- a/apps/web/src/app/api/check-spam/check-spam.spec.tsx +++ b/apps/web/src/app/api/check-spam/check-spam.spec.tsx @@ -1,7 +1,24 @@ import { render } from '@react-email/components'; +import { vi } from 'vitest'; import { checkSpam } from './check-spam'; import { StripeWelcomeEmail } from './testing/stripe-welcome-email'; +const mockChecks = [ + { description: 'Test rule 1', name: 'TEST_RULE_1', points: 2.5 }, + { description: 'Test rule 2', name: 'TEST_RULE_2', points: 3.0 }, +]; + +// Mock the parser to return a stable result, making the test more robust. +vi.mock('../../../utils/spam-assassin/parse-pointing-table-rows', () => ({ + parsePointingTableRows: () => mockChecks, +})); + +// Mock sendToSpamd just to prevent it from trying to make a real network connection. +// The actual return value doesn't matter because we mocked the parser. +vi.mock('../../../utils/spam-assassin/send-to-spamd', () => ({ + sendToSpamd: async () => 'mocked response', +})); + describe('checkSpam()', { timeout: 10_000 }, () => { test('with most spammy email', async () => { const template = ( diff --git a/apps/web/src/components/template.tsx b/apps/web/src/components/template.tsx index 3a5194cc08..bf85fd3736 100644 --- a/apps/web/src/components/template.tsx +++ b/apps/web/src/components/template.tsx @@ -1,8 +1,8 @@ 'use client'; import classNames from 'classnames'; -import Image from 'next/image'; import type { ImageLoader } from 'next/image'; +import Image from 'next/image'; import Link from 'next/link'; import * as React from 'react'; import { Heading } from './heading'; diff --git a/apps/web/src/utils/spam-assassin/parse-pointing-table-rows.ts b/apps/web/src/utils/spam-assassin/parse-pointing-table-rows.ts index 3afa39b89e..0ccfdfa717 100644 --- a/apps/web/src/utils/spam-assassin/parse-pointing-table-rows.ts +++ b/apps/web/src/utils/spam-assassin/parse-pointing-table-rows.ts @@ -27,7 +27,7 @@ export const parsePointingTableRows = (response: string) => { const responseFromTableStart = response.slice( tableStartMatch.index + tableStartMatch[0].length, ); - let currentRow: Row | undefined = undefined; + let currentRow: Row | undefined; for (const line of responseFromTableStart.split(/\r\n|\n|\r/)) { if (line.trim().length === 0) break; diff --git a/biome.json b/biome.json index 21871acfbb..5c35171042 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.0.0-beta.1/schema.json", + "$schema": "https://biomejs.dev/schemas/2.0.0-beta.5/schema.json", "assist": { "actions": { "source": { @@ -31,7 +31,6 @@ "noCommaOperator": "error", "useNodejsImportProtocol": "error", "useAsConstAssertion": "error", - "useNumericLiterals": "error", "useEnumInitializers": "error", "useSelfClosingElements": "error", "useConst": "error", @@ -54,6 +53,7 @@ "noDelete": "off" }, "a11y": { + "noStaticElementInteractions": "off", "noSvgWithoutTitle": "off", "noAutofocus": "off" }, @@ -67,6 +67,7 @@ "useSortedClasses": "off" }, "complexity": { + "useNumericLiterals": "error", "noUselessFragments": "off", "noForEach": "off" }, @@ -87,7 +88,7 @@ "!**/pnpm-lock.yaml", "!**/.next", "!**/public", - "!packages/react-email/src/actions/email-validation/caniemail-data.ts", + "!packages/preview-server/src/actions/email-validation/caniemail-data.ts", "!**/.react-email/**/*", "!**/node_modules/**/*", "!**/*.d.ts", diff --git a/package.json b/package.json index 72f6cc8f16..d00baa351b 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "test:watch": "turbo run test:watch" }, "devDependencies": { - "@biomejs/biome": "2.0.0-beta.1", + "@biomejs/biome": "2.0.0-beta.5", "@changesets/cli": "2.29.4", "@types/node": "22.14.1", "@types/react": "19.0.1", diff --git a/packages/button/CHANGELOG.md b/packages/button/CHANGELOG.md index bf65fb977e..e260c750fc 100644 --- a/packages/button/CHANGELOG.md +++ b/packages/button/CHANGELOG.md @@ -6,6 +6,12 @@ - 4c4c5ef: respect the order in which padding properties are defined +## 0.1.0-canary.0 + +### Minor Changes + +- 11c4600: respect the order in which padding properties are defined + ## 0.0.19 ### Patch Changes diff --git a/packages/code-block/CHANGELOG.md b/packages/code-block/CHANGELOG.md index 861d8d5580..3f6703f5c5 100644 --- a/packages/code-block/CHANGELOG.md +++ b/packages/code-block/CHANGELOG.md @@ -12,6 +12,12 @@ - 9aa033c: Use range of versions for dependencies +## 0.0.13-canary.0 + +### Patch Changes + +- 9b1adb0: Use range of versions for dependencies + ## 0.0.12 ### Patch Changes diff --git a/packages/column/src/column.tsx b/packages/column/src/column.tsx index 0b1ea32df3..910bb8abdd 100644 --- a/packages/column/src/column.tsx +++ b/packages/column/src/column.tsx @@ -1,11 +1,81 @@ +import type { CSSProperties } from 'react'; import * as React from 'react'; +import { borderParser } from '../../render/src/shared/utils/border-parser'; export type ColumnProps = Readonly>; export const Column = React.forwardRef( ({ children, style, ...props }, ref) => { + const columnStyle = { + ...style, + }; + + if (columnStyle.border && typeof columnStyle.border === 'string') { + const { borderWidth, borderColor } = borderParser(columnStyle.border); + + const tdStyle: CSSProperties = { ...columnStyle }; + delete tdStyle.border; + delete tdStyle.backgroundColor; + + const containerStyle: CSSProperties = { + backgroundColor: borderColor, + width: '100%', + }; + + const cellStyle: CSSProperties = { + padding: `${borderWidth}px`, + }; + + const contentStyle: CSSProperties = { + ...columnStyle, + border: undefined, + width: '100%', + backgroundColor: columnStyle.backgroundColor, + }; + + return ( + + + + + + + +
+ + + + + + +
{children}
+
+ + ); + } return ( - + {children} ); diff --git a/packages/components/CHANGELOG.md b/packages/components/CHANGELOG.md index b8a235b753..5122acc8b8 100644 --- a/packages/components/CHANGELOG.md +++ b/packages/components/CHANGELOG.md @@ -15,6 +15,100 @@ - @react-email/button@0.1.0 - @react-email/tailwind@1.0.5 +## 0.1.0-canary.4 + +### Patch Changes + +- Updated dependencies [e22cc83] + - @react-email/render@1.1.3-canary.0 + - @react-email/body@0.0.11 + - @react-email/button@0.1.0-canary.0 + - @react-email/code-block@0.0.13 + - @react-email/code-inline@0.0.5 + - @react-email/column@0.0.13 + - @react-email/container@0.0.15 + - @react-email/font@0.0.9 + - @react-email/head@0.0.12 + - @react-email/heading@0.0.15 + - @react-email/hr@0.0.11 + - @react-email/html@0.0.11 + - @react-email/img@0.0.11 + - @react-email/link@0.0.12 + - @react-email/markdown@0.0.15 + - @react-email/preview@0.0.13 + - @react-email/row@0.0.12 + - @react-email/section@0.0.16 + - @react-email/tailwind@1.1.0-canary.0 + - @react-email/text@0.1.4 + +## 0.1.0-canary.3 + +### Minor Changes + +- 11c4600: Updated @react-email/button@0.1.0-canary.0 + +### Patch Changes + +- Updated dependencies [11c4600] + - @react-email/button@0.1.0-canary.0 + - @react-email/tailwind@1.1.0-canary.0 + +## 0.1.0-canary.2 + +### Patch Changes + +- Updated dependencies [0405e2b] + - @react-email/render@1.1.2-canary.0 + - @react-email/body@0.0.11 + - @react-email/button@0.0.19 + - @react-email/code-block@0.0.13 + - @react-email/code-inline@0.0.5 + - @react-email/column@0.0.13 + - @react-email/container@0.0.15 + - @react-email/font@0.0.9 + - @react-email/head@0.0.12 + - @react-email/heading@0.0.15 + - @react-email/hr@0.0.11 + - @react-email/html@0.0.11 + - @react-email/img@0.0.11 + - @react-email/link@0.0.12 + - @react-email/markdown@0.0.15 + - @react-email/preview@0.0.13 + - @react-email/row@0.0.12 + - @react-email/section@0.0.16 + - @react-email/tailwind@1.1.0-canary.0 + - @react-email/text@0.1.4 + +## 0.1.0-canary.1 + +### Minor Changes + +- b6a1941: Update `@react-email/tailwind@1.1.0-canary.0` + +### Patch Changes + +- Updated dependencies [57ddfd9] + - @react-email/render@1.1.1-canary.0 + - @react-email/body@0.0.11 + - @react-email/button@0.0.19 + - @react-email/code-block@0.0.13 + - @react-email/code-inline@0.0.5 + - @react-email/column@0.0.13 + - @react-email/container@0.0.15 + - @react-email/font@0.0.9 + - @react-email/head@0.0.12 + - @react-email/heading@0.0.15 + - @react-email/hr@0.0.11 + - @react-email/html@0.0.11 + - @react-email/img@0.0.11 + - @react-email/link@0.0.12 + - @react-email/markdown@0.0.15 + - @react-email/preview@0.0.13 + - @react-email/row@0.0.12 + - @react-email/section@0.0.16 + - @react-email/tailwind@1.0.5 + - @react-email/text@0.1.4 + ## 0.0.42 ### Patch Changes @@ -55,7 +149,6 @@ ### Patch Changes - Updated dependencies [a77ef6f] - - @react-email/preview@0.0.13 - @react-email/render@1.1.1 - @react-email/body@0.0.11 - @react-email/button@0.0.19 @@ -71,6 +164,7 @@ - @react-email/img@0.0.11 - @react-email/link@0.0.12 - @react-email/markdown@0.0.15 + - @react-email/preview@0.0.13 - @react-email/row@0.0.12 - @react-email/section@0.0.16 - @react-email/tailwind@1.0.5 @@ -83,6 +177,13 @@ - Updated dependencies [0a7b555] - @react-email/text@0.1.4 +## 0.0.39-canary.0 + +### Patch Changes + +- Updated dependencies [934bc53] + - @react-email/tailwind@1.1.0-canary.0 + ## 0.0.38 ### Patch Changes @@ -119,6 +220,79 @@ - @react-email/row@0.0.12 - @react-email/section@0.0.16 +## 0.0.37-canary.4 + +### Patch Changes + +- Updated dependencies [bd58fa9] + - @react-email/text@0.1.2-canary.0 + +## 0.0.37-canary.3 + +### Patch Changes + +- Updated dependencies [8179dc6] + - @react-email/tailwind@1.1.0-canary.1 + +## 0.0.37-canary.2 + +### Patch Changes + +- Updated dependencies [9b1adb0] + - @react-email/code-block@0.0.13-canary.0 + - @react-email/markdown@0.0.15-canary.0 + - @react-email/render@1.1.0-canary.1 + - @react-email/body@0.0.11 + - @react-email/button@0.0.19 + - @react-email/code-inline@0.0.5 + - @react-email/column@0.0.13 + - @react-email/container@0.0.15 + - @react-email/font@0.0.9 + - @react-email/head@0.0.12 + - @react-email/heading@0.0.15 + - @react-email/hr@0.0.11 + - @react-email/html@0.0.11 + - @react-email/img@0.0.11 + - @react-email/link@0.0.12 + - @react-email/preview@0.0.12 + - @react-email/row@0.0.12 + - @react-email/section@0.0.16 + - @react-email/tailwind@1.1.0-canary.0 + - @react-email/text@0.1.1 + +## 0.0.37-canary.1 + +### Patch Changes + +- Updated dependencies [32372da] + - @react-email/render@1.1.0-canary.0 + - @react-email/body@0.0.11 + - @react-email/button@0.0.19 + - @react-email/code-block@0.0.12 + - @react-email/code-inline@0.0.5 + - @react-email/column@0.0.13 + - @react-email/container@0.0.15 + - @react-email/font@0.0.9 + - @react-email/head@0.0.12 + - @react-email/heading@0.0.15 + - @react-email/hr@0.0.11 + - @react-email/html@0.0.11 + - @react-email/img@0.0.11 + - @react-email/link@0.0.12 + - @react-email/markdown@0.0.14 + - @react-email/preview@0.0.12 + - @react-email/row@0.0.12 + - @react-email/section@0.0.16 + - @react-email/tailwind@1.1.0-canary.0 + - @react-email/text@0.1.1 + +## 0.0.37-canary.0 + +### Patch Changes + +- Updated dependencies [ae8c007] + - @react-email/tailwind@1.1.0-canary.0 + ## 0.0.36 ### Patch Changes diff --git a/packages/components/package.json b/packages/components/package.json index 6eed6a7bbf..65a9be458f 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -55,10 +55,10 @@ "@react-email/link": "workspace:0.0.12", "@react-email/markdown": "workspace:0.0.15", "@react-email/preview": "workspace:0.0.13", - "@react-email/render": "workspace:1.1.2", + "@react-email/render": "workspace:1.1.3-canary.0", "@react-email/row": "workspace:0.0.12", "@react-email/section": "workspace:0.0.16", - "@react-email/tailwind": "workspace:1.0.5", + "@react-email/tailwind": "workspace:1.1.0-canary.0", "@react-email/text": "workspace:0.1.5" }, "peerDependencies": { diff --git a/packages/container/src/container.tsx b/packages/container/src/container.tsx index 1a5b4d2261..369605eaa6 100644 --- a/packages/container/src/container.tsx +++ b/packages/container/src/container.tsx @@ -1,9 +1,72 @@ +import type { CSSProperties } from 'react'; import * as React from 'react'; +import { borderParser } from '../../render/src/shared/utils/border-parser'; export type ContainerProps = Readonly>; export const Container = React.forwardRef( ({ children, style, ...props }, ref) => { + const containerStyle = { + maxWidth: '37.5em', + ...style, + }; + + if (containerStyle.border && typeof containerStyle.border === 'string') { + const { borderWidth, borderColor } = borderParser(containerStyle.border); + + const newContainerStyle: CSSProperties = { + backgroundColor: borderColor, + width: '100%', + }; + + const cellStyle: CSSProperties = { + padding: `${borderWidth}px`, + }; + + const contentStyle: CSSProperties = { + ...containerStyle, + border: undefined, + width: '100%', + backgroundColor: containerStyle.backgroundColor, + }; + + return ( + + + + + + +
+ + + + + + +
{children}
+
+ ); + } + return ( ( cellSpacing="0" ref={ref} role="presentation" - style={{ maxWidth: '37.5em', ...style }} + style={containerStyle} > diff --git a/packages/create-email/CHANGELOG.md b/packages/create-email/CHANGELOG.md index d1e08b112a..5b12d75b84 100644 --- a/packages/create-email/CHANGELOG.md +++ b/packages/create-email/CHANGELOG.md @@ -16,12 +16,26 @@ - d689346: Show package.json's version on --version for create-email +## 1.2.0-canary.1 + +### Patch Changes + +- 0c3aaa3: Show package.json's version on --version for create-email + +## 1.2.0-canary.0 + +### Minor Changes + +- 237d115: Fetch react-email package versions at runtime from `--tag` option + ## 1.1.0 ### Minor Changes - 31fd7cc: use caret for dependency on react-email and @react-email/components +## 1.0.8-canary.0 + ## 1.0.7 ### Patch Changes diff --git a/packages/create-email/template/CHANGELOG.md b/packages/create-email/template/CHANGELOG.md index 195b70b0d4..7870c01034 100644 --- a/packages/create-email/template/CHANGELOG.md +++ b/packages/create-email/template/CHANGELOG.md @@ -2,6 +2,12 @@ ## 1.1.0 +## 1.0.8-canary.0 + +### Patch Changes + +- @react-email/components@0.0.37-canary.0 + ## 1.0.7 ## 1.0.6 diff --git a/packages/markdown/CHANGELOG.md b/packages/markdown/CHANGELOG.md index 6eb2ce0d6e..d0cd8933e0 100644 --- a/packages/markdown/CHANGELOG.md +++ b/packages/markdown/CHANGELOG.md @@ -6,6 +6,12 @@ - 9aa033c: Use range of versions for dependencies +## 0.0.15-canary.0 + +### Patch Changes + +- 9b1adb0: Use range of versions for dependencies + ## 0.0.14 ### Patch Changes diff --git a/packages/preview-server/.gitignore b/packages/preview-server/.gitignore new file mode 100644 index 0000000000..659fa846ed --- /dev/null +++ b/packages/preview-server/.gitignore @@ -0,0 +1,5 @@ +node_modules +.next + +# for testing +static diff --git a/packages/preview-server/.npmignore b/packages/preview-server/.npmignore new file mode 100644 index 0000000000..365b417e3c --- /dev/null +++ b/packages/preview-server/.npmignore @@ -0,0 +1,5 @@ +.react-email +./emails +./emails/static +node_modules +.turbo diff --git a/packages/preview-server/CHANGELOG.md b/packages/preview-server/CHANGELOG.md new file mode 100644 index 0000000000..ce51919396 --- /dev/null +++ b/packages/preview-server/CHANGELOG.md @@ -0,0 +1,25 @@ +# @react-email/preview-server + +## 4.1.0-canary.11 + +### Patch Changes + +- 272b21e: fix the forced `color-scheme: dark` for the preview + +## 4.1.0-canary.10 + +### Patch Changes + +- caa4a31: fix hot reloading with collapsed directories + +## 4.1.0-canary.9 + +### Patch Changes + +- 40fb596: Use the same version for the preview-server and react-email + +## 1.0.0-canary.1 + +### Patch Changes + +- efb4db2: fix `` not being flagged as incompatible diff --git a/packages/preview-server/index.mjs b/packages/preview-server/index.mjs new file mode 100644 index 0000000000..e2b8b0ed94 --- /dev/null +++ b/packages/preview-server/index.mjs @@ -0,0 +1,17 @@ +/** + * this file is required so that import.meta.resolve and require.resolve can properly can find the module for this package + */ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import url from 'node:url'; + +const filename = url.fileURLToPath(import.meta.url); +const dirname = path.dirname(filename); +const packageJson = JSON.parse( + await fs.readFile(path.join(dirname, 'package.json'), 'utf-8'), +); + +/** + * @type {string} + */ +export const version = packageJson.version; diff --git a/packages/preview-server/license.md b/packages/preview-server/license.md new file mode 100644 index 0000000000..5058f53ebb --- /dev/null +++ b/packages/preview-server/license.md @@ -0,0 +1,7 @@ +Copyright 2024 Plus Five Five, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/react-email/module-punycode.d.ts b/packages/preview-server/module-punycode.d.ts similarity index 100% rename from packages/react-email/module-punycode.d.ts rename to packages/preview-server/module-punycode.d.ts diff --git a/packages/react-email/next-env.d.ts b/packages/preview-server/next-env.d.ts similarity index 100% rename from packages/react-email/next-env.d.ts rename to packages/preview-server/next-env.d.ts diff --git a/packages/react-email/next.config.js b/packages/preview-server/next.config.js similarity index 100% rename from packages/react-email/next.config.js rename to packages/preview-server/next.config.js diff --git a/packages/preview-server/package.json b/packages/preview-server/package.json new file mode 100644 index 0000000000..9b311de403 --- /dev/null +++ b/packages/preview-server/package.json @@ -0,0 +1,82 @@ +{ + "name": "@react-email/preview-server", + "version": "4.1.0-canary.11", + "description": "A live preview of your emails right in your browser.", + "scripts": { + "caniemail:fetch": "node ./scripts/fill-caniemail-data.mjs", + "clean": "rm -rf dist", + "build": "node ./scripts/build-preview-server.mjs", + "test": "vitest run", + "test:watch": "vitest" + }, + "main": "./index.mjs", + "dependencies": { + "@babel/core": "7.26.10", + "@babel/parser": "^7.27.0", + "@babel/traverse": "^7.27.0", + "@lottiefiles/dotlottie-react": "0.13.3", + "@radix-ui/colors": "3.0.0", + "@radix-ui/react-collapsible": "1.1.7", + "@radix-ui/react-dropdown-menu": "2.1.10", + "@radix-ui/react-popover": "1.1.10", + "@radix-ui/react-slot": "1.2.0", + "@radix-ui/react-tabs": "1.1.7", + "@radix-ui/react-toggle-group": "1.1.6", + "@radix-ui/react-tooltip": "1.2.3", + "@types/node": "22.14.1", + "@types/normalize-path": "3.0.2", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "@types/webpack": "5.28.5", + "autoprefixer": "10.4.21", + "chalk": "^4.1.2", + "clsx": "2.1.1", + "esbuild": "^0.25.0", + "framer-motion": "12.7.5", + "json5": "2.2.3", + "log-symbols": "^4.1.0", + "module-punycode": "npm:punycode@2.3.1", + "next": "^15.3.2", + "node-html-parser": "7.0.1", + "ora": "^5.4.1", + "pretty-bytes": "6.1.1", + "prism-react-renderer": "2.4.1", + "react": "19.0.0", + "react-dom": "19.0.0", + "sharp": "0.34.1", + "socket.io-client": "4.8.1", + "sonner": "2.0.3", + "source-map-js": "1.2.1", + "spamc": "0.0.5", + "stacktrace-parser": "0.1.11", + "tailwind-merge": "3.2.0", + "tailwindcss": "3.4.0", + "use-debounce": "10.0.4", + "zod": "3.24.3" + }, + "devDependencies": { + "@react-email/components": "workspace:*", + "@types/babel__core": "7.20.5", + "@types/babel__traverse": "7.20.7", + "@types/fs-extra": "11.0.1", + "@types/mime-types": "2.1.4", + "@types/node": "22.10.2", + "@types/normalize-path": "3.0.2", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "@types/webpack": "5.28.5", + "autoprefixer": "10.4.21", + "postcss": "8.5.3", + "tailwindcss": "3.4.0", + "typescript": "5.8.3" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/resend/react-email.git", + "directory": "packages/preview-server" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/react-email/postcss.config.js b/packages/preview-server/postcss.config.js similarity index 100% rename from packages/react-email/postcss.config.js rename to packages/preview-server/postcss.config.js diff --git a/packages/preview-server/scripts/build-preview-server.mjs b/packages/preview-server/scripts/build-preview-server.mjs new file mode 100644 index 0000000000..36c0236651 --- /dev/null +++ b/packages/preview-server/scripts/build-preview-server.mjs @@ -0,0 +1,29 @@ +import { spawn } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; + +const filename = url.fileURLToPath(import.meta.url); +const dirname = path.dirname(filename); + +const nextBuildProcess = spawn('pnpm', ['next', 'build'], { + detached: true, + shell: true, + stdio: 'inherit', + cwd: path.resolve(dirname, '../'), +}); + +process.on('SIGINT', () => { + nextBuildProcess.kill('SIGINT'); +}); + +nextBuildProcess.on('exit', (code) => { + if (code !== 0) { + console.error(`next build failed with exit code ${code}`); + process.exit(code); + } + + fs.rmSync(path.resolve(dirname, '../.next/cache'), { + recursive: true, + }); +}); diff --git a/packages/react-email/scripts/fill-caniemail-data.mjs b/packages/preview-server/scripts/fill-caniemail-data.mjs similarity index 100% rename from packages/react-email/scripts/fill-caniemail-data.mjs rename to packages/preview-server/scripts/fill-caniemail-data.mjs diff --git a/packages/react-email/src/actions/email-validation/caniemail-data.ts b/packages/preview-server/src/actions/email-validation/caniemail-data.ts similarity index 100% rename from packages/react-email/src/actions/email-validation/caniemail-data.ts rename to packages/preview-server/src/actions/email-validation/caniemail-data.ts diff --git a/packages/react-email/src/actions/email-validation/check-compatibility.ts b/packages/preview-server/src/actions/email-validation/check-compatibility.ts similarity index 99% rename from packages/react-email/src/actions/email-validation/check-compatibility.ts rename to packages/preview-server/src/actions/email-validation/check-compatibility.ts index 8e045a211b..f55ee77ba2 100644 --- a/packages/react-email/src/actions/email-validation/check-compatibility.ts +++ b/packages/preview-server/src/actions/email-validation/check-compatibility.ts @@ -68,14 +68,14 @@ export type Platform = | 'windows-mail' | 'outlook-com'; -export type SupportEntryCategroy = 'html' | 'css' | 'image' | 'others'; +export type SupportEntryCategory = 'html' | 'css' | 'image' | 'others'; export interface SupportEntry { slug: string; title: string; description: string | null; url: string; - category: SupportEntryCategroy; + category: SupportEntryCategory; tags: string[]; keywords: string | null; last_test_date: string; diff --git a/packages/react-email/src/actions/email-validation/check-images.spec.tsx b/packages/preview-server/src/actions/email-validation/check-images.spec.tsx similarity index 100% rename from packages/react-email/src/actions/email-validation/check-images.spec.tsx rename to packages/preview-server/src/actions/email-validation/check-images.spec.tsx diff --git a/packages/react-email/src/actions/email-validation/check-images.ts b/packages/preview-server/src/actions/email-validation/check-images.ts similarity index 97% rename from packages/react-email/src/actions/email-validation/check-images.ts rename to packages/preview-server/src/actions/email-validation/check-images.ts index 7f4929d3b3..b6d2842df0 100644 --- a/packages/react-email/src/actions/email-validation/check-images.ts +++ b/packages/preview-server/src/actions/email-validation/check-images.ts @@ -103,7 +103,7 @@ export const checkImages = async (code: string, base: string) => { }); } - let res: IncomingMessage | undefined = undefined; + let res: IncomingMessage | undefined; try { res = await quickFetch(url); const hasSucceeded = @@ -132,7 +132,7 @@ export const checkImages = async (code: string, base: string) => { if (responseSizeBytes > 1_048_576 && result.status !== 'error') { result.status = 'warning'; } - } catch (exception) { + } catch (_exception) { result.checks.push({ type: 'fetch_attempt', passed: false, @@ -142,7 +142,7 @@ export const checkImages = async (code: string, base: string) => { }); result.status = 'error'; } - } catch (exception) { + } catch (_exception) { result.checks.push({ passed: false, type: 'syntax', diff --git a/packages/react-email/src/actions/email-validation/check-links.spec.tsx b/packages/preview-server/src/actions/email-validation/check-links.spec.tsx similarity index 100% rename from packages/react-email/src/actions/email-validation/check-links.spec.tsx rename to packages/preview-server/src/actions/email-validation/check-links.spec.tsx diff --git a/packages/react-email/src/actions/email-validation/check-links.ts b/packages/preview-server/src/actions/email-validation/check-links.ts similarity index 95% rename from packages/react-email/src/actions/email-validation/check-links.ts rename to packages/preview-server/src/actions/email-validation/check-links.ts index 83df39072f..9a79306048 100644 --- a/packages/react-email/src/actions/email-validation/check-links.ts +++ b/packages/preview-server/src/actions/email-validation/check-links.ts @@ -68,7 +68,7 @@ export const checkLinks = async (code: string) => { }); } - let res: IncomingMessage | undefined = undefined; + let res: IncomingMessage | undefined; try { res = await quickFetch(url); const hasSucceeded = @@ -85,7 +85,7 @@ export const checkLinks = async (code: string) => { ? 'warning' : 'error'; } - } catch (exception) { + } catch (_exception) { result.checks.push({ type: 'fetch_attempt', passed: false, @@ -95,7 +95,7 @@ export const checkLinks = async (code: string) => { }); result.status = 'error'; } - } catch (exception) { + } catch (_exception) { result.checks.push({ passed: false, type: 'syntax', diff --git a/packages/react-email/src/actions/email-validation/get-code-location-from-ast-element.ts b/packages/preview-server/src/actions/email-validation/get-code-location-from-ast-element.ts similarity index 100% rename from packages/react-email/src/actions/email-validation/get-code-location-from-ast-element.ts rename to packages/preview-server/src/actions/email-validation/get-code-location-from-ast-element.ts diff --git a/packages/react-email/src/actions/email-validation/quick-fetch.ts b/packages/preview-server/src/actions/email-validation/quick-fetch.ts similarity index 100% rename from packages/react-email/src/actions/email-validation/quick-fetch.ts rename to packages/preview-server/src/actions/email-validation/quick-fetch.ts index 3f5b1dcf22..d2ab9447e8 100644 --- a/packages/react-email/src/actions/email-validation/quick-fetch.ts +++ b/packages/preview-server/src/actions/email-validation/quick-fetch.ts @@ -1,5 +1,5 @@ -import http from 'node:http'; import type { IncomingMessage } from 'node:http'; +import http from 'node:http'; import https from 'node:https'; export const quickFetch = (url: URL) => { diff --git a/packages/react-email/src/actions/get-email-path-from-slug.ts b/packages/preview-server/src/actions/get-email-path-from-slug.ts similarity index 100% rename from packages/react-email/src/actions/get-email-path-from-slug.ts rename to packages/preview-server/src/actions/get-email-path-from-slug.ts diff --git a/packages/react-email/src/actions/get-emails-directory-metadata-action.ts b/packages/preview-server/src/actions/get-emails-directory-metadata-action.ts similarity index 100% rename from packages/react-email/src/actions/get-emails-directory-metadata-action.ts rename to packages/preview-server/src/actions/get-emails-directory-metadata-action.ts diff --git a/packages/react-email/src/actions/render-email-by-path.tsx b/packages/preview-server/src/actions/render-email-by-path.tsx similarity index 100% rename from packages/react-email/src/actions/render-email-by-path.tsx rename to packages/preview-server/src/actions/render-email-by-path.tsx diff --git a/packages/react-email/src/animated-icons-data/help.json b/packages/preview-server/src/animated-icons-data/help.json similarity index 100% rename from packages/react-email/src/animated-icons-data/help.json rename to packages/preview-server/src/animated-icons-data/help.json diff --git a/packages/react-email/src/animated-icons-data/link.json b/packages/preview-server/src/animated-icons-data/link.json similarity index 100% rename from packages/react-email/src/animated-icons-data/link.json rename to packages/preview-server/src/animated-icons-data/link.json diff --git a/packages/react-email/src/animated-icons-data/load.json b/packages/preview-server/src/animated-icons-data/load.json similarity index 100% rename from packages/react-email/src/animated-icons-data/load.json rename to packages/preview-server/src/animated-icons-data/load.json diff --git a/packages/react-email/src/animated-icons-data/mail.json b/packages/preview-server/src/animated-icons-data/mail.json similarity index 100% rename from packages/react-email/src/animated-icons-data/mail.json rename to packages/preview-server/src/animated-icons-data/mail.json diff --git a/packages/react-email/src/app/env.ts b/packages/preview-server/src/app/env.ts similarity index 100% rename from packages/react-email/src/app/env.ts rename to packages/preview-server/src/app/env.ts diff --git a/packages/react-email/src/app/favicon.ico b/packages/preview-server/src/app/favicon.ico similarity index 100% rename from packages/react-email/src/app/favicon.ico rename to packages/preview-server/src/app/favicon.ico diff --git a/packages/react-email/src/app/fonts.ts b/packages/preview-server/src/app/fonts.ts similarity index 100% rename from packages/react-email/src/app/fonts.ts rename to packages/preview-server/src/app/fonts.ts diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoBold.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoBold.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoBold.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoBold.otf diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoBoldItalic.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoBoldItalic.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoBoldItalic.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoBoldItalic.otf diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoHeavy.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoHeavy.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoHeavy.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoHeavy.otf diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoHeavyItalic.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoHeavyItalic.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoHeavyItalic.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoHeavyItalic.otf diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoLight.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoLight.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoLight.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoLight.otf diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoLightItalic.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoLightItalic.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoLightItalic.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoLightItalic.otf diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoMedium.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoMedium.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoMedium.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoMedium.otf diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoMediumItalic.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoMediumItalic.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoMediumItalic.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoMediumItalic.otf diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoRegular.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoRegular.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoRegular.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoRegular.otf diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoRegularItalic.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoRegularItalic.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoRegularItalic.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoRegularItalic.otf diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoSemibold.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoSemibold.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoSemibold.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoSemibold.otf diff --git a/packages/react-email/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf b/packages/preview-server/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf similarity index 100% rename from packages/react-email/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf rename to packages/preview-server/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf diff --git a/packages/react-email/src/app/globals.css b/packages/preview-server/src/app/globals.css similarity index 100% rename from packages/react-email/src/app/globals.css rename to packages/preview-server/src/app/globals.css diff --git a/packages/react-email/src/app/layout.tsx b/packages/preview-server/src/app/layout.tsx similarity index 100% rename from packages/react-email/src/app/layout.tsx rename to packages/preview-server/src/app/layout.tsx diff --git a/packages/react-email/src/app/logo.png b/packages/preview-server/src/app/logo.png similarity index 100% rename from packages/react-email/src/app/logo.png rename to packages/preview-server/src/app/logo.png diff --git a/packages/react-email/src/app/page.tsx b/packages/preview-server/src/app/page.tsx similarity index 100% rename from packages/react-email/src/app/page.tsx rename to packages/preview-server/src/app/page.tsx diff --git a/packages/react-email/src/app/preview/[...slug]/page.tsx b/packages/preview-server/src/app/preview/[...slug]/page.tsx similarity index 96% rename from packages/react-email/src/app/preview/[...slug]/page.tsx rename to packages/preview-server/src/app/preview/[...slug]/page.tsx index e9511d5f59..6f305c903c 100644 --- a/packages/react-email/src/app/preview/[...slug]/page.tsx +++ b/packages/preview-server/src/app/preview/[...slug]/page.tsx @@ -60,10 +60,9 @@ This is most likely not an issue with the preview server. Maybe there was a typo const serverEmailRenderingResult = await renderEmailByPath(emailPath); - let spamCheckingResult: SpamCheckingResult | undefined = undefined; - let lintingRows: LintingRow[] | undefined = undefined; - let compatibilityCheckingResults: CompatibilityCheckingResult[] | undefined = - undefined; + let spamCheckingResult: SpamCheckingResult | undefined; + let lintingRows: LintingRow[] | undefined; + let compatibilityCheckingResults: CompatibilityCheckingResult[] | undefined; if (isBuilding) { if ('error' in serverEmailRenderingResult) { diff --git a/packages/react-email/src/app/preview/[...slug]/preview.tsx b/packages/preview-server/src/app/preview/[...slug]/preview.tsx similarity index 98% rename from packages/react-email/src/app/preview/[...slug]/preview.tsx rename to packages/preview-server/src/app/preview/[...slug]/preview.tsx index 474c33e65a..d74f1bbba4 100644 --- a/packages/react-email/src/app/preview/[...slug]/preview.tsx +++ b/packages/preview-server/src/app/preview/[...slug]/preview.tsx @@ -162,7 +162,7 @@ const Preview = ({ emailTitle, className, ...props }: PreviewProps) => { width={width} >
(key: string) => { - let value: T | undefined = undefined; + let value: T | undefined; if ('localStorage' in global) { const storedValue = global.localStorage.getItem(key); if (storedValue !== null && storedValue !== 'undefined') { try { value = JSON.parse(storedValue) as T; - } catch (exception) { + } catch (_exception) { console.warn( 'Failed to load stored value for', key, diff --git a/packages/react-email/src/components/tooltip-content.tsx b/packages/preview-server/src/components/tooltip-content.tsx similarity index 100% rename from packages/react-email/src/components/tooltip-content.tsx rename to packages/preview-server/src/components/tooltip-content.tsx diff --git a/packages/react-email/src/components/tooltip.tsx b/packages/preview-server/src/components/tooltip.tsx similarity index 100% rename from packages/react-email/src/components/tooltip.tsx rename to packages/preview-server/src/components/tooltip.tsx diff --git a/packages/react-email/src/components/topbar.tsx b/packages/preview-server/src/components/topbar.tsx similarity index 100% rename from packages/react-email/src/components/topbar.tsx rename to packages/preview-server/src/components/topbar.tsx diff --git a/packages/react-email/src/components/topbar/active-view-toggle-group.tsx b/packages/preview-server/src/components/topbar/active-view-toggle-group.tsx similarity index 100% rename from packages/react-email/src/components/topbar/active-view-toggle-group.tsx rename to packages/preview-server/src/components/topbar/active-view-toggle-group.tsx diff --git a/packages/react-email/src/components/topbar/view-size-controls.tsx b/packages/preview-server/src/components/topbar/view-size-controls.tsx similarity index 100% rename from packages/react-email/src/components/topbar/view-size-controls.tsx rename to packages/preview-server/src/components/topbar/view-size-controls.tsx diff --git a/packages/react-email/src/contexts/emails.tsx b/packages/preview-server/src/contexts/emails.tsx similarity index 100% rename from packages/react-email/src/contexts/emails.tsx rename to packages/preview-server/src/contexts/emails.tsx diff --git a/packages/react-email/src/contexts/fragment-identifier.tsx b/packages/preview-server/src/contexts/fragment-identifier.tsx similarity index 100% rename from packages/react-email/src/contexts/fragment-identifier.tsx rename to packages/preview-server/src/contexts/fragment-identifier.tsx diff --git a/packages/react-email/src/contexts/preview.tsx b/packages/preview-server/src/contexts/preview.tsx similarity index 100% rename from packages/react-email/src/contexts/preview.tsx rename to packages/preview-server/src/contexts/preview.tsx diff --git a/packages/react-email/src/hooks/use-clamped-state.ts b/packages/preview-server/src/hooks/use-clamped-state.ts similarity index 100% rename from packages/react-email/src/hooks/use-clamped-state.ts rename to packages/preview-server/src/hooks/use-clamped-state.ts diff --git a/packages/react-email/src/hooks/use-email-rendering-result.ts b/packages/preview-server/src/hooks/use-email-rendering-result.ts similarity index 100% rename from packages/react-email/src/hooks/use-email-rendering-result.ts rename to packages/preview-server/src/hooks/use-email-rendering-result.ts diff --git a/packages/react-email/src/hooks/use-fragment-identifier.ts b/packages/preview-server/src/hooks/use-fragment-identifier.ts similarity index 100% rename from packages/react-email/src/hooks/use-fragment-identifier.ts rename to packages/preview-server/src/hooks/use-fragment-identifier.ts diff --git a/packages/react-email/src/hooks/use-hot-reload.ts b/packages/preview-server/src/hooks/use-hot-reload.ts similarity index 100% rename from packages/react-email/src/hooks/use-hot-reload.ts rename to packages/preview-server/src/hooks/use-hot-reload.ts diff --git a/packages/react-email/src/hooks/use-icon-animation.ts b/packages/preview-server/src/hooks/use-icon-animation.ts similarity index 100% rename from packages/react-email/src/hooks/use-icon-animation.ts rename to packages/preview-server/src/hooks/use-icon-animation.ts diff --git a/packages/react-email/src/hooks/use-rendering-metadata.ts b/packages/preview-server/src/hooks/use-rendering-metadata.ts similarity index 100% rename from packages/react-email/src/hooks/use-rendering-metadata.ts rename to packages/preview-server/src/hooks/use-rendering-metadata.ts diff --git a/packages/react-email/src/utils/__snapshots__/get-email-component.spec.ts.snap b/packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap similarity index 100% rename from packages/react-email/src/utils/__snapshots__/get-email-component.spec.ts.snap rename to packages/preview-server/src/utils/__snapshots__/get-email-component.spec.ts.snap diff --git a/packages/react-email/src/utils/caniemail/all-css-properties.ts b/packages/preview-server/src/utils/caniemail/all-css-properties.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/all-css-properties.ts rename to packages/preview-server/src/utils/caniemail/all-css-properties.ts diff --git a/packages/react-email/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap b/packages/preview-server/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap similarity index 100% rename from packages/react-email/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap rename to packages/preview-server/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap diff --git a/packages/react-email/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap b/packages/preview-server/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap similarity index 100% rename from packages/react-email/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap rename to packages/preview-server/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap diff --git a/packages/react-email/src/utils/caniemail/ast/get-object-variables.spec.ts b/packages/preview-server/src/utils/caniemail/ast/get-object-variables.spec.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/ast/get-object-variables.spec.ts rename to packages/preview-server/src/utils/caniemail/ast/get-object-variables.spec.ts diff --git a/packages/react-email/src/utils/caniemail/ast/get-object-variables.ts b/packages/preview-server/src/utils/caniemail/ast/get-object-variables.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/ast/get-object-variables.ts rename to packages/preview-server/src/utils/caniemail/ast/get-object-variables.ts index 5c0355793c..733fb23bf0 100644 --- a/packages/react-email/src/utils/caniemail/ast/get-object-variables.ts +++ b/packages/preview-server/src/utils/caniemail/ast/get-object-variables.ts @@ -1,5 +1,5 @@ -import traverse from '@babel/traverse'; import type { Node } from '@babel/traverse'; +import traverse from '@babel/traverse'; import type { AST } from '../../../actions/email-validation/check-compatibility'; export interface Position { diff --git a/packages/react-email/src/utils/caniemail/ast/get-used-style-properties.spec.ts b/packages/preview-server/src/utils/caniemail/ast/get-used-style-properties.spec.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/ast/get-used-style-properties.spec.ts rename to packages/preview-server/src/utils/caniemail/ast/get-used-style-properties.spec.ts diff --git a/packages/react-email/src/utils/caniemail/ast/get-used-style-properties.ts b/packages/preview-server/src/utils/caniemail/ast/get-used-style-properties.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/ast/get-used-style-properties.ts rename to packages/preview-server/src/utils/caniemail/ast/get-used-style-properties.ts diff --git a/packages/react-email/src/utils/caniemail/get-compatibility-stats-for-entry.ts b/packages/preview-server/src/utils/caniemail/get-compatibility-stats-for-entry.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/get-compatibility-stats-for-entry.ts rename to packages/preview-server/src/utils/caniemail/get-compatibility-stats-for-entry.ts diff --git a/packages/react-email/src/utils/caniemail/get-css-functions.ts b/packages/preview-server/src/utils/caniemail/get-css-functions.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/get-css-functions.ts rename to packages/preview-server/src/utils/caniemail/get-css-functions.ts diff --git a/packages/react-email/src/utils/caniemail/get-css-property-names.ts b/packages/preview-server/src/utils/caniemail/get-css-property-names.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/get-css-property-names.ts rename to packages/preview-server/src/utils/caniemail/get-css-property-names.ts diff --git a/packages/react-email/src/utils/caniemail/get-css-property-with-value.ts b/packages/preview-server/src/utils/caniemail/get-css-property-with-value.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/get-css-property-with-value.ts rename to packages/preview-server/src/utils/caniemail/get-css-property-with-value.ts diff --git a/packages/react-email/src/utils/caniemail/get-css-unit.ts b/packages/preview-server/src/utils/caniemail/get-css-unit.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/get-css-unit.ts rename to packages/preview-server/src/utils/caniemail/get-css-unit.ts diff --git a/packages/react-email/src/utils/caniemail/get-element-attributes.ts b/packages/preview-server/src/utils/caniemail/get-element-attributes.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/get-element-attributes.ts rename to packages/preview-server/src/utils/caniemail/get-element-attributes.ts diff --git a/packages/react-email/src/utils/caniemail/get-element-names.ts b/packages/preview-server/src/utils/caniemail/get-element-names.ts similarity index 58% rename from packages/react-email/src/utils/caniemail/get-element-names.ts rename to packages/preview-server/src/utils/caniemail/get-element-names.ts index ba108ac369..0dc9551b99 100644 --- a/packages/react-email/src/utils/caniemail/get-element-names.ts +++ b/packages/preview-server/src/utils/caniemail/get-element-names.ts @@ -4,16 +4,22 @@ export const getElementNames = (title: string, keywords: string | null) => { const [_full, elementName] = match; if (elementName) { - return [elementName]; + return [elementName.toLowerCase()]; } } if (keywords !== null && keywords.length > 0) { - return keywords.split(/\s*,\s*/).map((piece) => piece.trim()); + return keywords + .toLowerCase() + .split(/\s*,\s*/) + .map((piece) => piece.trim()); } if (title.split(',').length > 1) { - return title.split(/\s*,\s*/).map((piece) => piece.trim()); + return title + .toLowerCase() + .split(/\s*,\s*/) + .map((piece) => piece.trim()); } return []; diff --git a/packages/react-email/src/utils/caniemail/tailwind/generate-tailwind-rules.ts b/packages/preview-server/src/utils/caniemail/tailwind/generate-tailwind-rules.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/tailwind/generate-tailwind-rules.ts rename to packages/preview-server/src/utils/caniemail/tailwind/generate-tailwind-rules.ts index 59dfa145da..85ce82de58 100644 --- a/packages/react-email/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +++ b/packages/preview-server/src/utils/caniemail/tailwind/generate-tailwind-rules.ts @@ -1,5 +1,5 @@ -import postcss from 'postcss'; import type { Root, Rule } from 'postcss'; +import postcss from 'postcss'; import evaluateTailwindFunctions from 'tailwindcss/lib/lib/evaluateTailwindFunctions'; import { generateRules as rawGenerateRules } from 'tailwindcss/lib/lib/generateRules'; import type { JitContext } from 'tailwindcss/lib/lib/setupContextUtils'; diff --git a/packages/react-email/src/utils/caniemail/tailwind/get-tailwind-config.ts b/packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-config.ts similarity index 96% rename from packages/react-email/src/utils/caniemail/tailwind/get-tailwind-config.ts rename to packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-config.ts index 3b0137aa10..8e65f72602 100644 --- a/packages/react-email/src/utils/caniemail/tailwind/get-tailwind-config.ts +++ b/packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-config.ts @@ -1,7 +1,6 @@ -import fs from 'node:fs'; import path from 'node:path'; -import traverse from '@babel/traverse'; import type { Node } from '@babel/traverse'; +import traverse from '@babel/traverse'; import * as esbuild from 'esbuild'; import type { Config as TailwindOriginalConfig } from 'tailwindcss'; import type { AST } from '../../../actions/email-validation/check-compatibility'; @@ -24,14 +23,6 @@ export type TailwindConfig = Pick< | 'plugins' >; -const getFirstExistingFilepath = (filePaths: string[]) => { - for (const filePath of filePaths) { - if (fs.existsSync(filePath)) { - return filePath; - } - } -}; - type ImportDeclaration = Node & { type: 'ImportDeclaration' }; export const getTailwindConfig = async ( diff --git a/packages/react-email/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts b/packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts rename to packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts diff --git a/packages/react-email/src/utils/caniemail/tailwind/get-tailwind-metadata.ts b/packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-metadata.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/tailwind/get-tailwind-metadata.ts rename to packages/preview-server/src/utils/caniemail/tailwind/get-tailwind-metadata.ts diff --git a/packages/react-email/src/utils/caniemail/tailwind/setup-tailwind-context.ts b/packages/preview-server/src/utils/caniemail/tailwind/setup-tailwind-context.ts similarity index 100% rename from packages/react-email/src/utils/caniemail/tailwind/setup-tailwind-context.ts rename to packages/preview-server/src/utils/caniemail/tailwind/setup-tailwind-context.ts diff --git a/packages/react-email/src/utils/cn.ts b/packages/preview-server/src/utils/cn.ts similarity index 100% rename from packages/react-email/src/utils/cn.ts rename to packages/preview-server/src/utils/cn.ts diff --git a/packages/react-email/src/utils/constants.ts b/packages/preview-server/src/utils/constants.ts similarity index 100% rename from packages/react-email/src/utils/constants.ts rename to packages/preview-server/src/utils/constants.ts diff --git a/packages/react-email/src/utils/contains-email-template.spec.ts b/packages/preview-server/src/utils/contains-email-template.spec.ts similarity index 91% rename from packages/react-email/src/utils/contains-email-template.spec.ts rename to packages/preview-server/src/utils/contains-email-template.spec.ts index 129f4998b2..62e58e8969 100644 --- a/packages/react-email/src/utils/contains-email-template.spec.ts +++ b/packages/preview-server/src/utils/contains-email-template.spec.ts @@ -51,6 +51,13 @@ describe('containsEmailTemplate()', () => { }, ], }, + { + absolutePath: '/fake/path/emails/first/second', + directoryName: 'first/second', + relativePath: 'first/second', + emailFilenames: ['email'], + subDirectories: [], + }, { absolutePath: '/fake/path/emails/newsletters', directoryName: 'newsletters', @@ -104,6 +111,11 @@ describe('containsEmailTemplate()', () => { }, ], }; + + it('should work with collapsed email directory', () => { + expect(containsEmailTemplate('first/second/email', directory)).toBe(true); + }); + it('should work with email inside a single sub directory', () => { expect(containsEmailTemplate('welcome/koala-welcome', directory)).toBe( true, diff --git a/packages/preview-server/src/utils/contains-email-template.ts b/packages/preview-server/src/utils/contains-email-template.ts new file mode 100644 index 0000000000..a027b5be8c --- /dev/null +++ b/packages/preview-server/src/utils/contains-email-template.ts @@ -0,0 +1,50 @@ +import type { EmailsDirectory } from './get-emails-directory-metadata'; + +export const removeFilenameExtension = (filename: string): string => { + const parts = filename.split('.'); + + if (parts.length > 1) { + return parts.slice(0, -1).join('.'); + } + + return filename; +}; + +export const containsEmailTemplate = ( + relativeEmailPath: string, + directory: EmailsDirectory, +) => { + const emailPathSegments = relativeEmailPath + .replace(directory.relativePath, '') + .split('/') + .filter(Boolean); + + return containsEmailPathSegments(emailPathSegments, directory); +}; + +const containsEmailPathSegments = ( + relativeEmailSegments: string[], + directory: EmailsDirectory, +) => { + if (relativeEmailSegments.length === 1) { + const emailFilename = removeFilenameExtension(relativeEmailSegments[0]!); + return directory.emailFilenames.includes(emailFilename); + } + + const remainingPath = relativeEmailSegments.join('/'); + + for (const subDirectory of directory.subDirectories) { + if (remainingPath.startsWith(subDirectory.directoryName)) { + const matchedSegments = subDirectory.directoryName + .split('/') + .filter(Boolean).length; + + return containsEmailPathSegments( + relativeEmailSegments.slice(matchedSegments), + subDirectory, + ); + } + } + + return false; +}; diff --git a/packages/react-email/src/utils/copy-text-to-clipboard.ts b/packages/preview-server/src/utils/copy-text-to-clipboard.ts similarity index 100% rename from packages/react-email/src/utils/copy-text-to-clipboard.ts rename to packages/preview-server/src/utils/copy-text-to-clipboard.ts diff --git a/packages/preview-server/src/utils/esbuild/escape-string-for-regex.ts b/packages/preview-server/src/utils/esbuild/escape-string-for-regex.ts new file mode 100644 index 0000000000..71d343cd9a --- /dev/null +++ b/packages/preview-server/src/utils/esbuild/escape-string-for-regex.ts @@ -0,0 +1,3 @@ +export function escapeStringForRegex(string: string) { + return string.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&').replace(/-/g, '\\x2d'); +} diff --git a/packages/preview-server/src/utils/esbuild/renderring-utilities-exporter.ts b/packages/preview-server/src/utils/esbuild/renderring-utilities-exporter.ts new file mode 100644 index 0000000000..4856b15577 --- /dev/null +++ b/packages/preview-server/src/utils/esbuild/renderring-utilities-exporter.ts @@ -0,0 +1,63 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import type { Loader, PluginBuild, ResolveOptions } from 'esbuild'; +import { escapeStringForRegex } from './escape-string-for-regex'; + +/** + * Made to export the `render` function out of the user's email template + * so that issues like https://github.com/resend/react-email/issues/649 don't + * happen. + * + * This also exports the `createElement` from the user's React version as well + * to avoid mismatches. + * + * This avoids multiple versions of React being involved, i.e., the version + * in the CLI vs. the version the user has on their emails. + */ +export const renderingUtilitiesExporter = (emailTemplates: string[]) => ({ + name: 'rendering-utilities-exporter', + setup: (b: PluginBuild) => { + b.onLoad( + { + filter: new RegExp( + emailTemplates + .map((emailPath) => escapeStringForRegex(emailPath)) + .join('|'), + ), + }, + async ({ path: pathToFile }) => { + return { + contents: `${await fs.readFile(pathToFile, 'utf8')}; + export { render } from 'react-email-module-that-will-export-render' + export { createElement as reactEmailCreateReactElement } from 'react'; + `, + loader: path.extname(pathToFile).slice(1) as Loader, + }; + }, + ); + + b.onResolve( + { filter: /^react-email-module-that-will-export-render$/ }, + async (args) => { + const options: ResolveOptions = { + kind: 'import-statement', + importer: args.importer, + resolveDir: args.resolveDir, + namespace: args.namespace, + }; + let result = await b.resolve('@react-email/render', options); + if (result.errors.length === 0) { + return result; + } + + // If @react-email/render does not exist, resolve to @react-email/components + result = await b.resolve('@react-email/components', options); + if (result.errors.length > 0 && result.errors[0]) { + result.errors[0].text = + "Failed trying to import `render` from either `@react-email/render` or `@react-email/components` to be able to render your email template.\n Maybe you don't have either of them installed?"; + } + return result; + }, + ); + }, +}); diff --git a/packages/react-email/src/utils/get-email-component.spec.ts b/packages/preview-server/src/utils/get-email-component.spec.ts similarity index 100% rename from packages/react-email/src/utils/get-email-component.spec.ts rename to packages/preview-server/src/utils/get-email-component.spec.ts diff --git a/packages/react-email/src/utils/get-email-component.ts b/packages/preview-server/src/utils/get-email-component.ts similarity index 100% rename from packages/react-email/src/utils/get-email-component.ts rename to packages/preview-server/src/utils/get-email-component.ts diff --git a/packages/preview-server/src/utils/get-emails-directory-metadata.spec.ts b/packages/preview-server/src/utils/get-emails-directory-metadata.spec.ts new file mode 100644 index 0000000000..1f7aaae36b --- /dev/null +++ b/packages/preview-server/src/utils/get-emails-directory-metadata.spec.ts @@ -0,0 +1,82 @@ +import path from 'node:path'; +import { getEmailsDirectoryMetadata } from './get-emails-directory-metadata'; + +test('getEmailsDirectoryMetadata on demo emails', async () => { + const emailsDirectoryPath = path.resolve( + __dirname, + '../../../../apps/demo/emails', + ); + expect(await getEmailsDirectoryMetadata(emailsDirectoryPath)).toEqual({ + absolutePath: emailsDirectoryPath, + directoryName: 'emails', + relativePath: '', + emailFilenames: [], + subDirectories: [ + { + absolutePath: `${emailsDirectoryPath}/magic-links`, + directoryName: 'magic-links', + relativePath: 'magic-links', + emailFilenames: [ + 'aws-verify-email', + 'linear-login-code', + 'notion-magic-link', + 'plaid-verify-identity', + 'raycast-magic-link', + 'slack-confirm', + ], + subDirectories: [], + }, + { + absolutePath: `${emailsDirectoryPath}/newsletters`, + directoryName: 'newsletters', + relativePath: 'newsletters', + emailFilenames: [ + 'codepen-challengers', + 'google-play-policy-update', + 'stack-overflow-tips', + ], + subDirectories: [], + }, + { + absolutePath: `${emailsDirectoryPath}/notifications`, + directoryName: 'notifications', + relativePath: 'notifications', + emailFilenames: [ + 'github-access-token', + 'papermark-year-in-review', + 'vercel-invite-user', + 'yelp-recent-login', + ], + subDirectories: [], + }, + { + absolutePath: `${emailsDirectoryPath}/receipts`, + directoryName: 'receipts', + relativePath: 'receipts', + emailFilenames: ['apple-receipt', 'nike-receipt'], + subDirectories: [], + }, + { + absolutePath: `${emailsDirectoryPath}/reset-password`, + directoryName: 'reset-password', + relativePath: 'reset-password', + emailFilenames: ['dropbox-reset-password', 'twitch-reset-password'], + subDirectories: [], + }, + { + absolutePath: `${emailsDirectoryPath}/reviews`, + directoryName: 'reviews', + relativePath: 'reviews', + emailFilenames: ['airbnb-review', 'amazon-review'], + subDirectories: [], + }, + { + absolutePath: `${emailsDirectoryPath}/welcome`, + directoryName: 'welcome', + relativePath: 'welcome', + emailFilenames: ['koala-welcome', 'netlify-welcome', 'stripe-welcome'], + subDirectories: [], + }, + ], + }); +}); diff --git a/packages/preview-server/src/utils/get-emails-directory-metadata.ts b/packages/preview-server/src/utils/get-emails-directory-metadata.ts new file mode 100644 index 0000000000..939f71f895 --- /dev/null +++ b/packages/preview-server/src/utils/get-emails-directory-metadata.ts @@ -0,0 +1,141 @@ +/* eslint-disable @typescript-eslint/no-non-null-assertion */ +import fs from 'node:fs'; +import path from 'node:path'; + +const isFileAnEmail = async (fullPath: string): Promise => { + let fileHandle: fs.promises.FileHandle; + try { + fileHandle = await fs.promises.open(fullPath, 'r'); + } catch (exception) { + console.warn(exception); + return false; + } + const stat = await fileHandle.stat(); + + if (stat.isDirectory()) { + await fileHandle.close(); + return false; + } + + const { ext } = path.parse(fullPath); + + if (!['.js', '.tsx', '.jsx'].includes(ext)) { + await fileHandle.close(); + return false; + } + + // check with a heuristic to see if the file has at least + // a default export (ES6) or module.exports (CommonJS) or named exports (MDX) + const fileContents = await fileHandle.readFile('utf8'); + + await fileHandle.close(); + + // Check for ES6 export default syntax + const hasES6DefaultExport = /\bexport\s+default\b/gm.test(fileContents); + + // Check for CommonJS module.exports syntax + const hasCommonJSExport = /\bmodule\.exports\s*=/gm.test(fileContents); + + // Check for named exports (used in MDX files) and ensure at least one is marked as default + const hasNamedExport = /\bexport\s+\{[^}]*\bdefault\b[^}]*\}/gm.test( + fileContents, + ); + + return hasES6DefaultExport || hasCommonJSExport || hasNamedExport; +}; + +export interface EmailsDirectory { + absolutePath: string; + relativePath: string; + directoryName: string; + emailFilenames: string[]; + subDirectories: EmailsDirectory[]; +} + +const mergeDirectoriesWithSubDirectories = ( + emailsDirectoryMetadata: EmailsDirectory, +): EmailsDirectory => { + let currentResultingMergedDirectory: EmailsDirectory = + emailsDirectoryMetadata; + + while ( + currentResultingMergedDirectory.emailFilenames.length === 0 && + currentResultingMergedDirectory.subDirectories.length === 1 + ) { + const onlySubDirectory = currentResultingMergedDirectory.subDirectories[0]!; + currentResultingMergedDirectory = { + ...onlySubDirectory, + directoryName: path.join( + currentResultingMergedDirectory.directoryName, + onlySubDirectory.directoryName, + ), + }; + } + + return currentResultingMergedDirectory; +}; + +export const getEmailsDirectoryMetadata = async ( + absolutePathToEmailsDirectory: string, + keepFileExtensions = false, + isSubDirectory = false, + + baseDirectoryPath = absolutePathToEmailsDirectory, +): Promise => { + if (!fs.existsSync(absolutePathToEmailsDirectory)) return; + + const dirents = await fs.promises.readdir(absolutePathToEmailsDirectory, { + withFileTypes: true, + }); + + const isEmailPredicates = await Promise.all( + dirents.map((dirent) => + isFileAnEmail(path.join(absolutePathToEmailsDirectory, dirent.name)), + ), + ); + const emailFilenames = dirents + .filter((_, i) => isEmailPredicates[i]) + .map((dirent) => + keepFileExtensions + ? dirent.name + : dirent.name.replace(path.extname(dirent.name), ''), + ); + + const subDirectories = await Promise.all( + dirents + .filter( + (dirent) => + dirent.isDirectory() && + !dirent.name.startsWith('_') && + dirent.name !== 'static', + ) + .map((dirent) => { + const direntAbsolutePath = path.join( + absolutePathToEmailsDirectory, + dirent.name, + ); + + return getEmailsDirectoryMetadata( + direntAbsolutePath, + keepFileExtensions, + true, + baseDirectoryPath, + ) as Promise; + }), + ); + + const emailsMetadata = { + absolutePath: absolutePathToEmailsDirectory, + relativePath: path.relative( + baseDirectoryPath, + absolutePathToEmailsDirectory, + ), + directoryName: absolutePathToEmailsDirectory.split(path.sep).pop()!, + emailFilenames, + subDirectories, + } satisfies EmailsDirectory; + + return isSubDirectory + ? mergeDirectoriesWithSubDirectories(emailsMetadata) + : emailsMetadata; +}; diff --git a/packages/react-email/src/utils/get-line-and-column-from-offset.spec.ts b/packages/preview-server/src/utils/get-line-and-column-from-offset.spec.ts similarity index 100% rename from packages/react-email/src/utils/get-line-and-column-from-offset.spec.ts rename to packages/preview-server/src/utils/get-line-and-column-from-offset.spec.ts diff --git a/packages/react-email/src/utils/get-line-and-column-from-offset.ts b/packages/preview-server/src/utils/get-line-and-column-from-offset.ts similarity index 100% rename from packages/react-email/src/utils/get-line-and-column-from-offset.ts rename to packages/preview-server/src/utils/get-line-and-column-from-offset.ts diff --git a/packages/react-email/src/utils/improve-error-with-sourcemap.ts b/packages/preview-server/src/utils/improve-error-with-sourcemap.ts similarity index 99% rename from packages/react-email/src/utils/improve-error-with-sourcemap.ts rename to packages/preview-server/src/utils/improve-error-with-sourcemap.ts index fca0253ea0..475191b3a9 100644 --- a/packages/react-email/src/utils/improve-error-with-sourcemap.ts +++ b/packages/preview-server/src/utils/improve-error-with-sourcemap.ts @@ -80,7 +80,6 @@ export const improveErrorWithSourceMap = ( return { name: error.name, message: error.message, - cause: error.cause, stack, }; }; diff --git a/packages/preview-server/src/utils/index.ts b/packages/preview-server/src/utils/index.ts new file mode 100644 index 0000000000..7a3cd2e7b6 --- /dev/null +++ b/packages/preview-server/src/utils/index.ts @@ -0,0 +1,6 @@ +export * from './cn'; +export * from './copy-text-to-clipboard'; +export * from './language-map'; +export * from './sanitize'; +export * from './types/as'; +export * from './unreachable'; diff --git a/packages/react-email/src/utils/js-email-detection.spec.ts b/packages/preview-server/src/utils/js-email-detection.spec.ts similarity index 100% rename from packages/react-email/src/utils/js-email-detection.spec.ts rename to packages/preview-server/src/utils/js-email-detection.spec.ts diff --git a/packages/react-email/src/utils/language-map.ts b/packages/preview-server/src/utils/language-map.ts similarity index 100% rename from packages/react-email/src/utils/language-map.ts rename to packages/preview-server/src/utils/language-map.ts diff --git a/packages/react-email/src/utils/linting.ts b/packages/preview-server/src/utils/linting.ts similarity index 100% rename from packages/react-email/src/utils/linting.ts rename to packages/preview-server/src/utils/linting.ts diff --git a/packages/react-email/src/utils/load-stream.ts b/packages/preview-server/src/utils/load-stream.ts similarity index 100% rename from packages/react-email/src/utils/load-stream.ts rename to packages/preview-server/src/utils/load-stream.ts diff --git a/packages/preview-server/src/utils/register-spinner-autostopping.ts b/packages/preview-server/src/utils/register-spinner-autostopping.ts new file mode 100644 index 0000000000..2a4a156b4d --- /dev/null +++ b/packages/preview-server/src/utils/register-spinner-autostopping.ts @@ -0,0 +1,28 @@ +import logSymbols from 'log-symbols'; +import type { Ora } from 'ora'; + +const spinners = new Set(); + +process.on('SIGINT', () => { + spinners.forEach((spinner) => { + if (spinner.isSpinning) { + spinner.stop(); + } + }); +}); + +process.on('exit', (code) => { + if (code !== 0) { + spinners.forEach((spinner) => { + if (spinner.isSpinning) { + spinner.stopAndPersist({ + symbol: logSymbols.error, + }); + } + }); + } +}); + +export const registerSpinnerAutostopping = (spinner: Ora) => { + spinners.add(spinner); +}; diff --git a/packages/react-email/src/utils/result.ts b/packages/preview-server/src/utils/result.ts similarity index 88% rename from packages/react-email/src/utils/result.ts rename to packages/preview-server/src/utils/result.ts index 19125a920e..e6c519fbea 100644 --- a/packages/react-email/src/utils/result.ts +++ b/packages/preview-server/src/utils/result.ts @@ -32,7 +32,7 @@ export function mapResult( export function ok(value: NoInfer): Ok; // biome-ignore lint/suspicious/noConfusingVoidType: This is required for void return types on functions that can still error -export function ok(value: void): Ok; +export function ok<_T extends void = void, E = never>(value: void): Ok; export function ok(value: NoInfer): Ok { return { value, @@ -41,7 +41,7 @@ export function ok(value: NoInfer): Ok { export function err(error: NoInfer): Error; // biome-ignore lint/suspicious/noConfusingVoidType: This is required for void return types on functions that can still error -export function err(error: void): Error; +export function err(error: void): Error; export function err(error: NoInfer): Error { return { error, diff --git a/packages/react-email/src/utils/run-bundled-code.ts b/packages/preview-server/src/utils/run-bundled-code.ts similarity index 100% rename from packages/react-email/src/utils/run-bundled-code.ts rename to packages/preview-server/src/utils/run-bundled-code.ts diff --git a/packages/react-email/src/utils/sanitize.ts b/packages/preview-server/src/utils/sanitize.ts similarity index 100% rename from packages/react-email/src/utils/sanitize.ts rename to packages/preview-server/src/utils/sanitize.ts diff --git a/packages/react-email/src/utils/static-node-modules-for-vm.ts b/packages/preview-server/src/utils/static-node-modules-for-vm.ts similarity index 100% rename from packages/react-email/src/utils/static-node-modules-for-vm.ts rename to packages/preview-server/src/utils/static-node-modules-for-vm.ts diff --git a/packages/react-email/src/utils/testing/js-email-export-default.js b/packages/preview-server/src/utils/testing/js-email-export-default.js similarity index 100% rename from packages/react-email/src/utils/testing/js-email-export-default.js rename to packages/preview-server/src/utils/testing/js-email-export-default.js diff --git a/packages/react-email/src/utils/testing/js-email-test.js b/packages/preview-server/src/utils/testing/js-email-test.js similarity index 91% rename from packages/react-email/src/utils/testing/js-email-test.js rename to packages/preview-server/src/utils/testing/js-email-test.js index 10bb11a668..4239310c92 100644 --- a/packages/react-email/src/utils/testing/js-email-test.js +++ b/packages/preview-server/src/utils/testing/js-email-test.js @@ -1,5 +1,5 @@ // A simple JavaScript email component -const React = require('react'); +const _React = require('react'); const { Html, Button } = require('@react-email/components'); function Email() { diff --git a/packages/react-email/src/utils/testing/mdx-email-test.js b/packages/preview-server/src/utils/testing/mdx-email-test.js similarity index 100% rename from packages/react-email/src/utils/testing/mdx-email-test.js rename to packages/preview-server/src/utils/testing/mdx-email-test.js diff --git a/packages/react-email/src/utils/testing/request-response-email.tsx b/packages/preview-server/src/utils/testing/request-response-email.tsx similarity index 59% rename from packages/react-email/src/utils/testing/request-response-email.tsx rename to packages/preview-server/src/utils/testing/request-response-email.tsx index 0272314717..0156c1d3f1 100644 --- a/packages/react-email/src/utils/testing/request-response-email.tsx +++ b/packages/preview-server/src/utils/testing/request-response-email.tsx @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -const req = new Request('https://react.email'); -const res = new Response('{}'); +const _req = new Request('https://react.email'); +const _res = new Response('{}'); const Email = () => { return
; diff --git a/packages/react-email/src/utils/types/as.ts b/packages/preview-server/src/utils/types/as.ts similarity index 100% rename from packages/react-email/src/utils/types/as.ts rename to packages/preview-server/src/utils/types/as.ts diff --git a/packages/react-email/src/utils/types/email-template.ts b/packages/preview-server/src/utils/types/email-template.ts similarity index 100% rename from packages/react-email/src/utils/types/email-template.ts rename to packages/preview-server/src/utils/types/email-template.ts diff --git a/packages/react-email/src/utils/types/error-object.ts b/packages/preview-server/src/utils/types/error-object.ts similarity index 93% rename from packages/react-email/src/utils/types/error-object.ts rename to packages/preview-server/src/utils/types/error-object.ts index 324f94bec9..286f0d1582 100644 --- a/packages/react-email/src/utils/types/error-object.ts +++ b/packages/preview-server/src/utils/types/error-object.ts @@ -6,6 +6,6 @@ export interface ErrorObject { name: string; stack: string | undefined; - cause: unknown; + cause?: unknown; message: string; } diff --git a/packages/preview-server/src/utils/types/hot-reload-change.ts b/packages/preview-server/src/utils/types/hot-reload-change.ts new file mode 100644 index 0000000000..15b6be7d89 --- /dev/null +++ b/packages/preview-server/src/utils/types/hot-reload-change.ts @@ -0,0 +1,13 @@ +export interface HotReloadChange { + filename: string; + event: + | 'all' + | 'ready' + | 'add' + | 'change' + | 'addDir' + | 'unlink' + | 'unlinkDir' + | 'raw' + | 'error'; +} diff --git a/packages/react-email/src/utils/unreachable.ts b/packages/preview-server/src/utils/unreachable.ts similarity index 100% rename from packages/react-email/src/utils/unreachable.ts rename to packages/preview-server/src/utils/unreachable.ts diff --git a/packages/react-email/tailwind-internals.d.ts b/packages/preview-server/tailwind-internals.d.ts similarity index 100% rename from packages/react-email/tailwind-internals.d.ts rename to packages/preview-server/tailwind-internals.d.ts diff --git a/packages/react-email/tailwind.config.ts b/packages/preview-server/tailwind.config.ts similarity index 100% rename from packages/react-email/tailwind.config.ts rename to packages/preview-server/tailwind.config.ts diff --git a/packages/preview-server/tsconfig.json b/packages/preview-server/tsconfig.json new file mode 100644 index 0000000000..7a12ec6b6a --- /dev/null +++ b/packages/preview-server/tsconfig.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Next.js", + "compilerOptions": { + "composite": false, + "downlevelIteration": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "moduleResolution": "node", + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strictNullChecks": true, + "plugins": [ + { + "name": "next" + } + ], + "allowJs": true, + "declaration": false, + "declarationMap": false, + "incremental": false, + "jsx": "preserve", + "lib": ["dom", "dom.iterable", "esnext", "ESNext.AsyncIterable"], + "noEmit": true, + "strict": false, + "target": "ESNext", + "module": "CommonJS", + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "types": ["vitest/globals"], + "outDir": "dist" + }, + "include": [ + "next-env.d.ts", + "tailwind-internals.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [".next", "dist", "node_modules"] +} diff --git a/packages/preview-server/vitest.config.ts b/packages/preview-server/vitest.config.ts new file mode 100644 index 0000000000..bf6076a810 --- /dev/null +++ b/packages/preview-server/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'happy-dom', + }, + esbuild: { + tsconfigRaw: { + compilerOptions: { + jsx: 'react-jsx', + }, + }, + }, +}); diff --git a/packages/react-email/CHANGELOG.md b/packages/react-email/CHANGELOG.md index 5ded395f77..e371da5ec2 100644 --- a/packages/react-email/CHANGELOG.md +++ b/packages/react-email/CHANGELOG.md @@ -1,5 +1,76 @@ # react-email +## 4.1.0-canary.11 + +### Patch Changes + +- 19d4b45: fix static file serving security issue with logging + +## 4.1.0-canary.10 + +## 4.1.0-canary.9 + +### Patch Changes + +- 40fb596: Use the same version for the preview-server and react-email + +## 4.1.0-canary.8 + +### Minor Changes + +- ed2f46a: use a separate package for storing the preview server (@react-email/preview-server) + +## 4.1.0-canary.7 + +### Patch Changes + +- f00aff6: Fix prettier errors causing NextJS serialization error + +## 4.1.0-canary.6 + +### Patch Changes + +- 11c4600: Add support for hot reloading with tsconfig path aliases + +## 4.1.0-canary.5 + +### Patch Changes + +- b947f98: Pre-render email templates on hover + +## 4.1.0-canary.4 + +### Patch Changes + +- 5c6150d: Add .json import support for hot reloading +- aae2f59: Ensure dependencies outside emails directory are completely resolved +- b4b2373: Fix non-email files being rendered during hot reloading + +## 4.1.0-canary.3 + +### Patch Changes + +- 681d4ed: fix backwards compatibility with `render` versions + +## 4.1.0-canary.2 + +### Patch Changes + +- 9b1adb0: Use range of versions for dependencies + +## 4.1.0-canary.1 + +### Patch Changes + +- a587e17: Fix dependent of dependents not causing hot reloads +- dbf3a64: Add error message for when an email template does not have a default export + +## 4.1.0-canary.0 + +### Minor Changes + +- 4a0d4e3: Theme switcher for email template + ## 4.0.16 ### Patch Changes diff --git a/packages/react-email/dev/index.js b/packages/react-email/dev/index.js new file mode 100755 index 0000000000..11820ca0c0 --- /dev/null +++ b/packages/react-email/dev/index.js @@ -0,0 +1,40 @@ +import child_process from 'node:child_process'; +import path from 'node:path'; +import url from 'node:url'; + +const filename = url.fileURLToPath(import.meta.url); +const dirname = path.dirname(filename); + +const root = path.resolve(dirname, '../src/index.ts'); + +const tsxPath = path.resolve(dirname, './node_modules/.bin/tsx'); + +const tsx = child_process.spawn(tsxPath, [root, ...process.argv.slice(2)], { + shell: true, + cwd: process.cwd(), + stdio: 'inherit', +}); + +process.on('uncaughtExceptionMonitor', () => { + tsx.kill(); +}); + +process.on('exit', (code) => { + tsx.kill(code); +}); + +process.on('SIGINT', () => { + tsx.kill('SIGINT'); +}); + +process.on('SIGTERM', () => { + tsx.kill('SIGTERM'); +}); + +process.on('SIGUSR1', () => { + tsx.kill('SIGUSR1'); +}); + +process.on('SIGUSR2', () => { + tsx.kill('SIGUSR2'); +}); diff --git a/packages/react-email/dev/package.json b/packages/react-email/dev/package.json new file mode 100644 index 0000000000..7ba9a4b8ad --- /dev/null +++ b/packages/react-email/dev/package.json @@ -0,0 +1,13 @@ +{ + "name": "email-dev", + "version": "0.0.0", + "bin": "index.js", + "private": true, + "type": "module", + "scripts": { + "start": "node ." + }, + "dependencies": { + "tsx": "4.19.3" + } +} diff --git a/packages/react-email/package.json b/packages/react-email/package.json index 9956604bad..4bff5c0287 100644 --- a/packages/react-email/package.json +++ b/packages/react-email/package.json @@ -1,16 +1,15 @@ { "name": "react-email", - "version": "4.0.16", + "version": "4.1.0-canary.11", "description": "A live preview of your emails right in your browser.", "bin": { - "email": "./dist/cli/index.mjs" + "email": "./dist/index.js" }, + "type": "module", "scripts": { - "build": "tsup-node && node ./scripts/build-preview-server.mjs && pnpm install --frozen-lockfile", - "caniemail:fetch": "node ./scripts/fill-caniemail-data.mjs", + "build": "tsup-node", "clean": "rm -rf dist", - "dev": "tsup-node --watch", - "dev:preview": "cd ../../apps/demo && tsx ../../packages/react-email/src/cli/index.ts dev", + "dev": "tsup-node --watch src", "test": "vitest run", "test:watch": "vitest" }, @@ -36,59 +35,27 @@ "debounce": "^2.0.0", "esbuild": "^0.25.0", "glob": "^11.0.0", + "jiti": "2.4.2", "log-symbols": "^7.0.0", "mime-types": "^3.0.0", - "next": "^15.3.1", "normalize-path": "^3.0.0", + "nypm": "0.6.0", "ora": "^8.0.0", - "socket.io": "^4.8.1" + "prompts": "2.4.2", + "socket.io": "^4.8.1", + "tsconfig-paths": "4.2.0" }, "devDependencies": { - "@babel/core": "7.26.10", - "@lottiefiles/dotlottie-react": "0.13.3", - "@radix-ui/colors": "3.0.0", - "@radix-ui/react-collapsible": "1.1.7", - "@radix-ui/react-dropdown-menu": "2.1.10", - "@radix-ui/react-popover": "1.1.10", - "@radix-ui/react-slot": "1.2.0", - "@radix-ui/react-tabs": "1.1.7", - "@radix-ui/react-toggle-group": "1.1.6", - "@radix-ui/react-tooltip": "1.2.3", "@react-email/components": "workspace:*", - "@swc/core": "1.11.21", "@types/babel__core": "7.20.5", "@types/babel__traverse": "7.20.7", - "@types/fs-extra": "11.0.1", "@types/mime-types": "2.1.4", - "@types/node": "22.14.1", - "@types/normalize-path": "3.0.2", - "@types/react": "19.0.10", - "@types/react-dom": "19.0.4", - "@types/webpack": "5.28.5", - "autoprefixer": "10.4.21", - "clsx": "2.1.1", - "framer-motion": "12.7.5", - "jiti": "2.4.2", - "json5": "2.2.3", - "module-punycode": "npm:punycode@2.3.1", - "node-html-parser": "7.0.1", - "postcss": "8.5.3", - "pretty-bytes": "6.1.1", - "prism-react-renderer": "2.4.1", + "@types/prompts": "2.4.9", + "next": "^15.3.2", "react": "19.0.0", "react-dom": "19.0.0", - "sharp": "0.34.1", - "socket.io-client": "4.8.1", - "sonner": "2.0.3", - "source-map-js": "1.2.1", - "spamc": "0.0.5", - "stacktrace-parser": "0.1.11", - "tailwind-merge": "3.2.0", - "tailwindcss": "3.4.0", "tsup": "8.4.0", "tsx": "4.19.3", - "typescript": "5.8.3", - "use-debounce": "10.0.4", - "zod": "3.24.3" + "typescript": "5.8.3" } } diff --git a/packages/react-email/readme.md b/packages/react-email/readme.md index dba7837160..4073ac1b03 100644 --- a/packages/react-email/readme.md +++ b/packages/react-email/readme.md @@ -39,6 +39,22 @@ Generates the plain HTML files of your emails into a `out` directory. npx react-email export ``` +## Setting Up the Environment + +When working in the CLI, a lot of friction can get introduced with installing it and rebuilding for every change. To avoid that, we have a script that can be linked globally to directly run the source code of the CLI. You can use it the same as you would the standard CLI. + +### 1. Link `react-email` globally + +```sh +pnpm link ./dev -g +``` + +### 2. Run the CLI + +```sh +email-dev [command] [flags] +``` + ## License MIT License diff --git a/packages/react-email/scripts/build-preview-server.mjs b/packages/react-email/scripts/build-preview-server.mjs deleted file mode 100644 index 8c54a66083..0000000000 --- a/packages/react-email/scripts/build-preview-server.mjs +++ /dev/null @@ -1,33 +0,0 @@ -import { spawn } from 'node:child_process'; -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -const nextBuildProcess = spawn('pnpm', ['next', 'build'], { - detached: true, - shell: true, - stdio: 'inherit', - cwd: path.resolve(__dirname, '../'), -}); - -process.on('SIGINT', () => { - nextBuildProcess.kill('SIGINT'); -}); - -nextBuildProcess.on('exit', (code) => { - if (code !== 0) { - console.error(`next build failed with exit code ${code}`); - process.exit(code); - } - - const builtPreviewPath = path.resolve(__dirname, '../dist/preview'); - - if (fs.existsSync(builtPreviewPath)) { - fs.rmSync(builtPreviewPath, { recursive: true }); - } - fs.mkdirSync(builtPreviewPath, { recursive: true }); - fs.rmSync('.next/cache', { recursive: true }); - fs.renameSync('.next', path.join(builtPreviewPath, '/.next')); -}); diff --git a/packages/react-email/src/cli/commands/testing/export.spec.ts b/packages/react-email/src/cli/commands/testing/export.spec.ts deleted file mode 100644 index f75662ab54..0000000000 --- a/packages/react-email/src/cli/commands/testing/export.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import fs from 'node:fs'; -import path from 'node:path'; -import { exportTemplates } from '../export'; - -test( - 'email export', - async () => { - const pathToEmailsDirectory = path.resolve( - __dirname, - '../../../../../../apps/demo/emails', - ); - const pathToDumpMarkup = path.resolve(__dirname, './out'); - await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, { - silent: true, - pretty: true, - }); - - expect(fs.existsSync(pathToDumpMarkup)).toBe(true); - expect( - await fs.promises.readFile( - path.resolve( - pathToDumpMarkup, - './notifications/vercel-invite-user.html', - ), - 'utf8', - ), - ).toMatchSnapshot(); - }, - { retry: 3 }, -); diff --git a/packages/react-email/src/cli/utils/index.ts b/packages/react-email/src/cli/utils/index.ts deleted file mode 100644 index 0aaa0b2ead..0000000000 --- a/packages/react-email/src/cli/utils/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './preview'; -export * from './tree'; diff --git a/packages/react-email/src/cli/utils/preview/index.ts b/packages/react-email/src/cli/utils/preview/index.ts deleted file mode 100644 index 46a98521ab..0000000000 --- a/packages/react-email/src/cli/utils/preview/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './hot-reloading/setup-hot-reloading'; -export * from './start-dev-server'; diff --git a/packages/react-email/src/cli/utils/tree.spec.ts b/packages/react-email/src/cli/utils/tree.spec.ts deleted file mode 100644 index 29278f9393..0000000000 --- a/packages/react-email/src/cli/utils/tree.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { tree } from './tree'; - -test('tree(__dirname, 2)', async () => { - expect(await tree(__dirname, 2)).toMatchInlineSnapshot(`"utils -├── preview -│ ├── hot-reloading -│ ├── get-env-variables-for-preview-app.ts -│ ├── index.ts -│ ├── serve-static-file.ts -│ └── start-dev-server.ts -├── index.ts -├── tree.spec.ts -└── tree.ts"`); -}); diff --git a/packages/react-email/src/cli/commands/.npmignore b/packages/react-email/src/commands/.npmignore similarity index 100% rename from packages/react-email/src/cli/commands/.npmignore rename to packages/react-email/src/commands/.npmignore diff --git a/packages/react-email/src/cli/commands/build.ts b/packages/react-email/src/commands/build.ts similarity index 94% rename from packages/react-email/src/cli/commands/build.ts rename to packages/react-email/src/commands/build.ts index 56e529ee8d..a4394cc78e 100644 --- a/packages/react-email/src/cli/commands/build.ts +++ b/packages/react-email/src/commands/build.ts @@ -6,9 +6,9 @@ import ora from 'ora'; import { type EmailsDirectory, getEmailsDirectoryMetadata, -} from '../../utils/get-emails-directory-metadata'; -import { registerSpinnerAutostopping } from '../../utils/register-spinner-autostopping'; -import { cliPackageLocation } from '../utils'; +} from '../utils/get-emails-directory-metadata.js'; +import { getPreviewServerLocation } from '../utils/get-preview-server-location.js'; +import { registerSpinnerAutostopping } from '../utils/register-spinner-autostopping.js'; interface Args { dir: string; @@ -38,6 +38,39 @@ const buildPreviewApp = (absoluteDirectory: string) => { }); }; +const npmInstall = async ( + builtPreviewAppPath: string, + packageManager: string, +) => { + return new Promise((resolve, reject) => { + const childProc = spawn( + packageManager, + [ + 'install', + packageManager === 'deno' ? '' : '--include=dev', + packageManager === 'deno' ? '--quiet' : '--silent', + ], + { + cwd: builtPreviewAppPath, + shell: true, + }, + ); + childProc.stdout.pipe(process.stdout); + childProc.stderr.pipe(process.stderr); + childProc.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject( + new Error( + `Unable to install the dependencies and it exited with code: ${code}`, + ), + ); + } + }); + }); +}; + const setNextEnvironmentVariablesForBuild = async ( emailsDirRelativePath: string, builtPreviewAppPath: string, @@ -45,7 +78,7 @@ const setNextEnvironmentVariablesForBuild = async ( const nextConfigContents = ` const path = require('path'); const emailsDirRelativePath = path.normalize('${emailsDirRelativePath}'); -const userProjectLocation = path.resolve(process.cwd(), '../'); +const userProjectLocation = '${process.cwd()}'; /** @type {import('next').NextConfig} */ module.exports = { env: { @@ -170,8 +203,10 @@ const updatePackageJson = async (builtPreviewAppPath: string) => { }; packageJson.scripts.build = 'next build'; packageJson.scripts.start = 'next start'; + delete packageJson.scripts.postbuild; packageJson.name = 'preview-server'; + // We remove this one to avoid having resolve issues on our demo build process. // This is only used in the `export` command so it's irrelevant to have it here. // @@ -180,6 +215,7 @@ const updatePackageJson = async (builtPreviewAppPath: string) => { delete packageJson.devDependencies['@react-email/render']; delete packageJson.devDependencies['@react-email/components']; delete packageJson.scripts.prepare; + await fs.promises.writeFile( packageJsonPath, JSON.stringify(packageJson), @@ -187,44 +223,13 @@ const updatePackageJson = async (builtPreviewAppPath: string) => { ); }; -const npmInstall = async ( - builtPreviewAppPath: string, - packageManager: string, -) => { - return new Promise((resolve, reject) => { - const childProc = spawn( - packageManager, - [ - 'install', - packageManager === 'deno' ? '' : '--include=dev', - packageManager === 'deno' ? '--quiet' : '--silent', - ], - { - cwd: builtPreviewAppPath, - shell: true, - }, - ); - childProc.stdout.pipe(process.stdout); - childProc.stderr.pipe(process.stderr); - childProc.on('close', (code) => { - if (code === 0) { - resolve(); - } else { - reject( - new Error( - `Unable to install the dependencies and it exited with code: ${code}`, - ), - ); - } - }); - }); -}; - export const build = async ({ dir: emailsDirRelativePath, packageManager, }: Args) => { try { + const previewServerLocation = await getPreviewServerLocation(); + const spinner = ora({ text: 'Starting build process...', prefixText: ' ', @@ -247,7 +252,7 @@ export const build = async ({ } spinner.text = 'Copying preview app from CLI to `.react-email`'; - await fs.promises.cp(cliPackageLocation, builtPreviewAppPath, { + await fs.promises.cp(previewServerLocation, builtPreviewAppPath, { recursive: true, filter: (source: string) => { // do not copy the CLI files diff --git a/packages/react-email/src/cli/commands/dev.ts b/packages/react-email/src/commands/dev.ts similarity index 90% rename from packages/react-email/src/cli/commands/dev.ts rename to packages/react-email/src/commands/dev.ts index 6f9ed6803d..f499041abd 100644 --- a/packages/react-email/src/cli/commands/dev.ts +++ b/packages/react-email/src/commands/dev.ts @@ -1,5 +1,5 @@ import fs from 'node:fs'; -import { setupHotreloading, startDevServer } from '../utils'; +import { setupHotreloading, startDevServer } from '../utils/index.js'; interface Args { dir: string; diff --git a/packages/react-email/src/cli/commands/export.ts b/packages/react-email/src/commands/export.ts similarity index 95% rename from packages/react-email/src/cli/commands/export.ts rename to packages/react-email/src/commands/export.ts index 442c4ee0e1..4bbdb5db47 100644 --- a/packages/react-email/src/cli/commands/export.ts +++ b/packages/react-email/src/commands/export.ts @@ -9,13 +9,13 @@ import logSymbols from 'log-symbols'; import normalize from 'normalize-path'; import ora, { type Ora } from 'ora'; import type React from 'react'; -import { renderingUtilitiesExporter } from '../../utils/esbuild/renderring-utilities-exporter'; +import { renderingUtilitiesExporter } from '../utils/esbuild/renderring-utilities-exporter.js'; import { type EmailsDirectory, getEmailsDirectoryMetadata, -} from '../../utils/get-emails-directory-metadata'; -import { registerSpinnerAutostopping } from '../../utils/register-spinner-autostopping'; -import { tree } from '../utils'; +} from '../utils/get-emails-directory-metadata.js'; +import { tree } from '../utils/index.js'; +import { registerSpinnerAutostopping } from '../utils/register-spinner-autostopping.js'; const getEmailTemplatesFromDirectory = (emailDirectory: EmailsDirectory) => { const templatePaths = [] as string[]; diff --git a/packages/react-email/src/cli/commands/start.ts b/packages/react-email/src/commands/start.ts similarity index 73% rename from packages/react-email/src/cli/commands/start.ts rename to packages/react-email/src/commands/start.ts index e5d1005d09..2a316c1f9b 100644 --- a/packages/react-email/src/cli/commands/start.ts +++ b/packages/react-email/src/commands/start.ts @@ -1,9 +1,12 @@ import { spawn } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; +import { getPreviewServerLocation } from '../utils/get-preview-server-location.js'; export const start = async () => { try { + const previewServerLocation = await getPreviewServerLocation(); + const usersProjectLocation = process.cwd(); const builtPreviewPath = path.resolve( usersProjectLocation, @@ -16,8 +19,8 @@ export const start = async () => { process.exit(1); } - const nextStart = spawn('npm', ['start'], { - cwd: builtPreviewPath, + const nextStart = spawn('npx', ['next', 'start', builtPreviewPath], { + cwd: previewServerLocation, stdio: 'inherit', }); diff --git a/packages/react-email/src/cli/commands/testing/.gitignore b/packages/react-email/src/commands/testing/.gitignore similarity index 100% rename from packages/react-email/src/cli/commands/testing/.gitignore rename to packages/react-email/src/commands/testing/.gitignore diff --git a/packages/react-email/src/cli/commands/testing/__snapshots__/export.spec.ts.snap b/packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap similarity index 100% rename from packages/react-email/src/cli/commands/testing/__snapshots__/export.spec.ts.snap rename to packages/react-email/src/commands/testing/__snapshots__/export.spec.ts.snap diff --git a/packages/react-email/src/commands/testing/export.spec.ts b/packages/react-email/src/commands/testing/export.spec.ts new file mode 100644 index 0000000000..98a2731696 --- /dev/null +++ b/packages/react-email/src/commands/testing/export.spec.ts @@ -0,0 +1,23 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { exportTemplates } from '../export.js'; + +test('email export', { retry: 3 }, async () => { + const pathToEmailsDirectory = path.resolve( + __dirname, + '../../../../../apps/demo/emails', + ); + const pathToDumpMarkup = path.resolve(__dirname, './out'); + await exportTemplates(pathToDumpMarkup, pathToEmailsDirectory, { + silent: true, + pretty: true, + }); + + expect(fs.existsSync(pathToDumpMarkup)).toBe(true); + expect( + await fs.promises.readFile( + path.resolve(pathToDumpMarkup, './notifications/vercel-invite-user.html'), + 'utf8', + ), + ).toMatchSnapshot(); +}); diff --git a/packages/react-email/src/cli/index.ts b/packages/react-email/src/index.ts similarity index 86% rename from packages/react-email/src/cli/index.ts rename to packages/react-email/src/index.ts index 3cb25317c7..43f4fba483 100644 --- a/packages/react-email/src/cli/index.ts +++ b/packages/react-email/src/index.ts @@ -1,10 +1,10 @@ #!/usr/bin/env node import { program } from 'commander'; -import packageJson from '../../package.json'; -import { build } from './commands/build'; -import { dev } from './commands/dev'; -import { exportTemplates } from './commands/export'; -import { start } from './commands/start'; +import { build } from './commands/build.js'; +import { dev } from './commands/dev.js'; +import { exportTemplates } from './commands/export.js'; +import { start } from './commands/start.js'; +import { packageJson } from './utils/packageJson.js'; const PACKAGE_NAME = 'react-email'; diff --git a/packages/react-email/src/utils/__snapshots__/tree.spec.ts.snap b/packages/react-email/src/utils/__snapshots__/tree.spec.ts.snap new file mode 100644 index 0000000000..2b35dc16bc --- /dev/null +++ b/packages/react-email/src/utils/__snapshots__/tree.spec.ts.snap @@ -0,0 +1,27 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`tree(__dirname, 2) 1`] = ` +"utils +├── __snapshots__ +│ └── tree.spec.ts.snap +├── esbuild +│ ├── escape-string-for-regex.ts +│ └── renderring-utilities-exporter.ts +├── preview +│ ├── hot-reloading +│ ├── get-env-variables-for-preview-app.ts +│ ├── index.ts +│ ├── serve-static-file.ts +│ └── start-dev-server.ts +├── types +│ ├── hot-reload-change.ts +│ └── hot-reload-event.ts +├── get-emails-directory-metadata.spec.ts +├── get-emails-directory-metadata.ts +├── get-preview-server-location.ts +├── index.ts +├── packageJson.ts +├── register-spinner-autostopping.ts +├── tree.spec.ts +└── tree.ts" +`; diff --git a/packages/react-email/src/utils/contains-email-template.ts b/packages/react-email/src/utils/contains-email-template.ts deleted file mode 100644 index c402bab703..0000000000 --- a/packages/react-email/src/utils/contains-email-template.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { EmailsDirectory } from './get-emails-directory-metadata'; - -export const removeFilenameExtension = (filename: string): string => { - const parts = filename.split('.'); - - if (parts.length > 1) { - return parts.slice(0, -1).join('.'); - } - - return filename; -}; - -export const containsEmailTemplate = ( - relativeEmailPath: string, - directory: EmailsDirectory, -) => { - const remainingSegments = relativeEmailPath - .replace(directory.relativePath, '') - .split('/') - .filter(Boolean); - if (remainingSegments.length === 1) { - const emailFilename = removeFilenameExtension(remainingSegments[0]!); - return directory.emailFilenames.includes(emailFilename); - } - const subDirectory = directory.subDirectories.find( - (sub) => sub.directoryName === remainingSegments[0], - ); - if (subDirectory === undefined) { - return false; - } - - return containsEmailTemplate(relativeEmailPath, subDirectory); -}; diff --git a/packages/react-email/src/utils/esbuild/renderring-utilities-exporter.ts b/packages/react-email/src/utils/esbuild/renderring-utilities-exporter.ts index 4856b15577..7f8fe62cc2 100644 --- a/packages/react-email/src/utils/esbuild/renderring-utilities-exporter.ts +++ b/packages/react-email/src/utils/esbuild/renderring-utilities-exporter.ts @@ -1,7 +1,7 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; import type { Loader, PluginBuild, ResolveOptions } from 'esbuild'; -import { escapeStringForRegex } from './escape-string-for-regex'; +import { escapeStringForRegex } from './escape-string-for-regex.js'; /** * Made to export the `render` function out of the user's email template diff --git a/packages/react-email/src/utils/get-emails-directory-metadata.spec.ts b/packages/react-email/src/utils/get-emails-directory-metadata.spec.ts index 1f7aaae36b..b91577a1b7 100644 --- a/packages/react-email/src/utils/get-emails-directory-metadata.spec.ts +++ b/packages/react-email/src/utils/get-emails-directory-metadata.spec.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { getEmailsDirectoryMetadata } from './get-emails-directory-metadata'; +import { getEmailsDirectoryMetadata } from './get-emails-directory-metadata.js'; test('getEmailsDirectoryMetadata on demo emails', async () => { const emailsDirectoryPath = path.resolve( diff --git a/packages/react-email/src/utils/get-preview-server-location.ts b/packages/react-email/src/utils/get-preview-server-location.ts new file mode 100644 index 0000000000..5829bd1ef9 --- /dev/null +++ b/packages/react-email/src/utils/get-preview-server-location.ts @@ -0,0 +1,51 @@ +import path from 'node:path'; +import url from 'node:url'; +import { createJiti } from 'jiti'; +import { addDevDependency } from 'nypm'; +import prompts from 'prompts'; +import { packageJson } from './packageJson.js'; + +const ensurePreviewServerInstalled = async ( + message: string, +): Promise => { + const response = await prompts({ + type: 'confirm', + name: 'installPreviewServer', + message, + initial: true, + }); + if (response.installPreviewServer) { + console.log('Installing "@react-email/preview-server"'); + await addDevDependency( + `@react-email/preview-server@${packageJson.version}`, + ); + process.exit(0); + } else { + process.exit(0); + } +}; + +export const getPreviewServerLocation = async () => { + const usersProject = createJiti(process.cwd()); + let previewServerLocation!: string; + try { + previewServerLocation = path.dirname( + url.parse(usersProject.esmResolve('@react-email/preview-server'), true) + .path!, + ); + } catch (_exception) { + await ensurePreviewServerInstalled( + 'To run the preview server, the package "@react-email/preview-server" must be installed. Would you like to install it?', + ); + } + const { version } = await usersProject.import<{ + version: string; + }>('@react-email/preview-server'); + if (version !== packageJson.version) { + await ensurePreviewServerInstalled( + `To run the preview server, the version of "@react-email/preview-server" must match the version of "react-email" (${packageJson.version}). Would you like to install it?`, + ); + } + + return previewServerLocation; +}; diff --git a/packages/react-email/src/utils/index.ts b/packages/react-email/src/utils/index.ts index 7a3cd2e7b6..a52444541b 100644 --- a/packages/react-email/src/utils/index.ts +++ b/packages/react-email/src/utils/index.ts @@ -1,6 +1,2 @@ -export * from './cn'; -export * from './copy-text-to-clipboard'; -export * from './language-map'; -export * from './sanitize'; -export * from './types/as'; -export * from './unreachable'; +export * from './preview/index.js'; +export * from './tree.js'; diff --git a/packages/react-email/src/utils/packageJson.ts b/packages/react-email/src/utils/packageJson.ts new file mode 100644 index 0000000000..5c0b2d2a63 --- /dev/null +++ b/packages/react-email/src/utils/packageJson.ts @@ -0,0 +1,4 @@ +// @ts-expect-error Typescript doesn't want to allow this, but it's fine since we're using tsup +import packageJson from '../../package.json'; + +export { packageJson }; diff --git a/packages/react-email/src/cli/utils/preview/get-env-variables-for-preview-app.ts b/packages/react-email/src/utils/preview/get-env-variables-for-preview-app.ts similarity index 89% rename from packages/react-email/src/cli/utils/preview/get-env-variables-for-preview-app.ts rename to packages/react-email/src/utils/preview/get-env-variables-for-preview-app.ts index a293ca583f..ffb1634a62 100644 --- a/packages/react-email/src/cli/utils/preview/get-env-variables-for-preview-app.ts +++ b/packages/react-email/src/utils/preview/get-env-variables-for-preview-app.ts @@ -1,5 +1,5 @@ import path from 'node:path'; -import { isDev } from './start-dev-server'; +import { isDev } from './start-dev-server.js'; export const getEnvVariablesForPreviewApp = ( relativePathToEmailsDirectory: string, diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.spec.ts b/packages/react-email/src/utils/preview/hot-reloading/create-dependency-graph.spec.ts similarity index 70% rename from packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.spec.ts rename to packages/react-email/src/utils/preview/hot-reloading/create-dependency-graph.spec.ts index 493e455529..a99e6798ee 100644 --- a/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.spec.ts +++ b/packages/react-email/src/utils/preview/hot-reloading/create-dependency-graph.spec.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { createDependencyGraph, type DependencyGraph, -} from './create-dependency-graph'; +} from './create-dependency-graph.js'; const pathToFileForTestingDependencyGraph = path.join( __dirname, @@ -44,20 +44,39 @@ test('createDependencyGraph()', async () => { }; const initialDependencyGraph = convertPathsToAbsolute({ - '../../../../../package.json': { + '../../../../package.json': { dependencyPaths: [], - dependentPaths: ['../start-dev-server.ts'], + dependentPaths: ['../../packageJson.ts'], moduleDependencies: [], - path: '../../../../../package.json', + path: '../../../../package.json', }, 'create-dependency-graph.ts': { path: 'create-dependency-graph.ts', - dependencyPaths: ['../start-dev-server.ts', 'get-imported-modules.ts'], + dependencyPaths: [ + '../start-dev-server.ts', + 'get-imported-modules.ts', + 'resolve-path-aliases.ts', + ], dependentPaths: [ 'create-dependency-graph.spec.ts', 'setup-hot-reloading.ts', ], - moduleDependencies: ['node:fs', 'node:path', 'chokidar/handler'], + moduleDependencies: ['node:fs', 'node:path', 'chokidar/handler.js'], + }, + '../../get-preview-server-location.ts': { + dependencyPaths: ['../../packageJson.ts'], + dependentPaths: ['../../preview/start-dev-server.ts'], + moduleDependencies: ['node:path', 'node:url', 'jiti', 'nypm', 'prompts'], + path: '../../get-preview-server-location.ts', + }, + '../../packageJson.ts': { + dependencyPaths: ['../../../../package.json'], + dependentPaths: [ + '../../get-preview-server-location.ts', + '../../preview/start-dev-server.ts', + ], + moduleDependencies: [], + path: '../../packageJson.ts', }, 'create-dependency-graph.spec.ts': { path: 'create-dependency-graph.spec.ts', @@ -65,11 +84,32 @@ test('createDependencyGraph()', async () => { dependentPaths: [], moduleDependencies: ['node:fs', 'node:path'], }, - '../../../utils/preview/get-env-variables-for-preview-app.ts': { - dependencyPaths: ['../../../utils/preview/start-dev-server.ts'], - dependentPaths: ['../../../utils/preview/start-dev-server.ts'], + '../get-env-variables-for-preview-app.ts': { + dependencyPaths: ['../../preview/start-dev-server.ts'], + dependentPaths: ['../../preview/start-dev-server.ts'], + moduleDependencies: ['node:path'], + path: '../../preview/get-env-variables-for-preview-app.ts', + }, + './test/some-file.ts': { + dependencyPaths: [], + dependentPaths: [], + moduleDependencies: [], + path: '/home/gabriel/Projects/Resend/react-email/packages/react-email/src/cli/utils/preview/hot-reloading/test/some-file.ts', + }, + 'resolve-path-aliases.ts': { + path: 'resolve-path-aliases.ts', + dependentPaths: [ + 'create-dependency-graph.ts', + 'resolve-path-aliases.spec.ts', + ], + dependencyPaths: [], + moduleDependencies: ['node:path', 'tsconfig-paths'], + }, + 'resolve-path-aliases.spec.ts': { + path: 'resolve-path-aliases.spec.ts', + dependencyPaths: ['resolve-path-aliases.ts'], + dependentPaths: [], moduleDependencies: ['node:path'], - path: '../../../utils/preview/get-env-variables-for-preview-app.ts', }, 'get-imported-modules.ts': { path: 'get-imported-modules', @@ -89,7 +129,7 @@ test('createDependencyGraph()', async () => { 'setup-hot-reloading.ts': { path: 'setup-hot-reloading.ts', dependencyPaths: [ - '../../../../utils/types/hot-reload-change.ts', + '../../types/hot-reload-change.ts', 'create-dependency-graph.ts', ], dependentPaths: [], @@ -103,14 +143,15 @@ test('createDependencyGraph()', async () => { }, '../start-dev-server.ts': { dependencyPaths: [ - '../../../../../package.json', - '../../../../utils/register-spinner-autostopping.ts', - '../../../utils/preview/get-env-variables-for-preview-app.ts', - '../../../utils/preview/serve-static-file.ts', + '../../register-spinner-autostopping.ts', + '../../get-preview-server-location.ts', + '../../packageJson.ts', + '../../preview/get-env-variables-for-preview-app.ts', + '../../preview/serve-static-file.ts', ], path: '../start-dev-server.ts', dependentPaths: [ - '../../../utils/preview/get-env-variables-for-preview-app.ts', + '../../preview/get-env-variables-for-preview-app.ts', 'create-dependency-graph.ts', ], moduleDependencies: [ @@ -118,14 +159,14 @@ test('createDependencyGraph()', async () => { 'node:path', 'node:url', 'chalk', + 'jiti', 'log-symbols', - 'next', 'ora', ], }, - '../../../utils/preview/serve-static-file.ts': { + '../../preview/serve-static-file.ts': { dependencyPaths: [], - dependentPaths: ['../../../utils/preview/start-dev-server.ts'], + dependentPaths: ['../../preview/start-dev-server.ts'], moduleDependencies: [ 'node:fs', 'node:http', @@ -133,23 +174,23 @@ test('createDependencyGraph()', async () => { 'node:url', 'mime-types', ], - path: '../../../utils/preview/serve-static-file.ts', + path: '../../preview/serve-static-file.ts', }, - '../../../../utils/register-spinner-autostopping.ts': { + '../../register-spinner-autostopping.ts': { dependencyPaths: [], - dependentPaths: ['../../../utils/preview/start-dev-server.ts'], + dependentPaths: ['../../preview/start-dev-server.ts'], moduleDependencies: ['log-symbols', 'ora'], - path: '../../../../utils/register-spinner-autostopping.ts', + path: '../../register-spinner-autostopping.ts', }, - '../../../../utils/types/hot-reload-event.ts': { + '../../types/hot-reload-event.ts': { dependencyPaths: [], - dependentPaths: ['../../../../utils/types/hot-reload-change.ts'], - moduleDependencies: ['chokidar/handler'], - path: '../../../../utils/types/hot-reload-event.ts', + dependentPaths: ['../../types/hot-reload-change.ts'], + moduleDependencies: ['chokidar/handler.js'], + path: '../../types/hot-reload-event.ts', }, - '../../../../utils/types/hot-reload-change.ts': { - path: '../../../../utils/types/hot-reload-change.ts', - dependencyPaths: ['../../../../utils/types/hot-reload-event.ts'], + '../../types/hot-reload-change.ts': { + path: '../../types/hot-reload-change.ts', + dependencyPaths: ['../../types/hot-reload-event.ts'], dependentPaths: ['setup-hot-reloading.ts'], moduleDependencies: [], }, diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.ts b/packages/react-email/src/utils/preview/hot-reloading/create-dependency-graph.ts similarity index 97% rename from packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.ts rename to packages/react-email/src/utils/preview/hot-reloading/create-dependency-graph.ts index 418d66f3ab..1c50fd908b 100644 --- a/packages/react-email/src/cli/utils/preview/hot-reloading/create-dependency-graph.ts +++ b/packages/react-email/src/utils/preview/hot-reloading/create-dependency-graph.ts @@ -1,8 +1,9 @@ import { existsSync, promises as fs, statSync } from 'node:fs'; import path from 'node:path'; -import type { EventName } from 'chokidar/handler'; -import { isDev } from '../start-dev-server'; -import { getImportedModules } from './get-imported-modules'; +import type { EventName } from 'chokidar/handler.js'; +import { isDev } from '../start-dev-server.js'; +import { getImportedModules } from './get-imported-modules.js'; +import { resolvePathAliases } from './resolve-path-aliases.js'; interface Module { path: string; @@ -93,9 +94,8 @@ export const createDependencyGraph = async (directory: string) => { const getDependencyPaths = async (filePath: string) => { const contents = await fs.readFile(filePath, 'utf8'); - const importedPaths = isJavascriptModule(filePath) - ? getImportedModules(contents) + ? resolvePathAliases(getImportedModules(contents), path.dirname(filePath)) : []; const importedPathsRelativeToDirectory = importedPaths.map( (dependencyPath) => { diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.spec.ts b/packages/react-email/src/utils/preview/hot-reloading/get-imported-modules.spec.ts similarity index 96% rename from packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.spec.ts rename to packages/react-email/src/utils/preview/hot-reloading/get-imported-modules.spec.ts index 8e7d545271..f98bbb2808 100644 --- a/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.spec.ts +++ b/packages/react-email/src/utils/preview/hot-reloading/get-imported-modules.spec.ts @@ -1,5 +1,5 @@ import { promises as fs } from 'node:fs'; -import { getImportedModules } from './get-imported-modules'; +import { getImportedModules } from './get-imported-modules.js'; vi.mock('@babel/traverse', async () => { const traverse = await vi.importActual('@babel/traverse'); @@ -12,7 +12,7 @@ describe('getImportedModules()', () => { expect(getImportedModules(contents)).toEqual([ 'node:fs', - './get-imported-modules', + './get-imported-modules.js', ]); }); diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.ts b/packages/react-email/src/utils/preview/hot-reloading/get-imported-modules.ts similarity index 87% rename from packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.ts rename to packages/react-email/src/utils/preview/hot-reloading/get-imported-modules.ts index 554abc3bf2..fa08eb4880 100644 --- a/packages/react-email/src/cli/utils/preview/hot-reloading/get-imported-modules.ts +++ b/packages/react-email/src/utils/preview/hot-reloading/get-imported-modules.ts @@ -7,9 +7,7 @@ const traverse = // script's use of tsx typeof traverseModule === 'function' ? traverseModule - : // @ts-expect-error This is fine since the default export is wrapped in a - // default function. The problem is that babel/traverse is not ESM. - traverseModule.default; + : traverseModule.default; export const getImportedModules = (contents: string) => { const importedPaths: string[] = []; diff --git a/packages/react-email/src/utils/preview/hot-reloading/resolve-path-aliases.spec.ts b/packages/react-email/src/utils/preview/hot-reloading/resolve-path-aliases.spec.ts new file mode 100644 index 0000000000..be7635ec86 --- /dev/null +++ b/packages/react-email/src/utils/preview/hot-reloading/resolve-path-aliases.spec.ts @@ -0,0 +1,11 @@ +import path from 'node:path'; +import { resolvePathAliases } from './resolve-path-aliases.js'; + +test('resolveImports()', async () => { + expect( + resolvePathAliases( + ['@/some-file'], + path.resolve(import.meta.dirname, './test'), + ), + ).toEqual(['./some-file']); +}); diff --git a/packages/react-email/src/utils/preview/hot-reloading/resolve-path-aliases.ts b/packages/react-email/src/utils/preview/hot-reloading/resolve-path-aliases.ts new file mode 100644 index 0000000000..c7a42aa71f --- /dev/null +++ b/packages/react-email/src/utils/preview/hot-reloading/resolve-path-aliases.ts @@ -0,0 +1,32 @@ +import path from 'node:path'; +import { createMatchPath, loadConfig } from 'tsconfig-paths'; + +export const resolvePathAliases = ( + importPaths: string[], + projectPath: string, +) => { + const configLoadResult = loadConfig(projectPath); + + if (configLoadResult.resultType === 'success') { + const matchPath = createMatchPath( + configLoadResult.absoluteBaseUrl, + configLoadResult.paths, + ); + return importPaths.map((importedPath) => { + const unaliasedPath = matchPath(importedPath, undefined, undefined, [ + '.tsx', + '.ts', + '.js', + '.jsx', + '.cjs', + '.mjs', + ]); + if (unaliasedPath) { + return `./${path.relative(projectPath, unaliasedPath)}`; + } + return importedPath; + }); + } + + return importPaths; +}; diff --git a/packages/react-email/src/cli/utils/preview/hot-reloading/setup-hot-reloading.ts b/packages/react-email/src/utils/preview/hot-reloading/setup-hot-reloading.ts similarity index 97% rename from packages/react-email/src/cli/utils/preview/hot-reloading/setup-hot-reloading.ts rename to packages/react-email/src/utils/preview/hot-reloading/setup-hot-reloading.ts index d701cb339e..d6fb95e2bc 100644 --- a/packages/react-email/src/cli/utils/preview/hot-reloading/setup-hot-reloading.ts +++ b/packages/react-email/src/utils/preview/hot-reloading/setup-hot-reloading.ts @@ -3,8 +3,8 @@ import path from 'node:path'; import { watch } from 'chokidar'; import debounce from 'debounce'; import { type Socket, Server as SocketServer } from 'socket.io'; -import type { HotReloadChange } from '../../../../utils/types/hot-reload-change'; -import { createDependencyGraph } from './create-dependency-graph'; +import type { HotReloadChange } from '../../types/hot-reload-change.js'; +import { createDependencyGraph } from './create-dependency-graph.js'; export const setupHotreloading = async ( devServer: http.Server, diff --git a/packages/react-email/src/utils/preview/hot-reloading/test/some-file.ts b/packages/react-email/src/utils/preview/hot-reloading/test/some-file.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/react-email/src/utils/preview/hot-reloading/test/tsconfig.json b/packages/react-email/src/utils/preview/hot-reloading/test/tsconfig.json new file mode 100644 index 0000000000..e7b79fe3aa --- /dev/null +++ b/packages/react-email/src/utils/preview/hot-reloading/test/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "paths": { + "@/*": ["./*"] + } + }, + "include": ["**/*.ts", "**/*.tsx"] +} diff --git a/packages/react-email/src/utils/preview/index.ts b/packages/react-email/src/utils/preview/index.ts new file mode 100644 index 0000000000..508fedb6a9 --- /dev/null +++ b/packages/react-email/src/utils/preview/index.ts @@ -0,0 +1,2 @@ +export * from './hot-reloading/setup-hot-reloading.js'; +export * from './start-dev-server.js'; diff --git a/packages/react-email/src/cli/utils/preview/serve-static-file.ts b/packages/react-email/src/utils/preview/serve-static-file.ts similarity index 93% rename from packages/react-email/src/cli/utils/preview/serve-static-file.ts rename to packages/react-email/src/utils/preview/serve-static-file.ts index 1b68e36a6c..3c2c960fcd 100644 --- a/packages/react-email/src/cli/utils/preview/serve-static-file.ts +++ b/packages/react-email/src/utils/preview/serve-static-file.ts @@ -38,7 +38,8 @@ export const serveStaticFile = async ( } else { const sanitizedFilePath = fileAbsolutePath.replace(/\n|\r/g, ''); console.error( - `Could not read file at ${sanitizedFilePath} to be served, here's the exception:`, + `Could not read file at %s to be served, here's the exception:`, + sanitizedFilePath, exception, ); diff --git a/packages/react-email/src/cli/utils/preview/start-dev-server.ts b/packages/react-email/src/utils/preview/start-dev-server.ts similarity index 87% rename from packages/react-email/src/cli/utils/preview/start-dev-server.ts rename to packages/react-email/src/utils/preview/start-dev-server.ts index 41e420aa04..192d4f7d01 100644 --- a/packages/react-email/src/cli/utils/preview/start-dev-server.ts +++ b/packages/react-email/src/utils/preview/start-dev-server.ts @@ -2,13 +2,14 @@ import http from 'node:http'; import path from 'node:path'; import url from 'node:url'; import chalk from 'chalk'; +import { createJiti } from 'jiti'; import logSymbols from 'log-symbols'; -import next from 'next'; import ora from 'ora'; -import packageJson from '../../../../package.json'; -import { registerSpinnerAutostopping } from '../../../utils/register-spinner-autostopping'; -import { getEnvVariablesForPreviewApp } from './get-env-variables-for-preview-app'; -import { serveStaticFile } from './serve-static-file'; +import { registerSpinnerAutostopping } from '../../utils/register-spinner-autostopping.js'; +import { getPreviewServerLocation } from '../get-preview-server-location.js'; +import { packageJson } from '../packageJson.js'; +import { getEnvVariablesForPreviewApp } from './get-env-variables-for-preview-app.js'; +import { serveStaticFile } from './serve-static-file.js'; let devServer: http.Server | undefined; @@ -29,13 +30,7 @@ const safeAsyncServerListen = (server: http.Server, port: number) => { const filename = url.fileURLToPath(import.meta.url); const dirname = path.dirname(filename); -export const isDev = !filename.endsWith(path.join('cli', 'index.mjs')); -export const cliPackageLocation = isDev - ? path.resolve(dirname, '../../../..') - : path.resolve(dirname, '../..'); -export const previewServerLocation = isDev - ? path.resolve(dirname, '../../../..') - : path.resolve(dirname, '../preview'); +export const isDev = !dirname.includes('dist'); export const startDevServer = async ( emailsDirRelativePath: string, @@ -50,6 +45,12 @@ export const startDevServer = async ( process.exit(1); } + const previewServerLocation = await getPreviewServerLocation(); + const previewServer = createJiti(previewServerLocation); + + const { default: next } = + await previewServer.import('next'); + devServer = http.createServer((req, res) => { if (!req.url) { res.end(404); @@ -191,13 +192,17 @@ const makeExitHandler = | { shouldKillProcess: false } | { shouldKillProcess: true; killWithErrorCode: boolean }, ) => - (_codeOrSignal: number | NodeJS.Signals) => { + (codeSignalOrError: number | NodeJS.Signals | Error) => { if (typeof devServer !== 'undefined') { - console.log('\n shutting down dev server'); + console.log('\nshutting down dev server'); devServer.close(); devServer = undefined; } + if (codeSignalOrError instanceof Error) { + console.error(codeSignalOrError); + } + if (options?.shouldKillProcess) { process.exit(options.killWithErrorCode ? 1 : 0); } diff --git a/packages/react-email/src/utils/tree.spec.ts b/packages/react-email/src/utils/tree.spec.ts new file mode 100644 index 0000000000..a265e0a761 --- /dev/null +++ b/packages/react-email/src/utils/tree.spec.ts @@ -0,0 +1,5 @@ +import { tree } from './tree.js'; + +test('tree(__dirname, 2)', async () => { + expect(await tree(__dirname, 2)).toMatchSnapshot(); +}); diff --git a/packages/react-email/src/cli/utils/tree.ts b/packages/react-email/src/utils/tree.ts similarity index 100% rename from packages/react-email/src/cli/utils/tree.ts rename to packages/react-email/src/utils/tree.ts diff --git a/packages/react-email/src/utils/types/hot-reload-change.ts b/packages/react-email/src/utils/types/hot-reload-change.ts index cad7b9ebbf..8e4d308a91 100644 --- a/packages/react-email/src/utils/types/hot-reload-change.ts +++ b/packages/react-email/src/utils/types/hot-reload-change.ts @@ -1,4 +1,4 @@ -import type { HotReloadEvent } from './hot-reload-event'; +import type { HotReloadEvent } from './hot-reload-event.js'; export interface HotReloadChange { filename: string; diff --git a/packages/react-email/src/utils/types/hot-reload-event.ts b/packages/react-email/src/utils/types/hot-reload-event.ts index 92ed7cb358..509a17e514 100644 --- a/packages/react-email/src/utils/types/hot-reload-event.ts +++ b/packages/react-email/src/utils/types/hot-reload-event.ts @@ -1,3 +1,3 @@ -import type { EventName } from 'chokidar/handler'; +import type { EventName } from 'chokidar/handler.js'; export type HotReloadEvent = EventName; diff --git a/packages/react-email/tsconfig.json b/packages/react-email/tsconfig.json index 90c21e09c8..b10c1be9e0 100644 --- a/packages/react-email/tsconfig.json +++ b/packages/react-email/tsconfig.json @@ -8,7 +8,7 @@ "forceConsistentCasingInFileNames": true, "inlineSources": false, "isolatedModules": true, - "moduleResolution": "node", + "moduleResolution": "nodenext", "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true, @@ -28,18 +28,12 @@ "noEmit": true, "strict": false, "target": "ESNext", - "module": "ESNext", + "module": "NodeNext", "noUncheckedIndexedAccess": true, "resolveJsonModule": true, "types": ["vitest/globals"], "outDir": "dist" }, - "include": [ - "next-env.d.ts", - "tailwind-internals.d.ts", - "**/*.ts", - "**/*.tsx", - ".next/types/**/*.ts" - ], - "exclude": [".next", "dist", "node_modules"] + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules"] } diff --git a/packages/react-email/tsup.config.ts b/packages/react-email/tsup.config.ts index 44d6d43185..e1236ac842 100644 --- a/packages/react-email/tsup.config.ts +++ b/packages/react-email/tsup.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ dts: false, - entry: ['./src/cli/index.ts'], + entry: ['./src/index.ts'], format: ['esm'], - outDir: 'dist/cli', + outDir: 'dist', }); diff --git a/packages/render/CHANGELOG.md b/packages/render/CHANGELOG.md index 4ae263ae11..22ec740f26 100644 --- a/packages/render/CHANGELOG.md +++ b/packages/render/CHANGELOG.md @@ -1,5 +1,11 @@ # @react-email/render +## 1.1.3-canary.0 + +### Patch Changes + +- e22cc83: fix browser version including errors in the output instead of throwing them + ## 1.1.2 ### Patch Changes @@ -8,12 +14,24 @@ - e4598ab: Fixes `pretty` option breaking CJS projects - faa6944: accept React.ReactNode instead of React.ReactElement +## 1.1.2-canary.0 + +### Patch Changes + +- 0405e2b: fix react-dom/server import for the browser and some bundlers + ## 1.1.1 ### Patch Changes - a77ef6f: fix preview being rendered in plain-text +## 1.1.1-canary.0 + +### Patch Changes + +- 57ddfd9: fix react-dom/server import for the browser and some bundlers + ## 1.1.0 ### Minor Changes @@ -24,6 +42,18 @@ - 9aa033c: Use range of versions for dependencies +## 1.1.0-canary.1 + +### Patch Changes + +- 9b1adb0: Use range of versions for dependencies + +## 1.1.0-canary.0 + +### Minor Changes + +- 32372da: Deprecate the `pretty` option for `render` in favor of standalone `pretty` function + ## 1.0.6 ### Patch Changes diff --git a/packages/render/package.json b/packages/render/package.json index ed821c9f6b..4c15a5b0e1 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -1,6 +1,6 @@ { "name": "@react-email/render", - "version": "1.1.2", + "version": "1.1.3-canary.0", "description": "Transform React components into HTML email templates", "sideEffects": false, "main": "./dist/browser/index.js", diff --git a/packages/render/src/browser/__snapshots__/render-async-web.spec.tsx.snap b/packages/render/src/browser/__snapshots__/render-async-web.spec.tsx.snap deleted file mode 100644 index 873fa93880..0000000000 --- a/packages/render/src/browser/__snapshots__/render-async-web.spec.tsx.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`renderAsync on the browser environment > should handle characters with a higher byte count gracefully 1`] = `"

Test Normal 情報Ⅰコース担当者様

平素よりお世話になっております。 情報Ⅰサポートチームです。 情報Ⅰ本講座につきまして仕様変更のためご連絡させていただきました。

今後ジクタス上の講座につきましては、8回分の授業をひとまとまりとしてパート分けされた状態で公開されてまいります。

伴いまして、画面上の表示に一部変更がありますのでお知らせいたします。 ご登録いただいた各生徒の受講ペースに応じて公開パートが増えてまいります。 具体的な表示イメージは下記ページをご確認ください。

2024年8月末時点で情報Ⅰ本講座を受講していたアカウントにつきましては、 今まで公開していた第1~9回までの講座一覧に加え、パート分けされた講座が追加で公開されてまりいます。 第1~9回の表示はそのまま引き継がれますが、Webドリルの進捗表示はパート分けの講座には適用されません。ご了承くださいませ。 仕様変更に伴い、現在教室長もしくは講師に生徒アカウントログインをお願いしておりますが、今後は生徒自身にてログインをしていただいて問題ございません。

また、生徒が自宅等にてログインし復習に取り組むことも問題ございませんので、教室にてご指示いただければと存じます。 (実際にご指示いただくかは教室判断に委ねさせていただきます。)

受講ペースが変更したり、増コマが発生したりする場合などは、公開ペースを本部にて調整いたしますので、下記より必ずご連絡くださいませ。 また本件に関して不明な点がございましたら、同フォームよりお問い合わせください。 以上、引き続きよろしくお願い申し上げます。 情報Ⅰサポートチーム

"`; diff --git a/packages/render/src/browser/__snapshots__/render-web.spec.tsx.snap b/packages/render/src/browser/__snapshots__/render-web.spec.tsx.snap index 2a39925b95..45240e6774 100644 --- a/packages/render/src/browser/__snapshots__/render-web.spec.tsx.snap +++ b/packages/render/src/browser/__snapshots__/render-web.spec.tsx.snap @@ -1,3 +1,55 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`render on the browser environment > should handle characters with a higher byte count gracefully 1`] = `"

Test Normal 情報Ⅰコース担当者様

平素よりお世話になっております。 情報Ⅰサポートチームです。 情報Ⅰ本講座につきまして仕様変更のためご連絡させていただきました。

今後ジクタス上の講座につきましては、8回分の授業をひとまとまりとしてパート分けされた状態で公開されてまいります。

伴いまして、画面上の表示に一部変更がありますのでお知らせいたします。 ご登録いただいた各生徒の受講ペースに応じて公開パートが増えてまいります。 具体的な表示イメージは下記ページをご確認ください。

2024年8月末時点で情報Ⅰ本講座を受講していたアカウントにつきましては、 今まで公開していた第1~9回までの講座一覧に加え、パート分けされた講座が追加で公開されてまりいます。 第1~9回の表示はそのまま引き継がれますが、Webドリルの進捗表示はパート分けの講座には適用されません。ご了承くださいませ。 仕様変更に伴い、現在教室長もしくは講師に生徒アカウントログインをお願いしておりますが、今後は生徒自身にてログインをしていただいて問題ございません。

また、生徒が自宅等にてログインし復習に取り組むことも問題ございませんので、教室にてご指示いただければと存じます。 (実際にご指示いただくかは教室判断に委ねさせていただきます。)

受講ペースが変更したり、増コマが発生したりする場合などは、公開ペースを本部にて調整いたしますので、下記より必ずご連絡くださいませ。 また本件に関して不明な点がございましたら、同フォームよりお問い合わせください。 以上、引き続きよろしくお願い申し上げます。 情報Ⅰサポートチーム

"`; + +exports[`render on the browser environment > should properly wait for Suepsense boundaries to ending before resolving 1`] = ` +"" +`; + +exports[`render on the browser environment > should throw error of rendering an invalid element instead of writing them into a template tag 1`] = `[Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.]`; diff --git a/packages/render/src/browser/render-web.spec.tsx b/packages/render/src/browser/render-web.spec.tsx index 3031ebe78f..ff7752e7d9 100644 --- a/packages/render/src/browser/render-web.spec.tsx +++ b/packages/render/src/browser/render-web.spec.tsx @@ -2,6 +2,8 @@ * @vitest-environment jsdom */ +import { createElement } from 'react'; +import usePromise from 'react-promise-suspense'; import { Preview } from '../shared/utils/preview'; import { Template } from '../shared/utils/template'; import { render } from './render'; @@ -122,4 +124,26 @@ describe('render on the browser environment', () => { `"THIS SHOULD BE RENDERED IN PLAIN TEXT"`, ); }); + + it('should properly wait for Suepsense boundaries to ending before resolving', async () => { + const EmailTemplate = () => { + const html = usePromise( + () => fetch('https://example.com').then((res) => res.text()), + [], + ); + + return
; + }; + + const renderedTemplate = await render(); + + expect(renderedTemplate).toMatchSnapshot(); + }); + + // See https://github.com/resend/react-email/issues/2263 + it('should throw error of rendering an invalid element instead of writing them into a template tag', async () => { + // @ts-ignore we know this is not correct, and we want to test the error handling for it + const element = createElement(undefined); + await expect(render(element)).rejects.toThrowErrorMatchingSnapshot(); + }); }); diff --git a/packages/render/src/browser/render.tsx b/packages/render/src/browser/render.tsx index 9beb4cb3df..4d71696e6d 100644 --- a/packages/render/src/browser/render.tsx +++ b/packages/render/src/browser/render.tsx @@ -1,38 +1,28 @@ import { convert } from 'html-to-text'; import { Suspense } from 'react'; -import type { - PipeableStream, - ReactDOMServerReadableStream, -} from 'react-dom/server'; +import type { ReactDOMServerReadableStream } from 'react-dom/server'; import { pretty } from '../node'; import type { Options } from '../shared/options'; import { plainTextSelectors } from '../shared/plain-text-selectors'; const decoder = new TextDecoder('utf-8'); -const readStream = async ( - stream: PipeableStream | ReactDOMServerReadableStream, -) => { +const readStream = async (stream: ReactDOMServerReadableStream) => { const chunks: Uint8Array[] = []; - if ('pipeTo' in stream) { - // means it's a readable stream - const writableStream = new WritableStream({ - write(chunk: Uint8Array) { - chunks.push(chunk); - }, - }); - await stream.pipeTo(writableStream); - } else { - throw new Error( - 'For some reason, the Node version of `react-dom/server` has been imported instead of the browser one.', - { + const writableStream = new WritableStream({ + write(chunk: Uint8Array) { + chunks.push(chunk); + }, + abort(reason) { + throw new Error('Stream aborted', { cause: { - stream, + reason, }, - }, - ); - } + }); + }, + }); + await stream.pipeTo(writableStream); let length = 0; chunks.forEach((item) => { @@ -50,29 +40,22 @@ const readStream = async ( export const render = async (node: React.ReactNode, options?: Options) => { const suspendedElement = {node}; - const reactDOMServer = await import('react-dom/server').then( + const reactDOMServer = await import('react-dom/server.browser').then( // This is beacuse react-dom/server is CJS (m) => m.default, ); - let html!: string; - if (Object.hasOwn(reactDOMServer, 'renderToReadableStream')) { - html = await readStream( - await reactDOMServer.renderToReadableStream(suspendedElement), - ); - } else { - await new Promise((resolve, reject) => { - const stream = reactDOMServer.renderToPipeableStream(suspendedElement, { - async onAllReady() { - html = await readStream(stream); - resolve(); - }, - onError(error) { - reject(error as Error); + const html = await new Promise((resolve, reject) => { + reactDOMServer + .renderToReadableStream(suspendedElement, { + onError(error: unknown) { + reject(error); }, - }); - }); - } + }) + .then(readStream) + .then(resolve) + .catch(reject); + }); if (options?.plainText) { return convert(html, { diff --git a/packages/render/src/shared/options.ts b/packages/render/src/shared/options.ts index a2c9ad0526..ddc36b352f 100644 --- a/packages/render/src/shared/options.ts +++ b/packages/render/src/shared/options.ts @@ -1,5 +1,4 @@ import type { HtmlToTextOptions } from 'html-to-text'; -// biome-ignore lint/correctness/noUnusedImports: this is used in the deprecated doc import type { pretty } from './utils/pretty'; export type Options = { diff --git a/packages/render/src/shared/utils/border-parser.ts b/packages/render/src/shared/utils/border-parser.ts new file mode 100644 index 0000000000..cf95a820eb --- /dev/null +++ b/packages/render/src/shared/utils/border-parser.ts @@ -0,0 +1,8 @@ +export const borderParser = (border: string) => { + const [borderWidth, borderStyle, ...borderColor] = border.split(' '); + return { + borderWidth: Number.parseInt(borderWidth), + borderStyle, + borderColor: borderColor.join(' '), + }; +}; diff --git a/packages/row/src/row.tsx b/packages/row/src/row.tsx index ebecfc26e8..3e2320599b 100644 --- a/packages/row/src/row.tsx +++ b/packages/row/src/row.tsx @@ -1,4 +1,6 @@ +import type { CSSProperties } from 'react'; import * as React from 'react'; +import { borderParser } from '../../render/src/shared/utils/border-parser'; export type RowProps = Readonly< React.ComponentPropsWithoutRef<'table'> & { @@ -8,17 +10,74 @@ export type RowProps = Readonly< export const Row = React.forwardRef( ({ children, style, ...props }, ref) => { + const rowStyle = { + ...style, + }; + + if (rowStyle.border && typeof rowStyle.border === 'string') { + const { borderWidth, borderColor } = borderParser(rowStyle.border); + + const containerStyle: CSSProperties = { + backgroundColor: borderColor, + width: '100%', + }; + + const cellStyle: CSSProperties = { + padding: `${borderWidth}px`, + }; + + const contentStyle: CSSProperties = { + ...rowStyle, + border: undefined, + width: '100%', + backgroundColor: rowStyle.backgroundColor, + }; + + return ( +
+ + + + + +
+ + + {children} + +
+
+ ); + } return ( {children} diff --git a/packages/section/src/section.tsx b/packages/section/src/section.tsx index 20c494e2a8..671196a2f1 100644 --- a/packages/section/src/section.tsx +++ b/packages/section/src/section.tsx @@ -1,20 +1,82 @@ +import type { CSSProperties } from 'react'; import * as React from 'react'; +import { borderParser } from '../../render/src/shared/utils/border-parser'; export type SectionProps = Readonly>; export const Section = React.forwardRef( ({ children, style, ...props }, ref) => { + const sectionStyle = { + ...style, + }; + + if (sectionStyle.border && typeof sectionStyle.border === 'string') { + const { borderWidth, borderColor } = borderParser(sectionStyle.border); + + const containerStyle: CSSProperties = { + backgroundColor: borderColor, + width: '100%', + }; + + const cellStyle: CSSProperties = { + padding: `${borderWidth}px`, + }; + + const contentStyle: CSSProperties = { + ...sectionStyle, + border: undefined, + width: '100%', + backgroundColor: sectionStyle.backgroundColor, + }; + + return ( +
+ + + + + +
+ + + + + + +
{children}
+
+ ); + } + return ( diff --git a/packages/tailwind/CHANGELOG.md b/packages/tailwind/CHANGELOG.md index 8be91b1914..119d9ddb1b 100644 --- a/packages/tailwind/CHANGELOG.md +++ b/packages/tailwind/CHANGELOG.md @@ -1,5 +1,11 @@ # @react-email/tailwind +## 1.1.0-canary.0 + +### Minor Changes + +- 934bc53: Extract tailwind pseudo classes to stylesheet + ## 1.0.5 ### Patch Changes diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json index fab11dce3b..f210dceb8f 100644 --- a/packages/tailwind/package.json +++ b/packages/tailwind/package.json @@ -1,6 +1,6 @@ { "name": "@react-email/tailwind", - "version": "1.0.5", + "version": "1.1.0-canary.0", "description": "A React component to wrap emails with Tailwind CSS", "sideEffects": false, "main": "./dist/index.js", @@ -64,7 +64,7 @@ "tsconfig": "workspace:*", "tsup": "8.4.0", "typescript": "5.8.3", - "vite": "6.3.2", + "vite": "6.3.4", "vite-plugin-dts": "4.5.3", "yalc": "1.0.0-pre.53" }, diff --git a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap index 76d7a2b1da..5c36b87c7a 100644 --- a/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap +++ b/packages/tailwind/src/__snapshots__/tailwind.spec.tsx.snap @@ -14,42 +14,6 @@ exports[`Custom theme config > should be able to use custom spacing 1`] = `" should be able to use custom text alignment 1`] = `"
"`; -exports[`Responsive styles > should add css to and keep responsive class names 1`] = `"
"`; - -exports[`Responsive styles > should not have duplicate media queries 1`] = `"
"`; - -exports[`Responsive styles > should persist existing elements 1`] = `"
"`; - -exports[`Responsive styles > should throw an error when used without a 1`] = ` -[Error: You are trying to use the following Tailwind classes that cannot be inlined: sm:bg-red-500. -For the media queries to work properly on rendering, they need to be added into a
"`; - -exports[`Responsive styles > should work with arbitrarily deep (in the React tree) elements 2`] = `"
"`; - -exports[`Responsive styles > should work with relatively complex media query utilities 1`] = `"

I am some text

"`; - exports[`Tailwind component >