Contextual variable validation (#187)
* Added contextual environment variables validation to Dev Tool
This commit is contained in:
committed by
GitHub
parent
68c6d51d33
commit
a3bd62fb11
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Fragment, useState } from 'react';
|
||||
import { Fragment, useCallback, useState } from 'react';
|
||||
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
Copy,
|
||||
Eye,
|
||||
EyeOff,
|
||||
EyeOffIcon,
|
||||
InfoIcon,
|
||||
} from 'lucide-react';
|
||||
|
||||
@@ -39,6 +40,15 @@ import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { AppEnvState, EnvVariableState } from '../lib/types';
|
||||
|
||||
type ValidationResult = {
|
||||
success: boolean;
|
||||
error?: {
|
||||
issues: Array<{ message: string }>;
|
||||
};
|
||||
};
|
||||
|
||||
type VariableRecord = Record<string, string>;
|
||||
|
||||
export function AppEnvironmentVariablesManager({
|
||||
state,
|
||||
}: React.PropsWithChildren<{
|
||||
@@ -97,36 +107,149 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
return value || '(empty)';
|
||||
};
|
||||
|
||||
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: [],
|
||||
};
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const renderVariable = (varState: EnvVariableState) => {
|
||||
const isExpanded = expandedVars[varState.key] ?? false;
|
||||
const isClientBundledValue = varState.key.startsWith('NEXT_PUBLIC_');
|
||||
|
||||
// public variables are always visible
|
||||
const isValueVisible = showValues[varState.key] ?? isClientBundledValue;
|
||||
|
||||
// grab model is it's a kit variable
|
||||
const model = envVariables.find(
|
||||
(variable) => variable.name === varState.key,
|
||||
);
|
||||
|
||||
const allVariables = Object.values(appState.variables).reduce(
|
||||
(acc, variable) => ({
|
||||
...acc,
|
||||
[variable.key]: variable.effectiveValue,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
// Enhanced validation logic to handle both regular and contextual validation
|
||||
let validation: ValidationResult = {
|
||||
success: true,
|
||||
};
|
||||
|
||||
const validation = model?.validate
|
||||
? model.validate({
|
||||
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,
|
||||
})
|
||||
: {
|
||||
success: true,
|
||||
error: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
validation = {
|
||||
success: false,
|
||||
error: {
|
||||
issues: result.error.issues.map((issue) => ({
|
||||
message: issue.message,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const canExpand = varState.definitions.length > 1 || !validation.success;
|
||||
|
||||
@@ -134,12 +257,46 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
<div key={varState.key} className="animate-in fade-in rounded-lg border">
|
||||
<div className="p-4">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1 flex-col gap-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 flex-col gap-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="font-mono text-sm font-semibold">
|
||||
{varState.key}
|
||||
</span>
|
||||
|
||||
{model?.required && <Badge variant="outline">Required</Badge>}
|
||||
|
||||
{varState.effectiveSource === 'MISSING' && (
|
||||
<Badge
|
||||
variant={
|
||||
// Show destructive if required OR if contextual validation dependencies are not met
|
||||
model?.required ||
|
||||
model?.contextualValidation?.dependencies.some((dep) => {
|
||||
const dependencyValue =
|
||||
allVariables[dep.variable] ?? '';
|
||||
|
||||
const shouldValidate = dep.condition(
|
||||
dependencyValue,
|
||||
allVariables,
|
||||
);
|
||||
|
||||
if (!shouldValidate) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !model.contextualValidation!.validate({
|
||||
value: varState.effectiveValue,
|
||||
variables: allVariables,
|
||||
mode: appState.mode,
|
||||
}).success;
|
||||
})
|
||||
? 'destructive'
|
||||
: 'outline'
|
||||
}
|
||||
>
|
||||
Missing
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
{varState.isOverridden && (
|
||||
<Badge variant="warning">Overridden</Badge>
|
||||
)}
|
||||
@@ -147,7 +304,7 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
|
||||
<If condition={model}>
|
||||
{(model) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 py-1">
|
||||
<span className="text-muted-foreground text-xs font-normal">
|
||||
{model.description}
|
||||
</span>
|
||||
@@ -238,33 +395,35 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
</Badge>
|
||||
</If>
|
||||
|
||||
<Badge
|
||||
variant={'outline'}
|
||||
className={cn({
|
||||
'text-destructive':
|
||||
varState.effectiveSource === '.env.production',
|
||||
})}
|
||||
>
|
||||
{varState.effectiveSource}
|
||||
<If condition={varState.effectiveSource !== 'MISSING'}>
|
||||
<Badge
|
||||
variant={'outline'}
|
||||
className={cn({
|
||||
'text-destructive':
|
||||
varState.effectiveSource === '.env.production',
|
||||
})}
|
||||
>
|
||||
{varState.effectiveSource}
|
||||
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="ml-2 h-3 w-3" />
|
||||
</TooltipTrigger>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<InfoIcon className="ml-2 h-3 w-3" />
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent>
|
||||
{varState.effectiveSource === '.env.local'
|
||||
? `These variables are specific to this machine and are not committed`
|
||||
: varState.effectiveSource === '.env.development'
|
||||
? `These variables are only being used during development`
|
||||
: varState.effectiveSource === '.env'
|
||||
? `These variables are shared under all modes`
|
||||
: `These variables are only used in production mode`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Badge>
|
||||
<TooltipContent>
|
||||
{varState.effectiveSource === '.env.local'
|
||||
? `These variables are specific to this machine and are not committed`
|
||||
: varState.effectiveSource === '.env.development'
|
||||
? `These variables are only being used during development`
|
||||
: varState.effectiveSource === '.env'
|
||||
? `These variables are shared under all modes`
|
||||
: `These variables are only used in production mode`}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Badge>
|
||||
</If>
|
||||
|
||||
<If condition={varState.isOverridden}>
|
||||
<Badge variant="warning">
|
||||
@@ -313,13 +472,45 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
</Heading>
|
||||
|
||||
<Alert variant="destructive">
|
||||
<AlertTitle>Invalid Value</AlertTitle>
|
||||
<AlertTitle>
|
||||
{varState.effectiveSource === 'MISSING'
|
||||
? 'Missing Required Variable'
|
||||
: 'Invalid Value'}
|
||||
</AlertTitle>
|
||||
|
||||
<AlertDescription>
|
||||
The value for {varState.key} is invalid:
|
||||
<pre>
|
||||
<code>{JSON.stringify(validation, null, 2)}</code>
|
||||
</pre>
|
||||
<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) => (
|
||||
<div key={index} className="text-sm">
|
||||
• {issue.message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Display dependency information if available */}
|
||||
{model?.contextualValidation?.dependencies && (
|
||||
<div className="mt-4 space-y-1">
|
||||
<div className="font-medium">Dependencies:</div>
|
||||
|
||||
{model.contextualValidation.dependencies.map(
|
||||
(dep, index) => (
|
||||
<div key={index} className="text-sm">
|
||||
• Requires valid {dep.variable.toUpperCase()}{' '}
|
||||
when {dep.message}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
@@ -401,30 +592,63 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
}
|
||||
|
||||
if (invalidVars) {
|
||||
const allVariables = Object.values(appState.variables).reduce(
|
||||
(acc, variable) => ({
|
||||
...acc,
|
||||
[variable.key]: variable.effectiveValue,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
const allVariables = getEffectiveVariablesValue(appState);
|
||||
|
||||
const hasError =
|
||||
model && model.validate
|
||||
? !model.validate({
|
||||
value: varState.effectiveValue,
|
||||
variables: allVariables,
|
||||
mode: appState.mode,
|
||||
}).success
|
||||
: false;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasError && isInSearch) return true;
|
||||
}
|
||||
|
||||
if (isInSearch) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const groups = Object.values(appState.variables)
|
||||
// Update groups to use allVarsWithValidation instead of appState.variables
|
||||
const groups = Object.values(allVarsWithValidation)
|
||||
.filter(filterVariable)
|
||||
.reduce(
|
||||
(acc, variable) => {
|
||||
@@ -445,7 +669,7 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-8">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
<div className="flex items-center">
|
||||
<div className="flex w-full space-x-2">
|
||||
<div>
|
||||
@@ -501,7 +725,7 @@ function EnvList({ appState }: { appState: AppEnvState }) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1">
|
||||
<div className="flex flex-col">
|
||||
<Summary appState={appState} />
|
||||
|
||||
{groups.map((group) => (
|
||||
@@ -561,34 +785,13 @@ function FilterSwitcher(props: {
|
||||
invalid: boolean;
|
||||
};
|
||||
}) {
|
||||
const router = useRouter();
|
||||
|
||||
const secretVars = props.filters.secret;
|
||||
const publicVars = props.filters.public;
|
||||
const overriddenVars = props.filters.overridden;
|
||||
const privateVars = props.filters.private;
|
||||
const invalidVars = props.filters.invalid;
|
||||
|
||||
const handleFilterChange = (key: string, value: boolean) => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const path = window.location.pathname;
|
||||
|
||||
if (key === 'all' && value) {
|
||||
searchParams.delete('secret');
|
||||
searchParams.delete('public');
|
||||
searchParams.delete('overridden');
|
||||
searchParams.delete('private');
|
||||
searchParams.delete('invalid');
|
||||
} else {
|
||||
if (!value) {
|
||||
searchParams.delete(key);
|
||||
} else {
|
||||
searchParams.set(key, 'true');
|
||||
}
|
||||
}
|
||||
|
||||
router.push(`${path}?${searchParams.toString()}`);
|
||||
};
|
||||
const handleFilterChange = useUpdateFilteredVariables();
|
||||
|
||||
const buttonLabel = () => {
|
||||
const filters = [];
|
||||
@@ -678,44 +881,155 @@ 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();
|
||||
|
||||
const allVariables = varsArray.reduce(
|
||||
// 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,
|
||||
});
|
||||
|
||||
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;
|
||||
|
||||
return (
|
||||
<div className="flex justify-between space-x-4">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Badge variant={'outline'} className={'text-green-500'}>
|
||||
{validVariables} Valid
|
||||
</Badge>
|
||||
|
||||
<Badge
|
||||
variant={'outline'}
|
||||
className={cn({
|
||||
'text-destructive': errors.length > 0,
|
||||
'text-green-500': errors.length === 0,
|
||||
})}
|
||||
>
|
||||
{errors.length} Invalid
|
||||
</Badge>
|
||||
|
||||
<If condition={overridden.length > 0}>
|
||||
<Badge
|
||||
variant={'outline'}
|
||||
className={cn({ 'text-orange-500': overridden.length > 0 })}
|
||||
>
|
||||
{overridden.length} Overridden
|
||||
</Badge>
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<If condition={errors.length > 0}>
|
||||
<Button
|
||||
size={'sm'}
|
||||
variant={'ghost'}
|
||||
onClick={() => handleFilterChange('invalid', true, true)}
|
||||
>
|
||||
<EyeOffIcon className="mr-2 h-3 w-3" />
|
||||
Display Invalid only
|
||||
</Button>
|
||||
</If>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getEffectiveVariablesValue(
|
||||
appState: AppEnvState,
|
||||
): Record<string, string> {
|
||||
const varsArray = Object.values(appState.variables);
|
||||
|
||||
return varsArray.reduce(
|
||||
(acc, variable) => ({
|
||||
...acc,
|
||||
[variable.key]: variable.effectiveValue,
|
||||
}),
|
||||
{},
|
||||
);
|
||||
|
||||
const errors = varsArray.filter((variable) => {
|
||||
const model = envVariables.find((v) => variable.key === v.name);
|
||||
|
||||
const validation =
|
||||
model && model.validate
|
||||
? model.validate({
|
||||
value: variable.effectiveValue,
|
||||
variables: allVariables,
|
||||
mode: appState.mode,
|
||||
})
|
||||
: {
|
||||
success: true,
|
||||
};
|
||||
|
||||
return !validation.success;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Badge variant={errors.length === 0 ? 'success' : 'destructive'}>
|
||||
{errors.length} Errors
|
||||
</Badge>
|
||||
|
||||
<Badge variant={overridden.length === 0 ? 'success' : 'warning'}>
|
||||
{overridden.length} Overridden Variables
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useUpdateFilteredVariables() {
|
||||
const router = useRouter();
|
||||
|
||||
const handleFilterChange = (key: string, value: boolean, reset = false) => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const path = window.location.pathname;
|
||||
|
||||
const resetAll = () => {
|
||||
searchParams.delete('secret');
|
||||
searchParams.delete('public');
|
||||
searchParams.delete('overridden');
|
||||
searchParams.delete('private');
|
||||
searchParams.delete('invalid');
|
||||
};
|
||||
|
||||
if (reset) {
|
||||
resetAll();
|
||||
}
|
||||
|
||||
if (key === 'all' && value) {
|
||||
resetAll();
|
||||
} else {
|
||||
if (!value) {
|
||||
searchParams.delete(key);
|
||||
} else {
|
||||
searchParams.set(key, 'true');
|
||||
}
|
||||
}
|
||||
|
||||
router.push(`${path}?${searchParams.toString()}`);
|
||||
};
|
||||
|
||||
return useCallback(handleFilterChange, [router]);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'server-only';
|
||||
|
||||
import { envVariables } from '@/app/variables/lib/env-variables-model';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { envVariables } from './env-variables-model';
|
||||
import {
|
||||
AppEnvState,
|
||||
EnvFileInfo,
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
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 ContextualValidation = {
|
||||
dependencies: DependencyRule[];
|
||||
validate: (props: {
|
||||
value: string;
|
||||
variables: Record<string, string>;
|
||||
mode: EnvMode;
|
||||
}) => z.SafeParseReturnType<unknown, unknown>;
|
||||
};
|
||||
|
||||
export type EnvVariableModel = {
|
||||
name: string;
|
||||
description: string;
|
||||
secret?: boolean;
|
||||
required?: boolean;
|
||||
category: string;
|
||||
test?: (value: string) => Promise<boolean>;
|
||||
validate?: (props: {
|
||||
@@ -12,6 +28,7 @@ export type EnvVariableModel = {
|
||||
variables: Record<string, string>;
|
||||
mode: EnvMode;
|
||||
}) => z.SafeParseReturnType<unknown, unknown>;
|
||||
contextualValidation?: ContextualValidation;
|
||||
};
|
||||
|
||||
export const envVariables: EnvVariableModel[] = [
|
||||
@@ -20,6 +37,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description:
|
||||
'The URL of your site, used for generating absolute URLs. Must include the protocol.',
|
||||
category: 'Site Configuration',
|
||||
required: true,
|
||||
validate: ({ value, mode }) => {
|
||||
if (mode === 'development') {
|
||||
return z
|
||||
@@ -47,6 +65,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description:
|
||||
"Your product's name, used consistently across the application interface.",
|
||||
category: 'Site Configuration',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -62,6 +81,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description:
|
||||
"The site's title tag content, crucial for SEO and browser display.",
|
||||
category: 'Site Configuration',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -77,6 +97,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description:
|
||||
"Your site's meta description, important for SEO optimization.",
|
||||
category: 'Site Configuration',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -133,6 +154,27 @@ export const envVariables: EnvVariableModel[] = [
|
||||
'Your Cloudflare Captcha secret token for backend verification.',
|
||||
category: 'Security',
|
||||
secret: true,
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'NEXT_PUBLIC_CAPTCHA_SITE_KEY',
|
||||
condition: (value) => {
|
||||
return value !== '';
|
||||
},
|
||||
message:
|
||||
'CAPTCHA_SECRET_TOKEN is required when NEXT_PUBLIC_CAPTCHA_SITE_KEY is set',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The CAPTCHA_SECRET_TOKEN variable must be at least 1 character`,
|
||||
)
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -286,6 +328,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_SUPABASE_URL',
|
||||
description: 'Your Supabase project URL.',
|
||||
category: 'Supabase',
|
||||
required: true,
|
||||
validate: ({ value, mode }) => {
|
||||
if (mode === 'development') {
|
||||
return z
|
||||
@@ -312,6 +355,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_SUPABASE_ANON_KEY',
|
||||
description: 'Your Supabase anonymous API key.',
|
||||
category: 'Supabase',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -319,7 +363,6 @@ export const envVariables: EnvVariableModel[] = [
|
||||
1,
|
||||
`The NEXT_PUBLIC_SUPABASE_ANON_KEY variable must be at least 1 character`,
|
||||
)
|
||||
.optional()
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
@@ -328,6 +371,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Supabase service role key (keep this secret!).',
|
||||
category: 'Supabase',
|
||||
secret: true,
|
||||
required: true,
|
||||
validate: ({ value, variables }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -335,7 +379,6 @@ export const envVariables: EnvVariableModel[] = [
|
||||
1,
|
||||
`The SUPABASE_SERVICE_ROLE_KEY variable must be at least 1 character`,
|
||||
)
|
||||
.optional()
|
||||
.refine(
|
||||
(value) => {
|
||||
return value !== variables['NEXT_PUBLIC_SUPABASE_ANON_KEY'];
|
||||
@@ -352,6 +395,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Secret key for Supabase webhook verification.',
|
||||
category: 'Supabase',
|
||||
secret: true,
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -368,6 +412,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description:
|
||||
'Your chosen billing provider. Options: stripe or lemon-squeezy.',
|
||||
category: 'Billing',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['stripe', 'lemon-squeezy']).optional().safeParse(value);
|
||||
},
|
||||
@@ -376,6 +421,33 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY',
|
||||
description: 'Your Stripe publishable key.',
|
||||
category: 'Billing',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'NEXT_PUBLIC_BILLING_PROVIDER',
|
||||
condition: (value) => value === 'stripe',
|
||||
message:
|
||||
'NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY is required when NEXT_PUBLIC_BILLING_PROVIDER is set to "stripe"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY variable must be at least 1 character`,
|
||||
)
|
||||
.refine(
|
||||
(value) => {
|
||||
return value.startsWith('pk_');
|
||||
},
|
||||
{
|
||||
message: `The NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY variable must start with pk_`,
|
||||
},
|
||||
)
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -392,6 +464,30 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Stripe secret key.',
|
||||
category: 'Billing',
|
||||
secret: true,
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'NEXT_PUBLIC_BILLING_PROVIDER',
|
||||
condition: (value) => value === 'stripe',
|
||||
message:
|
||||
'STRIPE_SECRET_KEY is required when NEXT_PUBLIC_BILLING_PROVIDER is set to "stripe"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(1, `The STRIPE_SECRET_KEY variable must be at least 1 character`)
|
||||
.refine(
|
||||
(value) => {
|
||||
return value.startsWith('sk_') || value.startsWith('rk_');
|
||||
},
|
||||
{
|
||||
message: `The STRIPE_SECRET_KEY variable must start with sk_ or rk_`,
|
||||
},
|
||||
)
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -405,6 +501,33 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Stripe webhook secret.',
|
||||
category: 'Billing',
|
||||
secret: true,
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'NEXT_PUBLIC_BILLING_PROVIDER',
|
||||
condition: (value) => value === 'stripe',
|
||||
message:
|
||||
'STRIPE_WEBHOOK_SECRET is required when NEXT_PUBLIC_BILLING_PROVIDER is set to "stripe"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The STRIPE_WEBHOOK_SECRET variable must be at least 1 character`,
|
||||
)
|
||||
.refine(
|
||||
(value) => {
|
||||
return value.startsWith('whsec_');
|
||||
},
|
||||
{
|
||||
message: `The STRIPE_WEBHOOK_SECRET variable must start with whsec_`,
|
||||
},
|
||||
)
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -421,6 +544,25 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Lemon Squeezy secret key.',
|
||||
category: 'Billing',
|
||||
secret: true,
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'NEXT_PUBLIC_BILLING_PROVIDER',
|
||||
condition: (value) => value === 'lemon-squeezy',
|
||||
message:
|
||||
'LEMON_SQUEEZY_SECRET_KEY is required when NEXT_PUBLIC_BILLING_PROVIDER is set to "lemon-squeezy"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The LEMON_SQUEEZY_SECRET_KEY variable must be at least 1 character`,
|
||||
)
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -436,6 +578,25 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'LEMON_SQUEEZY_STORE_ID',
|
||||
description: 'Your Lemon Squeezy store ID.',
|
||||
category: 'Billing',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'NEXT_PUBLIC_BILLING_PROVIDER',
|
||||
condition: (value) => value === 'lemon-squeezy',
|
||||
message:
|
||||
'LEMON_SQUEEZY_STORE_ID is required when NEXT_PUBLIC_BILLING_PROVIDER is set to "lemon-squeezy"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The LEMON_SQUEEZY_STORE_ID variable must be at least 1 character`,
|
||||
)
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -452,6 +613,25 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Lemon Squeezy signing secret.',
|
||||
category: 'Billing',
|
||||
secret: true,
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'NEXT_PUBLIC_BILLING_PROVIDER',
|
||||
condition: (value) => value === 'lemon-squeezy',
|
||||
message:
|
||||
'LEMON_SQUEEZY_SIGNING_SECRET is required when NEXT_PUBLIC_BILLING_PROVIDER is set to "lemon-squeezy"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The LEMON_SQUEEZY_SIGNING_SECRET variable must be at least 1 character`,
|
||||
)
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -467,14 +647,16 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'MAILER_PROVIDER',
|
||||
description: 'Your email service provider. Options: nodemailer or resend.',
|
||||
category: 'Email',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['nodemailer', 'resend']).optional().safeParse(value);
|
||||
return z.enum(['nodemailer', 'resend']).safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EMAIL_SENDER',
|
||||
description: 'Default sender email address.',
|
||||
category: 'Email',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -486,6 +668,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'CONTACT_EMAIL',
|
||||
description: 'Email address for contact form submissions.',
|
||||
category: 'Email',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -499,42 +682,99 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Resend API key.',
|
||||
category: 'Email',
|
||||
secret: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(1, `The RESEND_API_KEY variable must be at least 1 character`)
|
||||
.safeParse(value);
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'MAILER_PROVIDER',
|
||||
condition: (value) => value === 'resend',
|
||||
message:
|
||||
'RESEND_API_KEY is required when MAILER_PROVIDER is set to "resend"',
|
||||
},
|
||||
],
|
||||
validate: ({ value, variables }) => {
|
||||
if (variables['MAILER_PROVIDER'] === 'resend') {
|
||||
return z
|
||||
.string()
|
||||
.min(1, `The RESEND_API_KEY variable must be at least 1 character`)
|
||||
.safeParse(value);
|
||||
}
|
||||
|
||||
return z.string().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EMAIL_HOST',
|
||||
description: 'SMTP host for Nodemailer configuration.',
|
||||
category: 'Email',
|
||||
validate: ({ value }) => {
|
||||
return z.string().safeParse(value);
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'MAILER_PROVIDER',
|
||||
condition: (value) => value === 'nodemailer',
|
||||
message:
|
||||
'EMAIL_HOST is required when MAILER_PROVIDER is set to "nodemailer"',
|
||||
},
|
||||
],
|
||||
validate: ({ value, variables }) => {
|
||||
if (variables['MAILER_PROVIDER'] === 'nodemailer') {
|
||||
return z
|
||||
.string()
|
||||
.min(1, 'The EMAIL_HOST variable must be at least 1 character')
|
||||
.safeParse(value);
|
||||
}
|
||||
return z.string().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EMAIL_PORT',
|
||||
description: 'SMTP port for Nodemailer configuration.',
|
||||
category: 'Email',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce
|
||||
.number()
|
||||
.min(1, `The EMAIL_PORT variable must be at least 1 character`)
|
||||
.max(65535, `The EMAIL_PORT variable must be at most 65535`)
|
||||
.safeParse(value);
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'MAILER_PROVIDER',
|
||||
condition: (value) => value === 'nodemailer',
|
||||
message:
|
||||
'EMAIL_PORT is required when MAILER_PROVIDER is set to "nodemailer"',
|
||||
},
|
||||
],
|
||||
validate: ({ value, variables }) => {
|
||||
if (variables['MAILER_PROVIDER'] === 'nodemailer') {
|
||||
return z.coerce
|
||||
.number()
|
||||
.min(1, 'The EMAIL_PORT variable must be at least 1')
|
||||
.max(65535, 'The EMAIL_PORT variable must be at most 65535')
|
||||
.safeParse(value);
|
||||
}
|
||||
return z.coerce.number().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EMAIL_USER',
|
||||
description: 'SMTP user for Nodemailer configuration.',
|
||||
category: 'Email',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(1, `The EMAIL_USER variable must be at least 1 character`)
|
||||
.safeParse(value);
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'MAILER_PROVIDER',
|
||||
condition: (value) => value === 'nodemailer',
|
||||
message:
|
||||
'EMAIL_USER is required when MAILER_PROVIDER is set to "nodemailer"',
|
||||
},
|
||||
],
|
||||
validate: ({ value, variables }) => {
|
||||
if (variables['MAILER_PROVIDER'] === 'nodemailer') {
|
||||
return z
|
||||
.string()
|
||||
.min(1, 'The EMAIL_USER variable must be at least 1 character')
|
||||
.safeParse(value);
|
||||
}
|
||||
|
||||
return z.string().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -542,17 +782,46 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'SMTP password for Nodemailer configuration.',
|
||||
category: 'Email',
|
||||
secret: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(1, `The EMAIL_PASSWORD variable must be at least 1 character`)
|
||||
.safeParse(value);
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'MAILER_PROVIDER',
|
||||
condition: (value) => value === 'nodemailer',
|
||||
message:
|
||||
'EMAIL_PASSWORD is required when MAILER_PROVIDER is set to "nodemailer"',
|
||||
},
|
||||
],
|
||||
validate: ({ value, variables }) => {
|
||||
if (variables['MAILER_PROVIDER'] === 'nodemailer') {
|
||||
return z
|
||||
.string()
|
||||
.min(1, 'The EMAIL_PASSWORD variable must be at least 1 character')
|
||||
.safeParse(value);
|
||||
}
|
||||
return z.string().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'EMAIL_TLS',
|
||||
description: 'Whether to use TLS for SMTP connection.',
|
||||
category: 'Email',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'MAILER_PROVIDER',
|
||||
condition: (value) => value === 'nodemailer',
|
||||
message:
|
||||
'EMAIL_TLS is required when MAILER_PROVIDER is set to "nodemailer"',
|
||||
},
|
||||
],
|
||||
validate: ({ value, variables }) => {
|
||||
if (variables['MAILER_PROVIDER'] === 'nodemailer') {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
}
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
@@ -569,23 +838,50 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND',
|
||||
description: 'Your Keystatic storage kind. Options: local, cloud, github.',
|
||||
category: 'CMS',
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['local', 'cloud', 'github']).optional().safeParse(value);
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'CMS_CLIENT',
|
||||
condition: (value) => value === 'keystatic',
|
||||
message:
|
||||
'NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND is required when CMS_CLIENT is set to "keystatic"',
|
||||
},
|
||||
],
|
||||
validate: ({ value, variables }) => {
|
||||
if (variables['CMS_CLIENT'] === 'keystatic') {
|
||||
return z
|
||||
.enum(['local', 'cloud', 'github'])
|
||||
.optional()
|
||||
.safeParse(value);
|
||||
}
|
||||
return z.enum(['local', 'cloud', 'github']).optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO',
|
||||
description: 'Your Keystatic storage repo.',
|
||||
category: 'CMS',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO variable must be at least 1 character`,
|
||||
)
|
||||
.optional()
|
||||
.safeParse(value);
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'CMS_CLIENT',
|
||||
condition: (value, variables) =>
|
||||
value === 'keystatic' &&
|
||||
variables['NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND'] === 'github',
|
||||
message:
|
||||
'NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO is required when CMS_CLIENT is set to "keystatic"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The NEXT_PUBLIC_KEYSTATIC_STORAGE_REPO variable must be at least 1 character`,
|
||||
)
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -593,58 +889,94 @@ export const envVariables: EnvVariableModel[] = [
|
||||
description: 'Your Keystatic GitHub token.',
|
||||
category: 'CMS',
|
||||
secret: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The KEYSTATIC_GITHUB_TOKEN variable must be at least 1 character`,
|
||||
)
|
||||
.optional()
|
||||
.safeParse(value);
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'CMS_CLIENT',
|
||||
condition: (value, variables) =>
|
||||
value === 'keystatic' &&
|
||||
variables['NEXT_PUBLIC_KEYSTATIC_STORAGE_KIND'] === 'github',
|
||||
message:
|
||||
'KEYSTATIC_GITHUB_TOKEN is required when CMS_CLIENT is set to "keystatic"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The KEYSTATIC_GITHUB_TOKEN variable must be at least 1 character`,
|
||||
)
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'KEYSTATIC_PATH_PREFIX',
|
||||
description: 'Your Keystatic path prefix.',
|
||||
category: 'CMS',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The KEYSTATIC_PATH_PREFIX variable must be at least 1 character`,
|
||||
)
|
||||
.optional()
|
||||
.safeParse(value);
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'CMS_CLIENT',
|
||||
condition: (value) => value === 'keystatic',
|
||||
message:
|
||||
'KEYSTATIC_PATH_PREFIX is required when CMS_CLIENT is set to "keystatic"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH',
|
||||
description: 'Your Keystatic content path.',
|
||||
category: 'CMS',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH variable must be at least 1 character`,
|
||||
)
|
||||
.optional()
|
||||
.safeParse(value);
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'CMS_CLIENT',
|
||||
condition: (value) => value === 'keystatic',
|
||||
message:
|
||||
'NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH is required when CMS_CLIENT is set to "keystatic"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The NEXT_PUBLIC_KEYSTATIC_CONTENT_PATH variable must be at least 1 character`,
|
||||
)
|
||||
.optional()
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'WORDPRESS_API_URL',
|
||||
description: 'WordPress API URL when using WordPress as CMS.',
|
||||
category: 'CMS',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.url({
|
||||
message: `The WORDPRESS_API_URL variable must be a valid URL`,
|
||||
})
|
||||
.safeParse(value);
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'CMS_CLIENT',
|
||||
condition: (value) => value === 'wordpress',
|
||||
message:
|
||||
'WORDPRESS_API_URL is required when CMS_CLIENT is set to "wordpress"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.url({
|
||||
message: `The WORDPRESS_API_URL variable must be a valid URL`,
|
||||
})
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -679,45 +1011,6 @@ export const envVariables: EnvVariableModel[] = [
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `ENABLE_REACT_COMPILER`,
|
||||
description: 'Enables the React compiler [experimental]',
|
||||
category: 'Features',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_MONITORING_PROVIDER',
|
||||
description: 'The monitoring provider to use.',
|
||||
category: 'Monitoring',
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['baselime', 'sentry']).optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_BASELIME_KEY',
|
||||
description: 'The Baselime key to use.',
|
||||
category: 'Monitoring',
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The NEXT_PUBLIC_BASELIME_KEY variable must be at least 1 character`,
|
||||
)
|
||||
.optional()
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'STRIPE_ENABLE_TRIAL_WITHOUT_CC',
|
||||
description: 'Enables trial plans without credit card.',
|
||||
category: 'Billing',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_VERSION_UPDATER_REFETCH_INTERVAL_SECONDS',
|
||||
description: 'The interval in seconds to check for updates.',
|
||||
@@ -737,10 +1030,61 @@ export const envVariables: EnvVariableModel[] = [
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: `ENABLE_REACT_COMPILER`,
|
||||
description: 'Enables the React compiler [experimental]',
|
||||
category: 'Build',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_MONITORING_PROVIDER',
|
||||
description: 'The monitoring provider to use.',
|
||||
category: 'Monitoring',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z.enum(['baselime', 'sentry', '']).optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_BASELIME_KEY',
|
||||
description: 'The Baselime key to use.',
|
||||
category: 'Monitoring',
|
||||
contextualValidation: {
|
||||
dependencies: [
|
||||
{
|
||||
variable: 'NEXT_PUBLIC_MONITORING_PROVIDER',
|
||||
condition: (value) => value === 'baselime',
|
||||
message:
|
||||
'NEXT_PUBLIC_BASELIME_KEY is required when NEXT_PUBLIC_MONITORING_PROVIDER is set to "baselime"',
|
||||
},
|
||||
],
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
.min(
|
||||
1,
|
||||
`The NEXT_PUBLIC_BASELIME_KEY variable must be at least 1 character`,
|
||||
)
|
||||
.optional()
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'STRIPE_ENABLE_TRIAL_WITHOUT_CC',
|
||||
description: 'Enables trial plans without credit card.',
|
||||
category: 'Billing',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_THEME_COLOR',
|
||||
description: 'The default theme color.',
|
||||
category: 'Theme',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -756,6 +1100,7 @@ export const envVariables: EnvVariableModel[] = [
|
||||
name: 'NEXT_PUBLIC_THEME_COLOR_DARK',
|
||||
description: 'The default theme color for dark mode.',
|
||||
category: 'Theme',
|
||||
required: true,
|
||||
validate: ({ value }) => {
|
||||
return z
|
||||
.string()
|
||||
@@ -767,4 +1112,12 @@ export const envVariables: EnvVariableModel[] = [
|
||||
.safeParse(value);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'NEXT_PUBLIC_DISPLAY_TERMS_AND_CONDITIONS_CHECKBOX',
|
||||
description: 'Whether to display the terms checkbox during sign-up.',
|
||||
category: 'Features',
|
||||
validate: ({ value }) => {
|
||||
return z.coerce.boolean().optional().safeParse(value);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user