Skip to content
Merged
Show file tree
Hide file tree
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
4 changes: 4 additions & 0 deletions clock-app/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
.git
npm-debug.log*
23 changes: 23 additions & 0 deletions clock-app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Build stage — glibc/Node 20 (consistent with the host frontend build).
FROM node:20-bookworm-slim AS build
WORKDIR /app
COPY package*.json ./
# Drop any host lockfile so npm resolves platform-native deps in this image.
RUN rm -f package-lock.json && npm install
COPY . .

# Build-time configuration (no host code, just URLs):
ARG VITE_HUB_API_URL=http://fuzefront.dev.local
ARG VITE_PUBLIC_URL=http://clock.dev.local
ENV VITE_HUB_API_URL=$VITE_HUB_API_URL
ENV VITE_PUBLIC_URL=$VITE_PUBLIC_URL
RUN npm run build

# Serve the built remote with nginx.
FROM nginx:alpine AS production
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q --spider http://localhost:8080/health || exit 1
CMD ["nginx", "-g", "daemon off;"]
12 changes: 12 additions & 0 deletions clock-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>FuzeClock — FuzeFront microfrontend</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
51 changes: 51 additions & 0 deletions clock-app/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
worker_processes auto;
pid /tmp/nginx.pid;

events {
worker_connections 1024;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
error_log /dev/stderr warn;
sendfile on;

server {
listen 8080;
server_name _;
root /usr/share/nginx/html;
index index.html;

# CORS so the host page can fetch remoteEntry.js and chunks cross-origin.
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' '*' always;
if ($request_method = 'OPTIONS') { return 204; }

# remoteEntry.js must never be cached stale.
location = /remoteEntry.js {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Cache-Control' 'no-cache, no-store, must-revalidate' always;
try_files $uri =404;
}

location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header 'Access-Control-Allow-Origin' '*' always;
try_files $uri =404;
}

location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}

location / {
try_files $uri $uri/ /index.html;
}
}
}
23 changes: 23 additions & 0 deletions clock-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "clock-app",
"version": "1.0.0",
"description": "Example on-the-fly FuzeFront microfrontend (runtime Module Federation)",
"type": "module",
"scripts": {
"dev": "vite --port 3003",
"build": "vite build",
"preview": "vite preview --port 3003"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@originjs/vite-plugin-federation": "^1.3.6",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"typescript": "^5.5.3",
"vite": "^5.4.0"
}
}
4 changes: 4 additions & 0 deletions clock-app/public/clock-icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 80 additions & 0 deletions clock-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { useEffect, useRef, useState } from 'react'
import {
getPlatformContext,
subscribeContext,
notify,
PlatformSnapshot,
} from './sdk'
import './index.css'

// This is the module the host loads at runtime (exposed as './App').
export default function App() {
const [now, setNow] = useState(() => new Date())
const [ctx, setCtx] = useState<PlatformSnapshot>(() => getPlatformContext())
const greeted = useRef(false)

useEffect(() => {
const t = setInterval(() => setNow(new Date()), 1000)
return () => clearInterval(t)
}, [])

// Live host context via the bridge (user, apps, etc.).
useEffect(() => subscribeContext(setCtx), [])

// Greet the host through the shared toaster, once, when mounted in-platform.
useEffect(() => {
if (ctx.isPlatformMode && !greeted.current) {
greeted.current = true
notify({
title: 'FuzeClock',
message: 'Loaded at runtime and connected to the host bridge 👋',
level: 'success',
appId: 'fuzeClock',
})
}
}, [ctx.isPlatformMode])

return (
<div className="fuzeclock">
<h2>🕒 FuzeClock</h2>
<div className="time">{now.toLocaleTimeString()}</div>
<div className="date">
{now.toLocaleDateString(undefined, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</div>
<p className="note">
Loaded at runtime via Module Federation. No build-time knowledge of the
host — only the SDK / <code>window.__FUZEFRONT__</code> bridge.
</p>
<ul className="ctx">
<li>
Mounted inside FuzeFront:{' '}
<b>{ctx.isPlatformMode ? 'yes' : 'no (standalone)'}</b>
</li>
<li>
User (live from host): <b>{ctx.user?.email ?? '—'}</b>
</li>
<li>
Apps known to host: <b>{ctx.apps.length}</b>
</li>
</ul>
<button
className="btn"
onClick={() =>
notify({
title: 'FuzeClock',
message: `Hello from the clock at ${new Date().toLocaleTimeString()}`,
level: 'info',
appId: 'fuzeClock',
})
}
>
Send a toast to the host
</button>
</div>
)
}
55 changes: 55 additions & 0 deletions clock-app/src/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
.fuzeclock {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
padding: 2rem;
text-align: center;
color: #1f2937;
}
.fuzeclock h2 {
margin: 0 0 0.5rem;
}
.fuzeclock .time {
font-size: 3rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: 0.02em;
}
.fuzeclock .date {
color: #6b7280;
margin-bottom: 1rem;
}
.fuzeclock .note {
max-width: 46ch;
margin: 1rem auto;
color: #9ca3af;
font-size: 0.9rem;
line-height: 1.4;
}
.fuzeclock .note code {
background: #f1f5f9;
padding: 0 4px;
border-radius: 4px;
}
.fuzeclock .ctx {
list-style: none;
padding: 0;
margin: 0 auto 1rem;
color: #374151;
font-size: 0.9rem;
display: inline-block;
text-align: left;
}
.fuzeclock .ctx li {
margin: 0.25rem 0;
}
.fuzeclock .btn {
background: #2563eb;
color: #fff;
border: none;
border-radius: 8px;
padding: 0.5rem 1rem;
font-size: 0.9rem;
cursor: pointer;
}
.fuzeclock .btn:hover {
background: #1d4ed8;
}
30 changes: 30 additions & 0 deletions clock-app/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import { registerWithHub } from './sdk'

// When opened STANDALONE (not mounted by the host), self-register with the hub
// so it appears in the launcher. The hub URL and this app's public URL come from
// build-time env (configuration) — never from importing host code.
const HUB_API_URL = import.meta.env.VITE_HUB_API_URL || 'http://fuzefront.dev.local'
const PUBLIC_URL = import.meta.env.VITE_PUBLIC_URL || 'http://clock.dev.local'

if (!(window as any).__FRONTFUSE_PLATFORM__) {
void registerWithHub({
hubApiUrl: HUB_API_URL,
name: 'FuzeClock',
url: PUBLIC_URL,
remoteUrl: PUBLIC_URL,
scope: 'fuzeClock',
module: './App',
iconUrl: `${PUBLIC_URL}/clock-icon.svg`,
description:
'Example on-the-fly microfrontend: a live clock loaded at runtime.',
})
}

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
)
Loading
Loading