From c185bcfa118c3877a17936746de8b15a1f4217c9 Mon Sep 17 00:00:00 2001 From: Giancarlo Buomprisco Date: Fri, 21 Feb 2025 13:29:42 +0700 Subject: [PATCH] 2.3.0 Dev Tools (#180) * 2.3.0 - Added new Dev Tools app --- apps/dev-tool/.gitignore | 41 + apps/dev-tool/README.md | 27 + .../[id]/components/email-tester-form.tsx | 199 +++++ apps/dev-tool/app/emails/[id]/page.tsx | 104 +++ apps/dev-tool/app/emails/lib/email-loader.tsx | 57 ++ .../emails/lib/email-tester-form-schema.ts | 11 + .../dev-tool/app/emails/lib/server-actions.ts | 38 + apps/dev-tool/app/emails/page.tsx | 83 ++ apps/dev-tool/app/layout.tsx | 27 + apps/dev-tool/app/lib/connectivity-service.ts | 213 +++++ apps/dev-tool/app/page.tsx | 48 ++ .../components/translations-comparison.tsx | 241 ++++++ .../translations/lib/translations-loader.ts | 46 ++ apps/dev-tool/app/translations/page.tsx | 35 + .../app-environment-variables-manager.tsx | 721 ++++++++++++++++ .../dev-tool/app/variables/lib/env-scanner.ts | 244 ++++++ .../app/variables/lib/env-variables-model.ts | 770 ++++++++++++++++++ apps/dev-tool/app/variables/lib/types.ts | 40 + apps/dev-tool/app/variables/page.tsx | 55 ++ apps/dev-tool/components/app-layout.tsx | 13 + apps/dev-tool/components/app-sidebar.tsx | 79 ++ .../dev-tool/components/env-mode-selector.tsx | 41 + apps/dev-tool/components/iframe.tsx | 35 + apps/dev-tool/components/root-providers.tsx | 32 + apps/dev-tool/components/status-tile.tsx | 57 ++ apps/dev-tool/next.config.ts | 16 + apps/dev-tool/package.json | 44 + apps/dev-tool/postcss.config.mjs | 5 + apps/dev-tool/styles/globals.css | 43 + apps/dev-tool/styles/shadcn-ui.css | 104 +++ apps/dev-tool/styles/theme.css | 116 +++ apps/dev-tool/styles/theme.utilities.css | 5 + apps/dev-tool/tsconfig.json | 28 + apps/web/package.json | 1 - package.json | 2 +- pnpm-lock.yaml | 193 +++-- 36 files changed, 3747 insertions(+), 67 deletions(-) create mode 100644 apps/dev-tool/.gitignore create mode 100644 apps/dev-tool/README.md create mode 100644 apps/dev-tool/app/emails/[id]/components/email-tester-form.tsx create mode 100644 apps/dev-tool/app/emails/[id]/page.tsx create mode 100644 apps/dev-tool/app/emails/lib/email-loader.tsx create mode 100644 apps/dev-tool/app/emails/lib/email-tester-form-schema.ts create mode 100644 apps/dev-tool/app/emails/lib/server-actions.ts create mode 100644 apps/dev-tool/app/emails/page.tsx create mode 100644 apps/dev-tool/app/layout.tsx create mode 100644 apps/dev-tool/app/lib/connectivity-service.ts create mode 100644 apps/dev-tool/app/page.tsx create mode 100644 apps/dev-tool/app/translations/components/translations-comparison.tsx create mode 100644 apps/dev-tool/app/translations/lib/translations-loader.ts create mode 100644 apps/dev-tool/app/translations/page.tsx create mode 100644 apps/dev-tool/app/variables/components/app-environment-variables-manager.tsx create mode 100644 apps/dev-tool/app/variables/lib/env-scanner.ts create mode 100644 apps/dev-tool/app/variables/lib/env-variables-model.ts create mode 100644 apps/dev-tool/app/variables/lib/types.ts create mode 100644 apps/dev-tool/app/variables/page.tsx create mode 100644 apps/dev-tool/components/app-layout.tsx create mode 100644 apps/dev-tool/components/app-sidebar.tsx create mode 100644 apps/dev-tool/components/env-mode-selector.tsx create mode 100644 apps/dev-tool/components/iframe.tsx create mode 100644 apps/dev-tool/components/root-providers.tsx create mode 100644 apps/dev-tool/components/status-tile.tsx create mode 100644 apps/dev-tool/next.config.ts create mode 100644 apps/dev-tool/package.json create mode 100644 apps/dev-tool/postcss.config.mjs create mode 100644 apps/dev-tool/styles/globals.css create mode 100644 apps/dev-tool/styles/shadcn-ui.css create mode 100644 apps/dev-tool/styles/theme.css create mode 100644 apps/dev-tool/styles/theme.utilities.css create mode 100644 apps/dev-tool/tsconfig.json diff --git a/apps/dev-tool/.gitignore b/apps/dev-tool/.gitignore new file mode 100644 index 000000000..5ef6a5207 --- /dev/null +++ b/apps/dev-tool/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/dev-tool/README.md b/apps/dev-tool/README.md new file mode 100644 index 000000000..8b91d2233 --- /dev/null +++ b/apps/dev-tool/README.md @@ -0,0 +1,27 @@ +# Dev Tool + +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +The Dev Tools is an application that helps you manage your Makerkit environment variables and other settings. + +## Getting Started + +First, run the development server: + +```bash +pnpm run --filter dev-tool dev +``` + +Open the link printed in the terminal to see the result. + +## Testing production environment variables + +To test your production environment variables, create a `.env.production.local` file in the `apps/web` directory and add your production environment variables. + +This environment variables are not committed to the repository, so you can use them for testing purposes. + +In the environment mode switcher, please select `Production` to test your production environment variables. + +## Don't publish this app + +This app is not intended to be published to the public. This is only meant to be used by for development purposes. diff --git a/apps/dev-tool/app/emails/[id]/components/email-tester-form.tsx b/apps/dev-tool/app/emails/[id]/components/email-tester-form.tsx new file mode 100644 index 000000000..5e5fec47b --- /dev/null +++ b/apps/dev-tool/app/emails/[id]/components/email-tester-form.tsx @@ -0,0 +1,199 @@ +'use client'; + +import Link from 'next/link'; + +import { EmailTesterFormSchema } from '@/app/emails/lib/email-tester-form-schema'; +import { sendEmailAction } from '@/app/emails/lib/server-actions'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; + +import { Button } from '@kit/ui/button'; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, +} from '@kit/ui/form'; +import { Input } from '@kit/ui/input'; +import { toast } from '@kit/ui/sonner'; +import { Switch } from '@kit/ui/switch'; + +export function EmailTesterForm(props: { + template: string; + settings: { + username: string; + password: string; + sender: string; + host: string; + port: number; + tls: boolean; + }; +}) { + const form = useForm({ + resolver: zodResolver(EmailTesterFormSchema), + defaultValues: { + username: props.settings.username, + password: props.settings.password, + sender: props.settings.sender, + host: props.settings.host, + port: props.settings.port, + tls: props.settings.tls, + to: '', + }, + }); + + return ( +
+

+ The settings below were filled from your environment variables. You can + change them to test different scenarios.{' '} + + Learn more about Nodemailer if you're not sure how to configure it. + +

+ +
+ { + const promise = sendEmailAction({ + template: props.template, + settings: { + username: data.username, + password: data.password, + sender: data.sender, + host: data.host, + port: data.port, + tls: data.tls, + to: data.to, + }, + }); + + toast.promise(promise, { + loading: 'Sending email...', + success: 'Email sent successfully', + error: 'Failed to send email', + }); + })} + > +
+ { + return ( + + Sender + + + + + ); + }} + /> + + { + return ( + + Recipient + + + + + ); + }} + /> +
+ +
+ { + return ( + + Username + + + + + ); + }} + /> + + { + return ( + + Password + + + + + ); + }} + /> +
+ +
+ { + return ( + + Host + + + + + ); + }} + /> + + { + return ( + + Port + + + + + ); + }} + /> + + { + return ( + + Secure (TLS) + + { + form.setValue('tls', value); + }} + /> + + + ); + }} + /> +
+ + +
+ +
+ ); +} diff --git a/apps/dev-tool/app/emails/[id]/page.tsx b/apps/dev-tool/app/emails/[id]/page.tsx new file mode 100644 index 000000000..514cd316c --- /dev/null +++ b/apps/dev-tool/app/emails/[id]/page.tsx @@ -0,0 +1,104 @@ +import { EmailTesterForm } from '@/app/emails/[id]/components/email-tester-form'; +import { loadEmailTemplate } from '@/app/emails/lib/email-loader'; +import { getVariable } from '@/app/variables/lib/env-scanner'; +import { EnvMode } from '@/app/variables/lib/types'; +import { EnvModeSelector } from '@/components/env-mode-selector'; +import { IFrame } from '@/components/iframe'; + +import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; +import { Button } from '@kit/ui/button'; +import { + Dialog, + DialogContent, + DialogTitle, + DialogTrigger, +} from '@kit/ui/dialog'; +import { Page, PageBody, PageHeader } from '@kit/ui/page'; + +type EmailPageProps = React.PropsWithChildren<{ + params: Promise<{ + id: string; + }>; + + searchParams: Promise<{ mode?: EnvMode }>; +}>; + +export const metadata = { + title: 'Email Template', +}; + +export default async function EmailPage(props: EmailPageProps) { + const { id } = await props.params; + const mode = (await props.searchParams).mode ?? 'development'; + + const template = await loadEmailTemplate(id); + const emailSettings = await getEmailSettings(mode); + + return ( + + + } + > + + + + +

+ Remember that the below is an approximation of the email. Always test + it in your inbox.{' '} +

+ + + + + + Send Test Email + + + + +

+ + +
+
+ ); +} + +async function getEmailSettings(mode: EnvMode) { + const sender = await getVariable('EMAIL_SENDER', mode); + const host = await getVariable('EMAIL_HOST', mode); + const port = await getVariable('EMAIL_PORT', mode); + const tls = await getVariable('EMAIL_TLS', mode); + const username = await getVariable('EMAIL_USER', mode); + const password = await getVariable('EMAIL_PASSWORD', mode); + + return { + sender, + host, + port: Number.isNaN(Number(port)) ? 487 : Number(port), + tls: tls === 'true', + username, + password, + }; +} diff --git a/apps/dev-tool/app/emails/lib/email-loader.tsx b/apps/dev-tool/app/emails/lib/email-loader.tsx new file mode 100644 index 000000000..28377eebd --- /dev/null +++ b/apps/dev-tool/app/emails/lib/email-loader.tsx @@ -0,0 +1,57 @@ +import { + renderAccountDeleteEmail, + renderInviteEmail, +} from '@kit/email-templates'; + +export async function loadEmailTemplate(id: string) { + if (id === 'account-delete-email') { + return renderAccountDeleteEmail({ + productName: 'Makerkit', + userDisplayName: 'Giancarlo', + }); + } + + if (id === 'invite-email') { + return renderInviteEmail({ + teamName: 'Makerkit', + teamLogo: + '', + inviter: 'Giancarlo', + invitedUserEmail: 'test@makerkit.dev', + link: 'https://makerkit.dev', + productName: 'Makerkit', + }); + } + + if (id === 'magic-link-email') { + return loadFromFileSystem('magic-link'); + } + + if (id === 'reset-password-email') { + return loadFromFileSystem('reset-password'); + } + + if (id === 'change-email-address-email') { + return loadFromFileSystem('change-email-address'); + } + + if (id === 'confirm-email') { + return loadFromFileSystem('confirm-email'); + } + + throw new Error(`Email template not found: ${id}`); +} + +async function loadFromFileSystem(fileName: string) { + const { readFileSync } = await import('node:fs'); + const { join } = await import('node:path'); + + const filePath = join( + process.cwd(), + `../web/supabase/templates/${fileName}.html`, + ); + + return { + html: readFileSync(filePath, 'utf8'), + }; +} diff --git a/apps/dev-tool/app/emails/lib/email-tester-form-schema.ts b/apps/dev-tool/app/emails/lib/email-tester-form-schema.ts new file mode 100644 index 000000000..dad963164 --- /dev/null +++ b/apps/dev-tool/app/emails/lib/email-tester-form-schema.ts @@ -0,0 +1,11 @@ +import { z } from 'zod'; + +export const EmailTesterFormSchema = z.object({ + username: z.string().min(1), + password: z.string().min(1), + sender: z.string().min(1), + to: z.string().email(), + host: z.string().min(1), + port: z.number().min(1), + tls: z.boolean(), +}); diff --git a/apps/dev-tool/app/emails/lib/server-actions.ts b/apps/dev-tool/app/emails/lib/server-actions.ts new file mode 100644 index 000000000..3b5a5c188 --- /dev/null +++ b/apps/dev-tool/app/emails/lib/server-actions.ts @@ -0,0 +1,38 @@ +'use server'; + +import { loadEmailTemplate } from '@/app/emails/lib/email-loader'; + +export async function sendEmailAction(params: { + template: string; + settings: { + username: string; + password: string; + sender: string; + host: string; + to: string; + port: number; + tls: boolean; + }; +}) { + const { settings } = params; + const { createTransport } = await import('nodemailer'); + + const transporter = createTransport({ + host: settings.host, + port: settings.port, + secure: settings.tls, + auth: { + user: settings.username, + pass: settings.password, + }, + }); + + const { html } = await loadEmailTemplate(params.template); + + return transporter.sendMail({ + html, + from: settings.sender, + to: settings.to, + subject: 'Test Email', + }); +} diff --git a/apps/dev-tool/app/emails/page.tsx b/apps/dev-tool/app/emails/page.tsx new file mode 100644 index 000000000..908499bd8 --- /dev/null +++ b/apps/dev-tool/app/emails/page.tsx @@ -0,0 +1,83 @@ +import Link from 'next/link'; + +import { + CardButton, + CardButtonHeader, + CardButtonTitle, +} from '@kit/ui/card-button'; +import { Heading } from '@kit/ui/heading'; +import { Page, PageBody, PageHeader } from '@kit/ui/page'; + +export const metadata = { + title: 'Emails', +}; + +export default async function EmailsPage() { + return ( + + + + +
+ Supabase Auth Emails + +
+ + + + Confirm Email + + + + + + + + Change Email Address Email + + + + + + + + Reset Password Email + + + + + + + + Magic Link Email + + + +
+
+ +
+ Transactional Emails + +
+ + + + Account Delete Email + + + + + + + + Invite Email + + + +
+
+
+
+ ); +} diff --git a/apps/dev-tool/app/layout.tsx b/apps/dev-tool/app/layout.tsx new file mode 100644 index 000000000..2a749fc1e --- /dev/null +++ b/apps/dev-tool/app/layout.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from 'next'; + +import { DevToolLayout } from '@/components/app-layout'; +import { RootProviders } from '@/components/root-providers'; + +import '../styles/globals.css'; + +export const metadata: Metadata = { + title: 'Makerkit | Dev Tool', + description: 'The dev tool for Makerkit', +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + ); +} diff --git a/apps/dev-tool/app/lib/connectivity-service.ts b/apps/dev-tool/app/lib/connectivity-service.ts new file mode 100644 index 000000000..788b33baa --- /dev/null +++ b/apps/dev-tool/app/lib/connectivity-service.ts @@ -0,0 +1,213 @@ +import { EnvMode } from '@/app/variables/lib/types'; + +import { getVariable } from '../variables/lib/env-scanner'; + +export function createConnectivityService(mode: EnvMode) { + return new ConnectivityService(mode); +} + +class ConnectivityService { + constructor(private mode: EnvMode = 'development') {} + + async checkSupabaseConnectivity() { + const url = await getVariable('NEXT_PUBLIC_SUPABASE_URL', this.mode); + + if (!url) { + return { + status: 'error' as const, + message: 'No Supabase URL found in environment variables', + }; + } + + const anonKey = await getVariable( + 'NEXT_PUBLIC_SUPABASE_ANON_KEY', + this.mode, + ); + + if (!anonKey) { + return { + status: 'error' as const, + message: 'No Supabase Anon Key found in environment variables', + }; + } + + const response = await fetch(`${url}/auth/v1/health`, { + headers: { + apikey: anonKey, + Authorization: `Bearer ${anonKey}`, + }, + }); + + if (!response.ok) { + return { + status: 'error' as const, + message: + 'Failed to connect to Supabase. The Supabase Anon Key or URL is not valid.', + }; + } + + return { + status: 'success' as const, + message: 'Connected to Supabase', + }; + } + + async checkSupabaseAdminConnectivity() { + const url = await getVariable('NEXT_PUBLIC_SUPABASE_URL', this.mode); + + if (!url) { + return { + status: 'error' as const, + message: 'No Supabase URL found in environment variables', + }; + } + + const endpoint = `${url}/rest/v1/accounts`; + + const apikey = await getVariable( + 'NEXT_PUBLIC_SUPABASE_ANON_KEY', + this.mode, + ); + + if (!apikey) { + return { + status: 'error' as const, + message: 'No Supabase Anon Key found in environment variables', + }; + } + + const adminKey = await getVariable('SUPABASE_SERVICE_ROLE_KEY', this.mode); + + if (!adminKey) { + return { + status: 'error' as const, + message: 'No Supabase Service Role Key found in environment variables', + }; + } + + const response = await fetch(endpoint, { + headers: { + apikey, + Authorization: `Bearer ${adminKey}`, + }, + }); + + if (!response.ok) { + return { + status: 'error' as const, + message: + 'Failed to connect to Supabase Admin. The Supabase Service Role Key is not valid.', + }; + } + + const data = await response.json(); + + if (data.length === 0) { + return { + status: 'error' as const, + message: 'No accounts found in Supabase Admin', + }; + } + + return { + status: 'success' as const, + message: 'Connected to Supabase Admin', + }; + } + + async checkStripeWebhookEndpoints() { + const secretKey = await getVariable('STRIPE_SECRET_KEY', this.mode); + + if (!secretKey) { + return { + status: 'error' as const, + message: 'No Stripe Secret Key found in environment variables', + }; + } + + const webhooksSecret = await getVariable( + 'STRIPE_WEBHOOKS_SECRET', + this.mode, + ); + + if (!webhooksSecret) { + return { + status: 'error' as const, + message: 'No Webhooks secret found in environment variables', + }; + } + + const url = `https://api.stripe.com`; + + const request = await fetch(`${url}/v1/webhook_endpoints`, { + headers: { + Authorization: `Bearer ${secretKey}`, + }, + }); + + if (!request.ok) { + return { + status: 'error' as const, + message: + 'Failed to connect to Stripe. The Stripe Webhook Secret is not valid.', + }; + } + + const webhooks = await request.json(); + + if (webhooks.length === 0) { + return { + status: 'error' as const, + message: 'No webhooks found in Stripe', + }; + } + + const allWebhooksShareTheSameSecret = webhooks.every( + (webhook: any) => webhook.secret === webhooksSecret, + ); + + if (!allWebhooksShareTheSameSecret) { + return { + status: 'error' as const, + message: 'All webhooks do not share the same secret', + }; + } + + return { + status: 'success' as const, + message: 'All webhooks share the same Webhooks secret', + }; + } + + async checkStripeConnected() { + const secretKey = await getVariable('STRIPE_SECRET_KEY', this.mode); + + if (!secretKey) { + return { + status: 'error' as const, + message: 'No Stripe Secret Key found in environment variables', + }; + } + + const url = `https://api.stripe.com`; + + const request = await fetch(`${url}/v1/prices`, { + headers: { + Authorization: `Bearer ${secretKey}`, + }, + }); + + if (!request.ok) { + return { + status: 'error' as const, + message: + 'Failed to connect to Stripe. The Stripe Secret Key is not valid.', + }; + } + + return { + status: 'success' as const, + message: 'Connected to Stripe', + }; + } +} diff --git a/apps/dev-tool/app/page.tsx b/apps/dev-tool/app/page.tsx new file mode 100644 index 000000000..2002f5d40 --- /dev/null +++ b/apps/dev-tool/app/page.tsx @@ -0,0 +1,48 @@ +import { ServiceCard } from '@/components/status-tile'; + +import { Page, PageBody, PageHeader } from '@kit/ui/page'; + +import { createConnectivityService } from './lib/connectivity-service'; +import {EnvMode} from "@/app/variables/lib/types"; +import {EnvModeSelector} from "@/components/env-mode-selector"; + +type DashboardPageProps = React.PropsWithChildren<{ + searchParams: Promise<{ mode?: EnvMode }>; +}>; + +export default async function DashboardPage(props: DashboardPageProps) { + const mode = (await props.searchParams).mode ?? 'development'; + const connectivityService = createConnectivityService(mode); + + const [ + supabaseStatus, + supabaseAdminStatus, + stripeStatus, + stripeWebhookStatus, + ] = await Promise.all([ + connectivityService.checkSupabaseConnectivity(), + connectivityService.checkSupabaseAdminConnectivity(), + connectivityService.checkStripeConnected(), + connectivityService.checkStripeWebhookEndpoints(), + ]); + + return ( + + + + + + +
+ + + + +
+
+
+ ); +} diff --git a/apps/dev-tool/app/translations/components/translations-comparison.tsx b/apps/dev-tool/app/translations/components/translations-comparison.tsx new file mode 100644 index 000000000..03e6b49c4 --- /dev/null +++ b/apps/dev-tool/app/translations/components/translations-comparison.tsx @@ -0,0 +1,241 @@ +'use client'; + +import { useState } from 'react'; + +import { ChevronDownIcon } from 'lucide-react'; + +import { Button } from '@kit/ui/button'; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@kit/ui/dropdown-menu'; +import { Input } from '@kit/ui/input'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@kit/ui/select'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@kit/ui/table'; +import { cn } from '@kit/ui/utils'; + +import { defaultI18nNamespaces } from '../../../../web/lib/i18n/i18n.settings'; +import type { TranslationData, Translations } from '../lib/translations-loader'; + +function flattenTranslations( + obj: TranslationData, + prefix = '', + result: Record = {}, +) { + for (const [key, value] of Object.entries(obj)) { + const newKey = prefix ? `${prefix}.${key}` : key; + + if (typeof value === 'string') { + result[newKey] = value; + } else { + flattenTranslations(value, newKey, result); + } + } + + return result; +} + +type FlattenedTranslations = Record>; + +export function TranslationsComparison({ + translations, +}: { + translations: Translations; +}) { + const [search, setSearch] = useState(''); + const [selectedLocales, setSelectedLocales] = useState>(); + + const [selectedNamespace, setSelectedNamespace] = useState( + defaultI18nNamespaces[0] as string, + ); + + const locales = Object.keys(translations); + + if (locales.length === 0) { + return
No translations found
; + } + + const baseLocale = locales[0]!; + + // Initialize selected locales if not set + if (!selectedLocales) { + setSelectedLocales(new Set(locales)); + return null; + } + + // Flatten translations for the selected namespace + const flattenedTranslations: FlattenedTranslations = {}; + + for (const locale of locales) { + const namespaceData = translations[locale]?.[selectedNamespace]; + if (namespaceData) { + flattenedTranslations[locale] = flattenTranslations(namespaceData); + } else { + flattenedTranslations[locale] = {}; + } + } + + // Get all unique keys across all translations + const allKeys = Array.from( + new Set( + Object.values(flattenedTranslations).flatMap((data) => Object.keys(data)), + ), + ).sort(); + + const filteredKeys = allKeys.filter((key) => + key.toLowerCase().includes(search.toLowerCase()), + ); + + const visibleLocales = locales.filter((locale) => + selectedLocales.has(locale), + ); + + const copyTranslation = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + } catch (error) { + console.error('Failed to copy text:', error); + } + }; + + const toggleLocale = (locale: string) => { + const newSelectedLocales = new Set(selectedLocales); + + if (newSelectedLocales.has(locale)) { + if (newSelectedLocales.size > 1) { + newSelectedLocales.delete(locale); + } + } else { + newSelectedLocales.add(locale); + } + + setSelectedLocales(newSelectedLocales); + }; + + return ( +
+
+
+ setSearch(e.target.value)} + className="max-w-sm" + /> + + + + + + + + {locales.map((locale) => ( + toggleLocale(locale)} + disabled={ + selectedLocales.size === 1 && selectedLocales.has(locale) + } + > + {locale} + + ))} + + + + +
+
+ +
+ + + + Key + {visibleLocales.map((locale) => ( + {locale} + ))} + + + + + {filteredKeys.map((key) => ( + + +
+ {key} +
+
+ + {visibleLocales.map((locale) => { + const translations = flattenedTranslations[locale] ?? {}; + + const baseTranslations = + flattenedTranslations[baseLocale] ?? {}; + + const value = translations[key]; + const baseValue = baseTranslations[key]; + const isMissing = !value; + const isDifferent = value !== baseValue; + + return ( + +
+ + {value || ( + Missing + )} + +
+
+ ); + })} +
+ ))} +
+
+
+
+ ); +} diff --git a/apps/dev-tool/app/translations/lib/translations-loader.ts b/apps/dev-tool/app/translations/lib/translations-loader.ts new file mode 100644 index 000000000..0c7f42426 --- /dev/null +++ b/apps/dev-tool/app/translations/lib/translations-loader.ts @@ -0,0 +1,46 @@ +import { readFileSync, readdirSync } from 'node:fs'; +import { join } from 'node:path'; + +const defaultI18nNamespaces = [ + 'common', + 'auth', + 'account', + 'teams', + 'billing', + 'marketing', +]; + +export type TranslationData = { + [key: string]: string | TranslationData; +}; + +export type Translations = { + [locale: string]: { + [namespace: string]: TranslationData; + }; +}; + +export async function loadTranslations() { + const localesPath = join(process.cwd(), '../web/public/locales'); + const locales = readdirSync(localesPath); + const translations: Translations = {}; + + for (const locale of locales) { + translations[locale] = {}; + + for (const namespace of defaultI18nNamespaces) { + try { + const filePath = join(localesPath, locale, `${namespace}.json`); + const content = readFileSync(filePath, 'utf8'); + translations[locale][namespace] = JSON.parse(content); + } catch (error) { + console.warn( + `Warning: Translation file not found for locale "${locale}" and namespace "${namespace}"`, + ); + translations[locale][namespace] = {}; + } + } + } + + return translations; +} diff --git a/apps/dev-tool/app/translations/page.tsx b/apps/dev-tool/app/translations/page.tsx new file mode 100644 index 000000000..3838f42f1 --- /dev/null +++ b/apps/dev-tool/app/translations/page.tsx @@ -0,0 +1,35 @@ +import { Metadata } from 'next'; + +import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; +import { Page, PageBody, PageHeader } from '@kit/ui/page'; + +import { TranslationsComparison } from './components/translations-comparison'; +import { loadTranslations } from './lib/translations-loader'; + +export const metadata: Metadata = { + title: 'Translations Comparison', + description: 'Compare translations across different languages', +}; + +export default async function TranslationsPage() { + const translations = await loadTranslations(); + + return ( + + + } + /> + + + + + + ); +} diff --git a/apps/dev-tool/app/variables/components/app-environment-variables-manager.tsx b/apps/dev-tool/app/variables/components/app-environment-variables-manager.tsx new file mode 100644 index 000000000..9f58a7d55 --- /dev/null +++ b/apps/dev-tool/app/variables/components/app-environment-variables-manager.tsx @@ -0,0 +1,721 @@ +'use client'; + +import { Fragment, useState } from 'react'; + +import { useRouter, useSearchParams } from 'next/navigation'; + +import { envVariables } from '@/app/variables/lib/env-variables-model'; +import { EnvModeSelector } from '@/components/env-mode-selector'; +import { + ChevronDown, + ChevronUp, + ChevronsUpDownIcon, + Copy, + Eye, + EyeOff, + InfoIcon, +} from 'lucide-react'; + +import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; +import { Badge } from '@kit/ui/badge'; +import { Button } from '@kit/ui/button'; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from '@kit/ui/dropdown-menu'; +import { Heading } from '@kit/ui/heading'; +import { If } from '@kit/ui/if'; +import { Input } from '@kit/ui/input'; +import { toast } from '@kit/ui/sonner'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@kit/ui/tooltip'; +import { cn } from '@kit/ui/utils'; + +import { AppEnvState, EnvVariableState } from '../lib/types'; + +export function AppEnvironmentVariablesManager({ + state, +}: React.PropsWithChildren<{ + state: AppEnvState; +}>) { + return ( +
+ Application: {state.appName} + +
+ +
+
+ ); +} + +function EnvList({ appState }: { appState: AppEnvState }) { + const [expandedVars, setExpandedVars] = useState>({}); + const [showValues, setShowValues] = useState>({}); + const [search, setSearch] = useState(''); + const searchParams = useSearchParams(); + + const secretVars = searchParams.get('secret') === 'true'; + const publicVars = searchParams.get('public') === 'true'; + const privateVars = searchParams.get('private') === 'true'; + const overriddenVars = searchParams.get('overridden') === 'true'; + const invalidVars = searchParams.get('invalid') === 'true'; + + const toggleExpanded = (key: string) => { + setExpandedVars((prev) => ({ + ...prev, + [key]: !prev[key], + })); + }; + + const toggleShowValue = (key: string) => { + setShowValues((prev) => ({ + ...prev, + [key]: !prev[key], + })); + }; + + const copyToClipboard = async (text: string) => { + try { + await navigator.clipboard.writeText(text); + } catch (err) { + console.error('Failed to copy:', err); + } + }; + + const renderValue = (value: string, isVisible: boolean) => { + if (!isVisible) { + return '••••••••'; + } + + return value || '(empty)'; + }; + + const renderVariable = (varState: EnvVariableState) => { + const isExpanded = expandedVars[varState.key] ?? false; + const isClientBundledValue = varState.key.startsWith('NEXT_PUBLIC_'); + + // public variables are always visible + const isValueVisible = showValues[varState.key] ?? isClientBundledValue; + + // grab model is it's a kit variable + const model = envVariables.find( + (variable) => variable.name === varState.key, + ); + + const allVariables = Object.values(appState.variables).reduce( + (acc, variable) => ({ + ...acc, + [variable.key]: variable.effectiveValue, + }), + {}, + ); + + const validation = model?.validate + ? model.validate({ + value: varState.effectiveValue, + variables: allVariables, + mode: appState.mode, + }) + : { + success: true, + error: undefined, + }; + + const canExpand = varState.definitions.length > 1 || !validation.success; + + return ( +
+
+
+
+
+ + {varState.key} + + + {varState.isOverridden && ( + Overridden + )} +
+ + + {(model) => ( +
+ + {model.description} + +
+ )} +
+ +
+
+ {renderValue(varState.effectiveValue, isValueVisible)} +
+ + + + + + +
+
+ + {canExpand && ( + + )} +
+ +
+ + {isClientBundledValue ? `Public variable` : `Private variable`} + + + + + + + + + {isClientBundledValue + ? `This variable will be bundled into the client side. If this is a private variable, do not use "NEXT_PUBLIC".` + : `This variable is private and will not be bundled client side, so you cannot access it from React components rendered client side`} + + + + + + + + Secret Variable + + + + + + + + This is a secret key. Keep it safe! + + + + + + + + {varState.effectiveSource} + + + + + + + + + {varState.effectiveSource === '.env.local' + ? `These variables are specific to this machine and are not committed` + : varState.effectiveSource === '.env.development' + ? `These variables are only being used during development` + : varState.effectiveSource === '.env' + ? `These variables are shared under all modes` + : `These variables are only used in production mode`} + + + + + + + + Overridden in {varState.effectiveSource} + + + + + + + + This variable was overridden by a variable in{' '} + {varState.effectiveSource} + + + + + + + + + Invalid Value + + + + + + + + This variable has an invalid value. Drop down to view the + errors. + + + + + +
+
+ + {isExpanded && canExpand && ( +
+ +
+ + Errors + + + + Invalid Value + + + The value for {varState.key} is invalid: +
+                      {JSON.stringify(validation, null, 2)}
+                    
+
+
+
+
+ + 1}> +
+ + Override Chain + + +
+ {varState.definitions.map((def) => ( +
+ + {def.source} + + +
+ {renderValue(def.value, isValueVisible)} +
+
+ ))} +
+
+
+
+ )} +
+ ); + }; + + const filterVariable = (varState: EnvVariableState) => { + const model = envVariables.find( + (variable) => variable.name === varState.key, + ); + + if ( + !search && + !secretVars && + !publicVars && + !privateVars && + !invalidVars && + !overriddenVars + ) { + return true; + } + + const isSecret = model?.secret; + const isPublic = varState.key.startsWith('NEXT_PUBLIC_'); + const isPrivate = !isPublic; + + const isInSearch = search + ? varState.key.toLowerCase().includes(search.toLowerCase()) + : true; + + if (isPublic && publicVars && isInSearch) { + return true; + } + + if (isSecret && secretVars && isInSearch) { + return true; + } + + if (isPrivate && privateVars && isInSearch) { + return true; + } + + if (overriddenVars && varState.isOverridden && isInSearch) { + return true; + } + + if (invalidVars) { + const allVariables = Object.values(appState.variables).reduce( + (acc, variable) => ({ + ...acc, + [variable.key]: variable.effectiveValue, + }), + {}, + ); + + const hasError = + model && model.validate + ? !model.validate({ + value: varState.effectiveValue, + variables: allVariables, + mode: appState.mode, + }).success + : false; + + if (hasError && isInSearch) return true; + } + + return false; + }; + + const groups = Object.values(appState.variables) + .filter(filterVariable) + .reduce( + (acc, variable) => { + const group = acc.find((group) => group.category === variable.category); + + if (!group) { + acc.push({ + category: variable.category, + variables: [variable], + }); + } else { + group.variables.push(variable); + } + + return acc; + }, + [] as Array<{ category: string; variables: Array }>, + ); + + return ( +
+
+
+
+ +
+ +
+ +
+ + setSearch(e.target.value)} + /> + + + + + + + + + Create a report from the environment variables. Useful for + creating support tickets. + + + +
+
+ +
+ + + {groups.map((group) => ( +
+
+ + {group.category} + +
+ +
+ {group.variables.map((item) => { + return ( + {renderVariable(item)} + ); + })} +
+
+ ))} + + +
+
+ No variables found +
+
+
+
+
+ ); +} + +function createReportFromEnvState(state: AppEnvState) { + let report = ``; + + for (const key in state.variables) { + const variable = state.variables[key]; + + const variableReport = `${key}: ${JSON.stringify(variable, null, 2)}`; + ``; + + report += variableReport + '\n'; + } + + return report; +} + +function FilterSwitcher(props: { + filters: { + secret: boolean; + public: boolean; + overridden: boolean; + private: boolean; + invalid: boolean; + }; +}) { + const router = useRouter(); + + const secretVars = props.filters.secret; + const publicVars = props.filters.public; + const overriddenVars = props.filters.overridden; + const privateVars = props.filters.private; + const invalidVars = props.filters.invalid; + + const handleFilterChange = (key: string, value: boolean) => { + const searchParams = new URLSearchParams(window.location.search); + const path = window.location.pathname; + + if (key === 'all' && value) { + searchParams.delete('secret'); + searchParams.delete('public'); + searchParams.delete('overridden'); + searchParams.delete('private'); + searchParams.delete('invalid'); + } else { + if (!value) { + searchParams.delete(key); + } else { + searchParams.set(key, 'true'); + } + } + + router.push(`${path}?${searchParams.toString()}`); + }; + + const buttonLabel = () => { + const filters = []; + + if (secretVars) filters.push('Secret'); + if (publicVars) filters.push('Public'); + if (overriddenVars) filters.push('Overridden'); + if (privateVars) filters.push('Private'); + if (invalidVars) filters.push('Invalid'); + + if (filters.length === 0) return 'Filter variables'; + + return filters.join(', '); + }; + + const allSelected = + !secretVars && !publicVars && !overriddenVars && !invalidVars; + + return ( + + + + + + + { + handleFilterChange('all', true); + }} + > + All + + + { + handleFilterChange('secret', !secretVars); + }} + > + Secret + + + { + handleFilterChange('private', !privateVars); + }} + > + Private + + + { + handleFilterChange('public', !publicVars); + }} + > + Public + + + { + handleFilterChange('invalid', !invalidVars); + }} + > + Invalid + + + { + handleFilterChange('overridden', !overriddenVars); + }} + > + Overridden + + + + ); +} + +function Summary({ appState }: { appState: AppEnvState }) { + const varsArray = Object.values(appState.variables); + const overridden = varsArray.filter((variable) => variable.isOverridden); + + const allVariables = varsArray.reduce( + (acc, variable) => ({ + ...acc, + [variable.key]: variable.effectiveValue, + }), + {}, + ); + + const errors = varsArray.filter((variable) => { + const model = envVariables.find((v) => variable.key === v.name); + + const validation = + model && model.validate + ? model.validate({ + value: variable.effectiveValue, + variables: allVariables, + mode: appState.mode, + }) + : { + success: true, + }; + + return !validation.success; + }); + + return ( +
+
+ + {errors.length} Errors + + + + {overridden.length} Overridden Variables + +
+
+ ); +} diff --git a/apps/dev-tool/app/variables/lib/env-scanner.ts b/apps/dev-tool/app/variables/lib/env-scanner.ts new file mode 100644 index 000000000..59a387397 --- /dev/null +++ b/apps/dev-tool/app/variables/lib/env-scanner.ts @@ -0,0 +1,244 @@ +import 'server-only'; + +import { envVariables } from '@/app/variables/lib/env-variables-model'; +import fs from 'fs/promises'; +import path from 'path'; + +import { + AppEnvState, + EnvFileInfo, + EnvMode, + EnvVariableState, + ScanOptions, +} from './types'; + +// Define precedence order for each mode +const ENV_FILE_PRECEDENCE: Record = { + development: [ + '.env', + '.env.development', + '.env.local', + '.env.development.local', + ], + production: [ + '.env', + '.env.production', + '.env.local', + '.env.production.local', + ], +}; + +function getSourcePrecedence(source: string, mode: EnvMode): number { + return ENV_FILE_PRECEDENCE[mode].indexOf(source); +} + +export async function scanMonorepoEnv( + options: ScanOptions, +): Promise { + const { + rootDir = path.resolve(process.cwd(), '../..'), + apps = ['web'], + mode, + } = options; + + const envTypes = ENV_FILE_PRECEDENCE[mode]; + const appsDir = path.join(rootDir, 'apps'); + const results: EnvFileInfo[] = []; + + try { + const appDirs = await fs.readdir(appsDir); + + for (const appName of appDirs) { + if (apps.length > 0 && !apps.includes(appName)) { + continue; + } + + const appDir = path.join(appsDir, appName); + const stat = await fs.stat(appDir); + + if (!stat.isDirectory()) { + continue; + } + + const appInfo: EnvFileInfo = { + appName, + filePath: appDir, + variables: [], + }; + + for (const envType of envTypes) { + const envPath = path.join(appDir, envType); + + try { + const content = await fs.readFile(envPath, 'utf-8'); + const vars = parseEnvFile(content, envType); + + appInfo.variables.push(...vars); + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + console.warn(`Error reading ${envPath}:`, error); + } + } + } + + if (appInfo.variables.length > 0) { + results.push(appInfo); + } + } + } catch (error) { + console.error('Error scanning monorepo:', error); + throw error; + } + + return results; +} + +function parseEnvFile(content: string, source: string) { + const variables: Array<{ key: string; value: string; source: string }> = []; + + const lines = content.split('\n'); + + for (const line of lines) { + // Skip comments and empty lines + if (line.trim().startsWith('#') || !line.trim()) { + continue; + } + + // Match KEY=VALUE pattern, handling quotes + const match = line.match(/^([^=]+)=(.*)$/); + if (match) { + const [, key = '', rawValue] = match; + let value = rawValue ?? ''; + + // Remove quotes if present + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + + // Handle escaped quotes within the value + value = value + .replace(/\\"/g, '"') + .replace(/\\'/g, "'") + .replace(/\\\\/g, '\\'); + + variables.push({ + key: key.trim(), + value: value.trim(), + source, + }); + } + } + + return variables; +} + +export function processEnvDefinitions( + envInfo: EnvFileInfo, + mode: EnvMode, +): AppEnvState { + const variableMap: Record = {}; + + // First pass: Collect all definitions + for (const variable of envInfo.variables) { + if (!variable) { + continue; + } + + const model = envVariables.find((v) => variable.key === v.name); + + if (!variableMap[variable.key]) { + variableMap[variable.key] = { + key: variable.key, + definitions: [], + effectiveValue: variable.value, + effectiveSource: variable.source, + isOverridden: false, + category: model ? model.category : 'Custom', + }; + } + + const varState = variableMap[variable.key]; + + if (!varState) { + continue; + } + + varState.definitions.push({ + key: variable.key, + value: variable.value, + source: variable.source, + }); + } + + // Second pass: Determine effective values and override status + for (const key in variableMap) { + const varState = variableMap[key]; + + if (!varState) { + continue; + } + + // Sort definitions by mode-specific precedence + varState.definitions.sort( + (a, b) => + getSourcePrecedence(a.source, mode) - + getSourcePrecedence(b.source, mode), + ); + + if (varState.definitions.length > 1) { + const lastDef = varState.definitions[varState.definitions.length - 1]; + + if (!lastDef) { + continue; + } + + const highestPrecedence = getSourcePrecedence(lastDef.source, mode); + + varState.isOverridden = true; + varState.effectiveValue = lastDef.value; + varState.effectiveSource = lastDef.source; + + // Check for conflicts at highest precedence + const conflictingDefs = varState.definitions.filter( + (def) => getSourcePrecedence(def.source, mode) === highestPrecedence, + ); + + if (conflictingDefs.length > 1) { + varState.effectiveSource = `${varState.effectiveSource}`; + } + } + } + + return { + appName: envInfo.appName, + filePath: envInfo.filePath, + mode, + variables: variableMap, + }; +} + +export async function getEnvState( + options: ScanOptions, +): Promise { + const envInfos = await scanMonorepoEnv(options); + return envInfos.map((info) => processEnvDefinitions(info, options.mode)); +} + +// Utility function to get list of env files for current mode +export function getEnvFilesForMode(mode: EnvMode): string[] { + return ENV_FILE_PRECEDENCE[mode]; +} + +export async function getVariable(key: string, mode: EnvMode) { + // Get the processed environment state for all apps (you can limit to 'web' via options) + const envStates = await getEnvState({ mode, apps: ['web'] }); + + // Find the state for the "web" app. + const webState = envStates.find((state) => state.appName === 'web'); + + // Return the effectiveValue based on override status. + return webState?.variables[key]?.effectiveValue ?? ''; +} diff --git a/apps/dev-tool/app/variables/lib/env-variables-model.ts b/apps/dev-tool/app/variables/lib/env-variables-model.ts new file mode 100644 index 000000000..b5537f47b --- /dev/null +++ b/apps/dev-tool/app/variables/lib/env-variables-model.ts @@ -0,0 +1,770 @@ +import { EnvMode } from '@/app/variables/lib/types'; +import { z } from 'zod'; + +export type EnvVariableModel = { + name: string; + description: string; + secret?: boolean; + category: string; + test?: (value: string) => Promise; + validate?: (props: { + value: string; + variables: Record; + mode: EnvMode; + }) => z.SafeParseReturnType; +}; + +export const envVariables: EnvVariableModel[] = [ + { + name: 'NEXT_PUBLIC_SITE_URL', + description: + 'The URL of your site, used for generating absolute URLs. Must include the protocol.', + category: 'Site Configuration', + validate: ({ value, mode }) => { + if (mode === 'development') { + return z + .string() + .url({ + message: `The NEXT_PUBLIC_SITE_URL variable must be a valid URL`, + }) + .safeParse(value); + } + + return z + .string() + .url({ + message: `The NEXT_PUBLIC_SITE_URL variable must be a valid URL`, + }) + .startsWith( + 'https', + `The NEXT_PUBLIC_SITE_URL variable must start with https`, + ) + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_PRODUCT_NAME', + description: + "Your product's name, used consistently across the application interface.", + category: 'Site Configuration', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_PRODUCT_NAME variable must be at least 1 character`, + ) + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_SITE_TITLE', + description: + "The site's title tag content, crucial for SEO and browser display.", + category: 'Site Configuration', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_SITE_TITLE variable must be at least 1 character`, + ) + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_SITE_DESCRIPTION', + description: + "Your site's meta description, important for SEO optimization.", + category: 'Site Configuration', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_SITE_DESCRIPTION variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_DEFAULT_LOCALE', + description: 'Sets the default language for your application.', + category: 'Localization', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_DEFAULT_LOCALE variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_AUTH_PASSWORD', + description: 'Enables or disables password-based authentication.', + category: 'Authentication', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_AUTH_MAGIC_LINK', + description: 'Enables or disables magic link authentication.', + category: 'Authentication', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_CAPTCHA_SITE_KEY', + description: 'Your Cloudflare Captcha site key for form protection.', + category: 'Security', + validate: ({ value }) => { + return z.string().optional().safeParse(value); + }, + }, + { + name: 'CAPTCHA_SECRET_TOKEN', + description: + 'Your Cloudflare Captcha secret token for backend verification.', + category: 'Security', + secret: true, + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The CAPTCHA_SECRET_TOKEN variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_USER_NAVIGATION_STYLE', + description: + 'Controls user navigation layout. Options: sidebar, header, or custom.', + category: 'Navigation', + validate: ({ value }) => { + return z + .enum(['sidebar', 'header', 'custom']) + .optional() + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_HOME_SIDEBAR_COLLAPSED', + description: 'Sets the default state of the home sidebar.', + category: 'Navigation', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_TEAM_NAVIGATION_STYLE', + description: + 'Controls team navigation layout. Options: sidebar, header, or custom.', + category: 'Navigation', + validate: ({ value }) => { + return z + .enum(['sidebar', 'header', 'custom']) + .optional() + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_TEAM_SIDEBAR_COLLAPSED', + description: 'Sets the default state of the team sidebar.', + category: 'Navigation', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_SIDEBAR_COLLAPSIBLE_STYLE', + description: + 'Defines sidebar collapse behavior. Options: offscreen, icon, or none.', + category: 'Navigation', + validate: ({ value }) => { + return z.enum(['offscreen', 'icon', 'none']).optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_DEFAULT_THEME_MODE', + description: + 'Controls the default theme appearance. Options: light, dark, or system.', + category: 'Theme', + validate: ({ value }) => { + return z.enum(['light', 'dark', 'system']).optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_ENABLE_THEME_TOGGLE', + description: 'Controls visibility of the theme toggle feature.', + category: 'Theme', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER', + description: 'Controls visibility of the sidebar trigger feature.', + category: 'Navigation', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION', + description: 'Allows users to delete their personal accounts.', + category: 'Features', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING', + description: 'Enables billing features for personal accounts.', + category: 'Features', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS', + description: 'Master switch for team account functionality.', + category: 'Features', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION', + description: 'Controls ability to create new team accounts.', + category: 'Features', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION', + description: 'Allows team account deletion.', + category: 'Features', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING', + description: 'Enables billing features for team accounts.', + category: 'Features', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_ENABLE_NOTIFICATIONS', + description: 'Controls the notification system.', + category: 'Notifications', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_REALTIME_NOTIFICATIONS', + description: 'Enables real-time notifications using Supabase Realtime.', + category: 'Notifications', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_SUPABASE_URL', + description: 'Your Supabase project URL.', + category: 'Supabase', + validate: ({ value, mode }) => { + if (mode === 'development') { + return z + .string() + .url({ + message: `The NEXT_PUBLIC_SUPABASE_URL variable must be a valid URL`, + }) + .safeParse(value); + } + + return z + .string() + .url({ + message: `The NEXT_PUBLIC_SUPABASE_URL variable must be a valid URL`, + }) + .startsWith( + 'https', + `The NEXT_PUBLIC_SUPABASE_URL variable must start with https`, + ) + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_SUPABASE_ANON_KEY', + description: 'Your Supabase anonymous API key.', + category: 'Supabase', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_SUPABASE_ANON_KEY variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'SUPABASE_SERVICE_ROLE_KEY', + description: 'Your Supabase service role key (keep this secret!).', + category: 'Supabase', + secret: true, + validate: ({ value, variables }) => { + return z + .string() + .min( + 1, + `The SUPABASE_SERVICE_ROLE_KEY variable must be at least 1 character`, + ) + .optional() + .refine( + (value) => { + return value !== variables['NEXT_PUBLIC_SUPABASE_ANON_KEY']; + }, + { + message: `The SUPABASE_SERVICE_ROLE_KEY variable must be different from NEXT_PUBLIC_SUPABASE_ANON_KEY`, + }, + ) + .safeParse(value); + }, + }, + { + name: 'SUPABASE_DB_WEBHOOK_SECRET', + description: 'Secret key for Supabase webhook verification.', + category: 'Supabase', + secret: true, + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The SUPABASE_DB_WEBHOOK_SECRET variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_BILLING_PROVIDER', + description: + 'Your chosen billing provider. Options: stripe or lemon-squeezy.', + category: 'Billing', + validate: ({ value }) => { + return z.enum(['stripe', 'lemon-squeezy']).optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY', + description: 'Your Stripe publishable key.', + category: 'Billing', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'STRIPE_SECRET_KEY', + description: 'Your Stripe secret key.', + category: 'Billing', + secret: true, + validate: ({ value }) => { + return z + .string() + .min(1, `The STRIPE_SECRET_KEY variable must be at least 1 character`) + .optional() + .safeParse(value); + }, + }, + { + name: 'STRIPE_WEBHOOK_SECRET', + description: 'Your Stripe webhook secret.', + category: 'Billing', + secret: true, + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The STRIPE_WEBHOOK_SECRET variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'LEMON_SQUEEZY_SECRET_KEY', + description: 'Your Lemon Squeezy secret key.', + category: 'Billing', + secret: true, + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The LEMON_SQUEEZY_SECRET_KEY variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'LEMON_SQUEEZY_STORE_ID', + description: 'Your Lemon Squeezy store ID.', + category: 'Billing', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The LEMON_SQUEEZY_STORE_ID variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'LEMON_SQUEEZY_SIGNING_SECRET', + description: 'Your Lemon Squeezy signing secret.', + category: 'Billing', + secret: true, + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The LEMON_SQUEEZY_SIGNING_SECRET variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'MAILER_PROVIDER', + description: 'Your email service provider. Options: nodemailer or resend.', + category: 'Email', + validate: ({ value }) => { + return z.enum(['nodemailer', 'resend']).optional().safeParse(value); + }, + }, + { + name: 'EMAIL_SENDER', + description: 'Default sender email address.', + category: 'Email', + validate: ({ value }) => { + return z + .string() + .min(1, `The EMAIL_SENDER variable must be at least 1 character`) + .safeParse(value); + }, + }, + { + name: 'CONTACT_EMAIL', + description: 'Email address for contact form submissions.', + category: 'Email', + validate: ({ value }) => { + return z + .string() + .email() + .min(1, `The CONTACT_EMAIL variable must be at least 1 character`) + .safeParse(value); + }, + }, + { + name: 'RESEND_API_KEY', + description: 'Your Resend API key.', + category: 'Email', + secret: true, + validate: ({ value }) => { + return z + .string() + .min(1, `The RESEND_API_KEY variable must be at least 1 character`) + .safeParse(value); + }, + }, + { + name: 'EMAIL_HOST', + description: 'SMTP host for Nodemailer configuration.', + category: 'Email', + validate: ({ value }) => { + return z.string().safeParse(value); + }, + }, + { + name: 'EMAIL_PORT', + description: 'SMTP port for Nodemailer configuration.', + category: 'Email', + validate: ({ value }) => { + return z.coerce + .number() + .min(1, `The EMAIL_PORT variable must be at least 1 character`) + .max(65535, `The EMAIL_PORT variable must be at most 65535`) + .safeParse(value); + }, + }, + { + name: 'EMAIL_USER', + description: 'SMTP user for Nodemailer configuration.', + category: 'Email', + validate: ({ value }) => { + return z + .string() + .min(1, `The EMAIL_USER variable must be at least 1 character`) + .safeParse(value); + }, + }, + { + name: 'EMAIL_PASSWORD', + description: 'SMTP password for Nodemailer configuration.', + category: 'Email', + secret: true, + validate: ({ value }) => { + return z + .string() + .min(1, `The EMAIL_PASSWORD variable must be at least 1 character`) + .safeParse(value); + }, + }, + { + name: 'EMAIL_TLS', + description: 'Whether to use TLS for SMTP connection.', + category: 'Email', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'CMS_CLIENT', + description: 'Your chosen CMS system. Options: wordpress or keystatic.', + category: 'CMS', + validate: ({ value }) => { + return z.enum(['wordpress', 'keystatic']).optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND', + description: 'Your Keystatic storage kind. Options: local, cloud, github.', + category: 'CMS', + validate: ({ value }) => { + return z.enum(['local', 'cloud', 'github']).optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO', + description: 'Your Keystatic storage repo.', + category: 'CMS', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'KEYSTATIC_GITHUB_TOKEN', + description: 'Your Keystatic GitHub token.', + category: 'CMS', + secret: true, + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The KEYSTATIC_GITHUB_TOKEN variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'KEYSTATIC_PATH_PREFIX', + description: 'Your Keystatic path prefix.', + category: 'CMS', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The KEYSTATIC_PATH_PREFIX variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH', + description: 'Your Keystatic content path.', + category: 'CMS', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'WORDPRESS_API_URL', + description: 'WordPress API URL when using WordPress as CMS.', + category: 'CMS', + validate: ({ value }) => { + return z + .string() + .url({ + message: `The WORDPRESS_API_URL variable must be a valid URL`, + }) + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_LOCALES_PATH', + description: 'The path to your locales folder.', + category: 'Localization', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_LOCALES_PATH variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_LANGUAGE_PRIORITY', + description: 'The priority setting as to how infer the language.', + category: 'Localization', + validate: ({ value }) => { + return z.enum(['user', 'application']).optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_ENABLE_VERSION_UPDATER', + description: + 'Enables the version updater to poll the latest version and notify the user.', + category: 'Features', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: `ENABLE_REACT_COMPILER`, + description: 'Enables the React compiler [experimental]', + category: 'Features', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_MONITORING_PROVIDER', + description: 'The monitoring provider to use.', + category: 'Monitoring', + validate: ({ value }) => { + return z.enum(['baselime', 'sentry']).optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_BASELIME_KEY', + description: 'The Baselime key to use.', + category: 'Monitoring', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_BASELIME_KEY variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'STRIPE_ENABLE_TRIAL_WITHOUT_CC', + description: 'Enables trial plans without credit card.', + category: 'Billing', + validate: ({ value }) => { + return z.coerce.boolean().optional().safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS', + description: 'The interval in seconds to check for updates.', + category: 'Features', + validate: ({ value }) => { + return z.coerce + .number() + .min( + 1, + `The NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS variable must be at least 1 character`, + ) + .max( + 86400, + `The NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS variable must be at most 86400`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_THEME_COLOR', + description: 'The default theme color.', + category: 'Theme', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_THEME_COLOR variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, + { + name: 'NEXT_PUBLIC_THEME_COLOR_DARK', + description: 'The default theme color for dark mode.', + category: 'Theme', + validate: ({ value }) => { + return z + .string() + .min( + 1, + `The NEXT_PUBLIC_THEME_COLOR_DARK variable must be at least 1 character`, + ) + .optional() + .safeParse(value); + }, + }, +]; diff --git a/apps/dev-tool/app/variables/lib/types.ts b/apps/dev-tool/app/variables/lib/types.ts new file mode 100644 index 000000000..253912a76 --- /dev/null +++ b/apps/dev-tool/app/variables/lib/types.ts @@ -0,0 +1,40 @@ +export type EnvMode = 'development' | 'production'; + +export type ScanOptions = { + apps?: string[]; + rootDir?: string; + mode: EnvMode; +}; + +export type EnvDefinition = { + key: string; + value: string; + source: string; +}; + +export type EnvVariableState = { + key: string; + category: string; + definitions: EnvDefinition[]; + effectiveValue: string; + isOverridden: boolean; + effectiveSource: string; +}; + +export type AppEnvState = { + appName: string; + filePath: string; + mode: EnvMode; + variables: Record; +}; + +export type EnvFileInfo = { + appName: string; + filePath: string; + + variables: Array<{ + key: string; + value: string; + source: string; + }>; +}; diff --git a/apps/dev-tool/app/variables/page.tsx b/apps/dev-tool/app/variables/page.tsx new file mode 100644 index 000000000..abf7ea3ec --- /dev/null +++ b/apps/dev-tool/app/variables/page.tsx @@ -0,0 +1,55 @@ +import { use } from 'react'; + +import { + processEnvDefinitions, + scanMonorepoEnv, +} from '@/app/variables/lib/env-scanner'; +import { EnvMode } from '@/app/variables/lib/types'; + +import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs'; +import { Page, PageBody, PageHeader } from '@kit/ui/page'; + +import { AppEnvironmentVariablesManager } from './components/app-environment-variables-manager'; + +type VariablesPageProps = { + searchParams: Promise<{ mode?: EnvMode }>; +}; + +export const metadata = { + title: 'Environment Variables', +}; + +export default function VariablesPage({ searchParams }: VariablesPageProps) { + const { mode = 'development' } = use(searchParams); + const apps = use(scanMonorepoEnv({ mode })); + + return ( + + + } + /> + + +
+ {apps.map((app) => { + const appEnvState = processEnvDefinitions(app, mode); + + return ( + + ); + })} +
+
+
+ ); +} diff --git a/apps/dev-tool/components/app-layout.tsx b/apps/dev-tool/components/app-layout.tsx new file mode 100644 index 000000000..c327a3e52 --- /dev/null +++ b/apps/dev-tool/components/app-layout.tsx @@ -0,0 +1,13 @@ +import { DevToolSidebar } from '@/components/app-sidebar'; + +import { SidebarInset, SidebarProvider } from '@kit/ui/shadcn-sidebar'; + +export function DevToolLayout(props: React.PropsWithChildren) { + return ( + + + + {props.children} + + ); +} diff --git a/apps/dev-tool/components/app-sidebar.tsx b/apps/dev-tool/components/app-sidebar.tsx new file mode 100644 index 000000000..d862f4b33 --- /dev/null +++ b/apps/dev-tool/components/app-sidebar.tsx @@ -0,0 +1,79 @@ +'use client'; + +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +import { + BoltIcon, + LanguagesIcon, + LayoutDashboardIcon, + MailIcon, +} from 'lucide-react'; + +import { + Sidebar, + SidebarGroup, + SidebarGroupLabel, + SidebarHeader, + SidebarMenu, + SidebarMenuButton, + SidebarMenuItem, +} from '@kit/ui/shadcn-sidebar'; +import { isRouteActive } from '@kit/ui/utils'; + +const routes = [ + { + label: 'Dashboard', + path: '/', + Icon: LayoutDashboardIcon, + }, + { + label: 'Environment Variables', + path: '/variables', + Icon: BoltIcon, + }, + { + label: 'Emails', + path: '/emails', + Icon: MailIcon, + }, + { + label: 'Translations', + path: '/translations', + Icon: LanguagesIcon, + }, +]; + +export function DevToolSidebar({ + ...props +}: React.ComponentProps) { + const pathname = usePathname(); + + return ( + + + Makerkit Dev Tool + + + + Dev Tools + + + {routes.map((route) => ( + + + + + {route.label} + + + + ))} + + + + ); +} diff --git a/apps/dev-tool/components/env-mode-selector.tsx b/apps/dev-tool/components/env-mode-selector.tsx new file mode 100644 index 000000000..0c36e5135 --- /dev/null +++ b/apps/dev-tool/components/env-mode-selector.tsx @@ -0,0 +1,41 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { EnvMode } from '@/app/variables/lib/types'; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@kit/ui/select'; + +export function EnvModeSelector({ mode }: { mode: EnvMode }) { + const router = useRouter(); + + const handleModeChange = (value: EnvMode) => { + const searchParams = new URLSearchParams(window.location.search); + const path = window.location.pathname; + + searchParams.set('mode', value); + + router.push(`${path}?${searchParams.toString()}`); + }; + + return ( +
+ +
+ ); +} diff --git a/apps/dev-tool/components/iframe.tsx b/apps/dev-tool/components/iframe.tsx new file mode 100644 index 000000000..d6fcb544e --- /dev/null +++ b/apps/dev-tool/components/iframe.tsx @@ -0,0 +1,35 @@ +'use client'; + +import { useState } from 'react'; + +import { createPortal } from 'react-dom'; + +export const IFrame: React.FC< + React.IframeHTMLAttributes & { + setInnerRef?: (ref: HTMLIFrameElement | undefined) => void; + appendStyles?: boolean; + theme?: 'light' | 'dark'; + transparent?: boolean; + } +> = ({ children, setInnerRef, appendStyles = true, theme, ...props }) => { + const [ref, setRef] = useState(); + const doc = ref?.contentWindow?.document as Document; + const mountNode = doc?.body; + + return ( + + ); +}; diff --git a/apps/dev-tool/components/root-providers.tsx b/apps/dev-tool/components/root-providers.tsx new file mode 100644 index 000000000..26b498657 --- /dev/null +++ b/apps/dev-tool/components/root-providers.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { useState } from 'react'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +import { Toaster } from '@kit/ui/sonner'; + +export function RootProviders({ children }: React.PropsWithChildren) { + return {children}; +} + +function ReactQueryProvider(props: React.PropsWithChildren) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, + }, + }, + }), + ); + + return ( + + {props.children} + + + + ); +} diff --git a/apps/dev-tool/components/status-tile.tsx b/apps/dev-tool/components/status-tile.tsx new file mode 100644 index 000000000..2f8eb976a --- /dev/null +++ b/apps/dev-tool/components/status-tile.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { AlertCircle, CheckCircle2, XCircle } from 'lucide-react'; + +import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert'; +import { Card, CardContent } from '@kit/ui/card'; + +export const ServiceStatus = { + CHECKING: 'checking', + SUCCESS: 'success', + ERROR: 'error', +} as const; + +type ServiceStatusType = (typeof ServiceStatus)[keyof typeof ServiceStatus]; + +const StatusIcons = { + [ServiceStatus.CHECKING]: , + [ServiceStatus.SUCCESS]: , + [ServiceStatus.ERROR]: , +}; + +interface ServiceCardProps { + name: string; + status: { + status: ServiceStatusType; + message?: string; + }; +} + +export const ServiceCard = ({ name, status }: ServiceCardProps) => { + return ( + + +
+
+
+ {StatusIcons[status.status]} + +
+

{name}

+ +

+ {status.message ?? + (status.status === ServiceStatus.CHECKING + ? 'Checking connection...' + : status.status === ServiceStatus.SUCCESS + ? 'Connected successfully' + : 'Connection failed')} +

+
+
+
+
+
+
+ ); +}; diff --git a/apps/dev-tool/next.config.ts b/apps/dev-tool/next.config.ts new file mode 100644 index 000000000..d40b676a8 --- /dev/null +++ b/apps/dev-tool/next.config.ts @@ -0,0 +1,16 @@ +import type { NextConfig } from 'next'; + +const nextConfig: NextConfig = { + reactStrictMode: true, + transpilePackages: ['@kit/ui', '@kit/shared'], + experimental: { + reactCompiler: true, + }, + logging: { + fetches: { + fullUrl: true, + }, + }, +}; + +export default nextConfig; diff --git a/apps/dev-tool/package.json b/apps/dev-tool/package.json new file mode 100644 index 000000000..b21f37602 --- /dev/null +++ b/apps/dev-tool/package.json @@ -0,0 +1,44 @@ +{ + "name": "dev-tool", + "version": "0.1.0", + "private": true, + "scripts": { + "clean": "git clean -xdf .next .turbo node_modules", + "dev": "next dev --turbo | pino-pretty -c", + "format": "prettier --check --write \"**/*.{js,cjs,mjs,ts,tsx,md,json}\"" + }, + "dependencies": { + "@hookform/resolvers": "^4.1.0", + "@tanstack/react-query": "5.66.7", + "lucide-react": "^0.475.0", + "next": "15.1.7", + "nodemailer": "^6.10.0", + "react": "19.0.0", + "react-dom": "19.0.0" + }, + "devDependencies": { + "@kit/email-templates": "workspace:*", + "@kit/prettier-config": "workspace:*", + "@kit/shared": "workspace:*", + "@kit/tsconfig": "workspace:*", + "@kit/ui": "workspace:*", + "@tailwindcss/postcss": "^4.0.7", + "@types/node": "^22.13.4", + "@types/nodemailer": "6.4.17", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "babel-plugin-react-compiler": "beta", + "pino-pretty": "^13.0.0", + "react-hook-form": "^7.54.2", + "tailwindcss": "4.0.7", + "tailwindcss-animate": "^1.0.7", + "typescript": "^5.7.3", + "zod": "^3.24.2" + }, + "prettier": "@kit/prettier-config", + "browserslist": [ + "last 1 versions", + "> 0.7%", + "not dead" + ] +} diff --git a/apps/dev-tool/postcss.config.mjs b/apps/dev-tool/postcss.config.mjs new file mode 100644 index 000000000..a34a3d560 --- /dev/null +++ b/apps/dev-tool/postcss.config.mjs @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {}, + }, +}; diff --git a/apps/dev-tool/styles/globals.css b/apps/dev-tool/styles/globals.css new file mode 100644 index 000000000..cfc707666 --- /dev/null +++ b/apps/dev-tool/styles/globals.css @@ -0,0 +1,43 @@ +/* +* global.css +* +* Global styles for the entire application + */ + +/* Tailwind CSS */ +@import 'tailwindcss'; + +/* local styles - update the below if you add a new style */ +@import './theme.css'; +@import './theme.utilities.css'; +@import './shadcn-ui.css'; + +/* plugins - update the below if you add a new plugin */ +@plugin "tailwindcss-animate"; + +/* content sources - update the below if you add a new path */ +@source "../../../packages/ui/src/**/*.{ts,tsx}"; +@source "../{app,components}/**/*.{ts,tsx}"; + +/* variants - update the below if you add a new variant */ +@variant dark (&:where(.dark, .dark *)); + +@layer base { + body { + @apply bg-background text-foreground; + font-feature-settings: "rlig" 1, "calt" 1; + } + + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--border, currentColor); + } + + input::placeholder, + textarea::placeholder { + color: theme(--color-muted-foreground); + } +} \ No newline at end of file diff --git a/apps/dev-tool/styles/shadcn-ui.css b/apps/dev-tool/styles/shadcn-ui.css new file mode 100644 index 000000000..731f38860 --- /dev/null +++ b/apps/dev-tool/styles/shadcn-ui.css @@ -0,0 +1,104 @@ +/* +* shadcn-ui.css +* +* Update the below to customize your Shadcn UI CSS Colors. +* Refer to https://ui.shadcn.com/themes for applying new colors. +* NB: apply the hsl function to the colors copied from the theme. + */ + +@layer base { + :root { + --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; + --font-heading: var(--font-sans); + + --background: var(--color-white); + --foreground: var(--color-neutral-950); + + --card: var(--color-white); + --card-foreground: var(--color-neutral-950); + + --popover: var(--color-white); + --popover-foreground: var(--color-neutral-950); + + --primary: var(--color-neutral-950); + --primary-foreground: var(--color-white); + + --secondary: oklch(96.76% 0.0013 286.38); + --secondary-foreground: oklch(21.03% 0.0318 264.65); + + --muted: oklch(96.71% 0.0029 264.54); + --muted-foreground: oklch(55.13% 0.0233 264.36); + + --accent: oklch(96.76% 0.0013 286.38); + --accent-foreground: oklch(21.03% 0.0318 264.65); + + --destructive: var(--color-red-500); + --destructive-foreground: var(--color-white); + + --border: var(--color-gray-100); + --input: var(--color-gray-200); + --ring: var(--color-neutral-800); + + --radius: 0.5rem; + + --chart-1: var(--color-orange-400); + --chart-2: var(--color-teal-600); + --chart-3: var(--color-green-800); + --chart-4: var(--color-yellow-200); + --chart-5: var(--color-orange-200); + + --sidebar-background: var(--color-neutral-50); + --sidebar-foreground: oklch(37.05% 0.012 285.8); + --sidebar-primary: var(--color-neutral-950); + --sidebar-primary-foreground: var(--color-white); + --sidebar-accent: var(--color-neutral-100); + --sidebar-accent-foreground: var(--color-neutral-950); + --sidebar-border: var(--border); + --sidebar-ring: var(--color-blue-500); + } + + .dark { + --background: var(--color-neutral-900); + --foreground: var(--color-white); + + --card: var(--color-neutral-900); + --card-foreground: var(--color-white); + + --popover: var(--color-neutral-900); + --popover-foreground: var(--color-white); + + --primary: var(--color-white); + --primary-foreground: var(--color-neutral-900); + + --secondary: var(--color-neutral-800); + --secondary-foreground: oklch(98.43% 0.0017 247.84); + + --muted: var(--color-neutral-800); + --muted-foreground: oklch(71.19% 0.0129 286.07); + + --accent: var(--color-neutral-800); + --accent-foreground: oklch(98.48% 0 0); + + --destructive: var(--color-red-700); + --destructive-foreground: var(--color-white); + + --border: var(--color-neutral-800); + --input: var(--color-neutral-700); + --ring: oklch(87.09% 0.0055 286.29); + + --chart-1: var(--color-blue-600); + --chart-2: var(--color-emerald-400); + --chart-3: var(--color-orange-400); + --chart-4: var(--color-purple-500); + --chart-5: var(--color-pink-500); + + --sidebar-background: var(--color-neutral-900); + --sidebar-foreground: var(--color-white); + --sidebar-primary: var(--color-blue-500); + --sidebar-primary-foreground: var(--color-white); + --sidebar-accent: var(--color-neutral-800); + --sidebar-accent-foreground: var(--color-white); + --sidebar-border: var(--border); + --sidebar-ring: var(--color-blue-500); + } +} \ No newline at end of file diff --git a/apps/dev-tool/styles/theme.css b/apps/dev-tool/styles/theme.css new file mode 100644 index 000000000..2ac60178e --- /dev/null +++ b/apps/dev-tool/styles/theme.css @@ -0,0 +1,116 @@ +/* +* theme.css +* +* Shadcn UI theme +* Use this file to add any custom styles or override existing Shadcn UI styles + */ + +/* container utility */ + +/* Shadcn UI theme */ +@theme { + --color-background: var(--background); + --color-foreground: var(--foreground); + + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + + --radius-radius: var(--radius); + + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + + --font-sans: -apple-system, var(--font-sans); + --font-heading: var(--font-heading); + + --color-sidebar: var(--sidebar-background); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --animate-fade-up: fade-up 0.5s; + --animate-fade-down: fade-down 0.5s; + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0; + } + + to { + height: var(--radix-accordion-content-height); + } + } + + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + + to { + height: 0; + } + } + + @keyframes fade-up { + 0% { + opacity: 0; + transform: translateY(10px); + } + 80% { + opacity: 0.6; + } + 100% { + opacity: 1; + transform: translateY(0px); + } + } + + @keyframes fade-down { + 0% { + opacity: 0; + transform: translateY(-10px); + } + 80% { + opacity: 0.6; + } + 100% { + opacity: 1; + transform: translateY(0px); + } + } +} \ No newline at end of file diff --git a/apps/dev-tool/styles/theme.utilities.css b/apps/dev-tool/styles/theme.utilities.css new file mode 100644 index 000000000..344b62741 --- /dev/null +++ b/apps/dev-tool/styles/theme.utilities.css @@ -0,0 +1,5 @@ +@utility container { + margin-inline: auto; + + @apply xl:max-w-[80rem] px-8; +} diff --git a/apps/dev-tool/tsconfig.json b/apps/dev-tool/tsconfig.json new file mode 100644 index 000000000..0acd2903a --- /dev/null +++ b/apps/dev-tool/tsconfig.json @@ -0,0 +1,28 @@ +{ + "extends": "@kit/tsconfig/base.json", + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/apps/web/package.json b/apps/web/package.json index 6e1264207..9cf4a848f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -79,7 +79,6 @@ "@kit/tsconfig": "workspace:*", "@next/bundle-analyzer": "15.1.7", "@tailwindcss/postcss": "^4.0.7", - "@types/mdx": "^2.0.13", "@types/node": "^22.13.4", "@types/react": "19.0.10", "@types/react-dom": "19.0.4", diff --git a/package.json b/package.json index f47984d0a..f0b1225cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "next-supabase-saas-kit-turbo", - "version": "2.2.0", + "version": "2.3.0", "private": true, "sideEffects": false, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a9a22562..bcd1c4230 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,6 +30,82 @@ importers: specifier: ^5.7.3 version: 5.7.3 + apps/dev-tool: + dependencies: + '@hookform/resolvers': + specifier: ^4.1.0 + version: 4.1.0(react-hook-form@7.54.2(react@19.0.0)) + '@tanstack/react-query': + specifier: 5.66.7 + version: 5.66.7(react@19.0.0) + lucide-react: + specifier: ^0.475.0 + version: 0.475.0(react@19.0.0) + next: + specifier: 15.1.7 + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + nodemailer: + specifier: ^6.10.0 + version: 6.10.0 + react: + specifier: 19.0.0 + version: 19.0.0 + react-dom: + specifier: 19.0.0 + version: 19.0.0(react@19.0.0) + devDependencies: + '@kit/email-templates': + specifier: workspace:* + version: link:../../packages/email-templates + '@kit/prettier-config': + specifier: workspace:* + version: link:../../tooling/prettier + '@kit/shared': + specifier: workspace:* + version: link:../../packages/shared + '@kit/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + '@kit/ui': + specifier: workspace:* + version: link:../../packages/ui + '@tailwindcss/postcss': + specifier: ^4.0.7 + version: 4.0.7 + '@types/node': + specifier: ^22.13.4 + version: 22.13.4 + '@types/nodemailer': + specifier: 6.4.17 + version: 6.4.17 + '@types/react': + specifier: 19.0.10 + version: 19.0.10 + '@types/react-dom': + specifier: 19.0.4 + version: 19.0.4(@types/react@19.0.10) + babel-plugin-react-compiler: + specifier: beta + version: 19.0.0-beta-21e868a-20250216 + pino-pretty: + specifier: ^13.0.0 + version: 13.0.0 + react-hook-form: + specifier: ^7.54.2 + version: 7.54.2(react@19.0.0) + tailwindcss: + specifier: 4.0.7 + version: 4.0.7 + tailwindcss-animate: + specifier: ^1.0.7 + version: 1.0.7(tailwindcss@4.0.7) + typescript: + specifier: ^5.7.3 + version: 5.7.3 + zod: + specifier: ^3.24.2 + version: 3.24.2 + apps/e2e: devDependencies: '@playwright/test': @@ -46,7 +122,7 @@ importers: dependencies: '@edge-csrf/nextjs': specifier: 2.5.3-cloudflare-rc1 - version: 2.5.3-cloudflare-rc1(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) + version: 2.5.3-cloudflare-rc1(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) '@hookform/resolvers': specifier: ^4.1.0 version: 4.1.0(react-hook-form@7.54.2(react@19.0.0)) @@ -109,7 +185,7 @@ importers: version: 0.0.8(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1) '@makerkit/data-loader-supabase-nextjs': specifier: ^1.2.3 - version: 1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) '@marsidev/react-turnstile': specifier: ^1.1.0 version: 1.1.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -133,10 +209,10 @@ importers: version: 0.475.0(react@19.0.0) next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-sitemap: specifier: ^4.2.3 - version: 4.2.3(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) + version: 4.2.3(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) next-themes: specifier: 0.4.4 version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -180,9 +256,6 @@ importers: '@tailwindcss/postcss': specifier: ^4.0.7 version: 4.0.7 - '@types/mdx': - specifier: ^2.0.13 - version: 2.0.13 '@types/node': specifier: ^22.13.4 version: 22.13.4 @@ -197,7 +270,7 @@ importers: version: 10.4.20(postcss@8.5.2) babel-plugin-react-compiler: specifier: beta - version: 19.0.0-beta-714736e-20250131 + version: 19.0.0-beta-21e868a-20250216 dotenv-cli: specifier: ^8.0.0 version: 8.0.0 @@ -302,7 +375,7 @@ importers: version: 0.475.0(react@19.0.0) next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -348,7 +421,7 @@ importers: version: 19.0.10 next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -397,7 +470,7 @@ importers: version: 4.1.0 next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -628,7 +701,7 @@ importers: version: 0.475.0(react@19.0.0) next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-themes: specifier: 0.4.4 version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -682,7 +755,7 @@ importers: version: 0.0.8(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1) '@makerkit/data-loader-supabase-nextjs': specifier: ^1.2.3 - version: 1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) + version: 1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) '@supabase/supabase-js': specifier: 2.48.1 version: 2.48.1 @@ -700,7 +773,7 @@ importers: version: 0.475.0(react@19.0.0) next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -757,7 +830,7 @@ importers: version: 0.475.0(react@19.0.0) next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react-hook-form: specifier: ^7.54.2 version: 7.54.2(react@19.0.0) @@ -881,7 +954,7 @@ importers: version: 0.475.0(react@19.0.0) next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -930,7 +1003,7 @@ importers: version: 5.66.7(react@19.0.0) next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -1167,7 +1240,7 @@ importers: version: 2.48.1 next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) zod: specifier: ^3.24.2 version: 3.24.2 @@ -1216,7 +1289,7 @@ importers: version: 19.0.10 next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: specifier: 19.0.0 version: 19.0.0 @@ -1349,7 +1422,7 @@ importers: version: 9.20.1(jiti@2.4.2) next: specifier: 15.1.7 - version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + version: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-themes: specifier: 0.4.4 version: 0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -2628,7 +2701,7 @@ packages: '@radix-ui/react-context@1.1.1': resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} peerDependencies: - '@types/react': '*' + '@types/react': npm:types-react@19.0.0-rc.1 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -2685,7 +2758,7 @@ packages: '@radix-ui/react-focus-guards@1.1.1': resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} peerDependencies: - '@types/react': '*' + '@types/react': npm:types-react@19.0.0-rc.1 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -3013,7 +3086,7 @@ packages: '@radix-ui/react-use-layout-effect@1.1.0': resolution: {integrity: sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==} peerDependencies: - '@types/react': '*' + '@types/react': npm:types-react@19.0.0-rc.1 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -3022,7 +3095,7 @@ packages: '@radix-ui/react-use-previous@1.1.0': resolution: {integrity: sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==} peerDependencies: - '@types/react': '*' + '@types/react': npm:types-react@19.0.0-rc.1 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -3040,7 +3113,7 @@ packages: '@radix-ui/react-use-size@1.1.0': resolution: {integrity: sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==} peerDependencies: - '@types/react': '*' + '@types/react': npm:types-react@19.0.0-rc.1 react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc peerDependenciesMeta: '@types/react': @@ -4141,9 +4214,6 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/mdx@2.0.13': - resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - '@types/minimatch@5.1.2': resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} @@ -4545,8 +4615,8 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} - babel-plugin-react-compiler@19.0.0-beta-714736e-20250131: - resolution: {integrity: sha512-frj2l6fRWVi26iw9WthFKyFyE4u5ZSHH3KdKiscOOwpz210seTtwnp0QbJmi8Zoa5HK7Fk2fH40JffN2y8GvLg==} + babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216: + resolution: {integrity: sha512-WDOBsm9t9P0RADm8CSlav5OqWvs+3mZFvrBo/qf3vuNtdz78OG5TFxOy7De8ePR3rA6qg1Qmcjjae6nR1pOpCA==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -4628,9 +4698,6 @@ packages: caniuse-lite@1.0.30001677: resolution: {integrity: sha512-fmfjsOlJUpMWu+mAAtZZZHz7UEwsUxIIvu1TJfO1HqFQvB/B+ii0xr9B5HpbZY/mC4XZ8SvjHJqtAY6pDPQEog==} - caniuse-lite@1.0.30001699: - resolution: {integrity: sha512-b+uH5BakXZ9Do9iK+CkDmctUSEqZl+SP056vc5usa0PL+ev5OHw003rZXcnjNDv3L8P5j6rwT6C0BPKSikW08w==} - caniuse-lite@1.0.30001700: resolution: {integrity: sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==} @@ -8118,7 +8185,7 @@ snapshots: '@babel/generator@7.26.5': dependencies: '@babel/parser': 7.26.7 - '@babel/types': 7.26.7 + '@babel/types': 7.26.8 '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 jsesc: 3.1.0 @@ -8168,11 +8235,11 @@ snapshots: '@babel/parser@7.26.2': dependencies: - '@babel/types': 7.26.7 + '@babel/types': 7.26.8 '@babel/parser@7.26.7': dependencies: - '@babel/types': 7.26.7 + '@babel/types': 7.26.8 '@babel/parser@7.26.8': dependencies: @@ -8199,7 +8266,7 @@ snapshots: dependencies: '@babel/code-frame': 7.26.2 '@babel/parser': 7.26.2 - '@babel/types': 7.26.7 + '@babel/types': 7.26.8 '@babel/template@7.26.8': dependencies: @@ -8213,7 +8280,7 @@ snapshots: '@babel/generator': 7.26.5 '@babel/parser': 7.26.7 '@babel/template': 7.25.9 - '@babel/types': 7.26.7 + '@babel/types': 7.26.8 debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: @@ -8277,9 +8344,9 @@ snapshots: '@discoveryjs/json-ext@0.5.7': {} - '@edge-csrf/nextjs@2.5.3-cloudflare-rc1(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))': + '@edge-csrf/nextjs@2.5.3-cloudflare-rc1(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))': dependencies: - next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@emnapi/runtime@1.3.1': dependencies: @@ -8702,7 +8769,7 @@ snapshots: react: 19.0.0 react-dom: 19.0.0(react@19.0.0) optionalDependencies: - next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) transitivePeerDependencies: - supports-color @@ -8790,7 +8857,7 @@ snapshots: '@keystatic/core': 0.5.45(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(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) '@types/react': 19.0.10 chokidar: 3.6.0 - next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(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) server-only: 0.0.1 @@ -8803,12 +8870,12 @@ snapshots: '@supabase/supabase-js': 2.48.1 ts-case-convert: 2.1.0 - '@makerkit/data-loader-supabase-nextjs@1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': + '@makerkit/data-loader-supabase-nextjs@1.2.3(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1)(@tanstack/react-query@5.66.7(react@19.0.0))(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0)': dependencies: '@makerkit/data-loader-supabase-core': 0.0.8(@supabase/postgrest-js@1.18.1)(@supabase/supabase-js@2.48.1) '@supabase/supabase-js': 2.48.1 '@tanstack/react-query': 5.66.7(react@19.0.0) - next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) react: 19.0.0 transitivePeerDependencies: - '@supabase/postgrest-js' @@ -11616,8 +11683,6 @@ snapshots: '@types/mdurl@2.0.0': optional: true - '@types/mdx@2.0.13': {} - '@types/minimatch@5.1.2': {} '@types/ms@2.1.0': {} @@ -12146,9 +12211,9 @@ snapshots: cosmiconfig: 7.1.0 resolve: 1.22.10 - babel-plugin-react-compiler@19.0.0-beta-714736e-20250131: + babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216: dependencies: - '@babel/types': 7.26.7 + '@babel/types': 7.26.8 balanced-match@1.0.2: {} @@ -12246,8 +12311,6 @@ snapshots: caniuse-lite@1.0.30001677: {} - caniuse-lite@1.0.30001699: {} - caniuse-lite@1.0.30001700: {} ccount@2.0.1: {} @@ -12899,8 +12962,8 @@ snapshots: '@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) eslint: 9.20.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-react: 7.37.4(eslint@9.20.1(jiti@2.4.2)) eslint-plugin-react-hooks: 5.1.0(eslint@9.20.1(jiti@2.4.2)) @@ -12925,7 +12988,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)): + eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.0 @@ -12937,22 +13000,22 @@ snapshots: is-glob: 4.0.3 stable-hash: 0.0.4 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.20.1(jiti@2.4.2)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) eslint: 9.20.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) + eslint-import-resolver-typescript: 3.7.0(eslint-plugin-import@2.31.0)(eslint@9.20.1(jiti@2.4.2)) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -12963,7 +13026,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.20.1(jiti@2.4.2) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)))(eslint@9.20.1(jiti@2.4.2)) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.7.0)(eslint@9.20.1(jiti@2.4.2)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -12975,7 +13038,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.24.0(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) + '@typescript-eslint/parser': 8.24.1(eslint@9.20.1(jiti@2.4.2))(typescript@5.7.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -14513,13 +14576,13 @@ snapshots: netmask@2.0.2: {} - next-sitemap@4.2.3(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)): + next-sitemap@4.2.3(next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)): dependencies: '@corex/deepmerge': 4.0.43 '@next/env': 13.5.7 fast-glob: 3.3.2 minimist: 1.2.8 - next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next: 15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-themes@0.4.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: @@ -14532,7 +14595,7 @@ snapshots: '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001699 + caniuse-lite: 1.0.30001700 postcss: 8.4.31 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -14553,13 +14616,13 @@ snapshots: - '@babel/core' - babel-plugin-macros - next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-714736e-20250131)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + next@15.1.7(@opentelemetry/api@1.9.0)(@playwright/test@1.50.1)(babel-plugin-react-compiler@19.0.0-beta-21e868a-20250216)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.1.7 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 - caniuse-lite: 1.0.30001699 + caniuse-lite: 1.0.30001700 postcss: 8.4.31 react: 19.0.0 react-dom: 19.0.0(react@19.0.0) @@ -14575,7 +14638,7 @@ snapshots: '@next/swc-win32-x64-msvc': 15.1.7 '@opentelemetry/api': 1.9.0 '@playwright/test': 1.50.1 - babel-plugin-react-compiler: 19.0.0-beta-714736e-20250131 + babel-plugin-react-compiler: 19.0.0-beta-21e868a-20250216 sharp: 0.33.5 transitivePeerDependencies: - '@babel/core'