Dev Tools improvements (#247)
* Refactor environment variables UI and update validation logic Enhanced the environment variables page layout for better responsiveness and structure by introducing new components and styles. Added `EnvListDisplay` for grouped variable display and adjusted several UI elements for clarity and consistency. Updated `NEXT_PUBLIC_SENTRY_ENVIRONMENT` validation to make it optional, aligning with updated requirements. * Add environment variable validation and enhance page headers Introduces robust validation for environment variables, ensuring correctness and contextual dependency checks. Updates page headers with titles and detailed descriptions for better usability and clarity. * Refactor variable page layout and improve code readability Rearranged className attributes in JSX for consistency and readability. Refactored map and enum validation logic for better formatting and maintainability. Applied minor corrections to types and formatting in other components. * Refactor styles and simplify component logic Updated badge variants to streamline styles and removed redundant hover states. Simplified logic in email page by extracting breadcrumb values and optimizing title rendering. Adjusted environment variables manager layout for cleaner rendering and removed unnecessary elements. * Add real-time translation updates with RxJS and UI improvements Introduced a Subject with debounce mechanism for handling translation updates, enhancing real-time editing in the translations comparison module. Improved UI components, including conditional rendering, better input handling, and layout adjustments. Implemented a server action for updating translations and streamlined type definitions in the emails page. * Enhance environment variable copying functionality and improve user feedback Updated the environment variables manager to copy structured environment variable data to the clipboard, improving usability. Adjusted toast notifications to provide clearer success and error messages during the copy process. Additionally, fixed a minor issue in the translations comparison component by ensuring proper filtering of keys based on the search input. * Add AI translation functionality and update dependencies Implemented a new action for translating missing strings using AI, enhancing the translations comparison component. Introduced a loading state during translation and improved error handling for translation updates. Updated package dependencies, including the addition of '@ai-sdk/openai' and 'ai' to facilitate AI-driven translations. Enhanced UI components for better user experience and streamlined translation management.
This commit is contained in:
committed by
GitHub
parent
cea46b06a1
commit
76bfeddd32
@@ -34,13 +34,7 @@ export default async function EmailPage(props: EmailPageProps) {
|
||||
const template = await loadEmailTemplate(id);
|
||||
const emailSettings = await getEmailSettings(mode);
|
||||
|
||||
return (
|
||||
<Page style={'custom'}>
|
||||
<PageHeader
|
||||
displaySidebarTrigger={false}
|
||||
description={
|
||||
<AppBreadcrumbs
|
||||
values={{
|
||||
const values: Record<string, string> = {
|
||||
emails: 'Emails',
|
||||
'invite-email': 'Invite Email',
|
||||
'account-delete-email': 'Account Delete Email',
|
||||
@@ -49,9 +43,14 @@ export default async function EmailPage(props: EmailPageProps) {
|
||||
'reset-password-email': 'Reset Password Email',
|
||||
'magic-link-email': 'Magic Link Email',
|
||||
'otp-email': 'OTP Email',
|
||||
}}
|
||||
/>
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Page style={'custom'}>
|
||||
<PageHeader
|
||||
displaySidebarTrigger={false}
|
||||
title={values[id]}
|
||||
description={<AppBreadcrumbs values={values} />}
|
||||
>
|
||||
<EnvModeSelector mode={mode} />
|
||||
</PageHeader>
|
||||
|
||||
@@ -15,7 +15,11 @@ export const metadata = {
|
||||
export default async function EmailsPage() {
|
||||
return (
|
||||
<Page style={'custom'}>
|
||||
<PageHeader displaySidebarTrigger={false} description="Emails" />
|
||||
<PageHeader
|
||||
displaySidebarTrigger={false}
|
||||
title="Emails"
|
||||
description={'Manage your application Email templates'}
|
||||
/>
|
||||
|
||||
<PageBody className={'gap-y-8'}>
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
|
||||
@@ -31,6 +31,7 @@ class ConnectivityService {
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${url}/auth/v1/health`, {
|
||||
headers: {
|
||||
apikey: anonKey,
|
||||
@@ -50,6 +51,12 @@ class ConnectivityService {
|
||||
status: 'success' as const,
|
||||
message: 'Connected to Supabase',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: `Failed to connect to Supabase. ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async checkSupabaseAdminConnectivity() {
|
||||
@@ -85,6 +92,7 @@ class ConnectivityService {
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(endpoint, {
|
||||
headers: {
|
||||
apikey,
|
||||
@@ -114,6 +122,12 @@ class ConnectivityService {
|
||||
status: 'success' as const,
|
||||
message: 'Connected to Supabase Admin',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: 'error' as const,
|
||||
message: `Failed to connect to Supabase Admin. ${error}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async checkStripeWebhookEndpoints() {
|
||||
|
||||
@@ -26,6 +26,7 @@ export default async function DashboardPage(props: DashboardPageProps) {
|
||||
<Page style={'custom'}>
|
||||
<PageHeader
|
||||
displaySidebarTrigger={false}
|
||||
title={'Dev Tool'}
|
||||
description={'Check the status of your Supabase and Stripe services'}
|
||||
>
|
||||
<EnvModeSelector mode={mode} />
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { ChevronDownIcon } from 'lucide-react';
|
||||
import { ChevronDownIcon, Loader2Icon } from 'lucide-react';
|
||||
import { Subject, debounceTime } from 'rxjs';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
@@ -11,6 +12,7 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from '@kit/ui/dropdown-menu';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Input } from '@kit/ui/input';
|
||||
import {
|
||||
Select,
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@kit/ui/select';
|
||||
import { toast } from '@kit/ui/sonner';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -30,6 +33,10 @@ import {
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { defaultI18nNamespaces } from '../../../../web/lib/i18n/i18n.settings';
|
||||
import {
|
||||
translateWithAIAction,
|
||||
updateTranslationAction,
|
||||
} from '../lib/server-actions';
|
||||
import type { TranslationData, Translations } from '../lib/translations-loader';
|
||||
|
||||
function flattenTranslations(
|
||||
@@ -58,31 +65,37 @@ export function TranslationsComparison({
|
||||
translations: Translations;
|
||||
}) {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedLocales, setSelectedLocales] = useState<Set<string>>();
|
||||
const [isTranslating, setIsTranslating] = useState(false);
|
||||
|
||||
const [selectedNamespace, setSelectedNamespace] = useState(
|
||||
defaultI18nNamespaces[0] as string,
|
||||
);
|
||||
|
||||
// Create RxJS Subject for handling translation updates
|
||||
const subject$ = useMemo(
|
||||
() =>
|
||||
new Subject<{
|
||||
locale: string;
|
||||
namespace: string;
|
||||
key: string;
|
||||
value: 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;
|
||||
}
|
||||
const [selectedLocales, setSelectedLocales] = useState<Set<string>>(
|
||||
new Set(locales),
|
||||
);
|
||||
|
||||
// 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 {
|
||||
@@ -105,14 +118,6 @@ export function TranslationsComparison({
|
||||
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);
|
||||
|
||||
@@ -127,9 +132,83 @@ export function TranslationsComparison({
|
||||
setSelectedLocales(newSelectedLocales);
|
||||
};
|
||||
|
||||
const handleTranslateWithAI = useCallback(async () => {
|
||||
try {
|
||||
setIsTranslating(true);
|
||||
|
||||
// Get missing translations for the selected namespace
|
||||
const missingTranslations: Record<string, string> = {};
|
||||
const baseTranslations = flattenedTranslations[baseLocale] ?? {};
|
||||
|
||||
for (const locale of visibleLocales) {
|
||||
if (locale === baseLocale) continue;
|
||||
|
||||
const localeTranslations = flattenedTranslations[locale] ?? {};
|
||||
|
||||
for (const [key, value] of Object.entries(baseTranslations)) {
|
||||
if (!localeTranslations[key]) {
|
||||
missingTranslations[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(missingTranslations).length > 0) {
|
||||
await translateWithAIAction({
|
||||
sourceLocale: baseLocale,
|
||||
targetLocale: locale,
|
||||
namespace: selectedNamespace,
|
||||
translations: missingTranslations,
|
||||
});
|
||||
|
||||
toast.success(`Translated missing strings to ${locale}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to translate: ' + (error as Error).message);
|
||||
} finally {
|
||||
setIsTranslating(false);
|
||||
}
|
||||
}, [flattenedTranslations, baseLocale, visibleLocales, selectedNamespace]);
|
||||
|
||||
// Calculate if there are any missing translations
|
||||
const hasMissingTranslations = useMemo(() => {
|
||||
if (!flattenedTranslations || !baseLocale || !visibleLocales) return false;
|
||||
|
||||
const baseTranslations = flattenedTranslations[baseLocale] ?? {};
|
||||
|
||||
return visibleLocales.some((locale) => {
|
||||
if (locale === baseLocale) return false;
|
||||
|
||||
const localeTranslations = flattenedTranslations[locale] ?? {};
|
||||
|
||||
return Object.keys(baseTranslations).some(
|
||||
(key) => !localeTranslations[key],
|
||||
);
|
||||
});
|
||||
}, [flattenedTranslations, baseLocale, visibleLocales]);
|
||||
|
||||
// Set up subscription to handle debounced updates
|
||||
useEffect(() => {
|
||||
const subscription = subject$.pipe(debounceTime(500)).subscribe((props) => {
|
||||
updateTranslationAction(props)
|
||||
.then(() => {
|
||||
toast.success(`Updated translation for ${props.key}`);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(`Failed to update translation: ${err.message}`);
|
||||
});
|
||||
});
|
||||
|
||||
return () => subscription.unsubscribe();
|
||||
}, [subject$]);
|
||||
|
||||
if (locales.length === 0) {
|
||||
return <div>No translations found</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-4 pb-24">
|
||||
<div className="flex w-full items-center">
|
||||
<div className="flex w-full items-center justify-between gap-2.5">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Input
|
||||
type="search"
|
||||
@@ -139,6 +218,7 @@ export function TranslationsComparison({
|
||||
className="max-w-sm"
|
||||
/>
|
||||
|
||||
<If condition={locales.length > 1}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" className="ml-auto">
|
||||
@@ -154,7 +234,8 @@ export function TranslationsComparison({
|
||||
checked={selectedLocales.has(locale)}
|
||||
onCheckedChange={() => toggleLocale(locale)}
|
||||
disabled={
|
||||
selectedLocales.size === 1 && selectedLocales.has(locale)
|
||||
selectedLocales.size === 1 &&
|
||||
selectedLocales.has(locale)
|
||||
}
|
||||
>
|
||||
{locale}
|
||||
@@ -162,6 +243,7 @@ export function TranslationsComparison({
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</If>
|
||||
|
||||
<Select
|
||||
value={selectedNamespace}
|
||||
@@ -180,6 +262,23 @@ export function TranslationsComparison({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
onClick={handleTranslateWithAI}
|
||||
disabled={isTranslating || !hasMissingTranslations}
|
||||
>
|
||||
{isTranslating ? (
|
||||
<>
|
||||
<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
|
||||
Translating...
|
||||
</>
|
||||
) : (
|
||||
'Translate missing with AI'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rounded-md border">
|
||||
@@ -196,7 +295,7 @@ export function TranslationsComparison({
|
||||
<TableBody>
|
||||
{filteredKeys.map((key) => (
|
||||
<TableRow key={key}>
|
||||
<TableCell className="font-mono text-sm">
|
||||
<TableCell width={350} className="text-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>{key}</span>
|
||||
</div>
|
||||
@@ -222,11 +321,33 @@ export function TranslationsComparison({
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>
|
||||
{value || (
|
||||
<span className="text-destructive">Missing</span>
|
||||
)}
|
||||
</span>
|
||||
<Input
|
||||
defaultValue={value || ''}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value.trim();
|
||||
|
||||
if (value === '') {
|
||||
toast.error('Translation cannot be empty');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === baseValue) {
|
||||
toast.info('Translation is the same as base');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
subject$.next({
|
||||
locale,
|
||||
namespace: selectedNamespace,
|
||||
key,
|
||||
value,
|
||||
});
|
||||
}}
|
||||
className="w-full font-mono text-sm"
|
||||
placeholder={isMissing ? 'Missing translation' : ''}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
);
|
||||
|
||||
161
apps/dev-tool/app/translations/lib/server-actions.ts
Normal file
161
apps/dev-tool/app/translations/lib/server-actions.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { openai } from '@ai-sdk/openai';
|
||||
import { generateText } from 'ai';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:url';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { getLogger } from '@kit/shared/logger';
|
||||
|
||||
const Schema = z.object({
|
||||
locale: z.string().min(1),
|
||||
namespace: z.string().min(1),
|
||||
key: z.string().min(1),
|
||||
value: z.string(),
|
||||
});
|
||||
|
||||
const TranslateSchema = z.object({
|
||||
sourceLocale: z.string(),
|
||||
targetLocale: z.string(),
|
||||
namespace: z.string(),
|
||||
translations: z.record(z.string(), z.string()),
|
||||
});
|
||||
|
||||
/**
|
||||
* Update a translation value in the specified locale and namespace.
|
||||
* @param props
|
||||
*/
|
||||
export async function updateTranslationAction(props: z.infer<typeof Schema>) {
|
||||
// Validate the input
|
||||
const { locale, namespace, key, value } = Schema.parse(props);
|
||||
|
||||
const root = resolve(process.cwd(), '..');
|
||||
const filePath = `${root}apps/web/public/locales/${locale}/${namespace}.json`;
|
||||
|
||||
try {
|
||||
// Read the current translations file
|
||||
const translationsFile = readFileSync(filePath, 'utf-8');
|
||||
const translations = JSON.parse(translationsFile) as Record<string, any>;
|
||||
|
||||
// Update the nested key value
|
||||
const keys = key.split('.') as string[];
|
||||
let current = translations;
|
||||
|
||||
// Navigate through nested objects until the second-to-last key
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
const currentKey = keys[i] as string;
|
||||
|
||||
if (!current[currentKey]) {
|
||||
current[currentKey] = {};
|
||||
}
|
||||
|
||||
current = current[currentKey];
|
||||
}
|
||||
|
||||
// Set the value at the final key
|
||||
const finalKey = keys[keys.length - 1] as string;
|
||||
current[finalKey] = value;
|
||||
|
||||
// Write the updated translations back to the file
|
||||
writeFileSync(filePath, JSON.stringify(translations, null, 2), 'utf-8');
|
||||
|
||||
revalidatePath(`/translations`);
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Failed to update translation:', error);
|
||||
throw new Error('Failed to update translation');
|
||||
}
|
||||
}
|
||||
|
||||
export const translateWithAIAction = async (
|
||||
data: z.infer<typeof TranslateSchema>,
|
||||
) => {
|
||||
const logger = await getLogger();
|
||||
|
||||
z.string().min(1).parse(process.env.OPENAI_API_KEY);
|
||||
|
||||
try {
|
||||
const { sourceLocale, targetLocale, namespace, translations } =
|
||||
TranslateSchema.parse(data);
|
||||
|
||||
// if the path does not exist, create it using an empty object
|
||||
const root = resolve(process.cwd(), '..');
|
||||
const folderPath = `${root}apps/web/public/locales/${targetLocale}`;
|
||||
|
||||
if (!existsSync(folderPath)) {
|
||||
// create the directory if it doesn't exist
|
||||
mkdirSync(folderPath, { recursive: true });
|
||||
}
|
||||
|
||||
const filePath = `${folderPath}/${namespace}.json`;
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
// create the file if it doesn't exist
|
||||
writeFileSync(filePath, JSON.stringify({}, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
const results: Record<string, string> = {};
|
||||
|
||||
// Process translations in batches of 5 for efficiency
|
||||
const entries = Object.entries(translations);
|
||||
const batches = [];
|
||||
|
||||
for (let i = 0; i < entries.length; i += 5) {
|
||||
batches.push(entries.slice(i, i + 5));
|
||||
}
|
||||
|
||||
for (const batch of batches) {
|
||||
const batchPromises = batch.map(async ([key, value]) => {
|
||||
const prompt = `Translate the following text from ${sourceLocale} to ${targetLocale}. Maintain any placeholders (like {name} or %{count}) and HTML tags. Only return the translated text, nothing else.
|
||||
|
||||
Original text: ${value}`;
|
||||
|
||||
const MODEL_NAME = process.env.LLM_MODEL_NAME ?? 'gpt-4o-mini';
|
||||
const model = openai(MODEL_NAME);
|
||||
|
||||
const { text } = await generateText({
|
||||
model,
|
||||
prompt,
|
||||
temperature: 0.3,
|
||||
maxTokens: 200,
|
||||
});
|
||||
|
||||
return [key, text.trim()] as [string, string];
|
||||
});
|
||||
|
||||
const batchResults = await Promise.all(batchPromises);
|
||||
|
||||
for (const [key, translation] of batchResults) {
|
||||
results[key] = translation;
|
||||
}
|
||||
}
|
||||
|
||||
// Update each translation
|
||||
for (const [key, translation] of Object.entries(results)) {
|
||||
await updateTranslationAction({
|
||||
locale: targetLocale,
|
||||
namespace,
|
||||
key,
|
||||
value: translation,
|
||||
});
|
||||
}
|
||||
|
||||
logger.info('AI translation completed', {
|
||||
sourceLocale,
|
||||
targetLocale,
|
||||
namespace,
|
||||
count: Object.keys(results).length,
|
||||
});
|
||||
|
||||
revalidatePath('/translations');
|
||||
|
||||
return { success: true, translations: results };
|
||||
} catch (error) {
|
||||
logger.error('AI translation failed', { error });
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
@@ -18,12 +18,9 @@ export default async function TranslationsPage() {
|
||||
<Page style={'custom'}>
|
||||
<PageHeader
|
||||
displaySidebarTrigger={false}
|
||||
title={'Translations'}
|
||||
description={
|
||||
<AppBreadcrumbs
|
||||
values={{
|
||||
translations: 'Translations',
|
||||
}}
|
||||
/>
|
||||
'Compare translations across different languages. Ensure consistency and accuracy in your translations.'
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment, useCallback, useState } from 'react';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
import { envVariables } from '@/app/variables/lib/env-variables-model';
|
||||
import { updateEnvironmentVariableAction } from '@/app/variables/lib/server-actions';
|
||||
import { EnvModeSelector } from '@/components/env-mode-selector';
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ChevronsUpDownIcon,
|
||||
Copy,
|
||||
CopyIcon,
|
||||
Eye,
|
||||
EyeOff,
|
||||
EyeOffIcon,
|
||||
InfoIcon,
|
||||
} from 'lucide-react';
|
||||
import { Subject, debounceTime } from 'rxjs';
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||
import { Badge } from '@kit/ui/badge';
|
||||
@@ -39,32 +41,77 @@ import {
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { AppEnvState, EnvVariableState } from '../lib/types';
|
||||
|
||||
type ValidationResult = {
|
||||
success: boolean;
|
||||
error?: {
|
||||
issues: Array<{ message: string }>;
|
||||
};
|
||||
};
|
||||
import { DynamicFormInput } from './dynamic-form-input';
|
||||
|
||||
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>
|
||||
return <EnvList appState={state} />;
|
||||
}
|
||||
|
||||
<div className={'flex flex-col space-y-4'}>
|
||||
<EnvList appState={state} />
|
||||
function EnvListDisplay({
|
||||
groups,
|
||||
className,
|
||||
hideSecret = false,
|
||||
}: {
|
||||
groups: Array<{
|
||||
category: string;
|
||||
variables: Array<EnvVariableState>;
|
||||
}>;
|
||||
|
||||
className: string;
|
||||
hideSecret?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<div
|
||||
className={
|
||||
'text-muted-foreground relative flex h-full flex-col rounded-lg font-mono text-xs'
|
||||
}
|
||||
>
|
||||
<div className="bg-muted/50 sticky top-0 flex flex-col gap-y-1 rounded-lg p-4">
|
||||
<div className={'sticky top-0 h-full overflow-auto pb-16 break-all'}>
|
||||
{groups.map((group) => (
|
||||
<div className="mb-4" key={group.category}>
|
||||
<span># {group.category}</span>
|
||||
|
||||
{group.variables.map((variable) => {
|
||||
const model = envVariables.find(
|
||||
(item) => item.name === variable.key,
|
||||
);
|
||||
|
||||
const isSecret = model?.secret;
|
||||
const value =
|
||||
isSecret && hideSecret
|
||||
? '••••••••'
|
||||
: variable.effectiveValue;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`#var_${variable.key.toLowerCase()}`}
|
||||
className={cn('block transition-all hover:underline', {
|
||||
['text-orange-500']: variable.isOverridden,
|
||||
['text-destructive']: !variable.validation.success,
|
||||
['opacity-20']: !variable.isVisible,
|
||||
})}
|
||||
key={variable.key}
|
||||
>
|
||||
<span>{variable.key}</span>: {value}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</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();
|
||||
@@ -75,13 +122,6 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
const showOverriddenVars = searchParams.get('overridden') === 'true';
|
||||
const showInvalidVars = searchParams.get('invalid') === 'true';
|
||||
|
||||
const toggleExpanded = (key: string) => {
|
||||
setExpandedVars((prev) => ({
|
||||
...prev,
|
||||
[key]: !prev[key],
|
||||
}));
|
||||
};
|
||||
|
||||
const toggleShowValue = (key: string) => {
|
||||
setShowValues((prev) => ({
|
||||
...prev,
|
||||
@@ -89,175 +129,86 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
}));
|
||||
};
|
||||
|
||||
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) {
|
||||
if (!value) {
|
||||
return `(empty)`;
|
||||
}
|
||||
|
||||
return '••••••••';
|
||||
}
|
||||
|
||||
return value || '(empty)';
|
||||
return value;
|
||||
};
|
||||
|
||||
const allVariables = getEffectiveVariablesValue(appState);
|
||||
|
||||
// Create a map of all variables including missing ones that have contextual validation
|
||||
const allVarsWithValidation = envVariables.reduce<
|
||||
Record<string, EnvVariableState>
|
||||
>((acc, model) => {
|
||||
// If the variable exists in appState, use that
|
||||
const existingVar = appState.variables[model.name];
|
||||
if (existingVar) {
|
||||
acc[model.name] = existingVar;
|
||||
} else if (
|
||||
// Show missing variables if they:
|
||||
model.required || // Are marked as required
|
||||
model.contextualValidation // OR have contextual validation
|
||||
) {
|
||||
// If it doesn't exist but is required or has contextual validation, create an empty state
|
||||
acc[model.name] = {
|
||||
key: model.name,
|
||||
effectiveValue: '',
|
||||
effectiveSource: 'MISSING',
|
||||
category: model.category,
|
||||
isOverridden: false,
|
||||
definitions: [],
|
||||
const subject$ = useMemo(
|
||||
() =>
|
||||
new Subject<{
|
||||
name: string;
|
||||
value: string;
|
||||
}>(),
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = subject$
|
||||
.pipe(debounceTime(1000))
|
||||
.subscribe((props) => {
|
||||
updateEnvironmentVariableAction({
|
||||
...props,
|
||||
mode: appState.mode,
|
||||
})
|
||||
.then((result) => {
|
||||
toast.success(result.message);
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.error(`Failed to update ${props.name}: ${err.message}`);
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
return subscription.unsubscribe();
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
}, [subject$]);
|
||||
|
||||
const onValueChanged = useCallback(
|
||||
(props: { value: string; name: string }) => {
|
||||
subject$.next({
|
||||
name: props.name,
|
||||
value: props.value,
|
||||
});
|
||||
},
|
||||
[subject$],
|
||||
);
|
||||
|
||||
const renderVariable = (varState: EnvVariableState) => {
|
||||
const isExpanded = expandedVars[varState.key] ?? false;
|
||||
const isClientBundledValue = varState.key.startsWith('NEXT_PUBLIC_');
|
||||
const isValueVisible = showValues[varState.key] ?? isClientBundledValue;
|
||||
|
||||
const model = envVariables.find(
|
||||
(variable) => variable.name === varState.key,
|
||||
);
|
||||
|
||||
// Enhanced validation logic to handle both regular and contextual validation
|
||||
let validation: ValidationResult = {
|
||||
success: true,
|
||||
};
|
||||
|
||||
if (model) {
|
||||
// First check if it's required but missing
|
||||
if (model.required && !varState.effectiveValue) {
|
||||
validation = {
|
||||
success: false,
|
||||
error: {
|
||||
issues: [
|
||||
{
|
||||
message: `This variable is required but missing from your environment files`,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
} else if (model.contextualValidation) {
|
||||
// Then check contextual validation
|
||||
const dependenciesMet = model.contextualValidation.dependencies.some(
|
||||
(dep) => {
|
||||
const dependencyValue = allVariables[dep.variable] ?? '';
|
||||
|
||||
return dep.condition(dependencyValue, allVariables);
|
||||
},
|
||||
);
|
||||
|
||||
if (dependenciesMet) {
|
||||
// Only check for missing value or run validation if dependencies are met
|
||||
if (!varState.effectiveValue) {
|
||||
const dependencyErrors = model.contextualValidation.dependencies
|
||||
.map((dep) => {
|
||||
const dependencyValue = allVariables[dep.variable] ?? '';
|
||||
|
||||
const shouldValidate = dep.condition(
|
||||
dependencyValue,
|
||||
allVariables,
|
||||
);
|
||||
|
||||
if (shouldValidate) {
|
||||
const { success } = model.contextualValidation!.validate({
|
||||
value: varState.effectiveValue,
|
||||
variables: allVariables,
|
||||
mode: appState.mode,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dep.message;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((message): message is string => message !== null);
|
||||
|
||||
validation = {
|
||||
success: dependencyErrors.length === 0,
|
||||
error: {
|
||||
issues: dependencyErrors.map((message) => ({ message })),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// If we have a value and dependencies are met, run contextual validation
|
||||
const result = model.contextualValidation.validate({
|
||||
value: varState.effectiveValue,
|
||||
variables: allVariables,
|
||||
mode: appState.mode,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
validation = {
|
||||
success: false,
|
||||
error: {
|
||||
issues: result.error.issues.map((issue) => ({
|
||||
message: issue.message,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (model.validate && varState.effectiveValue) {
|
||||
// Only run regular validation if:
|
||||
// 1. There's no contextual validation
|
||||
// 2. There's a value to validate
|
||||
const result = model.validate({
|
||||
value: varState.effectiveValue,
|
||||
variables: allVariables,
|
||||
mode: appState.mode,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
validation = {
|
||||
success: false,
|
||||
error: {
|
||||
issues: result.error.issues.map((issue) => ({
|
||||
message: issue.message,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const canExpand = varState.definitions.length > 1 || !validation.success;
|
||||
const isClientBundledValue = varState.key.startsWith('NEXT_PUBLIC_');
|
||||
const isValueVisible = showValues[varState.key] ?? !model?.secret;
|
||||
|
||||
return (
|
||||
<div key={varState.key} className="animate-in fade-in rounded-lg border">
|
||||
<div className="p-4">
|
||||
<div
|
||||
id={`var_${varState.key.toLowerCase()}`}
|
||||
key={varState.key}
|
||||
className={cn('animate-in fade-in py-6 transition-all', {
|
||||
hidden: !varState.isVisible,
|
||||
})}
|
||||
>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 flex-col gap-y-4">
|
||||
<div className="flex max-w-full flex-1 flex-col">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="font-mono text-sm font-semibold">
|
||||
<span
|
||||
className={cn('font-mono text-sm font-semibold', {
|
||||
'text-orange-500': varState.isOverridden,
|
||||
'text-destructive': !varState.validation.success,
|
||||
})}
|
||||
>
|
||||
{varState.key}
|
||||
</span>
|
||||
|
||||
@@ -311,11 +262,26 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
</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">
|
||||
<If
|
||||
condition={isValueVisible || !varState.effectiveValue}
|
||||
fallback={
|
||||
<div className="max-w-auto bg-muted text-muted-foreground flex h-9 w-auto flex-1 items-center overflow-x-auto rounded border px-2 py-2 font-mono text-xs">
|
||||
{renderValue(varState.effectiveValue, isValueVisible)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<DynamicFormInput
|
||||
type={model?.type ?? 'string'}
|
||||
name={varState.key}
|
||||
value={varState.effectiveValue}
|
||||
onChange={onValueChanged}
|
||||
placeholder={`Set a value for ${varState.key}`}
|
||||
enumValues={model?.values}
|
||||
className="text-xs"
|
||||
/>
|
||||
</If>
|
||||
|
||||
<If condition={!isClientBundledValue}>
|
||||
<If condition={model?.secret}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size={'icon'}
|
||||
@@ -325,6 +291,7 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
</Button>
|
||||
</If>
|
||||
|
||||
<If condition={model && model.type !== 'boolean'}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() => copyToClipboard(varState.effectiveValue)}
|
||||
@@ -332,23 +299,19 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
>
|
||||
<Copy size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</If>
|
||||
</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>
|
||||
<If condition={model?.hint}>
|
||||
{(hint) => (
|
||||
<div className="mt-2 flex items-center gap-2">
|
||||
<span className="text-muted-foreground text-xs font-normal">
|
||||
{hint}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex gap-x-2">
|
||||
@@ -441,7 +404,7 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
</Badge>
|
||||
</If>
|
||||
|
||||
<If condition={!validation.success}>
|
||||
<If condition={!varState.validation.success}>
|
||||
<Badge variant="destructive">
|
||||
Invalid Value
|
||||
<TooltipProvider>
|
||||
@@ -461,34 +424,22 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
</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 w-full flex-col gap-y-2 py-4">
|
||||
<If condition={!varState.validation.success}>
|
||||
<div className={'flex flex-col space-y-2'}>
|
||||
<Heading level={6} className="Errors">
|
||||
Errors
|
||||
</Heading>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>
|
||||
{varState.effectiveSource === 'MISSING'
|
||||
? 'Missing Required Variable'
|
||||
: 'Invalid Value'}
|
||||
? `The variable ${varState.key} is required but missing`
|
||||
: `The value for ${varState.key} is invalid`}
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
{varState.effectiveSource === 'MISSING'
|
||||
? `The variable ${varState.key} is required but missing from your environment files:`
|
||||
: `The value for ${varState.key} is invalid:`}
|
||||
</div>
|
||||
|
||||
{/* Enhanced error display */}
|
||||
<div className="space-y-1">
|
||||
{validation.error?.issues.map((issue, index) => (
|
||||
{varState.validation.error?.issues.map((issue, index) => (
|
||||
<div key={index} className="text-sm">
|
||||
• {issue.message}
|
||||
• {issue}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -501,8 +452,8 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
{model.contextualValidation.dependencies.map(
|
||||
(dep, index) => (
|
||||
<div key={index} className="text-sm">
|
||||
• Requires valid {dep.variable.toUpperCase()}{' '}
|
||||
when {dep.message}
|
||||
• Requires valid {dep.variable.toUpperCase()} when{' '}
|
||||
{dep.message}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
@@ -520,7 +471,7 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
Override Chain
|
||||
</Heading>
|
||||
|
||||
<div className="space-y-2">
|
||||
<div className="w-full space-y-2">
|
||||
{varState.definitions.map((def) => (
|
||||
<div
|
||||
key={`${def.key}-${def.source}`}
|
||||
@@ -534,17 +485,12 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
>
|
||||
{def.source}
|
||||
</Badge>
|
||||
|
||||
<div className="font-mono text-sm">
|
||||
{renderValue(def.value, isValueVisible)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</If>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -589,83 +535,20 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
return varState.isOverridden;
|
||||
}
|
||||
|
||||
if (showInvalidVars) {
|
||||
const allVariables = getEffectiveVariablesValue(appState);
|
||||
|
||||
let hasError = false;
|
||||
|
||||
if (model) {
|
||||
if (model.contextualValidation) {
|
||||
// Check for missing or invalid dependencies
|
||||
const dependencyErrors = model.contextualValidation.dependencies
|
||||
.map((dep) => {
|
||||
const dependencyValue = allVariables[dep.variable] ?? '';
|
||||
|
||||
const shouldValidate = dep.condition(
|
||||
dependencyValue,
|
||||
allVariables,
|
||||
);
|
||||
|
||||
if (shouldValidate) {
|
||||
const { error } = model.contextualValidation!.validate({
|
||||
value: varState.effectiveValue,
|
||||
variables: allVariables,
|
||||
mode: appState.mode,
|
||||
});
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
return false;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
if (dependencyErrors.length > 0) {
|
||||
hasError = true;
|
||||
}
|
||||
} else if (model.validate) {
|
||||
// Fall back to regular validation
|
||||
const result = model.validate({
|
||||
value: varState.effectiveValue,
|
||||
variables: allVariables,
|
||||
mode: appState.mode,
|
||||
});
|
||||
|
||||
hasError = !result.success;
|
||||
}
|
||||
}
|
||||
|
||||
return hasError && isInSearch;
|
||||
if (showInvalidVars && isInSearch) {
|
||||
return !varState.validation.success;
|
||||
}
|
||||
|
||||
return isInSearch;
|
||||
};
|
||||
|
||||
// Update groups to use allVarsWithValidation instead of appState.variables
|
||||
const groups = Object.values(allVarsWithValidation)
|
||||
.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> }>,
|
||||
);
|
||||
const groups = getGroups(appState, filterVariable);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex h-full flex-1 flex-col gap-y-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-full space-x-2">
|
||||
<div className="flex w-full space-x-2 py-0.5">
|
||||
<div>
|
||||
<EnvModeSelector mode={appState.mode} />
|
||||
</div>
|
||||
@@ -688,60 +571,47 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
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">
|
||||
<div className="flex flex-1 flex-col gap-y-4 overflow-hidden">
|
||||
<Summary appState={appState} />
|
||||
|
||||
{groups.map((group) => (
|
||||
<div className="flex w-full flex-1 space-x-4 overflow-hidden">
|
||||
<div className="flex w-6/12 flex-1 flex-col overflow-y-auto">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{groups.map((group) => {
|
||||
const visibleVariables = group.variables.filter(
|
||||
(item) => item.isVisible,
|
||||
);
|
||||
|
||||
if (visibleVariables.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={group.category}
|
||||
className="flex flex-col gap-y-2.5 border-b border-dashed py-8 last:border-b-0"
|
||||
className="flex flex-col rounded-lg border p-4"
|
||||
>
|
||||
<div>
|
||||
<span className={'text-sm font-bold uppercase'}>
|
||||
<span className={'text-lg font-bold'}>
|
||||
{group.category}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex flex-col">
|
||||
{group.variables.map((item) => {
|
||||
return (
|
||||
<Fragment key={item.key}>{renderVariable(item)}</Fragment>
|
||||
<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">
|
||||
@@ -752,24 +622,17 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EnvListDisplay
|
||||
className="sticky top-0 w-6/12 overflow-y-auto"
|
||||
groups={groups}
|
||||
/>
|
||||
</div>
|
||||
</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;
|
||||
@@ -875,66 +738,15 @@ function FilterSwitcher(props: {
|
||||
|
||||
function Summary({ appState }: { appState: AppEnvState }) {
|
||||
const varsArray = Object.values(appState.variables);
|
||||
const allVariables = getEffectiveVariablesValue(appState);
|
||||
const overridden = varsArray.filter((variable) => variable.isOverridden);
|
||||
const handleFilterChange = useUpdateFilteredVariables();
|
||||
|
||||
// Find all variables with errors (including missing required and contextual validation)
|
||||
const errors = envVariables.reduce<string[]>((acc, model) => {
|
||||
// Get the current value of this variable
|
||||
const varState = appState.variables[model.name];
|
||||
const value = varState?.effectiveValue;
|
||||
let hasError = false;
|
||||
|
||||
// Check if it's required but missing
|
||||
if (model.required && !value) {
|
||||
hasError = true;
|
||||
} else if (model.contextualValidation) {
|
||||
// Check if any dependency conditions are met
|
||||
const dependenciesErrors = model.contextualValidation.dependencies.some(
|
||||
(dep) => {
|
||||
const dependencyValue = allVariables[dep.variable] ?? '';
|
||||
|
||||
const shouldValidate = dep.condition(dependencyValue, allVariables);
|
||||
|
||||
if (shouldValidate) {
|
||||
const { error } = model.contextualValidation!.validate({
|
||||
value: varState?.effectiveValue ?? '',
|
||||
variables: allVariables,
|
||||
mode: appState.mode,
|
||||
const variablesWithErrors = varsArray.filter((variable) => {
|
||||
return !variable.validation.success;
|
||||
});
|
||||
|
||||
return error;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (dependenciesErrors) {
|
||||
hasError = true;
|
||||
}
|
||||
} else if (model.validate && value) {
|
||||
// Only run regular validation if:
|
||||
// 1. There's no contextual validation
|
||||
// 2. There's a value to validate
|
||||
const result = model.validate({
|
||||
value,
|
||||
variables: allVariables,
|
||||
mode: appState.mode,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
hasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
acc.push(model.name);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
const validVariables = varsArray.length - errors.length;
|
||||
const validVariables = varsArray.length - variablesWithErrors.length;
|
||||
|
||||
return (
|
||||
<div className="flex justify-between space-x-4">
|
||||
@@ -946,11 +758,11 @@ function Summary({ appState }: { appState: AppEnvState }) {
|
||||
<Badge
|
||||
variant={'outline'}
|
||||
className={cn({
|
||||
'text-destructive': errors.length > 0,
|
||||
'text-green-500': errors.length === 0,
|
||||
'text-destructive': variablesWithErrors.length > 0,
|
||||
'text-green-500': variablesWithErrors.length === 0,
|
||||
})}
|
||||
>
|
||||
{errors.length} Invalid
|
||||
{variablesWithErrors.length} Invalid
|
||||
</Badge>
|
||||
|
||||
<If condition={overridden.length > 0}>
|
||||
@@ -963,17 +775,59 @@ function Summary({ appState }: { appState: AppEnvState }) {
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<If condition={errors.length > 0}>
|
||||
<div className={'flex items-center gap-x-2'}>
|
||||
<If condition={variablesWithErrors.length > 0}>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'ghost'}
|
||||
variant={'outline'}
|
||||
onClick={() => handleFilterChange('invalid', true, true)}
|
||||
>
|
||||
<EyeOffIcon className="mr-2 h-3 w-3" />
|
||||
Display Invalid only
|
||||
</Button>
|
||||
</If>
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size={'sm'}
|
||||
onClick={() => {
|
||||
let data = '';
|
||||
|
||||
const groups = getGroups(appState, () => true);
|
||||
|
||||
groups.forEach((group) => {
|
||||
data += `# ${group.category}\n`;
|
||||
|
||||
group.variables.forEach((variable) => {
|
||||
data += `${variable.key}=${variable.effectiveValue}\n`;
|
||||
});
|
||||
|
||||
data += '\n';
|
||||
});
|
||||
|
||||
const promise = copyToClipboard(data);
|
||||
|
||||
toast.promise(promise, {
|
||||
loading: 'Copying environment variables...',
|
||||
success: 'Environment variables copied to clipboard.',
|
||||
error: 'Failed to copy environment variables to clipboard',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<CopyIcon className={'mr-2 h-4 w-4'} />
|
||||
<span>Copy env file to clipboard</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
Copy environment variables to clipboard. You can place it in your
|
||||
hosting provider to set up the full environment.
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -1027,3 +881,35 @@ function useUpdateFilteredVariables() {
|
||||
|
||||
return useCallback(handleFilterChange, [router]);
|
||||
}
|
||||
|
||||
async function copyToClipboard(text: string) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function getGroups(
|
||||
appState: AppEnvState,
|
||||
filterVariable: (variable: EnvVariableState) => boolean,
|
||||
) {
|
||||
return Object.values(appState.variables).reduce(
|
||||
(acc, variable) => {
|
||||
const group = acc.find((group) => group.category === variable.category);
|
||||
variable.isVisible = filterVariable(variable);
|
||||
|
||||
if (!group) {
|
||||
acc.push({
|
||||
category: variable.category,
|
||||
variables: [variable],
|
||||
});
|
||||
} else {
|
||||
group.variables.push(variable);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[] as Array<{ category: string; variables: Array<EnvVariableState> }>,
|
||||
);
|
||||
}
|
||||
|
||||
161
apps/dev-tool/app/variables/components/dynamic-form-input.tsx
Normal file
161
apps/dev-tool/app/variables/components/dynamic-form-input.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Input } from '@kit/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@kit/ui/select';
|
||||
import { Switch } from '@kit/ui/switch';
|
||||
import { Textarea } from '@kit/ui/textarea';
|
||||
|
||||
type ModelType =
|
||||
| 'string'
|
||||
| 'longString'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'enum'
|
||||
| 'url'
|
||||
| 'email';
|
||||
|
||||
interface DynamicFormInputProps {
|
||||
type: ModelType;
|
||||
value: string;
|
||||
name: string;
|
||||
onChange: (props: { name: string; value: string }) => void;
|
||||
placeholder?: string;
|
||||
enumValues?: Array<string | null>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function DynamicFormInput({
|
||||
type,
|
||||
value,
|
||||
name,
|
||||
onChange,
|
||||
placeholder,
|
||||
enumValues = [],
|
||||
className,
|
||||
}: DynamicFormInputProps) {
|
||||
const handleInputChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
onChange({
|
||||
name,
|
||||
value: e.target.value,
|
||||
});
|
||||
},
|
||||
[name, onChange],
|
||||
);
|
||||
|
||||
const handleSwitchChange = useCallback(
|
||||
(checked: boolean) => {
|
||||
onChange({
|
||||
name,
|
||||
value: checked ? 'true' : 'false',
|
||||
});
|
||||
},
|
||||
[name, onChange],
|
||||
);
|
||||
|
||||
const handleSelectChange = useCallback(
|
||||
(value: string) => {
|
||||
onChange({
|
||||
name,
|
||||
value: value === '' ? 'none' : value,
|
||||
});
|
||||
},
|
||||
[name, onChange],
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case 'longString':
|
||||
return (
|
||||
<Textarea
|
||||
defaultValue={value}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'number':
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
defaultValue={value}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'boolean':
|
||||
return (
|
||||
<label className="flex items-center gap-x-2">
|
||||
<Switch
|
||||
checked={value === 'true'}
|
||||
onCheckedChange={handleSwitchChange}
|
||||
/>
|
||||
|
||||
<span className={'text-sm uppercase'}>
|
||||
{value === 'true' ? 'True' : 'False'}
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
|
||||
case 'url':
|
||||
return (
|
||||
<Input
|
||||
type="url"
|
||||
defaultValue={value}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'email':
|
||||
return (
|
||||
<Input
|
||||
type="email"
|
||||
defaultValue={value}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'enum':
|
||||
return (
|
||||
<Select
|
||||
value={value === 'none' ? '' : value}
|
||||
onValueChange={handleSelectChange}
|
||||
>
|
||||
<SelectTrigger className={className}>
|
||||
<SelectValue placeholder={placeholder} />
|
||||
</SelectTrigger>
|
||||
|
||||
<SelectContent>
|
||||
{enumValues.map((enumValue) => (
|
||||
<SelectItem key={enumValue} value={enumValue as string}>
|
||||
{enumValue}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
||||
default:
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
defaultValue={value}
|
||||
onChange={handleInputChange}
|
||||
placeholder={placeholder}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -152,11 +152,18 @@ export function processEnvDefinitions(
|
||||
if (!variableMap[variable.key]) {
|
||||
variableMap[variable.key] = {
|
||||
key: variable.key,
|
||||
isVisible: true,
|
||||
definitions: [],
|
||||
effectiveValue: variable.value,
|
||||
effectiveSource: variable.source,
|
||||
isOverridden: false,
|
||||
category: model ? model.category : 'Custom',
|
||||
validation: {
|
||||
success: true,
|
||||
error: {
|
||||
issues: [],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -212,6 +219,231 @@ export function processEnvDefinitions(
|
||||
}
|
||||
}
|
||||
|
||||
// after computing the effective values, we can check for errors
|
||||
for (const key in variableMap) {
|
||||
const model = envVariables.find((v) => key === v.name);
|
||||
const varState = variableMap[key];
|
||||
|
||||
if (!varState) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let validation: {
|
||||
success: boolean;
|
||||
error: {
|
||||
issues: string[];
|
||||
};
|
||||
} = { success: true, error: { issues: [] } };
|
||||
|
||||
if (model) {
|
||||
const allVariables = Object.values(variableMap).reduce(
|
||||
(acc, variable) => {
|
||||
return {
|
||||
...acc,
|
||||
[variable.key]: variable.effectiveValue,
|
||||
};
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
// First check if it's required but missing
|
||||
if (model.required && !varState.effectiveValue) {
|
||||
validation = {
|
||||
success: false,
|
||||
error: {
|
||||
issues: [
|
||||
`This variable is required but missing from your environment files`,
|
||||
],
|
||||
},
|
||||
};
|
||||
} else if (model.contextualValidation) {
|
||||
// Then check contextual validation
|
||||
const dependenciesMet = model.contextualValidation.dependencies.some(
|
||||
(dep) => {
|
||||
const dependencyValue = allVariables[dep.variable] ?? '';
|
||||
|
||||
return dep.condition(dependencyValue, allVariables);
|
||||
},
|
||||
);
|
||||
|
||||
if (dependenciesMet) {
|
||||
// Only check for missing value or run validation if dependencies are met
|
||||
if (!varState.effectiveValue) {
|
||||
const dependencyErrors = model.contextualValidation.dependencies
|
||||
.map((dep) => {
|
||||
const dependencyValue = allVariables[dep.variable] ?? '';
|
||||
|
||||
const shouldValidate = dep.condition(
|
||||
dependencyValue,
|
||||
allVariables,
|
||||
);
|
||||
|
||||
if (shouldValidate) {
|
||||
const { success } = model.contextualValidation!.validate({
|
||||
value: varState.effectiveValue,
|
||||
variables: allVariables,
|
||||
mode,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dep.message;
|
||||
}
|
||||
|
||||
return null;
|
||||
})
|
||||
.filter((message): message is string => message !== null);
|
||||
|
||||
validation = {
|
||||
success: dependencyErrors.length === 0,
|
||||
error: {
|
||||
issues: dependencyErrors
|
||||
.map((message) => message)
|
||||
.filter((message) => !!message),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// If we have a value and dependencies are met, run contextual validation
|
||||
const result = model.contextualValidation.validate({
|
||||
value: varState.effectiveValue,
|
||||
variables: allVariables,
|
||||
mode,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
validation = {
|
||||
success: false,
|
||||
error: {
|
||||
issues: result.error.issues
|
||||
.map((issue) => issue.message)
|
||||
.filter((message) => !!message),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (model.validate && varState.effectiveValue) {
|
||||
// Only run regular validation if:
|
||||
// 1. There's no contextual validation
|
||||
// 2. There's a value to validate
|
||||
const result = model.validate({
|
||||
value: varState.effectiveValue,
|
||||
variables: allVariables,
|
||||
mode,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
validation = {
|
||||
success: false,
|
||||
error: {
|
||||
issues: result.error.issues
|
||||
.map((issue) => issue.message)
|
||||
.filter((message) => !!message),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
varState.validation = validation;
|
||||
}
|
||||
|
||||
// Final pass: Validate missing variables that are marked as required
|
||||
// or as having contextual validation
|
||||
for (const model of envVariables) {
|
||||
// If the variable exists in appState, use that
|
||||
const existingVar = variableMap[model.name];
|
||||
|
||||
if (existingVar) {
|
||||
// If the variable is already in the map, skip it
|
||||
continue;
|
||||
}
|
||||
|
||||
if (model.required || model.contextualValidation) {
|
||||
if (model.contextualValidation) {
|
||||
const allVariables = Object.values(variableMap).reduce(
|
||||
(acc, variable) => {
|
||||
return {
|
||||
...acc,
|
||||
[variable.key]: variable.effectiveValue,
|
||||
};
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
);
|
||||
|
||||
const errors =
|
||||
model?.contextualValidation?.dependencies
|
||||
.map((dep) => {
|
||||
const dependencyValue = allVariables[dep.variable] ?? '';
|
||||
const shouldValidate = dep.condition(
|
||||
dependencyValue,
|
||||
allVariables,
|
||||
);
|
||||
|
||||
if (!shouldValidate) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const effectiveValue = allVariables[dep.variable] ?? '';
|
||||
|
||||
const validation = model.contextualValidation!.validate({
|
||||
value: effectiveValue,
|
||||
variables: allVariables,
|
||||
mode,
|
||||
});
|
||||
|
||||
if (validation) {
|
||||
return [dep.message];
|
||||
}
|
||||
|
||||
return [];
|
||||
})
|
||||
.flat() ?? ([] as string[]);
|
||||
|
||||
if (errors.length === 0) {
|
||||
continue;
|
||||
} else {
|
||||
variableMap[model.name] = {
|
||||
key: model.name,
|
||||
effectiveValue: '',
|
||||
effectiveSource: 'MISSING',
|
||||
isVisible: true,
|
||||
category: model.category,
|
||||
isOverridden: false,
|
||||
definitions: [],
|
||||
validation: {
|
||||
success: false,
|
||||
error: {
|
||||
issues: errors.map((error) => error),
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// If it doesn't exist but is required or has contextual validation, create an empty state
|
||||
variableMap[model.name] = {
|
||||
key: model.name,
|
||||
effectiveValue: '',
|
||||
effectiveSource: 'MISSING',
|
||||
isVisible: true,
|
||||
category: model.category,
|
||||
isOverridden: false,
|
||||
definitions: [],
|
||||
validation: {
|
||||
success: false,
|
||||
error: {
|
||||
issues: [
|
||||
`This variable is required but missing from your environment files`,
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
appName: envInfo.appName,
|
||||
filePath: envInfo.filePath,
|
||||
@@ -227,11 +459,6 @@ export async function getEnvState(
|
||||
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'] });
|
||||
|
||||
@@ -1,34 +1,51 @@
|
||||
import { EnvMode } from '@/app/variables/lib/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
type DependencyRule = {
|
||||
variable: string;
|
||||
condition: (value: string, variables: Record<string, string>) => boolean;
|
||||
message: string;
|
||||
};
|
||||
type ModelType =
|
||||
| 'string'
|
||||
| 'longString'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'enum'
|
||||
| 'url'
|
||||
| 'email';
|
||||
|
||||
type ContextualValidation = {
|
||||
dependencies: DependencyRule[];
|
||||
validate: (props: {
|
||||
value: string;
|
||||
variables: Record<string, string>;
|
||||
mode: EnvMode;
|
||||
}) => z.SafeParseReturnType<unknown, unknown>;
|
||||
};
|
||||
type Values = Array<string | null>;
|
||||
|
||||
export type EnvVariableModel = {
|
||||
name: string;
|
||||
description: string;
|
||||
hint?: string;
|
||||
secret?: boolean;
|
||||
required?: boolean;
|
||||
type?: ModelType;
|
||||
values?: Values;
|
||||
category: string;
|
||||
test?: (value: string) => Promise<boolean>;
|
||||
validate?: (props: {
|
||||
required?: boolean;
|
||||
validate?: ({
|
||||
value,
|
||||
variables,
|
||||
mode,
|
||||
}: {
|
||||
value: string;
|
||||
variables: Record<string, string>;
|
||||
mode: EnvMode;
|
||||
}) => z.SafeParseReturnType<unknown, unknown>;
|
||||
contextualValidation?: ContextualValidation;
|
||||
contextualValidation?: {
|
||||
dependencies: Array<{
|
||||
variable: string;
|
||||
condition: (value: string, variables: Record<string, string>) => boolean;
|
||||
message: string;
|
||||
}>;
|
||||
validate: ({
|
||||
value,
|
||||
variables,
|
||||
mode,
|
||||
}: {
|
||||
value: string;
|
||||
variables: Record<string, string>;
|
||||
mode: EnvMode;
|
||||
}) => z.SafeParseReturnType<unknown, unknown>;
|
||||
};
|
||||
};
|
||||
|
||||
export const envVariables: EnvVariableModel[] = [
|
||||
@@ -38,6 +55,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
'The URL of your site, used for generating absolute URLs. Must include the protocol.',
|
||||
category: 'Site Configuration',
|
||||
required: true,
|
||||
type: 'url',
|
||||
hint: `Ex. https://example.com`,
|
||||
validate: ({ value, mode }) => {
|
||||
if (mode === 'development') {
|
||||
return z
|
||||
@@ -65,7 +84,9 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description:
|
||||
"Your product's name, used consistently across the application interface.",
|
||||
category: 'Site Configuration',
|
||||
hint: `Ex. "My Product"`,
|
||||
required: true,
|
||||
type: 'string',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -82,6 +103,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
"The site's title tag content, crucial for SEO and browser display.",
|
||||
category: 'Site Configuration',
|
||||
required: true,
|
||||
hint: `Ex. "My Product, the best product ever"`,
|
||||
type: 'string',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -94,6 +117,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_SITE_DESCRIPTION',
|
||||
type: 'longString',
|
||||
description:
|
||||
"Your site's meta description, important for SEO optimization.",
|
||||
category: 'Site Configuration',
|
||||
@@ -111,8 +135,10 @@ export const envVariables: EnvVariableModel[] = [
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_DEFAULT_LOCALE',
|
||||
type: 'string',
|
||||
description: 'Sets the default language for your application.',
|
||||
category: 'Localization',
|
||||
hint: `Ex. "en"`,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -128,6 +154,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_AUTH_PASSWORD',
|
||||
description: 'Enables or disables password-based authentication.',
|
||||
category: 'Authentication',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -136,6 +163,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_AUTH_MAGIC_LINK',
|
||||
description: 'Enables or disables magic link authentication.',
|
||||
category: 'Authentication',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -144,6 +172,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_CAPTCHA_SITE_KEY',
|
||||
description: 'Your Cloudflare Captcha site key for form protection.',
|
||||
category: 'Security',
|
||||
type: 'string',
|
||||
validate: ({ value }) => {
|
||||
return z.string().optional().safeParse(value);
|
||||
},
|
||||
@@ -154,6 +183,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
'Your Cloudflare Captcha secret token for backend verification.',
|
||||
category: 'Security',
|
||||
secret: true,
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -191,6 +221,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description:
|
||||
'Controls user navigation layout. Options: sidebar, header, or custom.',
|
||||
category: 'Navigation',
|
||||
type: 'enum',
|
||||
values: ['sidebar', 'header', 'custom'],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.enum(['sidebar', 'header', 'custom'])
|
||||
@@ -202,6 +234,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_HOME_SIDEBAR_COLLAPSED',
|
||||
description: 'Sets the default state of the home sidebar.',
|
||||
category: 'Navigation',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -211,6 +244,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description:
|
||||
'Controls team navigation layout. Options: sidebar, header, or custom.',
|
||||
category: 'Navigation',
|
||||
type: 'enum',
|
||||
values: ['sidebar', 'header', 'custom'],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.enum(['sidebar', 'header', 'custom'])
|
||||
@@ -222,6 +257,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_TEAM_SIDEBAR_COLLAPSED',
|
||||
description: 'Sets the default state of the team sidebar.',
|
||||
category: 'Navigation',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -231,6 +267,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description:
|
||||
'Defines sidebar collapse behavior. Options: offscreen, icon, or none.',
|
||||
category: 'Navigation',
|
||||
type: 'enum',
|
||||
values: ['offscreen', 'icon', 'none'],
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['offscreen', 'icon', 'none']).optional().safeParse(value);
|
||||
},
|
||||
@@ -240,6 +278,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description:
|
||||
'Controls the default theme appearance. Options: light, dark, or system.',
|
||||
category: 'Theme',
|
||||
type: 'enum',
|
||||
values: ['light', 'dark', 'system'],
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['light', 'dark', 'system']).optional().safeParse(value);
|
||||
},
|
||||
@@ -248,6 +288,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_ENABLE_THEME_TOGGLE',
|
||||
description: 'Controls visibility of the theme toggle feature.',
|
||||
category: 'Theme',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -256,6 +297,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER',
|
||||
description: 'Controls visibility of the sidebar trigger feature.',
|
||||
category: 'Navigation',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -264,6 +306,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_DELETION',
|
||||
description: 'Allows users to delete their personal accounts.',
|
||||
category: 'Features',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -272,6 +315,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_ENABLE_PERSONAL_ACCOUNT_BILLING',
|
||||
description: 'Enables billing features for personal accounts.',
|
||||
category: 'Features',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -280,6 +324,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS',
|
||||
description: 'Master switch for team account functionality.',
|
||||
category: 'Features',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -288,6 +333,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_CREATION',
|
||||
description: 'Controls ability to create new team accounts.',
|
||||
category: 'Features',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -296,6 +342,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_DELETION',
|
||||
description: 'Allows team account deletion.',
|
||||
category: 'Features',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -304,6 +351,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_ENABLE_TEAM_ACCOUNTS_BILLING',
|
||||
description: 'Enables billing features for team accounts.',
|
||||
category: 'Features',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -312,6 +360,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_ENABLE_NOTIFICATIONS',
|
||||
description: 'Controls the notification system.',
|
||||
category: 'Notifications',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -320,6 +369,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_REALTIME_NOTIFICATIONS',
|
||||
description: 'Enables real-time notifications using Supabase Realtime.',
|
||||
category: 'Notifications',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -328,7 +378,9 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_SUPABASE_URL',
|
||||
description: 'Your Supabase project URL.',
|
||||
category: 'Supabase',
|
||||
hint: `Ex. https://your-project.supabase.co`,
|
||||
required: true,
|
||||
type: 'url',
|
||||
validate: ({ value, mode }) => {
|
||||
if (mode === 'development') {
|
||||
return z
|
||||
@@ -356,6 +408,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Supabase anonymous API key.',
|
||||
category: 'Supabase',
|
||||
required: true,
|
||||
type: 'string',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -372,6 +425,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
category: 'Supabase',
|
||||
secret: true,
|
||||
required: true,
|
||||
type: 'string',
|
||||
validate: ({ value, variables }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -396,6 +450,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
category: 'Supabase',
|
||||
secret: true,
|
||||
required: true,
|
||||
type: 'string',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -413,6 +468,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
'Your chosen billing provider. Options: stripe or lemon-squeezy.',
|
||||
category: 'Billing',
|
||||
required: true,
|
||||
type: 'enum',
|
||||
values: ['stripe', 'lemon-squeezy'],
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['stripe', 'lemon-squeezy']).optional().safeParse(value);
|
||||
},
|
||||
@@ -420,7 +477,9 @@ export const envVariables: EnvVariableModel[] = [
|
||||
{
|
||||
name: 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY',
|
||||
description: 'Your Stripe publishable key.',
|
||||
hint: `Ex. pk_test_123456789012345678901234`,
|
||||
category: 'Billing',
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -463,7 +522,9 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'STRIPE_SECRET_KEY',
|
||||
description: 'Your Stripe secret key.',
|
||||
category: 'Billing',
|
||||
hint: `Ex. sk_test_123456789012345678901234`,
|
||||
secret: true,
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -500,7 +561,9 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'STRIPE_WEBHOOK_SECRET',
|
||||
description: 'Your Stripe webhook secret.',
|
||||
category: 'Billing',
|
||||
hint: `Ex. whsec_123456789012345678901234`,
|
||||
secret: true,
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -544,6 +607,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Lemon Squeezy secret key.',
|
||||
category: 'Billing',
|
||||
secret: true,
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -578,6 +642,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'LEMON_SQUEEZY_STORE_ID',
|
||||
description: 'Your Lemon Squeezy store ID.',
|
||||
category: 'Billing',
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -613,6 +678,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Lemon Squeezy signing secret.',
|
||||
category: 'Billing',
|
||||
secret: true,
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -648,6 +714,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your email service provider. Options: nodemailer or resend.',
|
||||
category: 'Email',
|
||||
required: true,
|
||||
type: 'enum',
|
||||
values: ['nodemailer', 'resend'],
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['nodemailer', 'resend']).safeParse(value);
|
||||
},
|
||||
@@ -656,7 +724,9 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'EMAIL_SENDER',
|
||||
description: 'Default sender email address.',
|
||||
category: 'Email',
|
||||
hint: `Ex. "Makerkit <admin@makerkit.dev>"`,
|
||||
required: true,
|
||||
type: 'string',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -668,7 +738,9 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'CONTACT_EMAIL',
|
||||
description: 'Email address for contact form submissions.',
|
||||
category: 'Email',
|
||||
hint: `Ex. "Makerkit <admin@makerkit.dev>"`,
|
||||
required: true,
|
||||
type: 'email',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -682,6 +754,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Resend API key.',
|
||||
category: 'Email',
|
||||
secret: true,
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -707,6 +780,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'EMAIL_HOST',
|
||||
description: 'SMTP host for Nodemailer configuration.',
|
||||
category: 'Email',
|
||||
type: 'string',
|
||||
hint: `Ex. "smtp.example.com"`,
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -731,6 +806,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'EMAIL_PORT',
|
||||
description: 'SMTP port for Nodemailer configuration.',
|
||||
category: 'Email',
|
||||
type: 'number',
|
||||
hint: `Ex. 587 or 465`,
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -756,6 +833,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'EMAIL_USER',
|
||||
description: 'SMTP user for Nodemailer configuration.',
|
||||
category: 'Email',
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -782,6 +860,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'SMTP password for Nodemailer configuration.',
|
||||
category: 'Email',
|
||||
secret: true,
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -804,8 +883,9 @@ export const envVariables: EnvVariableModel[] = [
|
||||
},
|
||||
{
|
||||
name: 'EMAIL_TLS',
|
||||
description: 'Whether to use TLS for SMTP connection.',
|
||||
description: 'Whether to use TLS for SMTP connection. Please check this in your SMTP provider settings.',
|
||||
category: 'Email',
|
||||
type: 'boolean',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -830,6 +910,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'CMS_CLIENT',
|
||||
description: 'Your chosen CMS system. Options: wordpress or keystatic.',
|
||||
category: 'CMS',
|
||||
type: 'enum',
|
||||
values: ['wordpress', 'keystatic'],
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['wordpress', 'keystatic']).optional().safeParse(value);
|
||||
},
|
||||
@@ -838,6 +920,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND',
|
||||
description: 'Your Keystatic storage kind. Options: local, cloud, github.',
|
||||
category: 'CMS',
|
||||
type: 'enum',
|
||||
values: ['local', 'cloud', 'github'],
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -862,6 +946,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO',
|
||||
description: 'Your Keystatic storage repo.',
|
||||
category: 'CMS',
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -889,6 +974,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Keystatic GitHub token.',
|
||||
category: 'CMS',
|
||||
secret: true,
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -915,6 +1001,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'KEYSTATIC_PATH_PREFIX',
|
||||
description: 'Your Keystatic path prefix.',
|
||||
category: 'CMS',
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -933,6 +1020,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH',
|
||||
description: 'Your Keystatic content path.',
|
||||
category: 'CMS',
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -958,6 +1046,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'WORDPRESS_API_URL',
|
||||
description: 'WordPress API URL when using WordPress as CMS.',
|
||||
category: 'CMS',
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -981,6 +1070,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_LOCALES_PATH',
|
||||
description: 'The path to your locales folder.',
|
||||
category: 'Localization',
|
||||
type: 'string',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -996,6 +1086,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_LANGUAGE_PRIORITY',
|
||||
description: 'The priority setting as to how infer the language.',
|
||||
category: 'Localization',
|
||||
type: 'enum',
|
||||
values: ['user', 'application'],
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['user', 'application']).optional().safeParse(value);
|
||||
},
|
||||
@@ -1005,6 +1097,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description:
|
||||
'Enables the version updater to poll the latest version and notify the user.',
|
||||
category: 'Features',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -1013,6 +1106,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS',
|
||||
description: 'The interval in seconds to check for updates.',
|
||||
category: 'Features',
|
||||
type: 'number',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce
|
||||
.number()
|
||||
@@ -1032,6 +1126,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: `ENABLE_REACT_COMPILER`,
|
||||
description: 'Enables the React compiler [experimental]',
|
||||
category: 'Build',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -1040,7 +1135,8 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_MONITORING_PROVIDER',
|
||||
description: 'The monitoring provider to use.',
|
||||
category: 'Monitoring',
|
||||
required: true,
|
||||
type: 'enum',
|
||||
values: ['baselime', 'sentry', 'none'],
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['baselime', 'sentry', '']).optional().safeParse(value);
|
||||
},
|
||||
@@ -1049,6 +1145,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_SENTRY_DSN',
|
||||
description: 'The Sentry DSN to use.',
|
||||
category: 'Monitoring',
|
||||
type: `string`,
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -1073,7 +1170,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_SENTRY_ENVIRONMENT',
|
||||
description: 'The Sentry environment to use.',
|
||||
category: 'Monitoring',
|
||||
required: true,
|
||||
type: 'string',
|
||||
validate: ({ value }) => {
|
||||
return z.string().optional().safeParse(value);
|
||||
},
|
||||
@@ -1082,6 +1179,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_BASELIME_KEY',
|
||||
description: 'The Baselime key to use.',
|
||||
category: 'Monitoring',
|
||||
type: 'string',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
@@ -1107,6 +1205,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'STRIPE_ENABLE_TRIAL_WITHOUT_CC',
|
||||
description: 'Enables trial plans without credit card.',
|
||||
category: 'Billing',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -1115,6 +1214,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_THEME_COLOR',
|
||||
description: 'The default theme color.',
|
||||
category: 'Theme',
|
||||
type: 'string',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
@@ -1132,6 +1232,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'The default theme color for dark mode.',
|
||||
category: 'Theme',
|
||||
required: true,
|
||||
type: 'string',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -1147,6 +1248,17 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX',
|
||||
description: 'Whether to display the terms checkbox during sign-up.',
|
||||
category: 'Features',
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ENABLE_STRICT_CSP',
|
||||
description: 'Enables strict Content Security Policy (CSP) headers.',
|
||||
category: 'Security',
|
||||
required: false,
|
||||
type: 'boolean',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
|
||||
84
apps/dev-tool/app/variables/lib/server-actions.ts
Normal file
84
apps/dev-tool/app/variables/lib/server-actions.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
'use server';
|
||||
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
import { envVariables } from '@/app/variables/lib/env-variables-model';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:url';
|
||||
import { z } from 'zod';
|
||||
|
||||
const Schema = z.object({
|
||||
name: z.string().min(1),
|
||||
value: z.string(),
|
||||
mode: z.enum(['development', 'production']),
|
||||
});
|
||||
|
||||
/**
|
||||
* Update the environment variable in the specified file.
|
||||
* @param props
|
||||
*/
|
||||
export async function updateEnvironmentVariableAction(
|
||||
props: z.infer<typeof Schema>,
|
||||
) {
|
||||
// Validate the input
|
||||
const { name, mode, value } = Schema.parse(props);
|
||||
const root = resolve(process.cwd(), '..');
|
||||
const model = envVariables.find((item) => item.name === name);
|
||||
|
||||
// Determine the source file based on the mode
|
||||
const source = (() => {
|
||||
const isSecret = model?.secret ?? true;
|
||||
|
||||
switch (mode) {
|
||||
case 'development':
|
||||
if (isSecret) {
|
||||
return '.env.local';
|
||||
} else {
|
||||
return '.env.development';
|
||||
}
|
||||
|
||||
case 'production':
|
||||
if (isSecret) {
|
||||
return '.env.production.local';
|
||||
} else {
|
||||
return '.env.production';
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid mode: ${mode}`);
|
||||
}
|
||||
})();
|
||||
|
||||
// check file exists, if not, create it
|
||||
const filePath = `${root}/apps/web/${source}`;
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
writeFileSync(filePath, '', 'utf-8');
|
||||
}
|
||||
|
||||
const sourceEnvFile = readFileSync(`${root}apps/web/${source}`, 'utf-8');
|
||||
|
||||
let updatedEnvFile = '';
|
||||
const isInSourceFile = sourceEnvFile.includes(name);
|
||||
const isCommentedOut = sourceEnvFile.includes(`#${name}=`);
|
||||
|
||||
if (isInSourceFile && !isCommentedOut) {
|
||||
updatedEnvFile = sourceEnvFile.replace(
|
||||
new RegExp(`^${name}=.*`, 'm'),
|
||||
`${name}=${value}`,
|
||||
);
|
||||
} else {
|
||||
// if the key does not exist, append it to the end of the file
|
||||
updatedEnvFile = `${sourceEnvFile}\n${name}=${value}`;
|
||||
}
|
||||
|
||||
// write the updated content back to the file
|
||||
writeFileSync(`${root}/apps/web/${source}`, updatedEnvFile, 'utf-8');
|
||||
|
||||
revalidatePath(`/variables`);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Updated ${name} in "${source}"`,
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,13 @@ export type EnvVariableState = {
|
||||
effectiveValue: string;
|
||||
isOverridden: boolean;
|
||||
effectiveSource: string;
|
||||
isVisible: boolean;
|
||||
validation: {
|
||||
success: boolean;
|
||||
error: {
|
||||
issues: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export type AppEnvState = {
|
||||
|
||||
@@ -25,19 +25,17 @@ export default function VariablesPage({ searchParams }: VariablesPageProps) {
|
||||
|
||||
return (
|
||||
<Page style={'custom'}>
|
||||
<div className={'flex h-screen flex-col overflow-hidden'}>
|
||||
<PageHeader
|
||||
displaySidebarTrigger={false}
|
||||
title={'Environment Variables'}
|
||||
description={
|
||||
<AppBreadcrumbs
|
||||
values={{
|
||||
variables: 'Environment Variables',
|
||||
}}
|
||||
/>
|
||||
'Manage environment variables for your applications. Validate and set them up easily.'
|
||||
}
|
||||
/>
|
||||
|
||||
<PageBody>
|
||||
<div className={'flex flex-col space-y-4 pb-16'}>
|
||||
<PageBody className={'overflow-hidden'}>
|
||||
<div className={'flex h-full flex-1 flex-col space-y-4'}>
|
||||
{apps.map((app) => {
|
||||
const appEnvState = processEnvDefinitions(app, mode);
|
||||
|
||||
@@ -50,6 +48,7 @@ export default function VariablesPage({ searchParams }: VariablesPageProps) {
|
||||
})}
|
||||
</div>
|
||||
</PageBody>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,13 +8,16 @@
|
||||
"format": "prettier --check --write \"**/*.{js,cjs,mjs,ts,tsx,md,json}\""
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^1.3.20",
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@tanstack/react-query": "5.74.4",
|
||||
"ai": "4.3.10",
|
||||
"lucide-react": "^0.503.0",
|
||||
"next": "15.3.1",
|
||||
"nodemailer": "^6.10.1",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0"
|
||||
"react-dom": "19.1.0",
|
||||
"rxjs": "^7.8.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/email-templates": "workspace:*",
|
||||
|
||||
@@ -12,7 +12,7 @@ const defaultLanguage = process.env.NEXT_PUBLIC_DEFAULT_LOCALE ?? 'en';
|
||||
* By default, only the default language is supported.
|
||||
* Add more languages here if needed.
|
||||
*/
|
||||
export const languages: string[] = [defaultLanguage];
|
||||
export const languages: string[] = [defaultLanguage, 'it'];
|
||||
|
||||
/**
|
||||
* The name of the cookie that stores the selected language.
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"@supabase/supabase-js": "2.49.4",
|
||||
"@tanstack/react-query": "5.74.4",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"ai": "4.3.10",
|
||||
"date-fns": "^4.1.0",
|
||||
"lucide-react": "^0.503.0",
|
||||
"next": "15.3.1",
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
"errorSendingCode": "Error sending code. Please try again."
|
||||
},
|
||||
"cookieBanner": {
|
||||
"title": "Hey, we use cookies \uD83C\uDF6A",
|
||||
"title": "Hey, we use cookies 🍪",
|
||||
"description": "This website uses cookies to ensure you get the best experience on our website.",
|
||||
"reject": "Reject",
|
||||
"accept": "Accept"
|
||||
|
||||
@@ -5,22 +5,17 @@ import { type VariantProps, cva } from 'class-variance-authority';
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const badgeVariants = cva(
|
||||
'focus:ring-ring inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-hidden',
|
||||
'focus:ring-ring inline-flex items-center rounded-md border px-1.5 py-0.5 text-xs font-semibold transition-colors focus:ring-2 focus:ring-offset-2 focus:outline-hidden',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
'bg-primary text-primary-foreground hover:bg-primary/80 border-transparent shadow-xs',
|
||||
secondary:
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80 border-transparent',
|
||||
destructive:
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/80 border-transparent shadow-xs',
|
||||
default: 'bg-primary text-primary-foreground border-transparent',
|
||||
secondary: 'bg-secondary text-secondary-foreground border-transparent',
|
||||
destructive: 'text-destructive border-destructive',
|
||||
outline: 'text-foreground',
|
||||
success:
|
||||
'border-transparent bg-green-50 text-green-500 hover:bg-green-50 dark:bg-green-500/20 dark:hover:bg-green-500/20',
|
||||
warning:
|
||||
'border-transparent bg-orange-50 text-orange-500 hover:bg-orange-50 dark:bg-orange-500/20 dark:hover:bg-orange-500/20',
|
||||
info: 'border-transparent bg-blue-50 text-blue-500 hover:bg-blue-50 dark:bg-blue-500/20 dark:hover:bg-blue-500/20',
|
||||
success: 'border-green-500 text-green-500',
|
||||
warning: 'border-orange-500 text-orange-500',
|
||||
info: 'border-blue-500 text-blue-500',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
307
pnpm-lock.yaml
generated
307
pnpm-lock.yaml
generated
@@ -33,12 +33,18 @@ importers:
|
||||
|
||||
apps/dev-tool:
|
||||
dependencies:
|
||||
'@ai-sdk/openai':
|
||||
specifier: ^1.3.20
|
||||
version: 1.3.20(zod@3.24.3)
|
||||
'@hookform/resolvers':
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1(react-hook-form@7.56.1(react@19.1.0))
|
||||
'@tanstack/react-query':
|
||||
specifier: 5.74.4
|
||||
version: 5.74.4(react@19.1.0)
|
||||
ai:
|
||||
specifier: 4.3.10
|
||||
version: 4.3.10(react@19.1.0)(zod@3.24.3)
|
||||
lucide-react:
|
||||
specifier: ^0.503.0
|
||||
version: 0.503.0(react@19.1.0)
|
||||
@@ -54,6 +60,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: 19.1.0
|
||||
version: 19.1.0(react@19.1.0)
|
||||
rxjs:
|
||||
specifier: ^7.8.2
|
||||
version: 7.8.2
|
||||
devDependencies:
|
||||
'@kit/email-templates':
|
||||
specifier: workspace:*
|
||||
@@ -211,6 +220,9 @@ importers:
|
||||
'@tanstack/react-table':
|
||||
specifier: ^8.21.3
|
||||
version: 8.21.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
|
||||
ai:
|
||||
specifier: 4.3.10
|
||||
version: 4.3.10(react@19.1.0)(zod@3.24.3)
|
||||
date-fns:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0
|
||||
@@ -1559,13 +1571,13 @@ importers:
|
||||
dependencies:
|
||||
'@trivago/prettier-plugin-sort-imports':
|
||||
specifier: 5.2.2
|
||||
version: 5.2.2(prettier@3.5.3)
|
||||
version: 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.5.3)(svelte@4.2.19)
|
||||
prettier:
|
||||
specifier: ^3.5.3
|
||||
version: 3.5.3
|
||||
prettier-plugin-tailwindcss:
|
||||
specifier: ^0.6.11
|
||||
version: 0.6.11(@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.5.3))(prettier@3.5.3)
|
||||
version: 0.6.11(@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.5.3)(svelte@4.2.19))(prettier@3.5.3)
|
||||
devDependencies:
|
||||
'@kit/tsconfig':
|
||||
specifier: workspace:*
|
||||
@@ -1588,6 +1600,38 @@ packages:
|
||||
graphql:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/openai@1.3.20':
|
||||
resolution: {integrity: sha512-/DflUy7ROG9k6n6YTXMBFPbujBKnbGY58f3CwvicLvDar9nDAloVnUWd3LUoOxpSVnX8vtQ7ngxF52SLWO6RwQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
|
||||
'@ai-sdk/provider-utils@2.2.7':
|
||||
resolution: {integrity: sha512-kM0xS3GWg3aMChh9zfeM+80vEZfXzR3JEUBdycZLtbRZ2TRT8xOj3WodGHPb06sUK5yD7pAXC/P7ctsi2fvUGQ==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.23.8
|
||||
|
||||
'@ai-sdk/provider@1.1.3':
|
||||
resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/react@1.2.9':
|
||||
resolution: {integrity: sha512-/VYm8xifyngaqFDLXACk/1czDRCefNCdALUyp+kIX6DUIYUWTM93ISoZ+qJ8+3E+FiJAKBQz61o8lIIl+vYtzg==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/ui-utils@1.2.8':
|
||||
resolution: {integrity: sha512-nls/IJCY+ks3Uj6G/agNhXqQeLVqhNfoJbuNgCny+nX2veY5ADB91EcZUqVeQ/ionul2SeUswPY6Q/DxteY29Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.23.8
|
||||
|
||||
'@alloc/quick-lru@5.2.0':
|
||||
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -4325,6 +4369,9 @@ packages:
|
||||
'@types/debug@4.1.12':
|
||||
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
|
||||
|
||||
'@types/diff-match-patch@1.0.36':
|
||||
resolution: {integrity: sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg==}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==}
|
||||
|
||||
@@ -4621,6 +4668,21 @@ packages:
|
||||
peerDependencies:
|
||||
'@urql/core': ^5.0.0
|
||||
|
||||
'@vue/compiler-core@3.5.13':
|
||||
resolution: {integrity: sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==}
|
||||
|
||||
'@vue/compiler-dom@3.5.13':
|
||||
resolution: {integrity: sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==}
|
||||
|
||||
'@vue/compiler-sfc@3.5.13':
|
||||
resolution: {integrity: sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==}
|
||||
|
||||
'@vue/compiler-ssr@3.5.13':
|
||||
resolution: {integrity: sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==}
|
||||
|
||||
'@vue/shared@3.5.13':
|
||||
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==}
|
||||
|
||||
@@ -4709,6 +4771,16 @@ packages:
|
||||
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ai@4.3.10:
|
||||
resolution: {integrity: sha512-jw+ahNu+T4SHj9gtraIKtYhanJI6gj2IZ5BFcfEHgoyQVMln5a5beGjzl/nQSX6FxyLqJ/UBpClRa279EEKK/Q==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
zod: ^3.23.8
|
||||
peerDependenciesMeta:
|
||||
react:
|
||||
optional: true
|
||||
|
||||
ajv-formats@2.1.1:
|
||||
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
|
||||
peerDependencies:
|
||||
@@ -4930,6 +5002,10 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chalk@5.4.1:
|
||||
resolution: {integrity: sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==}
|
||||
engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
|
||||
|
||||
change-case@3.1.0:
|
||||
resolution: {integrity: sha512-2AZp7uJZbYEzRPsFoa+ijKdvp9zsrnnt6+yFokfwEpeJm0xuJDVoxiRCAaTzyJND8GJkofo2IcKWaUZ/OECVzw==}
|
||||
|
||||
@@ -5007,6 +5083,9 @@ packages:
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
code-red@1.0.4:
|
||||
resolution: {integrity: sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==}
|
||||
|
||||
color-convert@1.9.3:
|
||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||
|
||||
@@ -5300,6 +5379,9 @@ packages:
|
||||
devlop@1.1.0:
|
||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
||||
|
||||
diff-match-patch@1.0.5:
|
||||
resolution: {integrity: sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==}
|
||||
|
||||
diff@4.0.2:
|
||||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
@@ -5582,6 +5664,9 @@ packages:
|
||||
estree-walker@2.0.2:
|
||||
resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||
|
||||
esutils@2.0.3:
|
||||
resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -6129,6 +6214,9 @@ packages:
|
||||
is-reference@1.2.1:
|
||||
resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==}
|
||||
|
||||
is-reference@3.0.3:
|
||||
resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==}
|
||||
|
||||
is-regex@1.2.1:
|
||||
resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6242,6 +6330,9 @@ packages:
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
json-schema@0.4.0:
|
||||
resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
@@ -6254,6 +6345,11 @@ packages:
|
||||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
resolution: {integrity: sha512-3QItJOXp2AP1uv7waBkao5nCvhEv+QmJAd38Ybq7wNI74Q+BBmnLn4EDKz6yI9xGAIQoUF87qHt+kc1IVxB4zQ==}
|
||||
engines: {node: ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
|
||||
jsonfile@6.1.0:
|
||||
resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==}
|
||||
|
||||
@@ -6365,6 +6461,9 @@ packages:
|
||||
resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==}
|
||||
engines: {node: '>=6.11.5'}
|
||||
|
||||
locate-character@3.0.0:
|
||||
resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==}
|
||||
|
||||
locate-path@6.0.0:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -6938,6 +7037,9 @@ packages:
|
||||
peberminta@0.9.0:
|
||||
resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==}
|
||||
|
||||
periscopic@3.1.0:
|
||||
resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==}
|
||||
|
||||
pg-int8@1.0.1:
|
||||
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||
engines: {node: '>=4.0.0'}
|
||||
@@ -7851,6 +7953,10 @@ packages:
|
||||
resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
svelte@4.2.19:
|
||||
resolution: {integrity: sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
svgo@3.3.2:
|
||||
resolution: {integrity: sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -7859,6 +7965,11 @@ packages:
|
||||
swap-case@1.1.2:
|
||||
resolution: {integrity: sha512-BAmWG6/bx8syfc6qXPprof3Mn5vQgf5dwdUNJhsNqU9WdPt5P+ES/wQ5bxfijy8zwZgZZHslC3iAsxsuQMCzJQ==}
|
||||
|
||||
swr@2.3.3:
|
||||
resolution: {integrity: sha512-dshNvs3ExOqtZ6kJBaAsabhPdHyeY4P2cKwRCniDVifBMoG/SVI7tfLWqPXriVspf2Rg4tPzXJTnwaihIeFw2A==}
|
||||
peerDependencies:
|
||||
react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
tabbable@6.2.0:
|
||||
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
|
||||
|
||||
@@ -7905,6 +8016,10 @@ packages:
|
||||
thread-stream@3.1.0:
|
||||
resolution: {integrity: sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==}
|
||||
|
||||
throttleit@2.1.0:
|
||||
resolution: {integrity: sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
through@2.3.8:
|
||||
resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==}
|
||||
|
||||
@@ -8349,6 +8464,11 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
zod-to-json-schema@3.24.5:
|
||||
resolution: {integrity: sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==}
|
||||
peerDependencies:
|
||||
zod: ^3.24.1
|
||||
|
||||
zod@3.24.3:
|
||||
resolution: {integrity: sha512-HhY1oqzWCQWuUqvBFnsyrtZRhyPeR7SUGv+C4+MsisMuVfSPx8HpwWqH8tRahSlt6M3PiFAcoeFhZAqIXTxoSg==}
|
||||
|
||||
@@ -8361,6 +8481,40 @@ snapshots:
|
||||
optionalDependencies:
|
||||
graphql: 16.10.0
|
||||
|
||||
'@ai-sdk/openai@1.3.20(zod@3.24.3)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.7(zod@3.24.3)
|
||||
zod: 3.24.3
|
||||
|
||||
'@ai-sdk/provider-utils@2.2.7(zod@3.24.3)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
nanoid: 3.3.11
|
||||
secure-json-parse: 2.7.0
|
||||
zod: 3.24.3
|
||||
|
||||
'@ai-sdk/provider@1.1.3':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/react@1.2.9(react@19.1.0)(zod@3.24.3)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider-utils': 2.2.7(zod@3.24.3)
|
||||
'@ai-sdk/ui-utils': 1.2.8(zod@3.24.3)
|
||||
react: 19.1.0
|
||||
swr: 2.3.3(react@19.1.0)
|
||||
throttleit: 2.1.0
|
||||
optionalDependencies:
|
||||
zod: 3.24.3
|
||||
|
||||
'@ai-sdk/ui-utils@1.2.8(zod@3.24.3)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.7(zod@3.24.3)
|
||||
zod: 3.24.3
|
||||
zod-to-json-schema: 3.24.5(zod@3.24.3)
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
|
||||
'@ampproject/remapping@2.3.0':
|
||||
@@ -11741,7 +11895,7 @@ snapshots:
|
||||
|
||||
'@tootallnate/quickjs-emscripten@0.23.0': {}
|
||||
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.5.3)':
|
||||
'@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.5.3)(svelte@4.2.19)':
|
||||
dependencies:
|
||||
'@babel/generator': 7.27.0
|
||||
'@babel/parser': 7.27.0
|
||||
@@ -11750,6 +11904,9 @@ snapshots:
|
||||
javascript-natural-sort: 0.7.1
|
||||
lodash: 4.17.21
|
||||
prettier: 3.5.3
|
||||
optionalDependencies:
|
||||
'@vue/compiler-sfc': 3.5.13
|
||||
svelte: 4.2.19
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -11846,6 +12003,8 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/ms': 2.1.0
|
||||
|
||||
'@types/diff-match-patch@1.0.36': {}
|
||||
|
||||
'@types/eslint-scope@3.7.7':
|
||||
dependencies:
|
||||
'@types/eslint': 9.6.1
|
||||
@@ -12189,6 +12348,43 @@ snapshots:
|
||||
'@urql/core': 5.1.1(graphql@16.10.0)
|
||||
wonka: 6.3.5
|
||||
|
||||
'@vue/compiler-core@3.5.13':
|
||||
dependencies:
|
||||
'@babel/parser': 7.27.0
|
||||
'@vue/shared': 3.5.13
|
||||
entities: 4.5.0
|
||||
estree-walker: 2.0.2
|
||||
source-map-js: 1.2.1
|
||||
optional: true
|
||||
|
||||
'@vue/compiler-dom@3.5.13':
|
||||
dependencies:
|
||||
'@vue/compiler-core': 3.5.13
|
||||
'@vue/shared': 3.5.13
|
||||
optional: true
|
||||
|
||||
'@vue/compiler-sfc@3.5.13':
|
||||
dependencies:
|
||||
'@babel/parser': 7.27.0
|
||||
'@vue/compiler-core': 3.5.13
|
||||
'@vue/compiler-dom': 3.5.13
|
||||
'@vue/compiler-ssr': 3.5.13
|
||||
'@vue/shared': 3.5.13
|
||||
estree-walker: 2.0.2
|
||||
magic-string: 0.30.17
|
||||
postcss: 8.5.3
|
||||
source-map-js: 1.2.1
|
||||
optional: true
|
||||
|
||||
'@vue/compiler-ssr@3.5.13':
|
||||
dependencies:
|
||||
'@vue/compiler-dom': 3.5.13
|
||||
'@vue/shared': 3.5.13
|
||||
optional: true
|
||||
|
||||
'@vue/shared@3.5.13':
|
||||
optional: true
|
||||
|
||||
'@webassemblyjs/ast@1.14.1':
|
||||
dependencies:
|
||||
'@webassemblyjs/helper-numbers': 1.13.2
|
||||
@@ -12300,6 +12496,18 @@ snapshots:
|
||||
clean-stack: 2.2.0
|
||||
indent-string: 4.0.0
|
||||
|
||||
ai@4.3.10(react@19.1.0)(zod@3.24.3):
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.1.3
|
||||
'@ai-sdk/provider-utils': 2.2.7(zod@3.24.3)
|
||||
'@ai-sdk/react': 1.2.9(react@19.1.0)(zod@3.24.3)
|
||||
'@ai-sdk/ui-utils': 1.2.8(zod@3.24.3)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
jsondiffpatch: 0.6.0
|
||||
zod: 3.24.3
|
||||
optionalDependencies:
|
||||
react: 19.1.0
|
||||
|
||||
ajv-formats@2.1.1(ajv@8.17.1):
|
||||
optionalDependencies:
|
||||
ajv: 8.17.1
|
||||
@@ -12565,6 +12773,8 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
chalk@5.4.1: {}
|
||||
|
||||
change-case@3.1.0:
|
||||
dependencies:
|
||||
camel-case: 3.0.0
|
||||
@@ -12654,6 +12864,15 @@ snapshots:
|
||||
- '@types/react'
|
||||
- '@types/react-dom'
|
||||
|
||||
code-red@1.0.4:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
'@types/estree': 1.0.7
|
||||
acorn: 8.14.1
|
||||
estree-walker: 3.0.3
|
||||
periscopic: 3.1.0
|
||||
optional: true
|
||||
|
||||
color-convert@1.9.3:
|
||||
dependencies:
|
||||
color-name: 1.1.3
|
||||
@@ -12951,6 +13170,8 @@ snapshots:
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
|
||||
diff-match-patch@1.0.5: {}
|
||||
|
||||
diff@4.0.2: {}
|
||||
|
||||
dir-glob@3.0.1:
|
||||
@@ -13151,8 +13372,8 @@ snapshots:
|
||||
'@typescript-eslint/parser': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
|
||||
eslint: 9.25.1(jiti@2.4.2)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-react: 7.37.5(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.25.1(jiti@2.4.2))
|
||||
@@ -13177,7 +13398,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)):
|
||||
eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2)):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.0
|
||||
@@ -13188,22 +13409,22 @@ snapshots:
|
||||
tinyglobby: 0.2.13
|
||||
unrs-resolver: 1.6.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)):
|
||||
eslint-module-utils@2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.25.1(jiti@2.4.2)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
|
||||
eslint: 9.25.1(jiti@2.4.2)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@9.25.1(jiti@2.4.2))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)):
|
||||
eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.8
|
||||
@@ -13214,7 +13435,7 @@ snapshots:
|
||||
doctrine: 2.1.0
|
||||
eslint: 9.25.1(jiti@2.4.2)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2)))(eslint@9.25.1(jiti@2.4.2))
|
||||
eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@9.25.1(jiti@2.4.2))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
@@ -13226,7 +13447,7 @@ snapshots:
|
||||
string.prototype.trimend: 1.0.9
|
||||
tsconfig-paths: 3.15.0
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.30.1(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
|
||||
'@typescript-eslint/parser': 8.31.0(eslint@9.25.1(jiti@2.4.2))(typescript@5.8.3)
|
||||
transitivePeerDependencies:
|
||||
- eslint-import-resolver-typescript
|
||||
- eslint-import-resolver-webpack
|
||||
@@ -13368,6 +13589,11 @@ snapshots:
|
||||
|
||||
estree-walker@2.0.2: {}
|
||||
|
||||
estree-walker@3.0.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
optional: true
|
||||
|
||||
esutils@2.0.3: {}
|
||||
|
||||
event-target-shim@6.0.2: {}
|
||||
@@ -13952,6 +14178,11 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
|
||||
is-reference@3.0.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
optional: true
|
||||
|
||||
is-regex@1.2.1:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -14050,6 +14281,8 @@ snapshots:
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-schema@0.4.0: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json5@1.0.2:
|
||||
@@ -14058,6 +14291,12 @@ snapshots:
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
jsondiffpatch@0.6.0:
|
||||
dependencies:
|
||||
'@types/diff-match-patch': 1.0.36
|
||||
chalk: 5.4.1
|
||||
diff-match-patch: 1.0.5
|
||||
|
||||
jsonfile@6.1.0:
|
||||
dependencies:
|
||||
universalify: 2.0.1
|
||||
@@ -14147,6 +14386,9 @@ snapshots:
|
||||
|
||||
loader-runner@4.3.0: {}
|
||||
|
||||
locate-character@3.0.0:
|
||||
optional: true
|
||||
|
||||
locate-path@6.0.0:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
@@ -14969,6 +15211,13 @@ snapshots:
|
||||
|
||||
peberminta@0.9.0: {}
|
||||
|
||||
periscopic@3.1.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.7
|
||||
estree-walker: 3.0.3
|
||||
is-reference: 3.0.3
|
||||
optional: true
|
||||
|
||||
pg-int8@1.0.1: {}
|
||||
|
||||
pg-protocol@1.8.0: {}
|
||||
@@ -15220,11 +15469,11 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier-plugin-tailwindcss@0.6.11(@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.5.3))(prettier@3.5.3):
|
||||
prettier-plugin-tailwindcss@0.6.11(@trivago/prettier-plugin-sort-imports@5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.5.3)(svelte@4.2.19))(prettier@3.5.3):
|
||||
dependencies:
|
||||
prettier: 3.5.3
|
||||
optionalDependencies:
|
||||
'@trivago/prettier-plugin-sort-imports': 5.2.2(prettier@3.5.3)
|
||||
'@trivago/prettier-plugin-sort-imports': 5.2.2(@vue/compiler-sfc@3.5.13)(prettier@3.5.3)(svelte@4.2.19)
|
||||
|
||||
prettier@3.5.3: {}
|
||||
|
||||
@@ -15959,6 +16208,24 @@ snapshots:
|
||||
|
||||
supports-preserve-symlinks-flag@1.0.0: {}
|
||||
|
||||
svelte@4.2.19:
|
||||
dependencies:
|
||||
'@ampproject/remapping': 2.3.0
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
'@jridgewell/trace-mapping': 0.3.25
|
||||
'@types/estree': 1.0.7
|
||||
acorn: 8.14.1
|
||||
aria-query: 5.3.2
|
||||
axobject-query: 4.1.0
|
||||
code-red: 1.0.4
|
||||
css-tree: 2.3.1
|
||||
estree-walker: 3.0.3
|
||||
is-reference: 3.0.3
|
||||
locate-character: 3.0.0
|
||||
magic-string: 0.30.17
|
||||
periscopic: 3.1.0
|
||||
optional: true
|
||||
|
||||
svgo@3.3.2:
|
||||
dependencies:
|
||||
'@trysound/sax': 0.2.0
|
||||
@@ -15974,6 +16241,12 @@ snapshots:
|
||||
lower-case: 1.1.4
|
||||
upper-case: 1.1.3
|
||||
|
||||
swr@2.3.3(react@19.1.0):
|
||||
dependencies:
|
||||
dequal: 2.0.3
|
||||
react: 19.1.0
|
||||
use-sync-external-store: 1.5.0(react@19.1.0)
|
||||
|
||||
tabbable@6.2.0: {}
|
||||
|
||||
tailwind-merge@3.2.0: {}
|
||||
@@ -16015,6 +16288,8 @@ snapshots:
|
||||
dependencies:
|
||||
real-require: 0.2.0
|
||||
|
||||
throttleit@2.1.0: {}
|
||||
|
||||
through@2.3.8: {}
|
||||
|
||||
tiny-invariant@1.0.6: {}
|
||||
@@ -16519,6 +16794,10 @@ snapshots:
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod-to-json-schema@3.24.5(zod@3.24.3):
|
||||
dependencies:
|
||||
zod: 3.24.3
|
||||
|
||||
zod@3.24.3: {}
|
||||
|
||||
zwitch@2.0.4: {}
|
||||
|
||||
Reference in New Issue
Block a user