committed by
GitHub
parent
59dfc0ad91
commit
c185bcfa11
41
apps/dev-tool/.gitignore
vendored
Normal file
41
apps/dev-tool/.gitignore
vendored
Normal file
@@ -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
|
||||
27
apps/dev-tool/README.md
Normal file
27
apps/dev-tool/README.md
Normal file
@@ -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.
|
||||
199
apps/dev-tool/app/emails/[id]/components/email-tester-form.tsx
Normal file
199
apps/dev-tool/app/emails/[id]/components/email-tester-form.tsx
Normal file
@@ -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 (
|
||||
<div className={'flex flex-col space-y-8'}>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
The settings below were filled from your environment variables. You can
|
||||
change them to test different scenarios.{' '}
|
||||
<Link className={'underline'} href={'https://www.nodemailer.com'}>
|
||||
Learn more about Nodemailer if you're not sure how to configure it.
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
className="flex flex-col space-y-6"
|
||||
onSubmit={form.handleSubmit(async (data) => {
|
||||
const promise = sendEmailAction({
|
||||
template: props.template,
|
||||
settings: {
|
||||
username: data.username,
|
||||
password: data.password,
|
||||
sender: data.sender,
|
||||
host: data.host,
|
||||
port: data.port,
|
||||
tls: data.tls,
|
||||
to: data.to,
|
||||
},
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: 'Sending email...',
|
||||
success: 'Email sent successfully',
|
||||
error: 'Failed to send email',
|
||||
});
|
||||
})}
|
||||
>
|
||||
<div className={'flex items-center space-x-2'}>
|
||||
<FormField
|
||||
name={'sender'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'w-full'}>
|
||||
<FormLabel>Sender</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={'Sender'} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name={'to'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'w-full'}>
|
||||
<FormLabel>Recipient</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} type={'email'} placeholder={'to'} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center space-x-2'}>
|
||||
<FormField
|
||||
name={'username'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'w-full'}>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={'Username'} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name={'password'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'w-full'}>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className={'w-full'}
|
||||
placeholder={'Password'}
|
||||
type={'password'}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={'flex items-center space-x-2'}>
|
||||
<FormField
|
||||
name={'host'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'w-full'}>
|
||||
<FormLabel>Host</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={'Host'} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name={'port'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'w-full'}>
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={'Port'} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name={'tls'}
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<FormItem className={'w-full'}>
|
||||
<FormLabel>Secure (TLS)</FormLabel>
|
||||
<FormControl>
|
||||
<Switch
|
||||
{...field}
|
||||
onCheckedChange={(value) => {
|
||||
form.setValue('tls', value);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button type={'submit'}>Send Test Email</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
104
apps/dev-tool/app/emails/[id]/page.tsx
Normal file
104
apps/dev-tool/app/emails/[id]/page.tsx
Normal file
@@ -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 (
|
||||
<Page style={'custom'}>
|
||||
<PageHeader
|
||||
displaySidebarTrigger={false}
|
||||
description={
|
||||
<AppBreadcrumbs
|
||||
values={{
|
||||
emails: 'Emails',
|
||||
'invite-email': 'Invite Email',
|
||||
'account-delete-email': 'Account Delete Email',
|
||||
'confirm-email': 'Confirm Email',
|
||||
'change-email-address-email': 'Change Email Address Email',
|
||||
'reset-password-email': 'Reset Password Email',
|
||||
'magic-link-email': 'Magic Link Email',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EnvModeSelector mode={mode} />
|
||||
</PageHeader>
|
||||
|
||||
<PageBody className={'flex flex-1 flex-col gap-y-4'}>
|
||||
<p className={'text-muted-foreground py-1 text-xs'}>
|
||||
Remember that the below is an approximation of the email. Always test
|
||||
it in your inbox.{' '}
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant={'link'} className="p-0 underline">
|
||||
Test Email
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent>
|
||||
<DialogTitle>Send Test Email</DialogTitle>
|
||||
|
||||
<EmailTesterForm settings={emailSettings} template={id} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</p>
|
||||
|
||||
<IFrame className={'flex flex-1 flex-col'}>
|
||||
<div
|
||||
className={'flex flex-1 flex-col'}
|
||||
dangerouslySetInnerHTML={{ __html: template.html }}
|
||||
/>
|
||||
</IFrame>
|
||||
</PageBody>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
57
apps/dev-tool/app/emails/lib/email-loader.tsx
Normal file
57
apps/dev-tool/app/emails/lib/email-loader.tsx
Normal file
@@ -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'),
|
||||
};
|
||||
}
|
||||
11
apps/dev-tool/app/emails/lib/email-tester-form-schema.ts
Normal file
11
apps/dev-tool/app/emails/lib/email-tester-form-schema.ts
Normal file
@@ -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(),
|
||||
});
|
||||
38
apps/dev-tool/app/emails/lib/server-actions.ts
Normal file
38
apps/dev-tool/app/emails/lib/server-actions.ts
Normal file
@@ -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',
|
||||
});
|
||||
}
|
||||
83
apps/dev-tool/app/emails/page.tsx
Normal file
83
apps/dev-tool/app/emails/page.tsx
Normal file
@@ -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 (
|
||||
<Page style={'custom'}>
|
||||
<PageHeader displaySidebarTrigger={false} description="Emails" />
|
||||
|
||||
<PageBody className={'gap-y-8'}>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<Heading level={5}>Supabase Auth Emails</Heading>
|
||||
|
||||
<div className={'grid grid-cols-1 gap-4 md:grid-cols-4'}>
|
||||
<CardButton asChild>
|
||||
<Link href={'/emails/confirm-email'}>
|
||||
<CardButtonHeader>
|
||||
<CardButtonTitle>Confirm Email</CardButtonTitle>
|
||||
</CardButtonHeader>
|
||||
</Link>
|
||||
</CardButton>
|
||||
|
||||
<CardButton asChild>
|
||||
<Link href={'/emails/change-email-address-email'}>
|
||||
<CardButtonHeader>
|
||||
<CardButtonTitle>Change Email Address Email</CardButtonTitle>
|
||||
</CardButtonHeader>
|
||||
</Link>
|
||||
</CardButton>
|
||||
|
||||
<CardButton asChild>
|
||||
<Link href={'/emails/reset-password-email'}>
|
||||
<CardButtonHeader>
|
||||
<CardButtonTitle>Reset Password Email</CardButtonTitle>
|
||||
</CardButtonHeader>
|
||||
</Link>
|
||||
</CardButton>
|
||||
|
||||
<CardButton asChild>
|
||||
<Link href={'/emails/magic-link-email'}>
|
||||
<CardButtonHeader>
|
||||
<CardButtonTitle>Magic Link Email</CardButtonTitle>
|
||||
</CardButtonHeader>
|
||||
</Link>
|
||||
</CardButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<Heading level={5}>Transactional Emails</Heading>
|
||||
|
||||
<div className={'grid grid-cols-1 gap-4 md:grid-cols-4'}>
|
||||
<CardButton asChild>
|
||||
<Link href={'/emails/account-delete-email'}>
|
||||
<CardButtonHeader>
|
||||
<CardButtonTitle>Account Delete Email</CardButtonTitle>
|
||||
</CardButtonHeader>
|
||||
</Link>
|
||||
</CardButton>
|
||||
|
||||
<CardButton asChild>
|
||||
<Link href={'/emails/invite-email'}>
|
||||
<CardButtonHeader>
|
||||
<CardButtonTitle>Invite Email</CardButtonTitle>
|
||||
</CardButtonHeader>
|
||||
</Link>
|
||||
</CardButton>
|
||||
</div>
|
||||
</div>
|
||||
</PageBody>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
27
apps/dev-tool/app/layout.tsx
Normal file
27
apps/dev-tool/app/layout.tsx
Normal file
@@ -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 (
|
||||
<html lang="en">
|
||||
<body>
|
||||
<RootProviders>
|
||||
<DevToolLayout>{children}</DevToolLayout>
|
||||
</RootProviders>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
213
apps/dev-tool/app/lib/connectivity-service.ts
Normal file
213
apps/dev-tool/app/lib/connectivity-service.ts
Normal file
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
48
apps/dev-tool/app/page.tsx
Normal file
48
apps/dev-tool/app/page.tsx
Normal file
@@ -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 (
|
||||
<Page style={'custom'}>
|
||||
<PageHeader
|
||||
displaySidebarTrigger={false}
|
||||
description={'Check the status of your Supabase and Stripe services'}
|
||||
>
|
||||
<EnvModeSelector mode={mode} />
|
||||
</PageHeader>
|
||||
|
||||
<PageBody className={'py-2'}>
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-4">
|
||||
<ServiceCard name={'Supabase API'} status={supabaseStatus} />
|
||||
<ServiceCard name={'Supabase Admin'} status={supabaseAdminStatus} />
|
||||
<ServiceCard name={'Stripe API'} status={stripeStatus} />
|
||||
<ServiceCard name={'Stripe Webhook'} status={stripeWebhookStatus} />
|
||||
</div>
|
||||
</PageBody>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -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<string, string> = {},
|
||||
) {
|
||||
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<string, Record<string, string>>;
|
||||
|
||||
export function TranslationsComparison({
|
||||
translations,
|
||||
}: {
|
||||
translations: Translations;
|
||||
}) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedLocales, setSelectedLocales] = useState<Set<string>>();
|
||||
|
||||
const [selectedNamespace, setSelectedNamespace] = useState(
|
||||
defaultI18nNamespaces[0] as string,
|
||||
);
|
||||
|
||||
const locales = Object.keys(translations);
|
||||
|
||||
if (locales.length === 0) {
|
||||
return <div>No translations found</div>;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search translations..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
Select Languages
|
||||
<ChevronDownIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent align="end" className="w-[200px]">
|
||||
{locales.map((locale) => (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={locale}
|
||||
checked={selectedLocales.has(locale)}
|
||||
onCheckedChange={() => toggleLocale(locale)}
|
||||
disabled={
|
||||
selectedLocales.size === 1 && selectedLocales.has(locale)
|
||||
}
|
||||
>
|
||||
{locale}
|
||||
</DropdownMenuCheckboxItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Select
|
||||
value={selectedNamespace}
|
||||
onValueChange={setSelectedNamespace}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select namespace" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
{defaultI18nNamespaces.map((namespace: string) => (
|
||||
<SelectItem key={namespace} value={namespace}>
|
||||
{namespace}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Key</TableHead>
|
||||
{visibleLocales.map((locale) => (
|
||||
<TableHead key={locale}>{locale}</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{filteredKeys.map((key) => (
|
||||
<TableRow key={key}>
|
||||
<TableCell className="font-mono text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{key}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
{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 (
|
||||
<TableCell
|
||||
key={locale}
|
||||
className={cn({
|
||||
'bg-destructive/10': isMissing,
|
||||
'bg-warning/10': !isMissing && isDifferent,
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>
|
||||
{value || (
|
||||
<span className="text-destructive">Missing</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
apps/dev-tool/app/translations/lib/translations-loader.ts
Normal file
46
apps/dev-tool/app/translations/lib/translations-loader.ts
Normal file
@@ -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;
|
||||
}
|
||||
35
apps/dev-tool/app/translations/page.tsx
Normal file
35
apps/dev-tool/app/translations/page.tsx
Normal file
@@ -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 (
|
||||
<Page style={'custom'}>
|
||||
<PageHeader
|
||||
displaySidebarTrigger={false}
|
||||
description={
|
||||
<AppBreadcrumbs
|
||||
values={{
|
||||
translations: 'Translations',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageBody className={'py-4'}>
|
||||
<TranslationsComparison translations={translations} />
|
||||
</PageBody>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex flex-1 flex-col gap-y-4">
|
||||
<Heading level={5}>Application: {state.appName}</Heading>
|
||||
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<EnvList appState={state} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
const [expandedVars, setExpandedVars] = useState<Record<string, boolean>>({});
|
||||
const [showValues, setShowValues] = useState<Record<string, boolean>>({});
|
||||
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 (
|
||||
<div key={varState.key} className="animate-in fade-in rounded-lg border">
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 flex-col gap-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm font-semibold">
|
||||
{varState.key}
|
||||
</span>
|
||||
|
||||
{varState.isOverridden && (
|
||||
<Badge variant="warning">Overridden</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<If condition={model}>
|
||||
{(model) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs font-normal">
|
||||
{model.description}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</If>
|
||||
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<div className="bg-muted text-muted-foreground flex-1 rounded px-2 py-2 font-mono text-xs">
|
||||
{renderValue(varState.effectiveValue, isValueVisible)}
|
||||
</div>
|
||||
|
||||
<If condition={!isClientBundledValue}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={'icon'}
|
||||
onClick={() => toggleShowValue(varState.key)}
|
||||
>
|
||||
{isValueVisible ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||
</Button>
|
||||
</If>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => copyToClipboard(varState.effectiveValue)}
|
||||
size={'icon'}
|
||||
>
|
||||
<Copy size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{canExpand && (
|
||||
<Button
|
||||
size={'icon'}
|
||||
variant="ghost"
|
||||
className="ml-4 rounded p-1 hover:bg-gray-100"
|
||||
onClick={() => toggleExpanded(varState.key)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex gap-x-2">
|
||||
<Badge
|
||||
variant="outline"
|
||||
className={cn({
|
||||
'text-orange-500': !isClientBundledValue,
|
||||
'text-green-500': isClientBundledValue,
|
||||
})}
|
||||
>
|
||||
{isClientBundledValue ? `Public variable` : `Private variable`}
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="ml-2 h-3 w-3" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
{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`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Badge>
|
||||
|
||||
<If condition={model?.secret}>
|
||||
<Badge variant="outline" className={'text-destructive'}>
|
||||
Secret Variable
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="ml-2 h-3 w-3" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
This is a secret key. Keep it safe!
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Badge>
|
||||
</If>
|
||||
|
||||
<Badge
|
||||
variant={'outline'}
|
||||
className={cn({
|
||||
'text-destructive':
|
||||
varState.effectiveSource === '.env.production',
|
||||
})}
|
||||
>
|
||||
{varState.effectiveSource}
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="ml-2 h-3 w-3" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
{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`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Badge>
|
||||
|
||||
<If condition={varState.isOverridden}>
|
||||
<Badge variant="warning">
|
||||
Overridden in {varState.effectiveSource}
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="ml-2 h-3 w-3" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
This variable was overridden by a variable in{' '}
|
||||
{varState.effectiveSource}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Badge>
|
||||
</If>
|
||||
|
||||
<If condition={!validation.success}>
|
||||
<Badge variant="destructive">
|
||||
Invalid Value
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="ml-2 h-3 w-3" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
This variable has an invalid value. Drop down to view the
|
||||
errors.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Badge>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && canExpand && (
|
||||
<div className="flex flex-col gap-y-2 border-t bg-gray-50 p-4">
|
||||
<If condition={!validation.success}>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<Heading level={6} className="Errors">
|
||||
Errors
|
||||
</Heading>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Invalid Value</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
The value for {varState.key} is invalid:
|
||||
<pre>
|
||||
<code>{JSON.stringify(validation, null, 2)}</code>
|
||||
</pre>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<If condition={varState.definitions.length > 1}>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<Heading level={6} className="text-sm font-medium">
|
||||
Override Chain
|
||||
</Heading>
|
||||
|
||||
<div className="space-y-2">
|
||||
{varState.definitions.map((def) => (
|
||||
<div
|
||||
key={`${def.key}-${def.source}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<Badge
|
||||
variant={'outline'}
|
||||
className={cn({
|
||||
'text-destructive': def.source === '.env.production',
|
||||
})}
|
||||
>
|
||||
{def.source}
|
||||
</Badge>
|
||||
|
||||
<div className="font-mono text-sm">
|
||||
{renderValue(def.value, isValueVisible)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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<EnvVariableState> }>,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-full space-x-2">
|
||||
<div>
|
||||
<EnvModeSelector mode={appState.mode} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<FilterSwitcher
|
||||
filters={{
|
||||
secret: secretVars,
|
||||
public: publicVars,
|
||||
overridden: overriddenVars,
|
||||
private: privateVars,
|
||||
invalid: invalidVars,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
className={'w-full'}
|
||||
placeholder="Search variables"
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
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
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
Create a report from the environment variables. Useful for
|
||||
creating support tickets.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<Summary appState={appState} />
|
||||
|
||||
{groups.map((group) => (
|
||||
<div
|
||||
key={group.category}
|
||||
className="flex flex-col gap-y-2.5 border-b border-dashed py-8 last:border-b-0"
|
||||
>
|
||||
<div>
|
||||
<span className={'text-sm font-bold uppercase'}>
|
||||
{group.category}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-4">
|
||||
{group.variables.map((item) => {
|
||||
return (
|
||||
<Fragment key={item.key}>{renderVariable(item)}</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<If condition={groups.length === 0}>
|
||||
<div className="flex h-full flex-1 flex-col items-center justify-center gap-y-4 py-16">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
No variables found
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="font-normal">
|
||||
{buttonLabel()}
|
||||
|
||||
<ChevronsUpDownIcon className="text-muted-foreground ml-1 h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={allSelected}
|
||||
onCheckedChange={() => {
|
||||
handleFilterChange('all', true);
|
||||
}}
|
||||
>
|
||||
All
|
||||
</DropdownMenuCheckboxItem>
|
||||
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={secretVars}
|
||||
onCheckedChange={() => {
|
||||
handleFilterChange('secret', !secretVars);
|
||||
}}
|
||||
>
|
||||
Secret
|
||||
</DropdownMenuCheckboxItem>
|
||||
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={privateVars}
|
||||
onCheckedChange={() => {
|
||||
handleFilterChange('private', !privateVars);
|
||||
}}
|
||||
>
|
||||
Private
|
||||
</DropdownMenuCheckboxItem>
|
||||
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={publicVars}
|
||||
onCheckedChange={() => {
|
||||
handleFilterChange('public', !publicVars);
|
||||
}}
|
||||
>
|
||||
Public
|
||||
</DropdownMenuCheckboxItem>
|
||||
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={invalidVars}
|
||||
onCheckedChange={() => {
|
||||
handleFilterChange('invalid', !invalidVars);
|
||||
}}
|
||||
>
|
||||
Invalid
|
||||
</DropdownMenuCheckboxItem>
|
||||
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={overriddenVars}
|
||||
onCheckedChange={() => {
|
||||
handleFilterChange('overridden', !overriddenVars);
|
||||
}}
|
||||
>
|
||||
Overridden
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Badge variant={errors.length === 0 ? 'success' : 'destructive'}>
|
||||
{errors.length} Errors
|
||||
</Badge>
|
||||
|
||||
<Badge variant={overridden.length === 0 ? 'success' : 'warning'}>
|
||||
{overridden.length} Overridden Variables
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
244
apps/dev-tool/app/variables/lib/env-scanner.ts
Normal file
244
apps/dev-tool/app/variables/lib/env-scanner.ts
Normal file
@@ -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<EnvMode, string[]> = {
|
||||
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<EnvFileInfo[]> {
|
||||
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<string, EnvVariableState> = {};
|
||||
|
||||
// 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<AppEnvState[]> {
|
||||
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 ?? '';
|
||||
}
|
||||
770
apps/dev-tool/app/variables/lib/env-variables-model.ts
Normal file
770
apps/dev-tool/app/variables/lib/env-variables-model.ts
Normal file
@@ -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<boolean>;
|
||||
validate?: (props: {
|
||||
value: string;
|
||||
variables: Record<string, string>;
|
||||
mode: EnvMode;
|
||||
}) => z.SafeParseReturnType<unknown, unknown>;
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
},
|
||||
];
|
||||
40
apps/dev-tool/app/variables/lib/types.ts
Normal file
40
apps/dev-tool/app/variables/lib/types.ts
Normal file
@@ -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<string, EnvVariableState>;
|
||||
};
|
||||
|
||||
export type EnvFileInfo = {
|
||||
appName: string;
|
||||
filePath: string;
|
||||
|
||||
variables: Array<{
|
||||
key: string;
|
||||
value: string;
|
||||
source: string;
|
||||
}>;
|
||||
};
|
||||
55
apps/dev-tool/app/variables/page.tsx
Normal file
55
apps/dev-tool/app/variables/page.tsx
Normal file
@@ -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 (
|
||||
<Page style={'custom'}>
|
||||
<PageHeader
|
||||
displaySidebarTrigger={false}
|
||||
description={
|
||||
<AppBreadcrumbs
|
||||
values={{
|
||||
variables: 'Environment Variables',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageBody>
|
||||
<div className={'flex flex-col space-y-4 pb-16'}>
|
||||
{apps.map((app) => {
|
||||
const appEnvState = processEnvDefinitions(app, mode);
|
||||
|
||||
return (
|
||||
<AppEnvironmentVariablesManager
|
||||
key={app.appName}
|
||||
state={appEnvState}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PageBody>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
13
apps/dev-tool/components/app-layout.tsx
Normal file
13
apps/dev-tool/components/app-layout.tsx
Normal file
@@ -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 (
|
||||
<SidebarProvider>
|
||||
<DevToolSidebar />
|
||||
|
||||
<SidebarInset>{props.children}</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
79
apps/dev-tool/components/app-sidebar.tsx
Normal file
79
apps/dev-tool/components/app-sidebar.tsx
Normal file
@@ -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<typeof Sidebar>) {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<Sidebar collapsible="icon" {...props}>
|
||||
<SidebarHeader>
|
||||
<b className="p-1 font-mono text-xs font-semibold">Makerkit Dev Tool</b>
|
||||
</SidebarHeader>
|
||||
|
||||
<SidebarGroup className="group-data-[collapsible=icon]:hidden">
|
||||
<SidebarGroupLabel>Dev Tools</SidebarGroupLabel>
|
||||
|
||||
<SidebarMenu>
|
||||
{routes.map((route) => (
|
||||
<SidebarMenuItem key={route.path}>
|
||||
<SidebarMenuButton
|
||||
isActive={isRouteActive(route.path, pathname, false)}
|
||||
asChild
|
||||
>
|
||||
<Link href={route.path}>
|
||||
<route.Icon className="h-4 w-4" />
|
||||
<span>{route.label}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroup>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
41
apps/dev-tool/components/env-mode-selector.tsx
Normal file
41
apps/dev-tool/components/env-mode-selector.tsx
Normal file
@@ -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 (
|
||||
<div>
|
||||
<Select name={'mode'} defaultValue={mode} onValueChange={handleModeChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select Mode" />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
<SelectItem value="development">Development</SelectItem>
|
||||
<SelectItem value="production">Production</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
apps/dev-tool/components/iframe.tsx
Normal file
35
apps/dev-tool/components/iframe.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
export const IFrame: React.FC<
|
||||
React.IframeHTMLAttributes<unknown> & {
|
||||
setInnerRef?: (ref: HTMLIFrameElement | undefined) => void;
|
||||
appendStyles?: boolean;
|
||||
theme?: 'light' | 'dark';
|
||||
transparent?: boolean;
|
||||
}
|
||||
> = ({ children, setInnerRef, appendStyles = true, theme, ...props }) => {
|
||||
const [ref, setRef] = useState<HTMLIFrameElement | null>();
|
||||
const doc = ref?.contentWindow?.document as Document;
|
||||
const mountNode = doc?.body;
|
||||
|
||||
return (
|
||||
<iframe
|
||||
{...props}
|
||||
ref={(ref) => {
|
||||
if (ref) {
|
||||
setRef(ref);
|
||||
|
||||
if (setInnerRef) {
|
||||
setInnerRef(ref);
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{mountNode ? createPortal(children, mountNode) : null}
|
||||
</iframe>
|
||||
);
|
||||
};
|
||||
32
apps/dev-tool/components/root-providers.tsx
Normal file
32
apps/dev-tool/components/root-providers.tsx
Normal file
@@ -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 <ReactQueryProvider>{children}</ReactQueryProvider>;
|
||||
}
|
||||
|
||||
function ReactQueryProvider(props: React.PropsWithChildren) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{props.children}
|
||||
|
||||
<Toaster position="top-center" />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
57
apps/dev-tool/components/status-tile.tsx
Normal file
57
apps/dev-tool/components/status-tile.tsx
Normal file
@@ -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]: <AlertCircle className="h-6 w-6 text-yellow-500" />,
|
||||
[ServiceStatus.SUCCESS]: <CheckCircle2 className="h-6 w-6 text-green-500" />,
|
||||
[ServiceStatus.ERROR]: <XCircle className="h-6 w-6 text-red-500" />,
|
||||
};
|
||||
|
||||
interface ServiceCardProps {
|
||||
name: string;
|
||||
status: {
|
||||
status: ServiceStatusType;
|
||||
message?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const ServiceCard = ({ name, status }: ServiceCardProps) => {
|
||||
return (
|
||||
<Card className="w-full max-w-2xl">
|
||||
<CardContent className="pt-6">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
{StatusIcons[status.status]}
|
||||
|
||||
<div>
|
||||
<h3 className="font-medium">{name}</h3>
|
||||
|
||||
<p className="text-sm text-gray-500">
|
||||
{status.message ??
|
||||
(status.status === ServiceStatus.CHECKING
|
||||
? 'Checking connection...'
|
||||
: status.status === ServiceStatus.SUCCESS
|
||||
? 'Connected successfully'
|
||||
: 'Connection failed')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
16
apps/dev-tool/next.config.ts
Normal file
16
apps/dev-tool/next.config.ts
Normal file
@@ -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;
|
||||
44
apps/dev-tool/package.json
Normal file
44
apps/dev-tool/package.json
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
5
apps/dev-tool/postcss.config.mjs
Normal file
5
apps/dev-tool/postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
43
apps/dev-tool/styles/globals.css
Normal file
43
apps/dev-tool/styles/globals.css
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
104
apps/dev-tool/styles/shadcn-ui.css
Normal file
104
apps/dev-tool/styles/shadcn-ui.css
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
116
apps/dev-tool/styles/theme.css
Normal file
116
apps/dev-tool/styles/theme.css
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
apps/dev-tool/styles/theme.utilities.css
Normal file
5
apps/dev-tool/styles/theme.utilities.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
|
||||
@apply xl:max-w-[80rem] px-8;
|
||||
}
|
||||
28
apps/dev-tool/tsconfig.json
Normal file
28
apps/dev-tool/tsconfig.json
Normal file
@@ -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"]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "next-supabase-saas-kit-turbo",
|
||||
"version": "2.2.0",
|
||||
"version": "2.3.0",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
|
||||
193
pnpm-lock.yaml
generated
193
pnpm-lock.yaml
generated
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user