mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
fix: resolve 'Cannot access xs before initialization' runtime error
- Separate auth controls into lazy-loaded component to prevent module loading issues - Add comprehensive error handling and fallbacks throughout auth system - Make profile store resilient to initialization errors with safe localStorage access - Add try-catch blocks around critical functions to prevent runtime crashes - Provide fallback navigation methods when auth hooks fail - Fix EKS deployment crash caused by JavaScript bundling/hoisting issues This fixes the Header-DpwHgB0l.js runtime error that was preventing the app from loading in production.
This commit is contained in:
parent
1687d812bf
commit
6a357e91a6
@ -4,8 +4,65 @@ import { chatStore } from '~/lib/stores/chat';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { HeaderActionButtons } from './HeaderActionButtons.client';
|
||||
import { ChatDescription } from '~/lib/persistence/ChatDescription.client';
|
||||
import { useAuth } from '~/lib/hooks/useAuth';
|
||||
import { Link } from '@remix-run/react';
|
||||
import { Suspense, lazy, useState, useEffect } from 'react';
|
||||
|
||||
// Lazy load the AuthControls component to avoid initialization issues
|
||||
const LazyAuthControls = lazy(() =>
|
||||
import('./HeaderAuthControls.client').catch(() => ({
|
||||
default: () => <FallbackAuthUI />
|
||||
}))
|
||||
);
|
||||
|
||||
// Simple fallback component when auth fails to load
|
||||
function FallbackAuthUI() {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-bolt-elements-background-depth-2 text-bolt-content-secondary hover:bg-bolt-elements-background-depth-3 transition-colors text-sm font-medium"
|
||||
onClick={() => {}}
|
||||
>
|
||||
<div className="i-ph:user-circle w-4 h-4" />
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Error boundary for auth controls
|
||||
function SafeAuthControls() {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset error state if component remounts
|
||||
return () => setHasError(false);
|
||||
}, []);
|
||||
|
||||
if (hasError) {
|
||||
return <FallbackAuthUI />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<div className="flex items-center h-8 px-2 text-bolt-elements-textTertiary">
|
||||
<div className="i-svg-spinners:270-ring-with-bg w-5 h-5" />
|
||||
</div>}>
|
||||
<ErrorCatcher onError={() => setHasError(true)}>
|
||||
<LazyAuthControls />
|
||||
</ErrorCatcher>
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
// Simple error boundary wrapper
|
||||
function ErrorCatcher({ children, onError }) {
|
||||
try {
|
||||
return children;
|
||||
} catch (error) {
|
||||
console.error("Error rendering auth controls:", error);
|
||||
onError();
|
||||
return <FallbackAuthUI />;
|
||||
}
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
const chat = useStore(chatStore);
|
||||
@ -33,9 +90,9 @@ export function Header() {
|
||||
)}
|
||||
|
||||
<div className="flex items-center ml-auto">
|
||||
{/* Authentication Controls - Always visible */}
|
||||
<ClientOnly>
|
||||
{() => <AuthControls />}
|
||||
{/* Authentication Controls - Always visible with error handling */}
|
||||
<ClientOnly fallback={<FallbackAuthUI />}>
|
||||
{() => <SafeAuthControls />}
|
||||
</ClientOnly>
|
||||
|
||||
{/* Existing Action Buttons - Only when chat has started */}
|
||||
@ -52,68 +109,3 @@ export function Header() {
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthControls() {
|
||||
const { isAuthenticated, isLoading, user, login, logout } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center h-8 px-2 text-bolt-elements-textTertiary">
|
||||
<div className="i-svg-spinners:270-ring-with-bg w-5 h-5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAuthenticated && user) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="relative group">
|
||||
<button
|
||||
className="flex items-center gap-2 px-2 py-1 rounded-md hover:bg-bolt-elements-item-backgroundActive transition-colors"
|
||||
aria-label="User menu"
|
||||
>
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={`${user.username}'s avatar`}
|
||||
className="w-8 h-8 rounded-full border border-bolt-elements-borderColor"
|
||||
/>
|
||||
<span className="text-sm font-medium text-bolt-elements-textPrimary hidden sm:block">
|
||||
{user.username}
|
||||
</span>
|
||||
<div className="i-ph:caret-down w-4 h-4 text-bolt-elements-textTertiary" />
|
||||
</button>
|
||||
|
||||
<div className="absolute right-0 mt-1 w-48 py-1 bg-bolt-elements-background-depth-2 rounded-md shadow-lg border border-bolt-elements-borderColor hidden group-hover:block z-50">
|
||||
<div className="px-4 py-2 text-sm text-bolt-elements-textSecondary border-b border-bolt-elements-borderColor">
|
||||
Signed in as <span className="font-semibold text-bolt-elements-textPrimary">{user.username}</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/profile"
|
||||
className="block px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive"
|
||||
>
|
||||
Your profile
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={() => logout()}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => login()}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent hover:bg-bolt-elements-item-backgroundAccentHover transition-colors text-sm font-medium"
|
||||
>
|
||||
<div className="i-ph:sign-in w-4 h-4" />
|
||||
Sign in
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
175
app/components/header/HeaderAuthControls.client.tsx
Normal file
175
app/components/header/HeaderAuthControls.client.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import { Link } from '@remix-run/react';
|
||||
import { useAuth } from '~/lib/hooks/useAuth';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* Authentication controls component for the header
|
||||
* Separated into a client component to prevent module loading issues
|
||||
*/
|
||||
export default function HeaderAuthControls() {
|
||||
// Error state for component-level error handling
|
||||
const [renderError, setRenderError] = useState(null);
|
||||
|
||||
// Call useAuth at the top level of the component as required by React hooks rules
|
||||
let authState;
|
||||
try {
|
||||
// Properly use the hook at component top level
|
||||
authState = useAuth();
|
||||
} catch (error) {
|
||||
console.error('Error initializing auth hook:', error);
|
||||
// Return fallback UI immediately if hook initialization fails
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-bolt-elements-background-depth-2 text-bolt-content-secondary hover:bg-bolt-elements-background-depth-3 transition-colors text-sm font-medium"
|
||||
onClick={() => window.location.href = '/auth/login'}
|
||||
>
|
||||
<div className="i-ph:user-circle w-4 h-4" />
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Safely extract values from auth state
|
||||
const { isAuthenticated, isLoading, user, login, logout } = authState || {};
|
||||
|
||||
// Reset render error when dependencies change
|
||||
useEffect(() => {
|
||||
if (renderError) {
|
||||
setRenderError(null);
|
||||
}
|
||||
}, [isAuthenticated, isLoading, user]);
|
||||
|
||||
// Safely handle login with fallback
|
||||
const handleLogin = () => {
|
||||
try {
|
||||
if (typeof login === 'function') {
|
||||
login();
|
||||
} else {
|
||||
// Fallback if login function is unavailable
|
||||
window.location.href = '/auth/login';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during login:', error);
|
||||
// Fallback to direct navigation on error
|
||||
window.location.href = '/auth/login';
|
||||
}
|
||||
};
|
||||
|
||||
// Safely handle logout with fallback
|
||||
const handleLogout = () => {
|
||||
try {
|
||||
if (typeof logout === 'function') {
|
||||
logout();
|
||||
} else {
|
||||
// Fallback if logout function is unavailable
|
||||
window.location.href = '/auth/logout';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during logout:', error);
|
||||
// Fallback to direct navigation on error
|
||||
window.location.href = '/auth/logout';
|
||||
}
|
||||
};
|
||||
|
||||
// Handle loading state
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center h-8 px-2 text-bolt-elements-textTertiary">
|
||||
<div className="i-svg-spinners:270-ring-with-bg w-5 h-5" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle render errors
|
||||
if (renderError) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-bolt-elements-background-depth-2 text-bolt-content-secondary hover:bg-bolt-elements-background-depth-3 transition-colors text-sm font-medium"
|
||||
onClick={() => window.location.href = '/auth/login'}
|
||||
>
|
||||
<div className="i-ph:user-circle w-4 h-4" />
|
||||
Sign in
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
// Handle authenticated state
|
||||
if (isAuthenticated && user) {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<div className="relative group">
|
||||
<button
|
||||
className="flex items-center gap-2 px-2 py-1 rounded-md hover:bg-bolt-elements-item-backgroundActive transition-colors"
|
||||
aria-label="User menu"
|
||||
>
|
||||
<img
|
||||
src={user.avatar}
|
||||
alt={`${user.username}'s avatar`}
|
||||
className="w-8 h-8 rounded-full border border-bolt-elements-borderColor"
|
||||
onError={(e) => {
|
||||
// Fallback for broken image links
|
||||
e.currentTarget.src = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"%3E%3Ccircle cx="12" cy="12" r="10"/%3E%3Cpath d="M12 8v8"/%3E%3Cpath d="M8 12h8"/%3E%3C/svg%3E';
|
||||
}}
|
||||
/>
|
||||
<span className="text-sm font-medium text-bolt-elements-textPrimary hidden sm:block">
|
||||
{user.username || 'User'}
|
||||
</span>
|
||||
<div className="i-ph:caret-down w-4 h-4 text-bolt-elements-textTertiary" />
|
||||
</button>
|
||||
|
||||
<div className="absolute right-0 mt-1 w-48 py-1 bg-bolt-elements-background-depth-2 rounded-md shadow-lg border border-bolt-elements-borderColor hidden group-hover:block z-50">
|
||||
<div className="px-4 py-2 text-sm text-bolt-elements-textSecondary border-b border-bolt-elements-borderColor">
|
||||
Signed in as <span className="font-semibold text-bolt-elements-textPrimary">{user.username || 'User'}</span>
|
||||
</div>
|
||||
|
||||
<Link
|
||||
to="/profile"
|
||||
className="block px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive"
|
||||
>
|
||||
Your profile
|
||||
</Link>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="block w-full text-left px-4 py-2 text-sm text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive"
|
||||
>
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Handle unauthenticated state (default)
|
||||
return (
|
||||
<button
|
||||
onClick={handleLogin}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-bolt-elements-item-backgroundAccent text-bolt-elements-item-contentAccent hover:bg-bolt-elements-item-backgroundAccentHover transition-colors text-sm font-medium"
|
||||
>
|
||||
<div className="i-ph:sign-in w-4 h-4" />
|
||||
Sign in
|
||||
</button>
|
||||
);
|
||||
} catch (error) {
|
||||
// Catch any rendering errors
|
||||
console.error('Error rendering auth controls:', error);
|
||||
setRenderError(error);
|
||||
|
||||
// Return fallback UI
|
||||
return (
|
||||
<button
|
||||
onClick={() => window.location.href = '/auth/login'}
|
||||
className="flex items-center gap-2 px-3 py-1.5 rounded-md bg-bolt-elements-background-depth-2 text-bolt-content-secondary hover:bg-bolt-elements-background-depth-3 transition-colors text-sm font-medium"
|
||||
>
|
||||
<div className="i-ph:user-circle w-4 h-4" />
|
||||
Sign in
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
@ -24,37 +24,70 @@ interface Profile {
|
||||
};
|
||||
}
|
||||
|
||||
// Default profile with all required fields
|
||||
const defaultProfile: Profile = {
|
||||
username: '',
|
||||
bio: '',
|
||||
avatar: '',
|
||||
isAuthenticated: false,
|
||||
};
|
||||
|
||||
// Safely get stored profile from localStorage with error handling
|
||||
function getSafeStoredProfile(): Profile | null {
|
||||
// Ensure we're in a browser environment
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Safely access localStorage
|
||||
const storedProfile = localStorage.getItem('bolt_profile');
|
||||
if (!storedProfile) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Safely parse JSON with error handling
|
||||
const parsedProfile = JSON.parse(storedProfile);
|
||||
return parsedProfile;
|
||||
} catch (error) {
|
||||
// Handle any errors (localStorage not available, JSON parse error, etc.)
|
||||
console.error('Error accessing profile from localStorage:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize with stored profile or defaults
|
||||
const storedProfile = typeof window !== 'undefined' ? localStorage.getItem('bolt_profile') : null;
|
||||
const storedProfile = getSafeStoredProfile();
|
||||
const initialProfile: Profile = storedProfile
|
||||
? {
|
||||
// Ensure backward compatibility with existing profile data
|
||||
...{
|
||||
username: '',
|
||||
bio: '',
|
||||
avatar: '',
|
||||
isAuthenticated: false,
|
||||
},
|
||||
...JSON.parse(storedProfile),
|
||||
// Start with default values for all required fields
|
||||
...defaultProfile,
|
||||
// Then apply stored values, ensuring type safety
|
||||
...(storedProfile as Partial<Profile>),
|
||||
}
|
||||
: {
|
||||
username: '',
|
||||
bio: '',
|
||||
avatar: '',
|
||||
isAuthenticated: false,
|
||||
};
|
||||
: defaultProfile;
|
||||
|
||||
// Create the store with safe initial values
|
||||
export const profileStore = atom<Profile>(initialProfile);
|
||||
|
||||
/**
|
||||
* Update profile with partial data
|
||||
*/
|
||||
export const updateProfile = (updates: Partial<Profile>) => {
|
||||
profileStore.set({ ...profileStore.get(), ...updates });
|
||||
try {
|
||||
profileStore.set({ ...profileStore.get(), ...updates });
|
||||
|
||||
// Persist to localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
localStorage.setItem('bolt_profile', JSON.stringify(profileStore.get()));
|
||||
// Safely persist to localStorage
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
localStorage.setItem('bolt_profile', JSON.stringify(profileStore.get()));
|
||||
} catch (error) {
|
||||
console.error('Error saving profile to localStorage:', error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating profile:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@ -69,45 +102,88 @@ export const setAuthenticatedUser = (githubUser: {
|
||||
avatar_url: string;
|
||||
html_url: string;
|
||||
}) => {
|
||||
const now = Date.now();
|
||||
|
||||
updateProfile({
|
||||
username: githubUser.login,
|
||||
avatar: githubUser.avatar_url,
|
||||
// Keep existing bio if available
|
||||
bio: profileStore.get().bio || githubUser.name || '',
|
||||
isAuthenticated: true,
|
||||
lastLogin: now,
|
||||
github: githubUser,
|
||||
});
|
||||
try {
|
||||
const now = Date.now();
|
||||
const currentProfile = profileStore.get();
|
||||
|
||||
updateProfile({
|
||||
username: githubUser.login,
|
||||
avatar: githubUser.avatar_url,
|
||||
// Keep existing bio if available
|
||||
bio: currentProfile.bio || githubUser.name || '',
|
||||
isAuthenticated: true,
|
||||
lastLogin: now,
|
||||
github: githubUser,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error setting authenticated user:', error);
|
||||
// Fallback to minimal profile update on error
|
||||
updateProfile({
|
||||
username: githubUser.login || 'User',
|
||||
avatar: githubUser.avatar_url || '',
|
||||
isAuthenticated: true,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the user is authenticated
|
||||
* With safe access pattern to prevent "Cannot access before initialization" errors
|
||||
*/
|
||||
export const isAuthenticated = (): boolean => {
|
||||
return profileStore.get().isAuthenticated;
|
||||
try {
|
||||
const profile = profileStore.get();
|
||||
return Boolean(profile && profile.isAuthenticated);
|
||||
} catch (error) {
|
||||
console.error('Error checking authentication state:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear authentication state
|
||||
*/
|
||||
export const clearAuthState = () => {
|
||||
const currentProfile = profileStore.get();
|
||||
|
||||
updateProfile({
|
||||
// Keep username/bio/avatar if user wants to
|
||||
// but clear authentication state
|
||||
isAuthenticated: false,
|
||||
github: undefined,
|
||||
lastLogin: undefined,
|
||||
});
|
||||
try {
|
||||
const currentProfile = profileStore.get();
|
||||
|
||||
updateProfile({
|
||||
// Keep username/bio/avatar if user wants to
|
||||
// but clear authentication state
|
||||
isAuthenticated: false,
|
||||
github: undefined,
|
||||
lastLogin: undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error clearing auth state:', error);
|
||||
// Fallback to resetting the entire profile on error
|
||||
profileStore.set(defaultProfile);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get GitHub user data if authenticated
|
||||
* With safe access pattern to prevent "Cannot access before initialization" errors
|
||||
*/
|
||||
export const getGitHubUser = () => {
|
||||
const profile = profileStore.get();
|
||||
return profile.isAuthenticated ? profile.github : null;
|
||||
try {
|
||||
const profile = profileStore.get();
|
||||
return (profile && profile.isAuthenticated && profile.github) ? profile.github : null;
|
||||
} catch (error) {
|
||||
console.error('Error getting GitHub user:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get safe profile data that won't throw if accessed before initialization
|
||||
* Useful for components that need to access profile data safely
|
||||
*/
|
||||
export const getSafeProfile = (): Profile => {
|
||||
try {
|
||||
return profileStore.get();
|
||||
} catch (error) {
|
||||
console.error('Error getting profile:', error);
|
||||
return defaultProfile;
|
||||
}
|
||||
};
|
||||
|
@ -35,7 +35,8 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
// Get form data
|
||||
const formData = await request.formData();
|
||||
const bio = formData.get('bio')?.toString() || '';
|
||||
const displayName = formData.get('displayName')?.toString() || user.login;
|
||||
// Fix TypeScript error by adding null check for user
|
||||
const displayName = formData.get('displayName')?.toString() || (user ? user.login : '');
|
||||
const theme = formData.get('theme')?.toString() || 'system';
|
||||
|
||||
// Here you would typically update the user data in your database
|
||||
|
Loading…
Reference in New Issue
Block a user