Migration from v6 to v7
Complete migration specification for upgrading to Dinou v7: breaking changes in data fetching contracts, horizontal and vertical layered segmentation (layout_functions and DinouCacheSlot), Partial Prerendering (PPR), 0-Fork Dual-Bundle in-memory compilation, and multi-runtime edge deployments.
1. Overview & Architectural Leap
Dinou v7 represents a quantum leap in the framework's lifecycle. It shifts from a Node-centric development server reliant on subprocess forking and disk manifests to a universal, 0-fork in-memory Dual-Bundle engine that runs identically across Node.js, Bun, Deno, Cloudflare Workers, and Netlify Functions.
| Dimension | Dinou v6 (Legacy) | Dinou v7 (Current) |
|---|---|---|
getProps Contract | Required nested wrapper: { page: { ... } } | Direct top-level props: { user, title } |
revalidate Unit | Declared in milliseconds (e.g. 60000 ms) | Strictly in seconds (e.g. 60 s) |
| Route Mode & Source of Truth | dynamic was the source of truth (dynamic = true for SSR). revalidate === 0 meant static forever. | revalidate is the single source of truth: revalidate = 0 (SSR), >= 1 (ISR), false (SSG). dynamic is removed. |
| SSG Generation & Startup | Generated at server startup (crawled on npm start, delaying server boot) | Generated at compilation (npm run build); production server boot is instantaneous (0ms) |
| Engine Architecture | Subprocess child_process.fork() with IPC serialization & double RAM | 0-Fork Dual-Bundle in-memory engine via native React 19 W3C Web Streams |
| Layouts & Navigation | Monolithic tree: layouts re-evaluated & re-mounted on navigation | Horizontal Segmentation: 100% layout state preservation (zero remounts) |
| Component Micro-Cache | Not supported (caching strictly tied to full page) | Vertical Segmentation: <DinouCacheSlot> Micro-ISR & client refreshSlot() |
| TTFB & Dynamic Routes | Blocked until slowest database query settles (no streaming shell) | 0ms TTFB with PPR: Instant static shell prelude + streaming dynamic <Suspense> |
| Deployment Targets | Restricted to Node.js environments (due to fork() and Express) | Universal "Deploy Everywhere": Node, Bun, Deno, Cloudflare Workers, Netlify v2 & Static SSG |
2. Breaking Changes (v6 vs v7)
Before launching your migrated application, audit your page_functions.ts (or .js) files, build pipelines, plugins, and development workflows against these seven breaking changes and architectural shifts:
2.1getProps Contract: Direct Props Object Return
In Dinou v6, getProps required wrapping the returned data under a page key. In Dinou v7, this artificial wrapper is completely eliminated. getProps directly returns the object that will be passed as props to your page component:
// src/users/[id]/page_functions.ts
export async function getProps(params) {
const user = await fetchUser(params.id);
return {
page: {
user,
title: `Profile of ${user.name}`,
},
};
}// src/users/[id]/page_functions.ts
export async function getProps(params) {
const user = await fetchUser(params.id);
return {
user,
title: `Profile of ${user.name}`,
};
}The corresponding Page component receives user and title directly as top-level props:
// src/users/[id]/page.tsx
export default function UserProfilePage({ user, title }: { user: User; title: string }) {
return (
<div>
<h1>{title}</h1>
<p>Email: {user.email}</p>
</div>
);
}2.2revalidate Time Unit: Strictly Seconds (s)
In Dinou v6, cache revalidation intervals were specified in milliseconds (ms). In Dinou v7, revalidate is strictly defined in seconds (s). This aligns with HTTP standards (RFC 7234 Cache-Control: max-age=N), edge CDN providers, and component-level cache slots:
// 60,000 milliseconds (1 minute)
export const revalidate = 60000;
// or function form
export function revalidate() {
return 60000;
}// 60 seconds (1 minute)
export const revalidate = 60;
// or function form
export function revalidate() {
return 60;
}💡 Tip: If you leave revalidate = 60000 in v7, your route will be cached for 60,000 seconds (over 16 hours) instead of 1 minute! Be sure to divide existing millisecond numbers by 1000.
2.3Elimination of dynamic: Single Source of Truth via revalidate
In Dinou v6, dynamic was the single source of truth for on-demand SSR (dynamic = true), and revalidate === 0 (or omitted) meant static forever (SSG). In Dinou v7, the dynamic keyword is completely removed in favor of unifying route execution and caching under revalidate as the single source of truth:
Value of revalidate | Rendering Strategy | Runtime Behavior |
|---|---|---|
| 0 (or < 1) | Dynamic (Live SSR) | Rendered on every incoming request. Bypasses static and ISR cache. |
| >= 1 (e.g. 60, 300) | Incremental Static (ISR) | Cached with background stale-while-revalidate expiring after N seconds. |
| false (or omitted) | Static Forever (SSG) | Pre-rendered at build time. Cached indefinitely until on-demand revalidation. |
// src/random/page_functions.ts
export function dynamic() {
return true; // Deprecated!
}// src/random/page_functions.ts
export const revalidate = 0; // Live SSR on every request2.4SSG Lifecycle: Build-Time Generation (dinou build) vs Server Startup
In Dinou v6, static site generation (SSG) was deferred until the production server started (dinou start). The server crawled routes, generated static HTML and RSC payloads, and delayed server readiness. In Dinou v7, static generation occurs strictly Ahead-of-Time during compilation (dinou build).
# v6: Build only packaged client assets
npm run build
# v6: Server crawl & SSG ran at startup (delayed boot)
npm run start# v7: All static HTML & RSC payloads pre-rendered here
npm run build
# v7: Instant boot (0ms) from pre-compiled storage
npm run startIf your deployment pipeline (e.g. Dockerfile or GitHub Actions) executed npm run build without backend database or CMS environment variables, expecting npm start to generate static pages inside the production container, your static pages will be missing.
Action Required: Ensure that your build environment (npm run build) has full network and credential access to any data sources required by getStaticPaths and static getProps.
2.5Layout Lifecycle: 100% State Preservation (Zero Remounts on Navigation)
In Dinou v6, navigation was monolithic: navigating between sibling routes that shared a layout (e.g., from /dashboard/analytics to /dashboard/settings) caused the layout component tree to re-evaluate and re-mount. In Dinou v7, Horizontal Segmentation separates the layout shell from dynamic child pages: layouts never unmount or re-execute during client-side navigation between sibling routes.
// src/dashboard/layout.tsx
export default function DashboardLayout({ children }) {
// In v6: Re-fired on every sibling page navigation
// because the entire layout re-mounted!
useEffect(() => {
trackPageView();
}, []);
return <section>{children}</section>;
}// src/dashboard/layout.tsx
import { usePathname } from "dinou";
export default function DashboardLayout({ children }) {
const pathname = usePathname();
// In v7: Layout never unmounts. Listen to pathname
// or place page-level tracking directly in page.tsx!
useEffect(() => {
trackPageView(pathname);
}, [pathname]);
return <section>{children}</section>;
}Any logic inside layout.tsx (such as useEffect without dependencies, search input states, or video players) will not re-initialize or reset when navigating across pages sharing that layout.
Action Required: If you relied on layout re-mounting to reset local UI state or trigger analytics, move that logic into the companion page.tsx or subscribe to route changes via usePathname().
2.6Elimination of Express onServerInit(app) in Plugins
In Dinou v5.2.0 and v6, plugins registered custom Express middlewares or webhook endpoints by receiving the Express application instance via onServerInit(app). In Dinou v7, Express has been completely eliminated in favor of pure W3C Web Standards (Request, Response, ReadableStream). As a result, onServerInit(app) is no longer invoked or supported.
// dinou.config.js (v6)
module.exports = {
plugins: [
{
name: "custom-api",
onServerInit(app) {
// Deprecated! Express app no longer exists in v7
app.get("/api/health", (req, res) => {
res.json({ ok: true });
});
},
},
],
};// dinou.config.mjs (v7)
import { defineConfig } from "dinou/config";
export default defineConfig({
plugins: [
{
name: "custom-api",
async onRequest(request, context) {
const url = new URL(request.url);
if (url.pathname === "/api/health") {
return Response.json({ ok: true });
}
},
},
],
});Any custom routes, webhooks, or route guards registered through Express must be rewritten using the onRequest(request, context) hook. Returning a standard Response short-circuits the pipeline and immediately responds to the client with zero overhead.
2.7Development Mode Assets: 100% In-Memory by Default (No .dinou/public on Disk)
In Dinou v6, dinou dev continuously wrote JavaScript chunks, CSS stylesheets, sourcemaps, and manifests to disk inside the .dinou/public/ directory. In Dinou v7, the development engine operates 100% in RAM via globalThis.__DINOU_MEM_FILES__ to eliminate disk I/O bottlenecks and Windows antivirus penalties. The .dinou/public/ directory is never created on disk during normal development.
# In v6, dev server continuously writes to disk:
my-app/
└── .dinou/
└── public/ # Created on disk
├── client.js
├── styles.css
└── manifest.json# In v7, served instantly from memory (<0.5ms):
globalThis.__DINOU_MEM_FILES__
├── /client.js (in-memory Buffer)
├── /styles.css (in-memory Buffer)
└── manifest.json (in-memory Buffer)
# (.dinou/public is NOT created on disk)If your local development scripts, reverse proxies (e.g. Nginx), or automated test runners expected physical files inside .dinou/public/ while npm run dev is running, they will find that the folder does not exist.
Action Required: If you need physical assets written to disk during development, set the DINOU_WRITE_TO_DISK=true environment variable:
DINOU_WRITE_TO_DISK=true npm run devNote: In production builds (npm run build), optimized assets are always written to disk inside .dinou/dist3/ and .dinou/dist2/.
3. Layered Segmentation Architecture
Dinou v7 introduces a two-dimensional rendering model: Horizontal Segmentation (decoupling layout shells from dynamic child pages) and Vertical Segmentation (micro-caching individual components inside a page with <DinouCacheSlot> and PPR).
3.1. Horizontal Segmentation & Layout State Preservation
In earlier meta-frameworks, navigating between sibling routes (e.g. /dashboard/analytics to /dashboard/settings) frequently caused the entire layout to re-evaluate, resetting video playback, collapsing open menus, and wiping sidebar search inputs.
Dinou v7 splits client requests into two decoupled endpoints:
- Layout Plane: Renders exclusively the outer layout shell and parallel slots, establishing the boundary where child content will reside.
- Page Plane: Renders exclusively the leaf page component (
<Page />).
When navigating between sibling routes sharing a layout, the layout never unmounts or re-executes. State is 100% preserved. Furthermore, the layout cache is query-string aware (searchPart), so modifying query parameters (e.g. ?tab=billing) updates layout slots in real time without tearing down the shell.
3.2. layout_functions for Decoupled Layout ISR
Layouts can now declare independent cache lifetimes, cache tags, and parameter validations via layout_functions.ts (or layout.functions.ts):
// src/dashboard/layout_functions.ts
export const revalidate = 3600; // Cache the layout shell for 1 hour
export const getCacheTags = ["dashboard-shell", "nav-links"];
export const allowISG = true;
export async function validateParams(params) {
return true;
}🚀 Granular Purging: Calling revalidateTag("dashboard-shell") purges only the layout shell without forcing the re-rendering of all nested sub-pages. To cascade layout invalidation down to all child pages, pass { cascade: true }.
3.3. Surgical Page Revalidation: revalidatePage (New in v7 API)
In Dinou v6, revalidatePage did not exist: on-demand revalidation was limited to revalidatePath, which re-rendered the entire monolithic route tree. Because Dinou v7 completely decouples layouts from child pages, it introduces the revalidatePage(path) API in "dinou/server":
// Inside a Server Function ("use server") or CMS Webhook
import { revalidatePage, revalidatePath } from "dinou/server";
export async function updateArticle(slug: string) {
await db.articles.update({ where: { slug }, data: { ... } });
// ⚡ New in v7: Regenerates strictly page.rsc and index.html
// Leaves parent layout.rsc completely untouched, saving significant CPU and DB queries!
await revalidatePage(`/blog/${slug}`);
}revalidatePage(path) (New in v7)Surgically revalidates only the leaf page (page.rsc) and its static HTML. Does not re-render or touch any layout shell.
revalidatePath(path, { cascade?: boolean })Revalidates both the page and layout in that route. When cascade: true is passed, propagates downward to all nested child pages to guarantee 0 hydration mismatch.
3.4. Vertical Segmentation: <DinouCacheSlot> & refreshSlot()
Dinou v7 introduces component-level micro-caching inside pages with <DinouCacheSlot>:
Level 1: Micro-ISR of Server Components & SWR
Wrap expensive Server Components in a cache slot. Stale content is served instantly while an asynchronous background task updates the cache in the active StorageAdapter:
// src/analytics/page.tsx
import { DinouCacheSlot } from "dinou/server";
export default function AnalyticsPage() {
return (
<div>
<h1>Analytics Dashboard</h1>
{/* Caches this component for 60 seconds; tags allow targeted invalidation */}
<DinouCacheSlot id="metrics-slot" tag="metrics-tag" revalidate={60}>
<ExpensiveMetricsGraph />
</DinouCacheSlot>
</div>
);
}Level 2: In-Place Live Refresh from Client (Zero Page Reload)
Client Components can programmatically trigger a live re-render of a single slot using refreshSlot(id) from "dinou" without reloading the page or touching sibling states:
"use client";
import { refreshSlot } from "dinou";
export function LiveRefreshButton() {
return (
<button onClick={() => refreshSlot("metrics-slot")}>
Refresh Metrics Only
</button>
);
}3.5. Partial Prerendering (PPR) Engine
Partial Prerendering combines the instant delivery of static sites (0ms TTFB) with the personalization of dynamic SSR in a single HTTP response:
- Static Shell Prelude: The outer layout and skeleton placeholders are served at 0ms TTFB directly from edge CDN cache.
- Streaming Dynamic Holes: Components wrapped in
<Suspense>execute concurrently on the server and stream in as chunks that swap the skeleton with live content.
Configuring PPR:
1. Global
In dinou.config.mjs:
defineConfig({ ppr: true })2. Layout Scope
In layout_functions.ts:
export const ppr = true;3. Page Override
In page_functions.ts:
export const ppr = false;// src/dashboard/page.tsx
import { Suspense } from "react";
import { getContext } from "dinou";
async function UserFeed() {
const ctx = getContext();
const user = ctx?.req?.cookies?.user || "Guest";
const data = await fetchFeedForUser(user);
return <FeedList items={data} />;
}
export default function DashboardPage() {
return (
<div>
{/* 🚀 Static Shell: Sent at 0ms TTFB */}
<header><h1>Dashboard</h1></header>
{/* ⚡ Dynamic Hole: Streamed and replaced on the fly */}
<Suspense fallback={<div className="skeleton">Loading feed...</div>}>
<UserFeed />
</Suspense>
</div>
);
}4. Dual-Bundle In-Memory Engine (0-Fork) & Pre-Rendering
React 19 imposes a strict module-resolution constraint: Server Components must be imported under the "react-server" condition, while SSR rendering requires client conditions ("browser" / react-dom/server.edge). Importing both in the same module scope throws a fatal conflict.
Earlier meta-frameworks circumvented this in Node.js via child_process.fork(). However, this introduced IPC serialization latency, doubled memory usage, and made deployment to edge runtimes (Cloudflare Workers, Deno Deploy) impossible.
Dinou v7 introduces an Ahead-of-Time (AOT) Dual-Bundle In-Memory Pipeline:
- Pass A (RSC Engine): Bundled with
["react-server"]conditions. Binds Server Functions and generates client reference descriptors. - Pass B (SSR Engine): Bundled with
["browser"]conditions. Packagesreact-dom/server.edgeand client manifests. - Pass C (Orchestrator): Interconnects Pass A and Pass B via standard W3C
ReadableStreamand handles static assets. In development, files exist purely inglobalThis.__DINOU_MEM_FILES__with <0.5ms response times.
In Dinou v6, static generation ran when starting the production server (dinou start). In Dinou v7, static generation takes place strictly during dinou build. When you run dinou start, every pre-rendered HTML file and RSC payload is already baked into the StorageAdapter. The production server boots instantaneously.
5. Updating Dependencies, StrictMode & Private Folders
Dinou v7 requires a minimum of React 19.3.0 (react >= 19.3.0 and react-dom >= 19.3.0) as peer dependencies. Update your dependencies:
npm install react@^19.3.0 react-dom@^19.3.0 dinou@latest
npm install -D @types/react@^19.3.0 @types/react-dom@^19.3.0React StrictMode by Default (Configurable via dinou.config.mjs)
In development mode (dinou dev), client component roots are automatically wrapped in <React.StrictMode> to detect side-effects early and ensure DEV-PROD parity with React Compiler. You can configure or disable it in dinou.config.mjs:
// dinou.config.mjs
import { defineConfig } from "dinou/config";
export default defineConfig({
ppr: true, // Enable Partial Prerendering globally
reactStrictMode: true, // Default: true in development
});Private Folders with Underscore Prefix (_)
Folders prefixed with an underscore (such as src/_components/, src/_legacy/ or src/_drafts/) completely escape the router scan. Even if the folder contains page.tsx or layout.tsx, Dinou will never register them as routes or pre-render them.
6. CLI Standardization & package.json Scripts
Dinou v7 introduces uniform CLI subcommands to select between esbuild (default), Rollup, and Webpack across all runtimes, native binaries, and export modes. Update your package.json scripts:
{
"scripts": {
"dev": "dinou dev",
"build": "dinou build",
"start": "dinou start",
"dev:esbuild": "dinou dev:esbuild",
"dev:rollup": "dinou dev:rollup",
"dev:webpack": "dinou dev:webpack",
"build:node": "dinou build:node",
"build:node:esbuild": "dinou build:node:esbuild",
"build:node:rollup": "dinou build:node:rollup",
"build:node:webpack": "dinou build:node:webpack",
"start:node": "dinou start:node",
"build:cloudflare": "dinou build:cloudflare",
"build:deno": "dinou build:deno",
"build:deno:compile": "dinou build:deno:compile",
"start:deno": "dinou start:deno",
"build:bun": "dinou build:bun",
"build:bun:pure": "dinou build:bun:pure",
"build:bun:compile": "dinou build:bun:compile",
"start:bun": "dinou start:bun",
"export-static": "dinou export-static",
"eject": "dinou eject"
}
}All three bundlers share 100% feature parity: RSC parsing, client directive splitting, server functions RPC, and CSS Modules.
7. Multi-Runtime, Serverless & Static Deployments
Dinou v7 decouples cache storage and runtime operations via StorageAdapter (FileSystemStorage, CloudflareKVStorage, DenoKVStorage, MemoryStorage). Deploy to any infrastructure with zero platform lock-in:
Deploy to Docker, VPS, AWS EC2, Railway, or Render with dinou build:node and dinou start:node. Full support for streaming SSR, Server Functions ("use server"), and background ISR via FileSystemStorage.
Deploy with official support for Netlify Functions v2 via dinou/adapters/netlify.js. Pre-configured with preferStatic: true, Netlify serves assets directly through its edge CDN, invoking serverless execution only for dynamic SSR and Server Functions.
Build edge-ready serverless bundles with dinou build:cloudflare. Backed by CloudflareKVStorage and standard Fetch handlers. Deploy with npx wrangler deploy.
Build for Deno Deploy and edge environments with dinou build:deno, featuring native Deno KV storage integration (Deno.openKv()). Compile single zero-dependency standalone executables with dinou build:deno:compile.
Leverage Bun's native HTTP server with dinou build:bun, build 100% native Node-free servers with dinou build:bun:pure, or compile to single-binary with dinou build:bun:compile.
Generate pure static HTML and assets inside out/ using dinou export-static. Ideal for zero-cost hosting on GitHub Pages, Cloudflare Pages, Surge, or AWS S3.
⚠️ Caveat on Server Functions: In static exports without an active backend server process, Server Functions ("use server" RPC calls) will not execute because there is no backend server listening to process mutations. For interactive server-side RPCs, deploy to a runtime server (Node, Netlify, Bun, Deno, or Cloudflare Workers).
8. Updating .gitignore
Update your .gitignore to ignore the output directories of the multi-runtime engine:
node_modules
.dinou
out
dist
.wrangler
.envout/stores static HTML and assets exported viadinou export-static.dist/holds standalone single-executable binaries (e.g., generated withbuild:deno:compileorbuild:bun:compile)..wrangler/is created automatically by Cloudflare when running local worker previews.