Skip to content

Clean up federation example, remove host modules #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -41,7 +41,7 @@ export function GeneralErrorBoundary({
}, [error, isResponse])

return (
<div className="container flex items-center justify-center p-20 text-h2">
<div className="container flex items-center justify-center p-20 text-h2 border-2 border-red-500">
{isResponse
? (statusHandlers?.[error.status] ?? defaultStatusHandler)({
error,
10 changes: 5 additions & 5 deletions examples/federation/epic-stack-remote/app/components/forms.tsx
Original file line number Diff line number Diff line change
@@ -24,7 +24,7 @@ export function ErrorList({
const errorsToRender = errors?.filter(Boolean)
if (!errorsToRender?.length) return null
return (
<ul id={id} className="flex flex-col gap-1">
<ul id={id} className="flex flex-col gap-1 border-2 border-red-500">
{errorsToRender.map((e) => (
<li key={e} className="text-[10px] text-foreground-destructive">
{e}
@@ -49,7 +49,7 @@ export function Field({
const id = inputProps.id ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
return (
<div className={className}>
<div className={className + " border-2 border-red-500"}>
<Label htmlFor={id} {...labelProps} />
<Input
id={id}
@@ -79,7 +79,7 @@ export function OTPField({
const id = inputProps.id ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
return (
<div className={className}>
<div className={className + " border-2 border-red-500"}>
<Label htmlFor={id} {...labelProps} />
<InputOTP
pattern={REGEXP_ONLY_DIGITS_AND_CHARS}
@@ -123,7 +123,7 @@ export function TextareaField({
const id = textareaProps.id ?? textareaProps.name ?? fallbackId
const errorId = errors?.length ? `${id}-error` : undefined
return (
<div className={className}>
<div className={className + " border-2 border-red-500"}>
<Label htmlFor={id} {...labelProps} />
<Textarea
id={id}
@@ -166,7 +166,7 @@ export function CheckboxField({
const errorId = errors?.length ? `${id}-error` : undefined

return (
<div className={className}>
<div className={className + " border-2 border-red-500"}>
<div className="flex gap-2">
<Checkbox
{...checkboxProps}
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ function EpicProgress() {
role="progressbar"
aria-hidden={delayedPending ? undefined : true}
aria-valuetext={delayedPending ? 'Loading' : undefined}
className="fixed inset-x-0 left-0 top-0 z-50 h-[0.20rem] animate-pulse"
className="fixed inset-x-0 left-0 top-0 z-50 h-[0.20rem] animate-pulse border-2 border-red-500"
>
<div
ref={ref}
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ export function SearchBar({
<Form
method="GET"
action="/users"
className="flex flex-wrap items-center justify-center gap-2"
className="flex flex-wrap items-center justify-center gap-2 border-2 border-red-500"
onChange={(e) => autoSubmit && handleFormChange(e.currentTarget)}
>
<div className="flex-1">
Original file line number Diff line number Diff line change
@@ -53,5 +53,5 @@ export function Spacer({
'4xl': 'h-44',
}
const className = options[size]
return <div className={className} />
return <div className={className + " border-2 border-red-500"} />
}
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import * as React from 'react'
import { cn } from '#app/utils/misc.tsx'

const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors outline-none focus-visible:ring-2 focus-within:ring-2 ring-ring ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors outline-none focus-visible:ring-2 focus-within:ring-2 ring-ring ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border-2 border-red-500',
{
variants: {
variant: {
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
'peer h-4 w-4 shrink-0 rounded-sm border-2 border-red-500 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
className,
)}
{...props}
Original file line number Diff line number Diff line change
@@ -180,7 +180,7 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn('ml-auto text-xs tracking-widest opacity-60', className)}
className={cn('ml-auto text-xs tracking-widest opacity-60 border-2 border-red-500', className)}
{...props}
/>
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type SVGProps } from 'react'
import { cn } from '#app/utils/misc.tsx'
//@ts-ignore
import href from './icons/sprite.svg'
import { type IconName } from '@/icon-name'

Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@ const InputOTPGroup = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex items-center', className)} {...props} />
<div ref={ref} className={cn('flex items-center border-2 border-red-500', className)} {...props} />
))
InputOTPGroup.displayName = 'InputOTPGroup'

@@ -40,7 +40,7 @@ const InputOTPSlot = React.forwardRef<
<div
ref={ref}
className={cn(
'relative flex h-10 w-10 items-center justify-center border-y border-r border-input text-base transition-all first:rounded-l-md first:border-l last:rounded-r-md md:text-sm',
'relative flex h-10 w-10 items-center justify-center border-2 border-red-500 text-base transition-all first:rounded-l-md first:border-l last:rounded-r-md md:text-sm',
isActive && 'z-10 ring-2 ring-ring ring-offset-background',
className,
)}
@@ -61,7 +61,7 @@ const InputOTPSeparator = React.forwardRef<
React.ElementRef<'div'>,
React.ComponentPropsWithoutRef<'div'>
>(({ ...props }, ref) => (
<div ref={ref} role="separator" {...props}>
<div ref={ref} role="separator" className="border-2 border-red-500" {...props}>
-
</div>
))
Original file line number Diff line number Diff line change
@@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
<input
type={type}
className={cn(
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-base file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-input-invalid md:text-sm md:file:text-sm',
'flex h-10 w-full rounded-md border-2 border-red-500 bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-base file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-input-invalid md:text-sm md:file:text-sm',
className,
)}
ref={ref}
Original file line number Diff line number Diff line change
@@ -27,23 +27,23 @@ export const StatusButton = React.forwardRef<
pending: delayedPending ? (
<div
role="status"
className="inline-flex h-6 w-6 items-center justify-center"
className="inline-flex h-6 w-6 items-center justify-center border-2 border-red-500"
>
<Icon name="update" className="animate-spin" title="loading" />
</div>
) : null,
success: (
<div
role="status"
className="inline-flex h-6 w-6 items-center justify-center"
className="inline-flex h-6 w-6 items-center justify-center border-2 border-red-500"
>
<Icon name="check" title="success" />
</div>
),
error: (
<div
role="status"
className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-destructive"
className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-destructive border-2 border-red-500"
>
<Icon
name="cross-1"
@@ -58,7 +58,7 @@ export const StatusButton = React.forwardRef<
return (
<Button
ref={ref}
className={cn('flex justify-center gap-4', className)}
className={cn('flex justify-center gap-4 border-2 border-red-500', className)}
{...props}
>
<div>{children}</div>
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
'flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-input-invalid md:text-sm',
'flex min-h-[80px] w-full rounded-md border-2 border-red-500 bg-background px-3 py-2 text-base ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid]:border-input-invalid md:text-sm',
className,
)}
ref={ref}
Original file line number Diff line number Diff line change
@@ -16,6 +16,7 @@ export function UserDropdown() {
const user = useUser()
const formRef = useRef<HTMLFormElement>(null)
return (
<div className="border-2 border-red-500">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button asChild variant="secondary">
@@ -64,5 +65,6 @@ export function UserDropdown() {
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenu>
</div>
)
}
2 changes: 2 additions & 0 deletions examples/federation/epic-stack-remote/app/root.tsx
Original file line number Diff line number Diff line change
@@ -11,7 +11,9 @@ import {
} from 'react-router'
import { HoneypotProvider } from 'remix-utils/honeypot/react'
import { type Route } from './+types/root.ts'
//@ts-ignore
import appleTouchIconAssetUrl from './assets/favicons/apple-touch-icon.png'
//@ts-ignore
import faviconAssetUrl from './assets/favicons/favicon.svg'
import { GeneralErrorBoundary } from './components/error-boundary.tsx'
import { EpicProgress } from './components/progress-bar.tsx'
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
//@ts-nocheck

import docker from './docker.svg'
import eslint from './eslint.svg'
import fakerJS from './faker.svg'
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//@ts-nocheck
/**
* @vitest-environment jsdom
*/
9 changes: 6 additions & 3 deletions examples/federation/epic-stack-remote/package.json
Original file line number Diff line number Diff line change
@@ -49,9 +49,9 @@
"@epic-web/remember": "1.1.0",
"@epic-web/totp": "2.1.1",
"@mjackson/form-data-parser": "0.7.0",
"@module-federation/enhanced": "0.0.0-next-20250321011937",
"@module-federation/node": "0.0.0-next-20250321011937",
"@module-federation/rsbuild-plugin": "0.0.0-next-20250321011937",
"@module-federation/enhanced": "0.0.0-next-20250325035711",
"@module-federation/node": "0.0.0-next-20250325035711",
"@module-federation/rsbuild-plugin": "0.0.0-next-20250325035711",
"@nasa-gcn/remix-seo": "2.0.1",
"@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0",
@@ -170,5 +170,8 @@
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"vitest": {
"setupFiles": ["./vitest.setup.ts"]
}
}
19 changes: 17 additions & 2 deletions examples/federation/epic-stack-remote/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -27,6 +27,12 @@ const sharedDependencies = {
'react-dom/': {
singleton: true,
},
"@prisma/client": {
singleton: true,
},
"remix-utils/": {
singleton: true,
}
}

// Common exposed components
@@ -52,15 +58,23 @@ const exposedComponents = {
'./components/ui/checkbox': './app/components/ui/checkbox',
"./utils/connections": "./app/utils/connections",
"./utils/misc": "./app/utils/misc",
"./routes/login": "./app/routes/_auth+/login?react-router-route",
"./routes/login.server": "./app/routes/_auth+/login.server.ts"
}

// Filter out .server exposes for web environment
const webExposedComponents = Object.fromEntries(
Object.entries(exposedComponents).filter(([_, value]) => !value.includes('.server'))
)

// Common Module Federation configuration
const commonFederationConfig = {
name: 'remote',
shareStrategy: "loaded-first" as const,
runtime: undefined,
exposes: exposedComponents,
shared: sharedDependencies
shared: sharedDependencies,
dts: false
}

// Web-specific federation config
@@ -69,6 +83,7 @@ const webFederationConfig = {
library: {
type: 'module'
},
exposes: webExposedComponents
}

// Node-specific federation config
@@ -82,7 +97,7 @@ const nodeFederationConfig = {
],
// see https://github.com/module-federation/core/blob/main/packages/manifest/src/ManifestManager.ts#L106
manifest:{
additionalData:(additionalDataOptions)=>{
additionalData:(additionalDataOptions: { stats: any })=>{
const { stats, } =additionalDataOptions;
stats.metaData.ssrRemoteEntry = stats.metaData.remoteEntry;
return stats;
4 changes: 2 additions & 2 deletions examples/federation/epic-stack-remote/tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ export default {
presets: [marketingPreset],
plugins: [animatePlugin, radixPlugin],
safelist: [
// Match everything
/.*/
// Using string pattern to match everything instead of RegExp
'*'
],
} satisfies Config
163 changes: 5 additions & 158 deletions examples/federation/epic-stack/app/routes/_auth+/login.server.ts
Original file line number Diff line number Diff line change
@@ -1,158 +1,5 @@
import { invariant } from '@epic-web/invariant'
import { redirect } from 'react-router'
import { safeRedirect } from 'remix-utils/safe-redirect'
import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx'
import { getUserId, sessionKey } from '#app/utils/auth.server.ts'
import { prisma } from '#app/utils/db.server.ts'
import { combineResponseInits } from 'remote/utils/misc'
import { authSessionStorage } from '#app/utils/session.server.ts'
import { redirectWithToast } from '#app/utils/toast.server.ts'
import { verifySessionStorage } from '#app/utils/verification.server.ts'
import { getRedirectToUrl, type VerifyFunctionArgs } from './verify.server.ts'

const verifiedTimeKey = 'verified-time'
const unverifiedSessionIdKey = 'unverified-session-id'
const rememberKey = 'remember'

export async function handleNewSession(
{
request,
session,
redirectTo,
remember,
}: {
request: Request
session: { userId: string; id: string; expirationDate: Date }
redirectTo?: string
remember: boolean
},
responseInit?: ResponseInit,
) {
const verification = await prisma.verification.findUnique({
select: { id: true },
where: {
target_type: { target: session.userId, type: twoFAVerificationType },
},
})
const userHasTwoFactor = Boolean(verification)

if (userHasTwoFactor) {
const verifySession = await verifySessionStorage.getSession()
verifySession.set(unverifiedSessionIdKey, session.id)
verifySession.set(rememberKey, remember)
const redirectUrl = getRedirectToUrl({
request,
type: twoFAVerificationType,
target: session.userId,
redirectTo,
})
return redirect(
`${redirectUrl.pathname}?${redirectUrl.searchParams}`,
combineResponseInits(
{
headers: {
'set-cookie':
await verifySessionStorage.commitSession(verifySession),
},
},
responseInit,
),
)
} else {
const authSession = await authSessionStorage.getSession(
request.headers.get('cookie'),
)
authSession.set(sessionKey, session.id)

return redirect(
safeRedirect(redirectTo),
combineResponseInits(
{
headers: {
'set-cookie': await authSessionStorage.commitSession(authSession, {
expires: remember ? session.expirationDate : undefined,
}),
},
},
responseInit,
),
)
}
}

export async function handleVerification({
request,
submission,
}: VerifyFunctionArgs) {
invariant(
submission.status === 'success',
'Submission should be successful by now',
)
const authSession = await authSessionStorage.getSession(
request.headers.get('cookie'),
)
const verifySession = await verifySessionStorage.getSession(
request.headers.get('cookie'),
)

const remember = verifySession.get(rememberKey)
const { redirectTo } = submission.value
const headers = new Headers()
authSession.set(verifiedTimeKey, Date.now())

const unverifiedSessionId = verifySession.get(unverifiedSessionIdKey)
if (unverifiedSessionId) {
const session = await prisma.session.findUnique({
select: { expirationDate: true },
where: { id: unverifiedSessionId },
})
if (!session) {
throw await redirectWithToast('/login', {
type: 'error',
title: 'Invalid session',
description: 'Could not find session to verify. Please try again.',
})
}
authSession.set(sessionKey, unverifiedSessionId)

headers.append(
'set-cookie',
await authSessionStorage.commitSession(authSession, {
expires: remember ? session.expirationDate : undefined,
}),
)
} else {
headers.append(
'set-cookie',
await authSessionStorage.commitSession(authSession),
)
}

headers.append(
'set-cookie',
await verifySessionStorage.destroySession(verifySession),
)

return redirect(safeRedirect(redirectTo), { headers })
}

export async function shouldRequestTwoFA(request: Request) {
const authSession = await authSessionStorage.getSession(
request.headers.get('cookie'),
)
const verifySession = await verifySessionStorage.getSession(
request.headers.get('cookie'),
)
if (verifySession.has(unverifiedSessionIdKey)) return true
const userId = await getUserId(request)
if (!userId) return false
// if it's over two hours since they last verified, we should request 2FA again
const userHasTwoFA = await prisma.verification.findUnique({
select: { id: true },
where: { target_type: { target: userId, type: twoFAVerificationType } },
})
if (!userHasTwoFA) return false
const verifiedTime = authSession.get(verifiedTimeKey) ?? new Date(0)
const twoHours = 1000 * 60 * 2
return Date.now() - verifiedTime > twoHours
}
export {
handleNewSession,
handleVerification,
shouldRequestTwoFA
} from 'remote/routes/login.server'
205 changes: 1 addition & 204 deletions examples/federation/epic-stack/app/routes/_auth+/login.tsx
Original file line number Diff line number Diff line change
@@ -1,204 +1 @@
import { getFormProps, getInputProps, useForm } from '@conform-to/react'
import { getZodConstraint, parseWithZod } from '@conform-to/zod'
import { type SEOHandle } from '@nasa-gcn/remix-seo'
import { data, Form, Link, useSearchParams } from 'react-router'
import { HoneypotInputs } from 'remix-utils/honeypot/react'
import { z } from 'zod'
import { GeneralErrorBoundary } from 'remote/components/error-boundary'
import { CheckboxField, ErrorList, Field } from 'remote/components/forms'
import { Spacer } from 'remote/components/spacer'
import { StatusButton } from 'remote/components/ui/status-button'
import { login, requireAnonymous } from '#app/utils/auth.server.ts'
import {
ProviderConnectionForm,
providerNames,
} from '#app/utils/connections.tsx'
import { checkHoneypot } from '#app/utils/honeypot.server.ts'
import { useIsPending } from 'remote/utils/misc'
import { PasswordSchema, UsernameSchema } from '#app/utils/user-validation.ts'
import { type Route } from './+types/login.ts'
import { handleNewSession } from './login.server.ts'

export const handle: SEOHandle = {
getSitemapEntries: () => null,
}

const LoginFormSchema = z.object({
username: UsernameSchema,
password: PasswordSchema,
redirectTo: z.string().optional(),
remember: z.boolean().optional(),
})

export async function loader({ request }: Route.LoaderArgs) {
await requireAnonymous(request)
return {}
}

export async function action({ request }: Route.ActionArgs) {
await requireAnonymous(request)
const formData = await request.formData()
await checkHoneypot(formData)
const submission = await parseWithZod(formData, {
schema: (intent) =>
LoginFormSchema.transform(async (data, ctx) => {
if (intent !== null) return { ...data, session: null }

const session = await login(data)
if (!session) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Invalid username or password',
})
return z.NEVER
}

return { ...data, session }
}),
async: true,
})

if (submission.status !== 'success' || !submission.value.session) {
return data(
{ result: submission.reply({ hideFields: ['password'] }) },
{ status: submission.status === 'error' ? 400 : 200 },
)
}

const { session, remember, redirectTo } = submission.value

return handleNewSession({
request,
session,
remember: remember ?? false,
redirectTo,
})
}

export default function LoginPage({ actionData }: Route.ComponentProps) {
const isPending = useIsPending()
const [searchParams] = useSearchParams()
const redirectTo = searchParams.get('redirectTo')

const [form, fields] = useForm({
id: 'login-form',
constraint: getZodConstraint(LoginFormSchema),
defaultValue: { redirectTo },
lastResult: actionData?.result,
onValidate({ formData }) {
return parseWithZod(formData, { schema: LoginFormSchema })
},
shouldRevalidate: 'onBlur',
})

return (
<div className="flex min-h-full flex-col justify-center pb-32 pt-20">
<div className="mx-auto w-full max-w-md">
<div className="flex flex-col gap-3 text-center">
<h1 className="text-h1">Welcome back!</h1>
<p className="text-body-md text-muted-foreground">
Please enter your details.
</p>
</div>
<Spacer size="xs" />

<div>
<div className="mx-auto w-full max-w-md px-8">
<Form method="POST" {...getFormProps(form)}>
<HoneypotInputs />
<Field
labelProps={{ children: 'Username' }}
inputProps={{
...getInputProps(fields.username, { type: 'text' }),
autoFocus: true,
className: 'lowercase',
autoComplete: 'username',
}}
errors={fields.username.errors}
/>

<Field
labelProps={{ children: 'Password' }}
inputProps={{
...getInputProps(fields.password, {
type: 'password',
}),
autoComplete: 'current-password',
}}
errors={fields.password.errors}
/>

<div className="flex justify-between">
<CheckboxField
labelProps={{
htmlFor: fields.remember.id,
children: 'Remember me',
}}
buttonProps={getInputProps(fields.remember, {
type: 'checkbox',
})}
errors={fields.remember.errors}
/>
<div>
<Link
to="/forgot-password"
className="text-body-xs font-semibold"
>
Forgot password?
</Link>
</div>
</div>

<input
{...getInputProps(fields.redirectTo, { type: 'hidden' })}
/>
<ErrorList errors={form.errors} id={form.errorId} />

<div className="flex items-center justify-between gap-6 pt-3">
<StatusButton
className="w-full"
status={isPending ? 'pending' : (form.status ?? 'idle')}
type="submit"
disabled={isPending}
>
Log in
</StatusButton>
</div>
</Form>
<ul className="mt-5 flex flex-col gap-5 border-b-2 border-t-2 border-border py-3">
{providerNames.map((providerName) => (
<li key={providerName}>
<ProviderConnectionForm
type="Login"
providerName={providerName}
redirectTo={redirectTo}
/>
</li>
))}
</ul>
<div className="flex items-center justify-center gap-2 pt-6">
<span className="text-muted-foreground">New here?</span>
<Link
to={
redirectTo
? `/signup?redirectTo=${encodeURIComponent(redirectTo)}`
: '/signup'
}
>
Create an account
</Link>
</div>
</div>
</div>
</div>
</div>
)
}

export const meta: Route.MetaFunction = () => {
return [{ title: 'Login to Epic Notes' }]
}

export function ErrorBoundary() {
return <GeneralErrorBoundary />
}
export { default, loader, action, meta, ErrorBoundary } from 'remote/routes/login'
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ import { type twoFAVerifyVerificationType } from '../settings+/profile.two-facto
import {
handleVerification as handleLoginTwoFactorVerification,
shouldRequestTwoFA,
} from './login.server.ts'
} from 'remote/routes/login.server'
import { handleVerification as handleOnboardingVerification } from './onboarding.server.ts'
import { handleVerification as handleResetPasswordVerification } from './reset-password.server.ts'
import {
6 changes: 3 additions & 3 deletions examples/federation/epic-stack/package.json
Original file line number Diff line number Diff line change
@@ -49,9 +49,9 @@
"@epic-web/remember": "1.1.0",
"@epic-web/totp": "2.1.1",
"@mjackson/form-data-parser": "0.7.0",
"@module-federation/enhanced": "0.0.0-next-20250321011937",
"@module-federation/node": "0.0.0-next-20250321011937",
"@module-federation/rsbuild-plugin": "0.0.0-next-20250321011937",
"@module-federation/enhanced": "0.0.0-next-20250325035711",
"@module-federation/node": "0.0.0-next-20250325035711",
"@module-federation/rsbuild-plugin": "0.0.0-next-20250325035711",
"@nasa-gcn/remix-seo": "2.0.1",
"@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0",
10 changes: 8 additions & 2 deletions examples/federation/epic-stack/rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -16,6 +16,9 @@ const sharedDependencies = {
react: {
singleton: true,
},
"@prisma/client": {
singleton: true,
},
'react/': {
singleton: true,
},
@@ -25,13 +28,17 @@ const sharedDependencies = {
'react-dom/': {
singleton: true,
},
"remix-utils/": {
singleton: true,
}
}

// Common Module Federation configuration
const commonFederationConfig = {
name: 'host',
shareStrategy: "loaded-first" as const,
shared: sharedDependencies
shared: sharedDependencies,
dts: false
}

// Web-specific federation config
@@ -46,7 +53,6 @@ const webFederationConfig = {
// Node-specific federation config
const nodeFederationConfig = {
...commonFederationConfig,
dts: false,
remotes: {
remote: 'remote@http://localhost:3001/static/static/js/remote.js',
},
2 changes: 1 addition & 1 deletion examples/federation/epic-stack/tailwind.config.ts
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ export default {
},
safelist: [
// Match everything
/.*/
"*"
],
presets: [marketingPreset],
plugins: [animatePlugin, radixPlugin],
316 changes: 182 additions & 134 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions setup-federation.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/bash

# Exit on error
set -e

# Source nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion

echo "🚀 Setting up federation examples..."

# Use nvm and install dependencies
echo "📦 Setting up Node.js environment..."
nvm use
pnpm install

# Build the main package
echo "🏗️ Building main package..."
pnpm build

# Function to setup a federation example
setup_example() {
local dir=$1
echo "🔄 Setting up $dir..."
cd "examples/federation/$dir"

echo "📦 Installing dependencies..."
pnpm install

echo "📦 Running Prisma and Playwright setup..."
pnpm prisma generate && \
pnpm prisma migrate reset --force && \
pnpm playwright install && \
pnpm run build

cd ../../..
}

# Setup both federation examples
setup_example "epic-stack"
setup_example "epic-stack-remote"

echo "✅ Setup complete!"