Fresh (Deno) Version โ
HaloLight Fresh version is built on Fresh 2 + Deno, using Islands architecture + Preact to deliver a zero-config, ultra-fast admin dashboard.
Live Preview: https://halolight-fresh.h7ml.cn
GitHub: https://github.com/halolight/halolight-fresh
Features โ
- ๐๏ธ Islands Architecture - Zero JS by default, hydrate on demand, ultimate performance
- โก Zero Config - Works out of the box, no build step required
- ๐จ Theme System - 11 skins, dark mode, View Transitions
- ๐ Authentication - Complete login/register/password recovery flow
- ๐ Dashboard - Data visualization and business management
- ๐ก๏ธ Permission Control - RBAC fine-grained permission management
- ๐ Secure by Default - Deno sandbox security model
- ๐ Edge First - Native support for Deno Deploy edge deployment
Tech Stack โ
| Technology | Version | Description |
|---|---|---|
| Fresh | 2.x | Deno full-stack framework |
| Deno | 2.x | Modern JavaScript runtime |
| Preact | 10.x | Lightweight UI library |
| @preact/signals | 2.x | Reactive state management |
| TypeScript | Built-in | Type safety |
| Tailwind CSS | Built-in | Atomic CSS |
| Zod | 3.x | Data validation |
| Chart.js | 4.x | Chart visualization |
Core Features โ
- Islands Architecture - Zero JS by default, only interactive components hydrate, ultimate performance
- Zero Config Development - JIT rendering, no build step, instant startup
- Permission System - RBAC permission control, route guards, permission components
- Theme System - 11 skins, dark mode, View Transitions
- Edge Deployment - Native support for Deno Deploy edge runtime
- Type Safety - Built-in TypeScript, no configuration needed
- Security Model - Deno sandbox, explicit permissions, secure by default
Directory Structure โ
halolight-fresh/
โโโ routes/ # File-based routing
โ โโโ _app.tsx # Root layout
โ โโโ _layout.tsx # Default layout
โ โโโ _middleware.ts # Global middleware
โ โโโ index.tsx # Homepage
โ โโโ auth/ # Auth pages
โ โ โโโ login.tsx
โ โ โโโ register.tsx
โ โ โโโ forgot-password.tsx
โ โ โโโ reset-password.tsx
โ โโโ dashboard/ # Dashboard pages
โ โ โโโ _layout.tsx # Dashboard layout
โ โ โโโ _middleware.ts # Auth middleware
โ โ โโโ index.tsx
โ โ โโโ users/
โ โ โ โโโ index.tsx
โ โ โ โโโ create.tsx
โ โ โ โโโ [id].tsx
โ โ โโโ roles.tsx
โ โ โโโ permissions.tsx
โ โ โโโ settings.tsx
โ โ โโโ profile.tsx
โ โโโ api/ # API routes
โ โโโ auth/
โ โโโ login.ts
โ โโโ register.ts
โ โโโ me.ts
โโโ islands/ # Interactive Islands
โ โโโ LoginForm.tsx
โ โโโ UserTable.tsx
โ โโโ DashboardGrid.tsx
โ โโโ ThemeToggle.tsx
โ โโโ Sidebar.tsx
โโโ components/ # Static components
โ โโโ ui/ # UI components
โ โ โโโ Button.tsx
โ โ โโโ Input.tsx
โ โ โโโ Card.tsx
โ โ โโโ ...
โ โโโ layout/ # Layout components
โ โ โโโ AdminLayout.tsx
โ โ โโโ AuthLayout.tsx
โ โ โโโ Header.tsx
โ โโโ shared/ # Shared components
โ โโโ PermissionGuard.tsx
โโโ lib/ # Utilities
โ โโโ auth.ts
โ โโโ permission.ts
โ โโโ session.ts
โ โโโ cn.ts
โโโ signals/ # State management
โ โโโ auth.ts
โ โโโ ui-settings.ts
โ โโโ dashboard.ts
โโโ static/ # Static assets
โโโ fresh.config.ts # Fresh config
โโโ deno.json # Deno config
โโโ tailwind.config.ts # Tailwind configQuick Start โ
Requirements โ
- Deno >= 2.x
Install Deno โ
# macOS/Linux
curl -fsSL https://deno.land/install.sh | sh
# Windows
irm https://deno.land/install.ps1 | iexInstallation โ
git clone https://github.com/halolight/halolight-fresh.git
cd halolight-freshEnvironment Variables โ
cp .env.example .env# .env
API_URL=/api
USE_MOCK=true
DEMO_EMAIL=admin@halolight.h7ml.cn
DEMO_PASSWORD=123456
SHOW_DEMO_HINT=true
APP_TITLE=Admin Pro
BRAND_NAME=Halolight
SESSION_SECRET=your-secret-keyStart Development โ
deno task devVisit http://localhost:8000
Production Build โ
deno task build
deno task startCore Features โ
State Management (@preact/signals) โ
// signals/auth.ts
import { signal, computed, effect } from '@preact/signals'
import { IS_BROWSER } from '$fresh/runtime.ts'
interface User {
id: number
name: string
email: string
permissions: string[]
}
export const user = signal<User | null>(null)
export const token = signal<string | null>(null)
export const loading = signal(false)
export const isAuthenticated = computed(() => !!token.value && !!user.value)
export const permissions = computed(() => user.value?.permissions ?? [])
// Only persist in browser
if (IS_BROWSER) {
const saved = localStorage.getItem('auth')
if (saved) {
const { user: savedUser, token: savedToken } = JSON.parse(saved)
user.value = savedUser
token.value = savedToken
}
effect(() => {
if (user.value && token.value) {
localStorage.setItem('auth', JSON.stringify({
user: user.value,
token: token.value,
}))
}
})
}
export async function login(credentials: { email: string; password: string }) {
loading.value = true
try {
const response = await fetch('/api/auth/login', {
method: 'POST',
body: JSON.stringify(credentials),
headers: { 'Content-Type': 'application/json' },
})
const data = await response.json()
user.value = data.user
token.value = data.token
} finally {
loading.value = false
}
}
export function logout() {
user.value = null
token.value = null
if (IS_BROWSER) {
localStorage.removeItem('auth')
}
}
export function hasPermission(permission: string): boolean {
const perms = permissions.value
return perms.some((p) =>
p === '*' || p === permission ||
(p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
)
}Data Fetching (Handlers) โ
// routes/api/auth/login.ts
import { Handlers } from '$fresh/server.ts'
import { z } from 'zod'
import { setCookie } from '$std/http/cookie.ts'
import { createToken } from '../../../lib/auth.ts'
const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(6),
})
export const handler: Handlers = {
async POST(req) {
try {
const body = await req.json()
const { email, password } = loginSchema.parse(body)
// Authenticate user (example)
const user = await authenticateUser(email, password)
if (!user) {
return new Response(
JSON.stringify({ error: 'Invalid email or password' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
)
}
const token = await createToken({ userId: user.id })
const response = new Response(
JSON.stringify({ user, token }),
{ headers: { 'Content-Type': 'application/json' } }
)
setCookie(response.headers, {
name: 'token',
value: token,
path: '/',
httpOnly: true,
sameSite: 'Lax',
maxAge: 60 * 60 * 24 * 7,
})
return response
} catch (e) {
if (e instanceof z.ZodError) {
return new Response(
JSON.stringify({ error: 'Validation failed', details: e.errors }),
{ status: 400, headers: { 'Content-Type': 'application/json' } }
)
}
return new Response(
JSON.stringify({ error: 'Server error' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
)
}
},
}Permission Control โ
// components/shared/PermissionGuard.tsx
import { ComponentChildren } from 'preact'
interface Props {
permission: string
userPermissions: string[]
children: ComponentChildren
fallback?: ComponentChildren
}
function checkPermission(
userPermissions: string[],
permission: string
): boolean {
return userPermissions.some((p) =>
p === '*' || p === permission ||
(p.endsWith(':*') && permission.startsWith(p.slice(0, -1)))
)
}
export function PermissionGuard({
permission,
userPermissions,
children,
fallback,
}: Props) {
if (!checkPermission(userPermissions, permission)) {
return fallback ?? null
}
return <>{children}</>
}// Usage (in server-side rendering)
<PermissionGuard
permission="users:delete"
userPermissions={ctx.state.user.permissions}
fallback={<span class="text-muted-foreground">No permission</span>}
>
<Button variant="destructive">Delete</Button>
</PermissionGuard>Islands Architecture โ
// islands/LoginForm.tsx
import { useSignal } from '@preact/signals'
import { login, loading } from '../signals/auth.ts'
import { Button } from '../components/ui/Button.tsx'
import { Input } from '../components/ui/Input.tsx'
interface Props {
redirectTo?: string
}
export default function LoginForm({ redirectTo = '/dashboard' }: Props) {
const email = useSignal('')
const password = useSignal('')
const error = useSignal('')
const handleSubmit = async (e: Event) => {
e.preventDefault()
error.value = ''
try {
await login({
email: email.value,
password: password.value,
})
globalThis.location.href = redirectTo
} catch (e) {
error.value = 'Invalid email or password'
}
}
return (
<form onSubmit={handleSubmit} class="space-y-4">
{error.value && (
<div class="text-destructive text-sm">{error.value}</div>
)}
<Input
type="email"
label="Email"
value={email.value}
onInput={(e) => email.value = e.currentTarget.value}
required
/>
<Input
type="password"
label="Password"
value={password.value}
onInput={(e) => password.value = e.currentTarget.value}
required
/>
<Button type="submit" class="w-full" disabled={loading.value}>
{loading.value ? 'Logging in...' : 'Login'}
</Button>
</form>
)
}// routes/auth/login.tsx
import { Handlers, PageProps } from '$fresh/server.ts'
import { AuthLayout } from '../../components/layout/AuthLayout.tsx'
import LoginForm from '../../islands/LoginForm.tsx'
export const handler: Handlers = {
GET(req, ctx) {
const url = new URL(req.url)
const redirect = url.searchParams.get('redirect') || '/dashboard'
return ctx.render({ redirect })
},
}
export default function LoginPage({ data }: PageProps<{ redirect: string }>) {
return (
<AuthLayout>
<div class="max-w-md mx-auto">
<h1 class="text-2xl font-bold text-center mb-8">Login</h1>
<LoginForm redirectTo={data.redirect} />
</div>
</AuthLayout>
)
}Theme System โ
Skin Presets โ
Supports 11 preset skins, switch via quick settings panel:
| Skin | Color | CSS Variable |
|---|---|---|
| Default | Purple | --primary: 51.1% 0.262 276.97 |
| Blue | Blue | --primary: 54.8% 0.243 264.05 |
| Emerald | Green | --primary: 64.6% 0.178 142.49 |
| Orange | Orange | --primary: 69.7% 0.186 37.37 |
| Rose | Rose | --primary: 62.8% 0.241 12.48 |
| Teal | Teal | --primary: 66.7% 0.151 193.65 |
| Amber | Amber | --primary: 77.5% 0.166 69.76 |
| Cyan | Cyan | --primary: 75.1% 0.146 204.66 |
| Pink | Pink | --primary: 65.7% 0.255 347.69 |
| Indigo | Indigo | --primary: 51.9% 0.235 272.75 |
| Lime | Lime | --primary: 78.1% 0.167 136.29 |
CSS Variables (OKLch) โ
/* Theme variable definitions */
:root {
--background: 100% 0 0;
--foreground: 14.9% 0.017 285.75;
--primary: 51.1% 0.262 276.97;
--primary-foreground: 100% 0 0;
--secondary: 96.1% 0.006 285.75;
--secondary-foreground: 14.9% 0.017 285.75;
--muted: 96.1% 0.006 285.75;
--muted-foreground: 44.7% 0.025 285.75;
--accent: 96.1% 0.006 285.75;
--accent-foreground: 14.9% 0.017 285.75;
--destructive: 62.8% 0.241 12.48;
--destructive-foreground: 100% 0 0;
--border: 89.8% 0.011 285.75;
--input: 89.8% 0.011 285.75;
--ring: 51.1% 0.262 276.97;
--radius: 0.5rem;
}Page Routes โ
| Path | Page | Permission |
|---|---|---|
/ | Homepage | Public |
/auth/login | Login | Public |
/auth/register | Register | Public |
/auth/forgot-password | Forgot Password | Public |
/auth/reset-password | Reset Password | Public |
/dashboard | Dashboard | dashboard:view |
/dashboard/users | User List | users:list |
/dashboard/users/create | Create User | users:create |
/dashboard/users/[id] | User Detail | users:view |
/dashboard/roles | Role Management | roles:list |
/dashboard/permissions | Permission Management | permissions:list |
/dashboard/settings | System Settings | settings:view |
/dashboard/profile | Profile | Logged in |
Common Commands โ
deno task dev # Start development server
deno task build # Production build
deno task start # Start production server
deno task check # Format and type check
deno task fmt # Format code
deno task fmt:check # Check code format
deno task lint # Lint code
deno task test # Run tests
deno task test:watch # Test watch mode
deno task test:coverage # Test coverage
deno task ci # Run full CI checkDeployment โ
Deno Deploy (Recommended) โ
# Install deployctl
deno install -A --no-check -r -f https://deno.land/x/deploy/deployctl.ts
# Deploy
deployctl deploy --project=halolight-fresh main.tsDocker โ
FROM denoland/deno:2.0.0
WORKDIR /app
COPY . .
RUN deno cache main.ts
EXPOSE 8000
CMD ["run", "-A", "main.ts"]docker build -t halolight-fresh .
docker run -p 8000:8000 halolight-freshOther Platforms โ
- Cloudflare Workers - Via Deno Deploy adapter
- Fly.io - Native Deno support
- Self-hosted - Run
deno task startdirectly
Demo Accounts โ
| Role | Password | |
|---|---|---|
| Admin | admin@halolight.h7ml.cn | 123456 |
| User | user@halolight.h7ml.cn | 123456 |
Testing โ
The project uses Deno's built-in testing framework, test files are located in the tests/ directory.
Test Structure โ
tests/
โโโ setup.ts # Test environment setup
โ โโโ localStorage mock
โ โโโ sessionStorage mock
โ โโโ matchMedia mock
โ โโโ Helper functions (createMockUser, mockAuthenticatedState, etc.)
โโโ lib/
โโโ utils.test.ts # Utility function tests
โโโ config.test.ts # Config tests
โโโ stores.test.ts # State management testsRun Tests โ
# Run all tests
deno task test
# Watch mode
deno task test:watch
# Test coverage
deno task test:coverage
# Coverage report output to coverage/lcov.infoTest Example โ
// tests/lib/config.test.ts
import { assertEquals, assertExists } from "$std/assert/mod.ts";
import "../setup.ts";
import { hasPermission } from "../../lib/config.ts";
import type { Permission } from "../../lib/types.ts";
Deno.test("hasPermission - permission check", async (t) => {
const userPermissions: Permission[] = ["dashboard:view", "users:view"];
await t.step("should return true when user has permission", () => {
const result = hasPermission(userPermissions, "dashboard:view");
assertEquals(result, true);
});
await t.step("should support wildcard permissions", () => {
const adminPermissions: Permission[] = ["*"];
const result = hasPermission(adminPermissions, "dashboard:view");
assertEquals(result, true);
});
});Configuration โ
Fresh Configuration โ
// fresh.config.ts
import { defineConfig } from '$fresh/server.ts'
import tailwind from '$fresh/plugins/tailwind.ts'
export default defineConfig({
plugins: [tailwind()],
})Deno Configuration โ
// deno.json
{
"lock": false,
"tasks": {
"check": "deno fmt --check && deno lint && deno check **/*.ts && deno check **/*.tsx",
"dev": "deno run -A --watch=static/,routes/ dev.ts",
"build": "deno run -A dev.ts build",
"start": "deno run -A main.ts",
"update": "deno run -A -r https://fresh.deno.dev/update ."
},
"imports": {
"$fresh/": "https://deno.land/x/fresh@2.0.0/",
"$std/": "https://deno.land/std@0.224.0/",
"preact": "https://esm.sh/preact@10.22.0",
"preact/": "https://esm.sh/preact@10.22.0/",
"@preact/signals": "https://esm.sh/@preact/signals@1.2.3",
"zod": "https://deno.land/x/zod@v3.23.0/mod.ts"
},
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "preact"
}
}CI/CD โ
The project uses GitHub Actions for continuous integration, configuration file is located at .github/workflows/ci.yml.
Workflow Tasks โ
| Task | Description | Trigger |
|---|---|---|
| lint | Format check, code check, type check | push/PR |
| test | Run tests and upload coverage | push/PR |
| build | Production build verification | After lint/test pass |
| security | Deno security audit | push/PR |
| dependency-review | Dependency security review | PR only |
Code Quality Configuration โ
// deno.json
{
"lint": {
"rules": {
"tags": ["recommended"],
"exclude": [
"no-explicit-any",
"explicit-function-return-type",
"explicit-module-boundary-types",
"jsx-button-has-type",
"no-unused-vars"
]
}
},
"fmt": {
"lineWidth": 100,
"indentWidth": 2,
"singleQuote": false,
"semiColons": true
}
}Advanced Features โ
Middleware System โ
// routes/dashboard/_middleware.ts
import { FreshContext } from '$fresh/server.ts'
import { getCookies } from '$std/http/cookie.ts'
import { verifyToken, getUser } from '../../lib/auth.ts'
export async function handler(req: Request, ctx: FreshContext) {
const cookies = getCookies(req.headers)
const token = cookies.token
if (!token) {
const url = new URL(req.url)
return new Response(null, {
status: 302,
headers: { Location: `/auth/login?redirect=${url.pathname}` },
})
}
try {
const payload = await verifyToken(token)
const user = await getUser(payload.userId)
ctx.state.user = user
ctx.state.token = token
} catch {
return new Response(null, {
status: 302,
headers: { Location: '/auth/login' },
})
}
return ctx.next()
}Nested Layouts โ
// routes/dashboard/_layout.tsx
import { PageProps } from '$fresh/server.ts'
import { AdminLayout } from '../../components/layout/AdminLayout.tsx'
import Sidebar from '../../islands/Sidebar.tsx'
export default function DashboardLayout({ Component, state }: PageProps) {
return (
<AdminLayout>
<div class="flex min-h-screen">
<Sidebar user={state.user} />
<main class="flex-1 p-6">
<Component />
</main>
</div>
</AdminLayout>
)
}Performance Optimization โ
Islands Architecture Optimization โ
Fresh defaults to zero JS, only interactive components need hydration:
// Static component (components/) - Zero JS
export function Card({ title, content }) {
return (
<div class="card">
<h2>{title}</h2>
<p>{content}</p>
</div>
)
}
// Interactive Island (islands/) - Hydrate on demand
export default function Counter() {
const count = useSignal(0)
return (
<button onClick={() => count.value++}>
Count: {count.value}
</button>
)
}Edge Deployment Optimization โ
// Leverage Deno Deploy edge runtime
export const handler: Handlers = {
async GET(req) {
// Execute at edge nodes, reduce latency
const data = await fetchFromDatabase()
return new Response(JSON.stringify(data))
}
}Preloading โ
// Preload critical resources
<link rel="preload" href="/api/auth/me" as="fetch" crossOrigin="anonymous" />FAQ โ
Q: How to share state between Islands and server components? โ
A: Use @preact/signals, which works on both server and client:
// signals/auth.ts
export const user = signal<User | null>(null)
// islands/UserProfile.tsx (client-side)
import { user } from '../signals/auth.ts'
export default function UserProfile() {
return <div>{user.value?.name}</div>
}
// routes/dashboard/index.tsx (server-side)
import { user } from '../signals/auth.ts'
export default function Dashboard({ data }: PageProps) {
return <div>Welcome {data.user.name}</div>
}Q: How to handle environment variables? โ
A: Fresh uses Deno's environment variable system:
// Read environment variable
const apiUrl = Deno.env.get('API_URL') || '/api'
// .env file (development)
// Automatically loaded with deno task devQ: How to implement data persistence? โ
A: Use Deno KV (built-in key-value database):
// lib/db.ts
const kv = await Deno.openKv()
export async function saveUser(user: User) {
await kv.set(['users', user.id], user)
}
export async function getUser(id: number) {
const result = await kv.get(['users', id])
return result.value as User
}Comparison with Other Versions โ
| Feature | Fresh Version | Astro Version | Next.js Version |
|---|---|---|---|
| Runtime | Deno | Node.js | Node.js |
| State Management | @preact/signals | - | Zustand |
| Data Fetching | Handlers | Load functions | TanStack Query |
| Form Validation | Zod | Zod | React Hook Form + Zod |
| Server-side | Built-in | @astrojs/node | API Routes |
| Component Library | Custom | - | shadcn/ui |
| Islands Architecture | โ | โ | โ |
| Zero Config | โ | โ | โ |
| Edge Deployment | Deno Deploy | Cloudflare | Vercel Edge |
| Build Step | Optional | Required | Required |