meta-frameworks-overview
Meta-Frameworks Overview
What is a Meta-Framework?
A meta-framework builds on top of a UI library (React, Vue, Svelte) to provide:
UI Library (React, Vue, Svelte)
↓ adds
Meta-Framework Features:
├── File-based routing
├── Server-side rendering (SSR)
├── Static site generation (SSG)
├── API routes / backend integration
├── Code splitting & bundling
├── Image optimization
├── Deployment optimizations
└── Full-stack capabilities
Why Meta-Frameworks?
Without a Meta-Framework (Manual Setup)
Create React App
├── Configure Webpack/Vite
├── Set up React Router
├── Configure SSR manually (complex)
├── Set up Express/Node server
├── Configure code splitting
├── Set up environment variables
├── Configure production build
└── Handle deployment
With a Meta-Framework
npx create-next-app / npm create astro
├── Routing: ✓ automatic
├── SSR/SSG: ✓ built-in
├── API routes: ✓ included
├── Bundling: ✓ optimized
├── Deployment: ✓ streamlined
└── Start building: ✓ immediately
Framework Comparison Matrix
| Framework | UI Library | Rendering | Philosophy |
|---|---|---|---|
| Next.js | React | SSR, SSG, ISR, CSR | Full-stack React |
| Remix | React | SSR, progressive enhancement | Web standards, forms |
| Nuxt | Vue | SSR, SSG, ISR | Full-stack Vue |
| SvelteKit | Svelte | SSR, SSG, CSR | Compiler-first |
| Astro | Any (React, Vue, Svelte, etc.) | SSG, SSR, Islands | Content-first, minimal JS |
| Solid Start | Solid | SSR, SSG | Fine-grained reactivity |
| Qwik City | Qwik | Resumable | Zero hydration |
Next.js
Full-stack React framework by Vercel.
Key Features
- App Router (new): React Server Components, nested layouts
- Pages Router (legacy): Traditional file-based routing
- Rendering options: SSG, SSR, ISR, CSR per route
- API Routes: Backend endpoints within the same project
- Image/Font optimization: Built-in components
Routing Model
app/
├── page.tsx → /
├── about/page.tsx → /about
├── blog/
│ ├── page.tsx → /blog
│ └── [slug]/page.tsx → /blog/:slug
└── api/
└── route.ts → API endpoint
Rendering Decision
// Static (default - SSG)
export default function Page() {
return <div>Static content</div>;
}
// Dynamic (SSR)
export const dynamic = 'force-dynamic';
// ISR
export const revalidate = 60; // seconds
Best For
- Production React applications
- E-commerce, SaaS, marketing sites
- Teams wanting batteries-included solution
- Vercel deployment (optimized)
Remix
Full-stack React framework focused on web fundamentals.
Key Features
- Nested routes: Parallel data loading
- Progressive enhancement: Works without JavaScript
- Form handling: Built-in
<Form>with server actions - Error boundaries: Per-route error handling
- Web standards: Uses fetch, FormData, Response
Philosophy
Traditional SPA:
Click → JS preventDefault → Fetch API → Update State → Re-render
Remix:
Click → Form submits → Server action → Return data → Re-render
(Works without JS, enhanced with JS)
Routing Model
app/
├── routes/
│ ├── _index.tsx → /
│ ├── about.tsx → /about
│ ├── blog._index.tsx → /blog
│ └── blog.$slug.tsx → /blog/:slug
└── root.tsx → Root layout
Data Loading
// Parallel loading of nested route data
export async function loader({ params }) {
return json(await getPost(params.slug));
}
export async function action({ request }) {
const formData = await request.formData();
// Handle form submission
}
Best For
- Form-heavy applications
- Progressive enhancement requirements
- Teams preferring web standards
- Apps that should work without JS
Nuxt
Full-stack Vue framework.
Key Features
- Auto-imports: Components, composables auto-imported
- File-based routing: Vue components as routes
- Nitro server: Universal deployment
- Modules ecosystem: Rich plugin system
Routing Model
pages/
├── index.vue → /
├── about.vue → /about
└── blog/
├── index.vue → /blog
└── [slug].vue → /blog/:slug
Data Fetching
<script setup>
// Runs on server, cached
const { data } = await useFetch('/api/posts');
// Always fresh
const { data } = await useFetch('/api/posts', { fresh: true });
</script>
Best For
- Vue.js teams
- Content sites, applications
- Teams wanting Vue-specific optimizations
SvelteKit
Full-stack Svelte framework.
Key Features
- Compiler-first: Svelte compiles to vanilla JS
- Adapters: Deploy anywhere (Node, Vercel, Cloudflare, etc.)
- Load functions: Unified data loading
- Form actions: Built-in form handling
Routing Model
src/routes/
├── +page.svelte → /
├── +layout.svelte → Shared layout
├── about/+page.svelte → /about
└── blog/
├── +page.svelte → /blog
└── [slug]/+page.svelte → /blog/:slug
Data Loading
// +page.server.js - runs on server only
export async function load({ params }) {
return {
post: await getPost(params.slug)
};
}
Best For
- Performance-critical applications
- Teams preferring Svelte's approach
- Smaller bundle size requirements
Astro
Content-first framework with islands architecture.
Key Features
- Zero JS by default: Ships no JavaScript unless needed
- Islands architecture: Hydrate only interactive components
- Framework agnostic: Use React, Vue, Svelte together
- Content collections: First-class Markdown/MDX support
Routing Model
src/pages/
├── index.astro → /
├── about.astro → /about
└── blog/
├── index.astro → /blog
└── [slug].astro → /blog/:slug
Islands Pattern
---
import Header from './Header.astro'; // No JS
import Counter from './Counter.jsx'; // React
import Search from './Search.vue'; // Vue
---
<Header />
<!-- Islands: only these ship JS -->
<Counter client:load />
<Search client:visible />
Client Directives
| Directive | When Hydrates |
|---|---|
client:load |
Immediately on page load |
client:idle |
When browser is idle |
client:visible |
When component is visible |
client:media |
When media query matches |
client:only |
Skip SSR, client render only |
Best For
- Content-heavy sites (blogs, docs, marketing)
- Performance-first approach
- Teams using multiple UI frameworks
- Static sites with some interactivity
Qwik City
Framework with resumability (zero hydration).
Key Features
- Resumability: No hydration cost
- Lazy loading: Loads JS only when needed
- Familiar syntax: JSX-like templates
How Resumability Works
Traditional SSR:
Server render → Download all JS → Hydrate (re-execute) → Interactive
Qwik:
Server render + serialize state → Load tiny runtime → Resume → Interactive
(No re-execution, state already in HTML)
Best For
- Performance-critical applications
- Large applications with many components
- Teams wanting to eliminate hydration cost
Framework Selection Guide
By Project Type
| Project Type | Recommended |
|---|---|
| Marketing site | Astro, Next.js (SSG) |
| Blog/Docs | Astro, Next.js, SvelteKit |
| E-commerce | Next.js, Remix, Nuxt |
| Dashboard/App | Next.js, SvelteKit, Remix |
| Highly interactive app | Next.js, SvelteKit, Remix |
| Performance-critical | Astro, Qwik, SvelteKit |
By Team Expertise
| Team Knows | Recommended |
|---|---|
| React | Next.js, Remix, Astro |
| Vue | Nuxt, Astro |
| Svelte | SvelteKit, Astro |
| Multiple / Learning | Astro |
By Priorities
| Priority | Recommended |
|---|---|
| SEO | Any (all support SSR/SSG) |
| Minimal JavaScript | Astro, Qwik |
| Progressive enhancement | Remix |
| Largest ecosystem | Next.js |
| Fastest runtime | SvelteKit, Qwik |
| Most flexible | Astro |
Common Patterns Across Frameworks
File-Based Routing
All meta-frameworks use file structure for routes:
pages/about.tsx → /about
pages/[id].tsx → /123, /abc (dynamic)
pages/[...slug].tsx → /a/b/c (catch-all)
Layouts
Shared UI across routes:
layout.tsx (Next.js)
+layout.svelte (SvelteKit)
layouts/default.vue (Nuxt)
Data Loading
Fetching data before render:
getServerSideProps / loader functions (Next.js)
loader functions (Remix)
load functions (SvelteKit)
useFetch (Nuxt)
frontmatter + getStaticProps (Astro)
API Routes
Backend endpoints in the same project:
app/api/route.ts (Next.js)
app/routes/api.$.ts (Remix)
server/api/ (Nuxt)
src/pages/api/ (Astro)
Migration Considerations
From SPA to Meta-Framework
- Choose framework matching your UI library
- Migrate routing to file-based
- Move data fetching to server (loaders)
- Update build/deploy pipeline
- Add SSR/SSG as needed
Between Meta-Frameworks
- Routing patterns are similar (map file structure)
- Data fetching patterns differ (loaders, getServerSideProps, useFetch)
- Styling approaches may vary
- Deployment adapters differ
Deep Dive: Understanding Meta-Frameworks From First Principles
What Problems Do Meta-Frameworks Solve?
To understand meta-frameworks, you must understand the problems of building production apps WITHOUT them:
PROBLEM 1: Build Configuration Hell
// React alone gives you:
import React from 'react';
function App() { return <div>Hello</div>; }
// But browsers don't understand JSX. You need:
// - Babel (transpile JSX → JavaScript)
// - Webpack/Vite (bundle modules)
// - PostCSS (process CSS)
// - Asset handling (images, fonts)
// - Environment variables
// - Development server with HMR
// - Production build optimization
// That's ~500 lines of config before writing any app code
PROBLEM 2: Routing is Non-Trivial
// React gives you components, not routes
// You need React Router or similar:
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// But then:
// - How to code-split per route?
// - How to preload routes?
// - How to handle nested layouts?
// - How to protect routes?
// - How to handle 404s?
PROBLEM 3: Data Fetching Complexity
// Basic fetching seems simple:
useEffect(() => {
fetch('/api/data').then(setData);
}, []);
// But production needs:
// - Loading states
// - Error handling
// - Caching
// - Deduplication
// - Revalidation
// - SSR considerations
PROBLEM 4: Server-Side Rendering is HARD
// SSR manually requires:
// - Node.js server (Express/Fastify)
// - renderToString on server
// - Hydration on client
// - Different entry points (server.js, client.js)
// - Matching router on both sides
// - Data serialization between server/client
// - Handling streaming
// - Environment differences (window undefined on server)
META-FRAMEWORK SOLUTION:
All of the above is pre-configured and optimized.
Just write your components and routes.
The Layered Architecture Model
Meta-frameworks are built in layers:
┌─────────────────────────────────────────────────────────────────┐
│ YOUR APPLICATION CODE │
│ (Pages, Components, API routes, Business Logic) │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ META-FRAMEWORK LAYER │
│ (Next.js / Nuxt / SvelteKit / Remix / Astro) │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Router │ │ Builder │ │ Server │ │ Optimizer │ │
│ │ System │ │ System │ │ Runtime │ │ Layer │ │
│ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ UI LIBRARY LAYER │
│ (React / Vue / Svelte / Solid) │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Component │ │ State │ │ Virtual │ │
│ │ Model │ │ Mgmt │ │ DOM │ │
│ └───────────┘ └───────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────┐
│ PLATFORM LAYER │
│ (Node.js, Deno, Bun, Edge) │
└─────────────────────────────────────────────────────────────────┘
How File-Based Routing Works Under the Hood
The magic of "file = route" is compile-time transformation:
// YOUR FILE STRUCTURE:
// app/
// ├── page.tsx
// ├── about/page.tsx
// └── blog/[slug]/page.tsx
// AT BUILD TIME, framework generates something like:
const routes = [
{
pattern: /^\/$/,
component: () => import('./app/page.tsx'),
type: 'page',
},
{
pattern: /^\/about$/,
component: () => import('./app/about/page.tsx'),
type: 'page',
},
{
pattern: /^\/blog\/([^/]+)$/,
component: () => import('./app/blog/[slug]/page.tsx'),
type: 'page',
params: ['slug'],
},
];
// The framework's router uses this table at runtime
function matchRoute(pathname) {
for (const route of routes) {
const match = pathname.match(route.pattern);
if (match) {
return {
component: route.component,
params: extractParams(route.params, match),
};
}
}
return null; // 404
}
Server Components: A Paradigm Shift
React Server Components (used by Next.js App Router) fundamentally change the component model:
// TRADITIONAL REACT (All components run everywhere):
// This runs on server (for SSR) AND client (for hydration)
function ProductPage() {
const [product, setProduct] = useState(null);
useEffect(() => {
// Runs only on client
fetch('/api/product').then(r => r.json()).then(setProduct);
}, []);
return <div>{product?.name}</div>;
}
// SHIPPED TO CLIENT:
// - ProductPage component code
// - useState implementation
// - useEffect implementation
// - All dependencies
// REACT SERVER COMPONENTS:
// This runs ONLY on server, never shipped to client
async function ProductPage() {
// Direct database access! No API needed!
const product = await db.products.findById(123);
// This HTML is sent, not the component code
return <div>{product.name}</div>;
}
// SHIPPED TO CLIENT:
// - Only the rendered HTML
// - Zero JavaScript for this component
// BUT INTERACTIVITY NEEDS CLIENT COMPONENTS:
'use client'; // This directive makes it a client component
function AddToCartButton({ productId }) {
const [loading, setLoading] = useState(false);
async function handleClick() {
setLoading(true);
await addToCart(productId);
setLoading(false);
}
return <button onClick={handleClick}>{loading ? '...' : 'Add'}</button>;
}
// This component IS shipped to client
The composition model:
// Server Component (default in App Router)
async function ProductPage({ params }) {
const product = await getProduct(params.id); // Server-only
return (
<div>
<h1>{product.name}</h1> {/* Static, no JS */}
<p>{product.description}</p> {/* Static, no JS */}
<AddToCartButton id={product.id} /> {/* Client Component, has JS */}
</div>
);
}
// Rendered output:
// <div>
// <h1>Product Name</h1> ← Plain HTML
// <p>Description here</p> ← Plain HTML
// <button>Add to Cart</button> ← Hydrated with JS
// </div>
Data Loading: The Core Pattern Differences
Each framework has a different philosophy:
// NEXT.JS APP ROUTER:
// Data fetching in the component itself (async components)
async function ProductPage({ params }) {
// This fetch is automatically deduplicated and cached
const product = await fetch(`/api/products/${params.id}`).then(r => r.json());
return <Product data={product} />;
}
// Caching controls:
// fetch(url, { cache: 'force-cache' }) // SSG-like
// fetch(url, { cache: 'no-store' }) // SSR-like
// fetch(url, { next: { revalidate: 60 } }) // ISR-like
// REMIX:
// Explicit loader/action separation
export async function loader({ params }) {
// Runs on server for GET requests
const product = await db.products.find(params.id);
return json(product);
}
export async function action({ request }) {
// Runs on server for POST/PUT/DELETE
const formData = await request.formData();
await db.products.update(formData);
return redirect('/products');
}
export default function ProductPage() {
const product = useLoaderData(); // Data from loader
return <Product data={product} />;
}
// SVELTEKIT:
// Separate load files
// +page.server.ts
export async function load({ params }) {
return {
product: await getProduct(params.id)
};
}
// +page.svelte
<script>
export let data; // Receives load() return value
</script>
<h1>{data.product.name}</h1>
// ASTRO:
// Frontmatter-based (build-time by default)
---
const product = await fetch('...').then(r => r.json());
---
<h1>{product.name}</h1>
Adapter Pattern: Deploy Anywhere
SvelteKit pioneered the adapter pattern, now common across frameworks:
YOUR CODE
│
▼
┌─────────────────────────────┐
│ Build Process │
│ (Framework-specific) │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Adapter │
│ (Platform-specific) │
└──────────────┬──────────────┘
│
┌──────────┼──────────┬──────────┐
▼ ▼ ▼ ▼
┌───────┐ ┌───────┐ ┌───────┐ ┌───────┐
│ Node │ │Vercel │ │ CF │ │Netlify│
│Server │ │Funcs │ │Workers│ │Funcs │
└───────┘ └───────┘ └───────┘ └───────┘
How adapters work:
// Framework produces platform-agnostic output:
// - Static files (HTML, CSS, JS, images)
// - Server functions (request handlers)
// - Prerender information
// ADAPTER transforms to platform-specific format:
// adapter-node:
// Creates Node.js server with Express/Polka
// adapter-vercel:
// Creates Vercel Functions configuration
// Outputs to .vercel/output directory
// adapter-cloudflare:
// Creates Worker script
// Uses Cloudflare's fetch handler format
// adapter-static:
// Generates only static files
// No server runtime needed
Edge Runtime: The Modern Frontier
Edge computing brings code execution to CDN nodes:
// TRADITIONAL SSR:
// Request: Tokyo user → CDN (Tokyo) → Origin Server (Virginia)
// ↓
// Render HTML
// ↓
// Response: Tokyo user ← CDN (Tokyo) ← Origin Server (Virginia)
//
// Latency: 150-300ms for server response
// EDGE SSR:
// Request: Tokyo user → Edge Node (Tokyo)
// ↓
// Render HTML locally
// ↓
// Response: Tokyo user ← Edge Node (Tokyo)
//
// Latency: 10-50ms for server response
Edge runtime limitations:
// Edge runtime is NOT full Node.js
// It's a minimal JavaScript environment
// AVAILABLE:
// - fetch() API
// - Web Crypto API
// - Basic JavaScript APIs
// NOT AVAILABLE:
// - fs (file system)
// - Native Node modules
// - Full npm ecosystem
// - Long-running processes
// NEXT.JS EDGE RUNTIME:
export const runtime = 'edge';
export async function GET(request) {
// Must use fetch-based APIs
const data = await fetch('https://api.example.com/data');
return Response.json(await data.json());
}
Framework Philosophies Explained
Next.js Philosophy: "The Platform for React"
- Embrace React fully (including experimental features)
- Provide every possible rendering mode
- Deep Vercel integration
- Enterprise-ready out of the box
Remix Philosophy: "Web Standards First"
- Use browser APIs, not framework abstractions
- Forms work without JavaScript
- Progressive enhancement by default
- Don't fight the platform
Astro Philosophy: "Less JavaScript is More"
- Static by default, dynamic when needed
- Islands of interactivity, not oceans
- Use any UI framework, or none
- Content sites deserve great tools
SvelteKit Philosophy: "Compiled, Not Runtime"
- Move work to compile time
- Smaller bundles through compilation
- Unified platform via adapters
- Developer experience matters
Build Process: What Actually Happens
When you run npm run build:
┌──────────────────────────────────────────────────────────────────┐
│ BUILD PIPELINE │
└──────────────────────────────────────────────────────────────────┘
STEP 1: ROUTE ANALYSIS
├── Scan file system for routes
├── Identify static vs dynamic routes
├── Build route manifest
└── Determine data dependencies
STEP 2: CODE TRANSFORMATION
├── Transpile TypeScript → JavaScript
├── Transform JSX → createElement calls
├── Process CSS (PostCSS, Tailwind, etc.)
└── Optimize imports (tree shaking)
STEP 3: BUNDLING
├── Create entry points per route
├── Code split appropriately
├── Generate chunk hashes
└── Create source maps
STEP 4: STATIC GENERATION (SSG routes)
├── Execute getStaticProps / load functions
├── Render components to HTML strings
├── Write HTML files to output directory
└── Copy static assets
STEP 5: SERVER BUNDLE (SSR routes)
├── Bundle server-side code separately
├── Exclude client-only code
├── Generate server entry points
└── Create serverless function bundles (if applicable)
STEP 6: OPTIMIZATION
├── Minify JavaScript (Terser, ESBuild)
├── Minify CSS
├── Optimize images
├── Generate asset manifests
└── Create preload hints
OUTPUT:
├── .next/ or .output/ or dist/
│ ├── static/ (CDN-served assets)
│ ├── server/ (Server functions)
│ ├── routes-manifest (Route configuration)
│ └── build-manifest (Asset mapping)
Choosing the Right Framework: Decision Tree
START: What UI library does your team know?
│
├─► React
│ │
│ └─► What's your priority?
│ ├─► Maximum flexibility, enterprise features → Next.js
│ ├─► Web standards, progressive enhancement → Remix
│ └─► Static content with some interactivity → Astro (with React islands)
│
├─► Vue
│ │
│ └─► Full-stack Vue app → Nuxt
│ Static with Vue components → Astro (with Vue islands)
│
├─► Svelte
│ │
│ └─► Full-stack Svelte → SvelteKit
│ Static with Svelte components → Astro (with Svelte islands)
│
├─► None / Learning
│ │
│ └─► What are you building?
│ ├─► Content site (blog, docs) → Astro
│ ├─► Web application → SvelteKit (easiest learning curve)
│ └─► Enterprise app → Next.js (most resources, hiring)
│
└─► Maximum Performance
│
└─► Static content → Astro (zero JS by default)
Interactive app → Qwik (resumable, no hydration)
For Framework Authors: Building Meta-Frameworks
Implementation Note: The patterns and code examples below represent one proven approach to building meta-frameworks. Meta-framework architecture varies significantly—Next.js tightly integrates with React, Nuxt builds on Vue's ecosystem, and Astro is UI-agnostic. The direction shown here provides foundational architecture that most meta-frameworks share. Your implementation will depend on your target UI framework, deployment strategy, and the developer experience you want to provide.
Core Meta-Framework Architecture
// META-FRAMEWORK CORE ARCHITECTURE
class MetaFramework {
constructor(config) {
this.config = config;
this.plugins = [];
this.hooks = new HookSystem();
this.router = new FileBasedRouter();
this.bundler = new Bundler();
this.renderer = new UniversalRenderer();
}
// Plugin system
use(plugin) {
this.plugins.push(plugin);
plugin.install?.(this);
}
// Development server
async dev() {
await this.hooks.call('beforeDev');
// Scan routes
const routes = await this.router.scan(this.config.pagesDir);
// Start HMR bundler
await this.bundler.startDev({
routes,
onUpdate: (updates) => this.handleHMR(updates),
});
// Start dev server
const server = await this.createDevServer(routes);
await this.hooks.call('afterDev', { server });
}
// Production build
async build() {
await this.hooks.call('beforeBuild');
const routes = await this.router.scan(this.config.pagesDir);
// Client build
const clientManifest = await this.bundler.buildClient(routes);
// Server build
const serverManifest = await this.bundler.buildServer(routes);
// Static generation
await this.generateStatic(routes, { clientManifest, serverManifest });
// Generate deployment artifacts
await this.adapter.generate(this.config);
await this.hooks.call('afterBuild');
}
}
// Hook system for extensibility
class HookSystem {
constructor() {
this.hooks = new Map();
}
on(name, handler) {
if (!this.hooks.has(name)) {
this.hooks.set(name, []);
}
this.hooks.get(name).push(handler);
}
async call(name, context = {}) {
const handlers = this.hooks.get(name) || [];
for (const handler of handlers) {
await handler(context);
}
}
}
Building the Plugin System
// PLUGIN SYSTEM IMPLEMENTATION
class PluginManager {
constructor(framework) {
this.framework = framework;
this.plugins = [];
}
async register(plugin) {
// Validate plugin
if (!plugin.name) {
throw new Error('Plugin must have a name');
}
// Check dependencies
for (const dep of plugin.dependencies || []) {
if (!this.plugins.find(p => p.name === dep)) {
throw new Error(`Plugin ${plugin.name} requires ${dep}`);
}
}
// Initialize plugin
const instance = await plugin.setup?.(this.framework) || {};
// Register hooks
if (plugin.hooks) {
for (const [hook, handler] of Object.entries(plugin.hooks)) {
this.framework.hooks.on(hook, handler.bind(instance));
}
}
// Add Vite plugins
if (plugin.vitePlugins) {
this.framework.bundler.addPlugins(plugin.vitePlugins);
}
this.plugins.push({ ...plugin, instance });
}
}
// Example plugin
const imageOptimizationPlugin = {
name: 'image-optimization',
async setup(framework) {
return {
cache: new Map(),
};
},
hooks: {
async beforeBuild() {
console.log('Preparing image optimization...');
},
async transformAsset(context) {
if (!isImage(context.path)) return;
const optimized = await optimizeImage(context.content, {
quality: 80,
formats: ['webp', 'avif'],
});
return optimized;
},
},
vitePlugins: [
{
name: 'vite-plugin-images',
transform(code, id) {
if (id.endsWith('.jpg') || id.endsWith('.png')) {
// Transform image imports
}
},
},
],
};
Implementing Adapters (Deploy Anywhere)
// ADAPTER SYSTEM IMPLEMENTATION
class AdapterManager {
constructor() {
this.adapters = new Map();
}
register(name, adapter) {
this.adapters.set(name, adapter);
}
async build(adapterName, context) {
const adapter = this.adapters.get(adapterName);
if (!adapter) {
throw new Error(`Unknown adapter: ${adapterName}`);
}
return adapter.adapt(context);
}
}
// Node.js adapter
const nodeAdapter = {
name: 'node',
async adapt(context) {
const { serverEntry, staticDir, outputDir } = context;
// Generate Node.js server
const serverCode = `
import { createServer } from 'http';
import { handler } from './handler.js';
const server = createServer(async (req, res) => {
const response = await handler(req);
res.writeHead(response.status, Object.fromEntries(response.headers));
res.end(response.body);
});
server.listen(process.env.PORT || 3000);
`;
await fs.writeFile(`${outputDir}/index.js`, serverCode);
await fs.cp(staticDir, `${outputDir}/static`, { recursive: true });
return { type: 'node', entry: 'index.js' };
},
};
// Cloudflare Workers adapter
const cloudflareAdapter = {
name: 'cloudflare',
async adapt(context) {
const { serverEntry, staticDir, outputDir } = context;
// Generate Worker script
const workerCode = `
import { handler } from './handler.js';
export default {
async fetch(request, env, ctx) {
// Try static files first
const url = new URL(request.url);
const staticResponse = await env.ASSETS.fetch(request);
if (staticResponse.status !== 404) return staticResponse;
// Fall back to SSR handler
return handler(request, env, ctx);
},
};
`;
await fs.writeFile(`${outputDir}/_worker.js`, workerCode);
// Generate wrangler.toml
const wranglerConfig = `
name = "${context.config.name}"
main = "_worker.js"
compatibility_date = "2024-01-01"
[site]
bucket = "./static"
`;
await fs.writeFile(`${outputDir}/wrangler.toml`, wranglerConfig);
return { type: 'cloudflare-workers' };
},
};
// Vercel adapter
const vercelAdapter = {
name: 'vercel',
async adapt(context) {
const { routes, serverEntry, staticDir, outputDir } = context;
// Generate Vercel config
const vercelConfig = {
version: 3,
routes: [
// Static assets
{ src: '/static/(.*)', dest: '/static/$1' },
// API routes
...routes.filter(r => r.type === 'api').map(r => ({
src: r.path,
dest: `/api${r.path}`,
})),
// SSR routes
{ src: '/(.*)', dest: '/render' },
],
functions: {
'api/**/*.js': { runtime: 'nodejs20.x' },
'render.js': { runtime: 'nodejs20.x' },
},
};
await fs.writeFile(
`${outputDir}/vercel.json`,
JSON.stringify(vercelConfig, null, 2)
);
// Generate serverless functions
for (const route of routes) {
if (route.type === 'api') {
await generateAPIFunction(route, outputDir);
}
}
// Generate SSR function
await generateSSRFunction(context, outputDir);
return { type: 'vercel' };
},
};
Server Component Architecture
// SERVER COMPONENT IMPLEMENTATION
class ServerComponentRuntime {
constructor() {
this.componentCache = new Map();
this.clientReferences = new Map();
}
// Mark component as client boundary
registerClientReference(id, exportName) {
this.clientReferences.set(`${id}#${exportName}`, {
id,
exportName,
chunks: [], // Filled during build
});
}
// Render server component to RSC payload
async renderToPayload(element, context) {
const payload = [];
await this.walkTree(element, payload, context);
return payload;
}
async walkTree(element, payload, context) {
if (!element) return null;
// Primitive values
if (typeof element !== 'object') {
return element;
}
// Array of children
if (Array.isArray(element)) {
return Promise.all(element.map(e => this.walkTree(e, payload, context)));
}
const { type, props } = element;
// HTML element
if (typeof type === 'string') {
return {
$$typeof: Symbol.for('react.element'),
type,
props: {
...props,
children: await this.walkTree(props.children, payload, context),
},
};
}
// Client component reference
if (this.clientReferences.has(type.$$id)) {
const ref = this.clientReferences.get(type.$$id);
return {
$$typeof: Symbol.for('react.client.reference'),
$$id: ref.id,
$$name: ref.exportName,
props: await this.serializeProps(props),
};
}
// Server component - execute
const result = await type(props);
return this.walkTree(result, payload, context);
}
// Serialize props for client
async serializeProps(props) {
const serialized = {};
for (const [key, value] of Object.entries(props)) {
if (typeof value === 'function') {
// Server actions become references
serialized[key] = {
$$typeof: Symbol.for('react.server.reference'),
$$id: registerServerAction(value),
};
} else {
serialized[key] = value;
}
}
return serialized;
}
}
// Build-time transform for 'use client' directive
function transformClientDirective(code, id) {
if (!code.startsWith("'use client'") && !code.startsWith('"use client"')) {
return null;
}
// Parse exports
const exports = parseExports(code);
// Generate proxy module for server
const serverProxy = exports.map(exp =>
`export const ${exp} = { $$id: "${id}#${exp}", $$typeof: Symbol.for('react.client.reference') };`
).join('\n');
return serverProxy;
}
Build Pipeline Orchestration
// BUILD PIPELINE ORCHESTRATION
class BuildPipeline {
constructor(config) {
this.config = config;
this.steps = [];
}
addStep(step) {
this.steps.push(step);
}
async run() {
const context = {
config: this.config,
routes: [],
assets: new Map(),
manifest: {},
};
for (const step of this.steps) {
console.log(`[build] ${step.name}...`);
const start = Date.now();
try {
await step.execute(context);
console.log(`[build] ${step.name} (${Date.now() - start}ms)`);
} catch (error) {
console.error(`[build] ${step.name} failed:`, error);
throw error;
}
}
return context;
}
}
// Standard build steps
const buildSteps = [
{
name: 'scan-routes',
async execute(ctx) {
ctx.routes = await scanRoutes(ctx.config.pagesDir);
},
},
{
name: 'bundle-client',
async execute(ctx) {
ctx.clientBuild = await bundleClient({
routes: ctx.routes,
outDir: `${ctx.config.outDir}/client`,
});
},
},
{
name: 'bundle-server',
async execute(ctx) {
ctx.serverBuild = await bundleServer({
routes: ctx.routes,
outDir: `${ctx.config.outDir}/server`,
});
},
},
{
name: 'prerender-static',
async execute(ctx) {
const staticRoutes = ctx.routes.filter(r => !r.isDynamic);
await Promise.all(staticRoutes.map(r => prerenderRoute(r, ctx)));
},
},
{
name: 'generate-manifest',
async execute(ctx) {
ctx.manifest = {
routes: ctx.routes.map(r => ({
path: r.path,
type: r.type,
prerendered: r.prerendered,
})),
assets: Object.fromEntries(ctx.assets),
buildId: Date.now().toString(36),
};
await fs.writeFile(
`${ctx.config.outDir}/manifest.json`,
JSON.stringify(ctx.manifest, null, 2)
);
},
},
{
name: 'adapt',
async execute(ctx) {
const adapter = getAdapter(ctx.config.adapter);
await adapter.adapt(ctx);
},
},
];
Related Skills
- See web-app-architectures for SPA vs MPA
- See rendering-patterns for SSR/SSG/ISR
- See hydration-patterns for hydration strategies
- See routing-patterns for routing concepts