diff --git a/packages/preview-server/package.json b/packages/preview-server/package.json index ebec48a7b3..2b8299694c 100644 --- a/packages/preview-server/package.json +++ b/packages/preview-server/package.json @@ -13,6 +13,7 @@ }, "main": "./index.mjs", "dependencies": { + "@t3-oss/env-nextjs": "0.13.10", "next": "16.1.1" }, "devDependencies": { @@ -79,7 +80,7 @@ "tailwindcss": "4.1.17", "typescript": "5.8.3", "use-debounce": "10.0.6", - "zod": "4.1.12" + "zod": "4.3.5" }, "license": "MIT", "repository": { diff --git a/packages/preview-server/scripts/dev.mts b/packages/preview-server/scripts/dev.mts index dbb1bc095c..5f62fc5f03 100644 --- a/packages/preview-server/scripts/dev.mts +++ b/packages/preview-server/scripts/dev.mts @@ -26,7 +26,9 @@ await fs.writeFile( EMAILS_DIR_ABSOLUTE_PATH=${emailsDirectoryPath} USER_PROJECT_LOCATION=${previewServerRoot} PREVIEW_SERVER_LOCATION=${previewServerRoot} -NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT=true`, +NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT=true +COMPATIBILITY_EMAIL_CLIENTS=gmail,apple-mail,outlook,yahoo +`, 'utf8', ); diff --git a/packages/preview-server/src/actions/email-validation/caniemail-data.ts b/packages/preview-server/src/actions/email-validation/caniemail-data.ts index 35fb5a8c9f..fd9d275396 100644 --- a/packages/preview-server/src/actions/email-validation/caniemail-data.ts +++ b/packages/preview-server/src/actions/email-validation/caniemail-data.ts @@ -1,4 +1,4 @@ -import type { SupportEntry } from "./check-compatibility"; +import type { SupportEntry } from "../../utils/caniemail/email-clients"; export const nicenames = { "family": { diff --git a/packages/preview-server/src/actions/email-validation/check-compatibility.ts b/packages/preview-server/src/actions/email-validation/check-compatibility.ts index 5852040d26..541c991ae5 100644 --- a/packages/preview-server/src/actions/email-validation/check-compatibility.ts +++ b/packages/preview-server/src/actions/email-validation/check-compatibility.ts @@ -11,6 +11,10 @@ import { doesPropertyHaveLocation, getUsedStyleProperties, } from '../../utils/caniemail/ast/get-used-style-properties'; +import { + relevantEmailClients, + type SupportEntry, +} from '../../utils/caniemail/email-clients'; import type { CompatibilityStats, SupportStatus, @@ -33,90 +37,6 @@ export interface CompatibilityCheckingResult { statsPerEmailClient: CompatibilityStats['perEmailClient']; } -export type EmailClient = - | 'gmail' - | 'outlook' - | 'yahoo' - | 'apple-mail' - | 'aol' - | 'thunderbird' - | 'microsoft' - | 'samsung-email' - | 'sfr' - | 'orange' - | 'protonmail' - | 'hey' - | 'mail-ru' - | 'fastmail' - | 'laposte' - | 't-online-de' - | 'free-fr' - | 'gmx' - | 'web-de' - | 'ionos-1and1' - | 'rainloop' - | 'wp-pl'; - -export type Platform = - | 'desktop-app' - | 'desktop-webmail' - | 'mobile-webmail' - | 'webmail' - | 'ios' - | 'android' - | 'windows' - | 'macos' - | 'windows-mail' - | 'outlook-com'; - -export type SupportEntryCategory = 'html' | 'css' | 'image' | 'others'; - -export interface SupportEntry { - slug: string; - title: string; - description: string | null; - url: string; - category: SupportEntryCategory; - tags: string[]; - keywords: string | null; - last_test_date: string; - test_url: string; - test_results_url: string | null; - stats: Partial< - Record< - EmailClient, - Partial< - Record< - Platform, - /* - This last Record has only one key, as the - ordered version of caniemail's data is meant to be something like: - - [ - { "1.0": "u" }, - { "2.0": "y" }, - { "3.0": "p #1" }, - ] - - So only one key for each object inside of this array, TypeScript can't really - describe this though AFAIK. - */ - Record[] - > - > - > - >; - notes: string | null; - notes_by_num: Record | null; -} - -const relevantEmailClients: EmailClient[] = [ - 'gmail', - 'apple-mail', - 'outlook', - 'yahoo', -]; - export const checkCompatibility = async ( reactCode: string, emailPath: string, diff --git a/packages/preview-server/src/actions/export-single-template.ts b/packages/preview-server/src/actions/export-single-template.ts index eb6f0ccd10..3b82521fd9 100644 --- a/packages/preview-server/src/actions/export-single-template.ts +++ b/packages/preview-server/src/actions/export-single-template.ts @@ -2,7 +2,7 @@ import { Resend } from 'resend'; import { z } from 'zod'; -import { resendApiKey } from '../app/env'; +import { env } from '../app/env'; import { baseActionClient } from './safe-action'; export const exportSingleTemplate = baseActionClient @@ -16,7 +16,7 @@ export const exportSingleTemplate = baseActionClient }), ) .action(async ({ parsedInput }) => { - const resend = new Resend(resendApiKey); + const resend = new Resend(env.RESEND_API_KEY); const response = await resend.templates.create({ name: parsedInput.name, diff --git a/packages/preview-server/src/actions/get-email-path-from-slug.ts b/packages/preview-server/src/actions/get-email-path-from-slug.ts index de15ee1e7c..2d4e461cc2 100644 --- a/packages/preview-server/src/actions/get-email-path-from-slug.ts +++ b/packages/preview-server/src/actions/get-email-path-from-slug.ts @@ -3,13 +3,13 @@ import fs from 'node:fs'; import path from 'node:path'; import { cache } from 'react'; -import { emailsDirectoryAbsolutePath } from '../app/env'; +import { env } from '../app/env'; export const getEmailPathFromSlug = cache(async (slug: string) => { if (['.tsx', '.jsx', '.ts', '.js'].includes(path.extname(slug))) - return path.join(emailsDirectoryAbsolutePath, slug); + return path.join(env.EMAILS_DIR_ABSOLUTE_PATH, slug); - const pathWithoutExtension = path.join(emailsDirectoryAbsolutePath, slug); + const pathWithoutExtension = path.join(env.EMAILS_DIR_ABSOLUTE_PATH, slug); if (fs.existsSync(`${pathWithoutExtension}.tsx`)) { return `${pathWithoutExtension}.tsx`; diff --git a/packages/preview-server/src/actions/render-email-by-path.tsx b/packages/preview-server/src/actions/render-email-by-path.tsx index fbbc4bb41c..350e4e1da9 100644 --- a/packages/preview-server/src/actions/render-email-by-path.tsx +++ b/packages/preview-server/src/actions/render-email-by-path.tsx @@ -5,12 +5,7 @@ import path from 'node:path'; import logSymbols from 'log-symbols'; import ora, { type Ora } from 'ora'; import type React from 'react'; -import { - isBuilding, - isPreviewDevelopment, - previewServerLocation, - userProjectLocation, -} from '../app/env'; +import { env } from '../app/env'; import { convertStackWithSourceMap } from '../utils/convert-stack-with-sourcemap'; import { createJsxRuntime } from '../utils/create-jsx-runtime'; import { getEmailComponent } from '../utils/get-email-component'; @@ -36,8 +31,8 @@ export interface RenderedEmailMetadata { export type EmailRenderingResult = | RenderedEmailMetadata | { - error: ErrorObject; - }; + error: ErrorObject; + }; const cache = new Map(); @@ -100,7 +95,10 @@ export const renderEmailByPath = async ( const emailFilename = path.basename(emailPath); let spinner: Ora | undefined; - if (!isBuilding && !isPreviewDevelopment) { + if ( + env.NEXT_PUBLIC_IS_BUILDING === 'false' && + env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT === 'false' + ) { logBufferer.buffer(); errorBufferer.buffer(); infoBufferer.buffer(); @@ -115,11 +113,11 @@ export const renderEmailByPath = async ( const timeBeforeEmailBundled = performance.now(); const originalJsxRuntimePath = path.resolve( - previewServerLocation, + env.PREVIEW_SERVER_LOCATION, 'jsx-runtime', ); const jsxRuntimePath = await createJsxRuntime( - userProjectLocation, + env.USER_PROJECT_LOCATION, originalJsxRuntimePath, ); const componentResult = await getEmailComponent(emailPath, jsxRuntimePath); diff --git a/packages/preview-server/src/app/env.ts b/packages/preview-server/src/app/env.ts index d16b317581..2bf9ff48b6 100644 --- a/packages/preview-server/src/app/env.ts +++ b/packages/preview-server/src/app/env.ts @@ -1,17 +1,28 @@ -/** ONLY ACCESSIBLE ON THE SERVER */ -export const userProjectLocation = process.env.USER_PROJECT_LOCATION!; +import { createEnv } from '@t3-oss/env-nextjs'; +import { z } from 'zod'; -/** ONLY ACCESSIBLE ON THE SERVER */ -export const previewServerLocation = process.env.PREVIEW_SERVER_LOCATION!; - -/** ONLY ACCESSIBLE ON THE SERVER */ -export const emailsDirectoryAbsolutePath = - process.env.EMAILS_DIR_ABSOLUTE_PATH!; - -/** ONLY ACCESSIBLE ON THE SERVER */ -export const resendApiKey = process.env.RESEND_API_KEY; - -export const isBuilding = process.env.NEXT_PUBLIC_IS_BUILDING === 'true'; - -export const isPreviewDevelopment = - process.env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT === 'true'; +export const env = createEnv({ + server: { + USER_PROJECT_LOCATION: z.string(), + PREVIEW_SERVER_LOCATION: z.string(), + EMAILS_DIR_ABSOLUTE_PATH: z.string(), + RESEND_API_KEY: z.string().optional(), + COMPATIBILITY_EMAIL_CLIENTS: z.string(), + }, + client: { + NEXT_PUBLIC_IS_BUILDING: z + .union([z.literal('true'), z.literal('false')]) + .optional() + .default('false'), + NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT: z + .union([z.literal('true'), z.literal('false')]) + .optional() + .default('false'), + }, + skipValidation: true, + experimental__runtimeEnv: { + NEXT_PUBLIC_IS_BUILDING: process.env.NEXT_PUBLIC_IS_BUILDING, + NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT: + process.env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT, + }, +}); diff --git a/packages/preview-server/src/app/layout.tsx b/packages/preview-server/src/app/layout.tsx index 4a99619456..5f0434bc39 100644 --- a/packages/preview-server/src/app/layout.tsx +++ b/packages/preview-server/src/app/layout.tsx @@ -3,7 +3,7 @@ import './globals.css'; import type { Metadata } from 'next'; import { EmailsProvider } from '../contexts/emails'; import { getEmailsDirectoryMetadata } from '../utils/get-emails-directory-metadata'; -import { emailsDirectoryAbsolutePath } from './env'; +import { env } from './env'; import { inter, sfMono } from './fonts'; export const metadata: Metadata = { @@ -18,12 +18,12 @@ export default async function RootLayout({ children: React.ReactNode; }) { const emailsDirectoryMetadata = await getEmailsDirectoryMetadata( - emailsDirectoryAbsolutePath, + env.EMAILS_DIR_ABSOLUTE_PATH, ); if (typeof emailsDirectoryMetadata === 'undefined') { throw new Error( - `Could not find the emails directory specified under ${emailsDirectoryAbsolutePath}!`, + `Could not find the emails directory specified under ${env.EMAILS_DIR_ABSOLUTE_PATH}!`, ); } diff --git a/packages/preview-server/src/app/page.tsx b/packages/preview-server/src/app/page.tsx index 93b604aea0..72167acd69 100644 --- a/packages/preview-server/src/app/page.tsx +++ b/packages/preview-server/src/app/page.tsx @@ -4,11 +4,11 @@ import Link from 'next/link'; import { Button, Heading, Text } from '../components'; import CodeSnippet from '../components/code-snippet'; import { Shell } from '../components/shell'; -import { emailsDirectoryAbsolutePath } from './env'; +import { env } from './env'; import logo from './logo.png'; export default function Home() { - const baseEmailsDirectoryName = path.basename(emailsDirectoryAbsolutePath); + const baseEmailsDirectoryName = path.basename(env.EMAILS_DIR_ABSOLUTE_PATH); return ( diff --git a/packages/preview-server/src/app/preview/[...slug]/page.tsx b/packages/preview-server/src/app/preview/[...slug]/page.tsx index 36f0674da5..512c110135 100644 --- a/packages/preview-server/src/app/preview/[...slug]/page.tsx +++ b/packages/preview-server/src/app/preview/[...slug]/page.tsx @@ -16,12 +16,9 @@ import { ToolbarProvider } from '../../../contexts/toolbar'; import { getEmailsDirectoryMetadata } from '../../../utils/get-emails-directory-metadata'; import { getLintingSources, loadLintingRowsFrom } from '../../../utils/linting'; import { loadStream } from '../../../utils/load-stream'; -import { - emailsDirectoryAbsolutePath, - isBuilding, - resendApiKey, -} from '../../env'; +import { env } from '../../env'; import Preview from './preview'; +import type { Metadata } from 'next'; export const dynamicParams = true; @@ -41,12 +38,12 @@ export default async function Page({ // ex: ['authentication', 'verify-password.tsx'] const slug = decodeURIComponent(params.slug.join('/')); const emailsDirMetadata = await getEmailsDirectoryMetadata( - emailsDirectoryAbsolutePath, + env.EMAILS_DIR_ABSOLUTE_PATH, ); if (typeof emailsDirMetadata === 'undefined') { throw new Error( - `Could not find the emails directory specified under ${emailsDirectoryAbsolutePath}! + `Could not find the emails directory specified under ${env.EMAILS_DIR_ABSOLUTE_PATH}! This is most likely not an issue with the preview server. Maybe there was a typo on the "--dir" flag?`, ); @@ -69,7 +66,7 @@ This is most likely not an issue with the preview server. Maybe there was a typo let lintingRows: LintingRow[] | undefined; let compatibilityCheckingResults: CompatibilityCheckingResult[] | undefined; - if (isBuilding) { + if (env.NEXT_PUBLIC_IS_BUILDING === 'true') { if ('error' in serverEmailRenderingResult) { throw new Error(serverEmailRenderingResult.error.message, { cause: serverEmailRenderingResult.error, @@ -137,7 +134,9 @@ This is most likely not an issue with the preview server. Maybe there was a typo - 0}> + 0} + > ; -}) { +}): Promise { const { slug } = await params; return { title: `${path.basename(slug.join('/'))} — React Email` }; diff --git a/packages/preview-server/src/components/toolbar.tsx b/packages/preview-server/src/components/toolbar.tsx index e843a6f925..d99be61172 100644 --- a/packages/preview-server/src/components/toolbar.tsx +++ b/packages/preview-server/src/components/toolbar.tsx @@ -6,7 +6,7 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import type { ComponentProps } from 'react'; import * as React from 'react'; import type { CompatibilityCheckingResult } from '../actions/email-validation/check-compatibility'; -import { isBuilding } from '../app/env'; +import { env } from '../app/env'; import { usePreviewContext } from '../contexts/preview'; import { useToolbarContext } from '../contexts/toolbar'; import { cn } from '../utils'; @@ -115,7 +115,7 @@ const ToolbarInner = ({ initialResults: serverCompatibilityResults ?? cachedCompatibilityResults, }); - if (!isBuilding) { + if (env.NEXT_PUBLIC_IS_BUILDING === 'false') { // biome-ignore lint/correctness/useHookAtTopLevel: This is fine since isBuilding does not change at runtime React.useEffect(() => { (async () => { @@ -190,7 +190,8 @@ const ToolbarInner = ({ > - {isBuilding || activeTab === 'resend' ? null : ( + {env.NEXT_PUBLIC_IS_BUILDING === 'true' || + activeTab === 'resend' ? null : ( (undefined); @@ -32,7 +32,10 @@ export const EmailsProvider = (props: { const [emailsDirectoryMetadata, setEmailsDirectoryMetadata] = useState(props.initialEmailsDirectoryMetadata); - if (!isBuilding && !isPreviewDevelopment) { + if ( + env.NEXT_PUBLIC_IS_BUILDING === 'false' && + env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT === 'false' + ) { // biome-ignore lint/correctness/useHookAtTopLevel: this will not change on runtime so it doesn't violate the rules of hooks useHotreload(async () => { const metadata = await getEmailsDirectoryMetadataAction( diff --git a/packages/preview-server/src/contexts/preview.tsx b/packages/preview-server/src/contexts/preview.tsx index fca9da2eb0..b07fba57e5 100644 --- a/packages/preview-server/src/contexts/preview.tsx +++ b/packages/preview-server/src/contexts/preview.tsx @@ -5,19 +5,19 @@ import type { EmailRenderingResult, RenderedEmailMetadata, } from '../actions/render-email-by-path'; -import { isBuilding, isPreviewDevelopment } from '../app/env'; +import { env } from '../app/env'; import { useEmailRenderingResult } from '../hooks/use-email-rendering-result'; import { useHotreload } from '../hooks/use-hot-reload'; import { useRenderingMetadata } from '../hooks/use-rendering-metadata'; export const PreviewContext = createContext< | { - renderedEmailMetadata: RenderedEmailMetadata | undefined; - renderingResult: EmailRenderingResult; + renderedEmailMetadata: RenderedEmailMetadata | undefined; + renderingResult: EmailRenderingResult; - emailSlug: string; - emailPath: string; - } + emailSlug: string; + emailPath: string; + } | undefined >(undefined); @@ -49,7 +49,10 @@ export const PreviewProvider = ({ serverRenderingResult, ); - if (!isBuilding && !isPreviewDevelopment) { + if ( + env.NEXT_PUBLIC_IS_BUILDING === 'false' && + env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT === 'false' + ) { // biome-ignore lint/correctness/useHookAtTopLevel: this will not change on runtime so it doesn't violate the rules of hooks useHotreload((changes) => { const changeForThisEmail = changes.find((change) => diff --git a/packages/preview-server/src/hooks/use-email-rendering-result.ts b/packages/preview-server/src/hooks/use-email-rendering-result.ts index 3f4c848d87..74b58c3873 100644 --- a/packages/preview-server/src/hooks/use-email-rendering-result.ts +++ b/packages/preview-server/src/hooks/use-email-rendering-result.ts @@ -4,7 +4,7 @@ import { type EmailRenderingResult, renderEmailByPath, } from '../actions/render-email-by-path'; -import { isBuilding, isPreviewDevelopment } from '../app/env'; +import { env } from '../app/env'; import { useEmails } from '../contexts/emails'; import { containsEmailTemplate } from '../utils/contains-email-template'; import { useHotreload } from './use-hot-reload'; @@ -19,7 +19,10 @@ export const useEmailRenderingResult = ( const { emailsDirectoryMetadata } = useEmails(); - if (!isBuilding && !isPreviewDevelopment) { + if ( + env.NEXT_PUBLIC_IS_BUILDING === 'false' && + env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT === 'false' + ) { // biome-ignore lint/correctness/useHookAtTopLevel: This is fine since isBuilding does not change at runtime useHotreload(async (changes) => { for await (const change of changes) { diff --git a/packages/preview-server/src/utils/caniemail/email-clients.ts b/packages/preview-server/src/utils/caniemail/email-clients.ts new file mode 100644 index 0000000000..43aecaeb29 --- /dev/null +++ b/packages/preview-server/src/utils/caniemail/email-clients.ts @@ -0,0 +1,89 @@ +import { env } from '../../app/env'; + +/** + * All supported email clients for compatibility checking. + * This is used both as a type and as a runtime validation list. + */ +export const allEmailClients = [ + 'gmail', + 'outlook', + 'yahoo', + 'apple-mail', + 'aol', + 'thunderbird', + 'microsoft', + 'samsung-email', + 'sfr', + 'orange', + 'protonmail', + 'hey', + 'mail-ru', + 'fastmail', + 'laposte', + 't-online-de', + 'free-fr', + 'gmx', + 'web-de', + 'ionos-1and1', + 'rainloop', + 'wp-pl', +] as const; + +export const relevantEmailClients = ( + env.COMPATIBILITY_EMAIL_CLIENTS ?? '' +).split(',') as EmailClient[]; + +export type EmailClient = (typeof allEmailClients)[number]; + +export type Platform = + | 'desktop-app' + | 'desktop-webmail' + | 'mobile-webmail' + | 'webmail' + | 'ios' + | 'android' + | 'windows' + | 'macos' + | 'windows-mail' + | 'outlook-com'; + +export type SupportEntryCategory = 'html' | 'css' | 'image' | 'others'; + +export interface SupportEntry { + slug: string; + title: string; + description: string | null; + url: string; + category: SupportEntryCategory; + tags: string[]; + keywords: string | null; + last_test_date: string; + test_url: string; + test_results_url: string | null; + stats: Partial< + Record< + EmailClient, + Partial< + Record< + Platform, + /* + This last Record has only one key, as the + ordered version of caniemail's data is meant to be something like: + + [ + { "1.0": "u" }, + { "2.0": "y" }, + { "3.0": "p #1" }, + ] + + So only one key for each object inside of this array, TypeScript can't really + describe this though AFAIK. + */ + Record[] + > + > + > + >; + notes: string | null; + notes_by_num: Record | null; +} diff --git a/packages/preview-server/src/utils/caniemail/get-compatibility-stats-for-entry.ts b/packages/preview-server/src/utils/caniemail/get-compatibility-stats-for-entry.ts index e4e6a403b1..251335a300 100644 --- a/packages/preview-server/src/utils/caniemail/get-compatibility-stats-for-entry.ts +++ b/packages/preview-server/src/utils/caniemail/get-compatibility-stats-for-entry.ts @@ -1,8 +1,4 @@ -import type { - EmailClient, - Platform, - SupportEntry, -} from '../../actions/email-validation/check-compatibility'; +import type { EmailClient, Platform, SupportEntry } from './email-clients'; export type SupportStatus = DetailedSupportStatus['status']; diff --git a/packages/preview-server/tsconfig.json b/packages/preview-server/tsconfig.json index 6eebf9112b..f1cb69a51b 100644 --- a/packages/preview-server/tsconfig.json +++ b/packages/preview-server/tsconfig.json @@ -8,7 +8,7 @@ "forceConsistentCasingInFileNames": true, "inlineSources": false, "isolatedModules": true, - "moduleResolution": "node", + "moduleResolution": "bundler", "noUnusedLocals": false, "noUnusedParameters": false, "preserveWatchOutput": true, @@ -24,14 +24,21 @@ "declarationMap": false, "incremental": false, "jsx": "react-jsx", - "lib": ["dom", "dom.iterable", "esnext", "ESNext.AsyncIterable"], + "lib": [ + "dom", + "dom.iterable", + "esnext", + "ESNext.AsyncIterable" + ], "noEmit": true, "strict": false, "target": "ESNext", "module": "esnext", "noUncheckedIndexedAccess": true, "resolveJsonModule": true, - "types": ["vitest/globals"], + "types": [ + "vitest/globals" + ], "outDir": "dist" }, "include": [ @@ -44,5 +51,10 @@ ".next/dev/types/**/*.ts", "next.config.mjs" ], - "exclude": [".next", "dist", "emails", "node_modules"] + "exclude": [ + ".next", + "dist", + "emails", + "node_modules" + ] } diff --git a/packages/react-email/src/commands/build.ts b/packages/react-email/src/commands/build.ts index a8ef54c3ec..fa267d2ae2 100644 --- a/packages/react-email/src/commands/build.ts +++ b/packages/react-email/src/commands/build.ts @@ -9,15 +9,18 @@ import { } from '../utils/get-emails-directory-metadata.js'; import { getPreviewServerLocation } from '../utils/get-preview-server-location.js'; import { registerSpinnerAutostopping } from '../utils/register-spinner-autostopping.js'; +import { validateEmailClientList } from '../utils/validate-email-client-list.js'; interface Args { dir: string; packageManager: PackageManagerName; + clients: string; } const setNextEnvironmentVariablesForBuild = async ( emailsDirRelativePath: string, builtPreviewAppPath: string, + compatibilityEmailClients: string[], ) => { const nextConfigContents = ` import path from 'path'; @@ -31,7 +34,8 @@ const nextConfig = { EMAILS_DIR_RELATIVE_PATH: emailsDirRelativePath, EMAILS_DIR_ABSOLUTE_PATH: path.resolve(userProjectLocation, emailsDirRelativePath), PREVIEW_SERVER_LOCATION: previewServerLocation, - USER_PROJECT_LOCATION: userProjectLocation + USER_PROJECT_LOCATION: userProjectLocation, + COMPATIBILITY_EMAIL_CLIENTS: '${compatibilityEmailClients.join(',')}', }, outputFileTracingRoot: previewServerLocation, serverExternalPackages: ['esbuild'], @@ -160,10 +164,13 @@ const updatePackageJson = async (builtPreviewAppPath: string) => { export const build = async ({ dir: emailsDirRelativePath, packageManager, + clients, }: Args) => { try { const previewServerLocation = await getPreviewServerLocation(); + const clientsArray = validateEmailClientList(clients); + const spinner = ora({ text: 'Starting build process...', prefixText: ' ', @@ -216,6 +223,7 @@ export const build = async ({ await setNextEnvironmentVariablesForBuild( emailsDirRelativePath, builtPreviewAppPath, + clientsArray, ); spinner.text = 'Setting server side generation for the email preview pages'; diff --git a/packages/react-email/src/commands/dev.ts b/packages/react-email/src/commands/dev.ts index dac1a871cb..ab0ced1767 100644 --- a/packages/react-email/src/commands/dev.ts +++ b/packages/react-email/src/commands/dev.ts @@ -1,22 +1,31 @@ import fs from 'node:fs'; import { setupHotreloading, startDevServer } from '../utils/index.js'; +import { validateEmailClientList } from '../utils/validate-email-client-list.js'; interface Args { dir: string; port: string; + clients: string; } -export const dev = async ({ dir: emailsDirRelativePath, port }: Args) => { +export const dev = async ({ + dir: emailsDirRelativePath, + port, + clients, +}: Args) => { try { if (!fs.existsSync(emailsDirRelativePath)) { console.error(`Missing ${emailsDirRelativePath} folder`); process.exit(1); } + const clientsArray = validateEmailClientList(clients); + const devServer = await startDevServer( emailsDirRelativePath, emailsDirRelativePath, // defaults to ./emails/static for the static files that are served to the preview Number.parseInt(port, 10), + clientsArray, ); await setupHotreloading(devServer, emailsDirRelativePath); diff --git a/packages/react-email/src/index.ts b/packages/react-email/src/index.ts index f2151d8a4b..719b05575d 100644 --- a/packages/react-email/src/index.ts +++ b/packages/react-email/src/index.ts @@ -20,6 +20,11 @@ program .description('Starts the preview email development app') .option('-d, --dir ', 'Directory with your email templates', './emails') .option('-p --port ', 'Port to run dev server on', '3000') + .option( + '-c, --clients ', + 'Comma-separated list of email clients to show compatibility warnings for (e.g., "outlook,gmail")', + 'gmail,apple-mail,outlook,yahoo', + ) .action(dev); program @@ -31,6 +36,11 @@ program 'Package name to use on installation on `.react-email`', 'npm', ) + .option( + '-c, --clients ', + 'Comma-separated list of email clients to show compatibility warnings for (e.g., "outlook,gmail")', + 'gmail,apple-mail,outlook,yahoo', + ) .action(build); program diff --git a/packages/react-email/src/utils/preview/get-env-variables-for-preview-app.ts b/packages/react-email/src/utils/preview/get-env-variables-for-preview-app.ts index 5d032bff0c..f8a9679db0 100644 --- a/packages/react-email/src/utils/preview/get-env-variables-for-preview-app.ts +++ b/packages/react-email/src/utils/preview/get-env-variables-for-preview-app.ts @@ -4,6 +4,7 @@ export const getEnvVariablesForPreviewApp = ( relativePathToEmailsDirectory: string, previewServerLocation: string, cwd: string, + compatibilityClients: string[], resendApiKey?: string, ) => { return { @@ -12,5 +13,6 @@ export const getEnvVariablesForPreviewApp = ( PREVIEW_SERVER_LOCATION: previewServerLocation, USER_PROJECT_LOCATION: cwd, RESEND_API_KEY: resendApiKey, + COMPATIBILITY_EMAIL_CLIENTS: compatibilityClients.join(','), } as const; }; diff --git a/packages/react-email/src/utils/preview/start-dev-server.ts b/packages/react-email/src/utils/preview/start-dev-server.ts index 49d4c160ad..805414f9ad 100644 --- a/packages/react-email/src/utils/preview/start-dev-server.ts +++ b/packages/react-email/src/utils/preview/start-dev-server.ts @@ -32,6 +32,7 @@ export const startDevServer = async ( emailsDirRelativePath: string, staticBaseDirRelativePath: string, port: number, + compatibilityClients: string[], ): Promise => { const [majorNodeVersion] = process.versions.node.split('.'); if (majorNodeVersion && Number.parseInt(majorNodeVersion, 10) < 20) { @@ -97,6 +98,7 @@ export const startDevServer = async ( emailsDirRelativePath, staticBaseDirRelativePath, nextPortToTry, + compatibilityClients, ); } @@ -132,6 +134,7 @@ export const startDevServer = async ( path.normalize(emailsDirRelativePath), previewServerLocation, process.cwd(), + compatibilityClients, conf.get('resendApiKey'), ), }; @@ -194,21 +197,21 @@ const makeExitHandler = | { shouldKillProcess: false } | { shouldKillProcess: true; killWithErrorCode: boolean }, ) => - (codeSignalOrError: number | NodeJS.Signals | Error) => { - if (typeof devServer !== 'undefined') { - console.log('\nshutting down dev server'); - devServer.close(); - devServer = undefined; - } + (codeSignalOrError: number | NodeJS.Signals | Error) => { + if (typeof devServer !== 'undefined') { + console.log('\nshutting down dev server'); + devServer.close(); + devServer = undefined; + } - if (codeSignalOrError instanceof Error) { - console.error(codeSignalOrError); - } + if (codeSignalOrError instanceof Error) { + console.error(codeSignalOrError); + } - if (options?.shouldKillProcess) { - process.exit(options.killWithErrorCode ? 1 : 0); - } - }; + if (options?.shouldKillProcess) { + process.exit(options.killWithErrorCode ? 1 : 0); + } + }; // do something when app is closing process.on('exit', makeExitHandler()); diff --git a/packages/react-email/src/utils/validate-email-client-list.ts b/packages/react-email/src/utils/validate-email-client-list.ts new file mode 100644 index 0000000000..60c2061043 --- /dev/null +++ b/packages/react-email/src/utils/validate-email-client-list.ts @@ -0,0 +1,37 @@ +const validEmailClients = [ + 'gmail', + 'outlook', + 'yahoo', + 'apple-mail', + 'aol', + 'thunderbird', + 'microsoft', + 'samsung-email', + 'sfr', + 'orange', + 'protonmail', + 'hey', + 'mail-ru', + 'fastmail', + 'laposte', + 't-online-de', + 'free-fr', + 'gmx', + 'web-de', + 'ionos-1and1', + 'rainloop', + 'wp-pl', +] as const; + +export function validateEmailClientList(clients: string) { + const clientsArray = clients.split(/\s*,\s*/); + for (const client of clientsArray) { + if (!(validEmailClients as readonly string[]).includes(client)) { + console.error( + `${client} is not one of the clients we support. Valid options are ${validEmailClients.join(', ')}`, + ); + process.exit(1); + } + } + return clientsArray; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c5eba12b9..4358c04421 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,7 +89,7 @@ importers: dependencies: mintlify: specifier: 4.2.258 - version: 4.2.258(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@22.14.1)(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) + version: 4.2.258(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@22.14.1)(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) zod: specifier: 3.24.3 version: 3.24.3 @@ -638,6 +638,9 @@ importers: packages/preview-server: dependencies: + '@t3-oss/env-nextjs': + specifier: 0.13.10 + version: 0.13.10(arktype@2.1.27)(typescript@5.8.3)(zod@4.3.5) next: specifier: 16.1.1 version: 16.1.1(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -832,8 +835,8 @@ importers: specifier: 10.0.6 version: 10.0.6(react@19.0.0) zod: - specifier: 4.1.12 - version: 4.1.12 + specifier: 4.3.5 + version: 4.3.5 packages/react-email: dependencies: @@ -4392,6 +4395,40 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} + '@t3-oss/env-core@0.13.10': + resolution: {integrity: sha512-NNFfdlJ+HmPHkLi2HKy7nwuat9SIYOxei9K10lO2YlcSObDILY7mHZNSHsieIM3A0/5OOzw/P/b+yLvPdaG52g==} + peerDependencies: + arktype: ^2.1.0 + typescript: '>=5.0.0' + valibot: ^1.0.0-beta.7 || ^1.0.0 + zod: ^3.24.0 || ^4.0.0 + peerDependenciesMeta: + arktype: + optional: true + typescript: + optional: true + valibot: + optional: true + zod: + optional: true + + '@t3-oss/env-nextjs@0.13.10': + resolution: {integrity: sha512-JfSA2WXOnvcc/uMdp31paMsfbYhhdvLLRxlwvrnlPE9bwM/n0Z+Qb9xRv48nPpvfMhOrkrTYw1I5Yc06WIKBJQ==} + peerDependencies: + arktype: ^2.1.0 + typescript: '>=5.0.0' + valibot: ^1.0.0-beta.7 || ^1.0.0 + zod: ^3.24.0 || ^4.0.0 + peerDependenciesMeta: + arktype: + optional: true + typescript: + optional: true + valibot: + optional: true + zod: + optional: true + '@tailwindcss/node@4.1.17': resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} @@ -9692,8 +9729,8 @@ packages: zod@3.24.3: resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==} - zod@4.1.12: - resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + zod@4.3.5: + resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} zustand@4.5.7: resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} @@ -11171,36 +11208,6 @@ snapshots: - acorn - supports-color - '@mdx-js/mdx@3.1.0(acorn@8.15.0)': - dependencies: - '@types/estree': 1.0.8 - '@types/estree-jsx': 1.0.5 - '@types/hast': 3.0.4 - '@types/mdx': 2.0.13 - collapse-white-space: 2.1.0 - devlop: 1.1.0 - estree-util-is-identifier-name: 3.0.0 - estree-util-scope: 1.0.0 - estree-walker: 3.0.3 - hast-util-to-jsx-runtime: 2.3.3 - markdown-extensions: 2.0.0 - recma-build-jsx: 1.0.0 - recma-jsx: 1.0.0(acorn@8.15.0) - recma-stringify: 1.0.0 - rehype-recma: 1.0.0 - remark-mdx: 3.1.0 - remark-parse: 11.0.0 - remark-rehype: 11.1.1 - source-map: 0.7.4 - unified: 11.0.5 - unist-util-position-from-estree: 2.0.0 - unist-util-stringify-position: 4.0.0 - unist-util-visit: 5.0.0 - vfile: 6.0.3 - transitivePeerDependencies: - - acorn - - supports-color - '@mdx-js/react@3.1.0(@types/react@19.2.7)(react@19.0.0)': dependencies: '@types/mdx': 2.0.13 @@ -11209,15 +11216,15 @@ snapshots: '@mediapipe/tasks-vision@0.10.17': {} - '@mintlify/cli@4.0.862(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@22.14.1)(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3)': + '@mintlify/cli@4.0.862(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@22.14.1)(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3)': dependencies: '@inquirer/prompts': 7.9.0(@types/node@22.14.1) '@mintlify/common': 1.0.651(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/link-rot': 3.0.802(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/link-rot': 3.0.802(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) '@mintlify/models': 0.0.250 - '@mintlify/prebuild': 1.0.783(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/previewing': 4.0.837(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) - '@mintlify/validation': 0.1.550(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/prebuild': 1.0.783(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/previewing': 4.0.837(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) + '@mintlify/validation': 0.1.550(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) adm-zip: 0.5.16 chalk: 5.2.0 color: 4.2.3 @@ -11308,12 +11315,12 @@ snapshots: - ts-node - typescript - '@mintlify/link-rot@3.0.802(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': + '@mintlify/link-rot@3.0.802(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': dependencies: '@mintlify/common': 1.0.651(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/prebuild': 1.0.783(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/previewing': 4.0.837(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) - '@mintlify/validation': 0.1.550(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/prebuild': 1.0.783(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/previewing': 4.0.837(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) + '@mintlify/validation': 0.1.550(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) fs-extra: 11.1.0 unist-util-visit: 4.1.2 transitivePeerDependencies: @@ -11359,33 +11366,6 @@ snapshots: - supports-color - typescript - '@mintlify/mdx@3.0.4(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': - dependencies: - '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@shikijs/transformers': 3.15.0 - '@shikijs/twoslash': 3.15.0(typescript@5.9.3) - arktype: 2.1.27 - hast-util-to-string: 3.0.1 - mdast-util-from-markdown: 2.0.2 - mdast-util-gfm: 3.1.0 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-to-hast: 13.2.0 - next-mdx-remote-client: 1.0.7(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - rehype-katex: 7.0.1 - remark-gfm: 4.0.1 - remark-math: 6.0.0 - remark-smartypants: 3.0.2 - shiki: 3.15.0 - unified: 11.0.5 - unist-util-visit: 5.0.0 - transitivePeerDependencies: - - '@types/react' - - acorn - - supports-color - - typescript - '@mintlify/models@0.0.250': dependencies: axios: 1.10.0 @@ -11402,12 +11382,12 @@ snapshots: leven: 4.0.0 yaml: 2.6.1 - '@mintlify/prebuild@1.0.783(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': + '@mintlify/prebuild@1.0.783(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': dependencies: '@mintlify/common': 1.0.651(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) '@mintlify/openapi-parser': 0.0.8 '@mintlify/scraping': 4.0.512(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/validation': 0.1.550(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/validation': 0.1.550(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) chalk: 5.3.0 favicons: 7.2.0 front-matter: 4.0.2 @@ -11433,11 +11413,11 @@ snapshots: - typescript - utf-8-validate - '@mintlify/previewing@4.0.837(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3)': + '@mintlify/previewing@4.0.837(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3)': dependencies: '@mintlify/common': 1.0.651(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/prebuild': 1.0.783(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/validation': 0.1.550(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/prebuild': 1.0.783(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) + '@mintlify/validation': 0.1.550(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) better-opn: 3.0.2 chalk: 5.2.0 chokidar: 3.5.3 @@ -11526,29 +11506,6 @@ snapshots: - supports-color - typescript - '@mintlify/validation@0.1.550(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3)': - dependencies: - '@mintlify/mdx': 3.0.4(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)(typescript@5.9.3) - '@mintlify/models': 0.0.250 - arktype: 2.1.27 - js-yaml: 4.1.0 - lcm: 0.0.3 - lodash: 4.17.21 - object-hash: 3.0.0 - openapi-types: 12.1.3 - uuid: 11.1.0 - zod: 3.21.4 - zod-to-json-schema: 3.20.4(zod@3.21.4) - transitivePeerDependencies: - - '@radix-ui/react-popover' - - '@types/react' - - acorn - - debug - - react - - react-dom - - supports-color - - typescript - '@monogrid/gainmap-js@3.1.0(three@0.170.0)': dependencies: promise-worker-transferable: 1.0.4 @@ -13320,6 +13277,20 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@t3-oss/env-core@0.13.10(arktype@2.1.27)(typescript@5.8.3)(zod@4.3.5)': + optionalDependencies: + arktype: 2.1.27 + typescript: 5.8.3 + zod: 4.3.5 + + '@t3-oss/env-nextjs@0.13.10(arktype@2.1.27)(typescript@5.8.3)(zod@4.3.5)': + dependencies: + '@t3-oss/env-core': 0.13.10(arktype@2.1.27)(typescript@5.8.3)(zod@4.3.5) + optionalDependencies: + arktype: 2.1.27 + typescript: 5.8.3 + zod: 4.3.5 + '@tailwindcss/node@4.1.17': dependencies: '@jridgewell/remapping': 2.3.5 @@ -16933,9 +16904,9 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 - mintlify@4.2.258(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@22.14.1)(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3): + mintlify@4.2.258(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@22.14.1)(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3): dependencies: - '@mintlify/cli': 4.0.862(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@22.14.1)(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) + '@mintlify/cli': 4.0.862(@radix-ui/react-popover@1.1.15(@types/react-dom@19.0.1)(@types/react@19.2.7)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/node@22.14.1)(@types/react@19.2.7)(acorn@8.11.2)(react-dom@19.0.0(react@19.0.0))(typescript@5.9.3) transitivePeerDependencies: - '@radix-ui/react-popover' - '@types/node' @@ -17013,22 +16984,6 @@ snapshots: - acorn - supports-color - next-mdx-remote-client@1.0.7(@types/react@19.2.7)(acorn@8.15.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): - dependencies: - '@babel/code-frame': 7.27.1 - '@mdx-js/mdx': 3.1.0(acorn@8.15.0) - '@mdx-js/react': 3.1.0(@types/react@19.2.7)(react@19.0.0) - react: 19.0.0 - react-dom: 19.0.0(react@19.0.0) - remark-mdx-remove-esm: 1.1.0 - serialize-error: 12.0.0 - vfile: 6.0.3 - vfile-matter: 5.0.0 - transitivePeerDependencies: - - '@types/react' - - acorn - - supports-color - next-safe-action@8.0.11(next@16.1.1(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: next: 16.1.1(@babel/core@7.26.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -17898,16 +17853,6 @@ snapshots: transitivePeerDependencies: - acorn - recma-jsx@1.0.0(acorn@8.15.0): - dependencies: - acorn-jsx: 5.3.2(acorn@8.15.0) - estree-util-to-js: 2.0.0 - recma-parse: 1.0.0 - recma-stringify: 1.0.0 - unified: 11.0.5 - transitivePeerDependencies: - - acorn - recma-parse@1.0.0: dependencies: '@types/estree': 1.0.8 @@ -19890,7 +19835,7 @@ snapshots: zod@3.24.3: {} - zod@4.1.12: {} + zod@4.3.5: {} zustand@4.5.7(@types/react@19.0.1)(immer@9.0.21)(react@19.0.0): dependencies: