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.
+
+
+
+
+
+
+ );
+}
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.{' '}
+
+
+
+ Test Email
+
+
+
+
+ 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"
+ />
+
+
+
+
+ Select Languages
+
+
+
+
+
+ {locales.map((locale) => (
+ toggleLocale(locale)}
+ disabled={
+ selectedLocales.size === 1 && selectedLocales.has(locale)
+ }
+ >
+ {locale}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {defaultI18nNamespaces.map((namespace: string) => (
+
+ {namespace}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ 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)}
+
+
+
+ toggleShowValue(varState.key)}
+ >
+ {isValueVisible ? : }
+
+
+
+
copyToClipboard(varState.effectiveValue)}
+ size={'icon'}
+ >
+
+
+
+
+
+ {canExpand && (
+
toggleExpanded(varState.key)}
+ >
+ {isExpanded ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+
+ {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)}
+ />
+
+
+
+
+ {
+ const report = createReportFromEnvState(appState);
+ const promise = copyToClipboard(report);
+
+ toast.promise(promise, {
+ loading: 'Copying report...',
+ success:
+ 'Report copied to clipboard. Please paste it in your ticket.',
+ error: 'Failed to copy report to clipboard',
+ });
+ }}
+ >
+ Copy to Clipboard
+
+
+
+
+ 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 (
+
+
+
+ {buttonLabel()}
+
+
+
+
+
+
+ {
+ 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 (
+
+
+
+
+
+
+
+ Development
+ Production
+
+
+
+ );
+}
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'