Dinou Internals & Architecture
Deep dive into how Dinou implements React Server Components, Server-Side Rendering (SSR), Incremental Static Generation (ISG), and Server Functions under the hood.
This guide is written for developers who have ejected the framework or want to understand exactly how a custom React Server Components framework is implemented from scratch using Express.js.
β οΈ Disclaimer & Source of Truth
The goal of this page is to bring the details of the ejected framework closer to the end developer to facilitate its understanding. However, given the high complexity of the internal engine, this analysis has been prepared with the assistance of an Artificial Intelligence (AI) agent and may contain inaccuracies or conceptual errors. The ultimate source of truth is always the actual code inside your ejected folder.
π‘ Tip: If you want to explore the ejected codebase yourself, we highly encourage feeding your local ./dinou/ folder to an AI coding assistant. Refer to the Ejected Folder Reference for a comprehensive file-by-file breakdown, or read more in AI-Friendly Design.
π‘ Overview & Architecture Blueprint
Dinou is built on standard, vanilla Node.js primitives. There are no black boxes; when you run npm run eject, the entire framework code (found under the ./dinou/ directory, including the core engine under ./dinou/core/ and the bundler integrations under ./dinou/esbuild/, ./dinou/rollup/, and ./dinou/webpack/) is copied into your repository, giving you complete freedom to inspect and modify it. For a quick map of what each generated file does, see the Ejected Folder Reference.
Under the hood, Dinou coordinates a dual module system (CommonJS and ES Modules) and splits execution across two distinct Node.js processes to render pages. The blueprint below visualizes this request lifecycle:
1. The CJS-to-ESM Bridge (The Code Jump)
The main server (server.js) is written in CommonJS (CJS) using standard require() statements. However, all page components and layouts are written in ES Modules (ESM) containing JSX and TypeScript.
Because CJS cannot synchronously load ESM files, Dinou implements import-module.js. When a route is requested, Express triggers a dynamic asynchronous await import(fileUrl). This call acts as the boundary jump from the CJS server execution loop to the asynchronous ES Module registry.
This dynamic import instantly fires the custom babel-esm-loader.js, which intercepts the request, transpiles JSX/TS on the fly via Babel, parses exports, and delivers executable JS back to the process.
2. The Two-Process SSR Pipeline (The Process Separation)
React 19 ships two incompatible module graphs: a server-side variant for rendering RSC (react.react-server.js) and a standard client-side variant for rendering HTML (react-dom/server). Loading both in the same Node.js process causes V8 memory collisions.
Dinou resolves this by running two isolated Node.js processes:
- Parent Process (
server.js): Launched with the--conditions=react-serverflag. It evaluates page components in the ESM graph and produces the binary RSC Flight Stream. - Child Process (
render-html.js): Forked dynamically without conditions. It receives the Flight Stream from the parent via a dedicated data pipe (file descriptorfd:4), deserializes it with client-side React, renders the final HTML shell viarenderToPipeableStream, and streams the HTML back to the parent'sstdoutto be flushed to the browser.
π¦ 1. The Module System
Node.js by default is unaware of React Server Components (RSC) build constraints and JSX specifiers. React 19 ships two incompatible module graphs:
react(standard client-side variant)react.react-server(server-side variant for rendering RSC payloads)
Running both graphs in the same Node.js process causes naming and execution boundary conflicts (such as V8 loading two mismatched, duplicate instances of React in memory, which crashes the rendering engine).
Dinou resolves this conflict by forcing the entire server process to run strictly under the React Server (RSC) graph. To achieve this, it overrides module resolution in both environments:
- ES Modules (ESM) Resolver: Native imports (like
import react from "react") are routed to the.react-serverbuilds natively by running Node.js with the--conditions=react-serverflag. - CommonJS (CJS) Resolver: Any synchronous require calls (like
require("react")) inside the Express server or the ESM loader thread are intercepted by overriding Node's internalModule._resolveFilenameto redirect them to the.react-serverentry points.
Inside the CommonJS environment (used by Express to resolve dependencies, require paths, and parse style module JSON structures), Dinou hooks into the runtime through three main pillars:
A. Overriding Module._resolveFilename
To direct React imports to their RSC equivalent, Dinou intercepts Node's module resolution pipeline in server.js:
// dinou/core/server.js
Module._resolveFilename = function (request, parent, isMain, options) {
if (!isWebpack) { // Webpack resolves its own graph using manifests
if (request === "react") return reactServerPath;
if (request === "react-dom") return reactDomServerPath;
if (request === "react/jsx-runtime") return reactJsxRuntimePath;
if (request === "react/jsx-dev-runtime") return reactJsxDevRuntimePath;
}
return originalResolveFilename.call(this, request, parent, isMain, options);
};This resolution mapping is skipped if using Webpack (isWebpack = true) because the react-server-dom-webpack/node-register package automatically hooks the runtime loader.
B. Runtime TSConfig Paths Registration
Before requiring components, Dinou imports dinou/core/register-paths.js. This file dynamically reads the project's tsconfig.json or jsconfig.json at startup, extracts the compilerOptions.paths configurations, and registers them using the tsconfig-paths library:
// dinou/core/register-paths.js
const configFile = getConfigFileIfExists(); // checks tsconfig.json -> jsconfig.json
if (configFile) {
const config = require(configFile);
const { baseUrl, paths } = config.compilerOptions || {};
if (baseUrl && paths) {
tsconfigPaths.register({
baseUrl: path.resolve(process.cwd(), baseUrl),
paths,
});
}
}This enables Node's CJS runtime to resolve clean path aliases (e.g. require("@/components/Header")) without crashing.
C. Require Extensions Hooks (CSS & Assets)
In the CJS environment, importing non-JavaScript files normally causes crashes. Dinou overrides Node's native require.extensions to dynamically preprocess style modules and media assets at import time:
- CSS Modules Hook (
css-require-hook.js): Interceptsrequire.extensions[".css"]. It reads the file, parses class names synchronously using PostCSS, hashes local class names deterministically via a custom hashing utility, and exports a JSON object mapping original classes to hashed classes (mimicking webpack's CSS modules):require.extensions[".css"] = function (module, filename) { const cssContent = fs.readFileSync(filename, "utf8"); const jsonResult = {}; // PostCSS plugin traverses rules, matches local class selectors, // hashes them and writes mapping to jsonResult... module.exports = jsonResult; }; - Asset Loader Hook (
asset-require-hook.js): Intercepts media extensions (like.png,.svg,.jpg). It reads the asset, generates a hashed URL prefix (e.g./assets/logo-xyz.png) vialoader-utils, and registers the string as the default export:require.extensions[".png"] = function (module, filename) { const url = compile(filename); // Returns e.g. "/assets/logo-[hash].png" module._compile("module.exports = " + JSON.stringify(url), filename); };
D. The CommonJS-to-ESM Bridge (import-module.js)
In a standard Node.js environment, CommonJS modules cannot synchronously load ES Modules using require() (doing so throws a ERR_REQUIRE_ESM exception).
Initially, Dinou resolved this by attempting a require() first and falling back to a dynamic await import() if it failed. The current refactored version segregates this logic based on the active bundler (Webpack vs. Rollup / esbuild) for two critical reasons:
- Ensuring ESM Loader Interception (Rollup & esbuild): For Rollup and esbuild builds, Dinou utilizes a custom Node.js ESM loader (
babel-esm-loader.js) to handle runtime TS/JSX transpilation and Server Component reference registration (viaregisterServerReference). Node.js only runs custom ESM loader hooks on modules fetched viaimportstatements. If the engine attempted arequire()first, it would either crash or bypass the loader entirely (using legacy CJS transpilation registers), breaking React Server Component hydration. Forcingawait import()directly ensures the compilation pipeline is consistently intercepted. - Webpack Cache Invalidation & Hybrid Module Loading: Webpack produces a hybrid CommonJS/ESM module output. To allow Hot Module Replacement (HMR) in development, Dinou must invalidate the server modules from memory when they change.
Bypassing vs. Clearing Cache: In Node.js, V8's native ESM registry is immutable and does not provide an API to delete entries. While we can bypass this in Rollup/esbuild by appending a query timestamp (e.g.?t=timestamp), this forces V8 to instantiate a new module side-by-side in memory (a controlled development memory leak). In Webpack, however, this timestamp bypass breaks Webpack's internal dependency resolution and manifests. Webpack requires a physical cache clearance of Node's CommonJS module registry:Attemptingdelete require.cache[require.resolve(absPath)];require()first on Webpack configurations allows Dinou to purge the physical file cache cleanly in development. If the resource is a native ESM bundle, the caught exception safely redirects it to a dynamicimport().
// dinou/core/import-module.js
async function importModule(modulePath) {
const absPath = path.isAbsolute(modulePath) ? modulePath : path.resolve(process.cwd(), modulePath);
if (!isWebpack) {
let fileUrl = pathToFileURL(absPath).href;
if (process.env['NODE_ENV'] !== "production") {
fileUrl += `?t=${Date.now()}`; // Cache-busting for ESM modules
}
const mod = await import(fileUrl); // Direct ESM Loader entry
return mod;
}
try {
if (process.env['NODE_ENV'] !== "production") {
try {
const resolved = require.resolve(absPath);
delete require.cache[resolved]; // CJS Cache clearing
} catch (e) {}
}
return require(absPath);
} catch (err) {
if (err.code === "ERR_REQUIRE_ESM" || /require\(\) of ES Module/.test(err.message)) {
let fileUrl = pathToFileURL(absPath).href;
if (process.env['NODE_ENV'] !== "production") {
fileUrl += `?t=${Date.now()}`;
}
const mod = await import(fileUrl); // Fallback for ES bundles
return mod;
}
throw err;
}
}βοΈ 2. The ESM Loader
When using native ES Modules (import/export), Node.js bypasses CommonJS's CJS-specific require overrides (like tsconfig-paths and require.extensions). Dinou solves this by registering a custom Node.js ESM loader (babel-esm-loader.js) via Node's official module.register() inside register-loader.mjs.
The loader hooks into the ESM resolution and loading lifecycles through two hooks: resolve and load.
CJS/ESM Synergy: To avoid duplicate parsing logic, the ESM loader coordinates directly with CJS hooks. For instance, when loading CSS modules inside the ESM graph, the loader calls CJS require() internally to trigger the CJS PostCSS Require Hook (Section 1.C), captures the returned class name JSON map, and wraps it into an ESM-compliant virtual default export.A. The resolve Hook: Alias & Extension Resolution
Because Node.js ES Modules strictly require explicit file extensions (unlike CommonJS) and do not natively support path aliases, the resolve hook intercepts import specifiers and resolves them dynamically using get-abs-path-with-ext.js:
// dinou/core/babel-esm-loader.js -> resolve hook
exports.resolve = async function resolve(specifier, context, defaultResolve) {
const absPathWithExt = getAbsPathWithExt(specifier, context);
if (absPathWithExt) {
return {
url: pathToFileURL(absPathWithExt).href,
shortCircuit: true,
};
}
return defaultResolve(specifier, context, defaultResolve);
};How getAbsPathWithExt resolves paths:
- Alias Mapping: Parses
tsconfig.jsonorjsconfig.jsononce at startup. If a specifier matches an alias (e.g.@/components/Welcome), it maps it to the target absolute base directory. - Extension Probing: Probes for files by checking for standard extensions in order:
.js,.ts,.jsx,.tsx. If the target is a directory, it automatically probes for/index.[ext]files inside it.
B. Thread Isolation & Resolution Dualism (Why Module._resolveFilename is in both files)
You will notice that Module._resolveFilename is overridden in both server.js (main thread) and babel-esm-loader.js (ESM loader context). This duplication is critical:
- ESM Native Resolution: For native ES Module imports (e.g.
import react from "react"), Node.js uses its built-in ESM resolver. Since Dinou is launched with the--conditions=react-serverflag, Node automatically directs these imports to thereact-serverconditions defined in the package exports. - CommonJS Resolution Fallback: However, both the main Express server and the ESM loader thread frequently execute legacy CommonJS code (such as compiling stylesheets with PostCSS or running Babel register). When these modules execute a sΓncrone
require("react"), they bypass the ESM conditions resolver and fall back to the CommonJS registry. - Process Safety: Because Node.js handles ESM custom loaders in a separate execution context (or worker thread), the loader thread has its own separate CommonJS resolution cache. Overriding
Module._resolveFilenamein both contexts guarantees that neither thread ever mistakenly imports standard client React, preventing V8 environment conflicts or duplicate runtime crashes.
C. The load Hook: Asset Transformation & Transpilation
The load hook intercepts the file URLs resolved in the previous stage, reads the contents, and compiles them into compatible ESM format on the fly.
To load CSS modules inside the ESM graph, Dinou leverages an elegant CJS-ESM synergy. Instead of re-implementing the PostCSS parser, the loader calls the CJS require() hook on the stylesheet, which outputs the hashed JSON class map. The loader then wraps this class map in a virtual ESM export:
if (ext === ".css") {
const classMap = require(fileURLToPath(url)); // Triggers CJS css-require-hook
return {
format: "module",
source: `export default ${JSON.stringify(classMap)};`,
shortCircuit: true,
};
}For media files, the loader reads the file path and outputs a virtual default export string pointing to the public hashed asset path (e.g., export default "/assets/logo-abc.png";).
If the loaded file is executed under the react-server environment (detected by checking if react-server exists in process.execArgv) and contains a "use client" header:
- The loader parses the file's AST with Babel to isolate all exported symbols.
- It builds and returns a mock ESM module where every symbol is a registered client component reference stub using React's official
registerClientReferenceutility:
// Generated on-the-fly by the loader for esbuild/rollup builds:
import pkg from "@roggc/react-server-dom-esm/server.node.js";
const { registerClientReference } = pkg;
export const myComponent = registerClientReference(
function() { throw new Error("Attempted to call myComponent() from server but it is on the client."); },
"file:///src/components/myComponent.tsx",
"myComponent"
);
export default registerClientReference(
function() { throw new Error("Attempted to call the default export of myComponent from server..."); },
"file:///src/components/myComponent.tsx",
"default"
);This stubbing prevents the server from executing client React code, while providing React's serializer with the metadata required to generate the correct client hydration references in the RSC Flight payload.
If a file does not trigger a "use client" bailout, the loader compiles JSX and TypeScript code synchronously using Babel with @babel/preset-react (configured with runtime: "automatic" to inject standard JSX runtime modules) and @babel/preset-typescript.
π 3. File-System Router
A. Gatekeeper: Anti-Bot Shield (DoS Mitigation)
Before matching URL paths to the filesystem, Dinou runs an Express-level gatekeeper middleware in server.js. This middleware checks the request path against common scanner target extensions (like .php, .env, or wp-admin) to terminate malicious bot requests instantly with a raw 404 Not Found. This prevents bot traffic from touching routing code, compiling pages, or spawning expensive child processes:
// server.js -> Anti-bot middleware
const botGarbagePatterns = [
/\.php$/i, /\.env$/i, /\.git\b/i, /\.sql$/i, /\.bak$/i, /\.log$/i,
/wp-admin/i, /wp-content/i, /wp-includes/i, /xmlrpc\.php/i,
/\.asp$/i, /\.jsp$/i, /\.cgi$/i
];
app.use((req, res, next) => {
const isGarbage = botGarbagePatterns.some(pattern => pattern.test(req.path));
if (isGarbage) {
return res.status(404).send("Not Found"); // Terminate instantly
}
next();
});B. Route Pattern Resolution
Dinou traverses the src/ directory recursively (using get-file-path-and-dynamic-params.js) to match URL paths to page files:
| Pattern | Example Folder | Behavior |
|---|---|---|
| Static | src/about/ | Matches /about directly. |
| Dynamic Parameter | src/[slug]/ | Captures a route segment (e.g. params.slug). |
| Optional Parameter | src/[[id]]/ | Matches optionally (with or without id segment). |
| Catch-all | src/[...rest]/ | Captures all trailing route segments as an array. |
| Route Group | src/(marketing)/ | Organizes code structure without affecting public URLs. |
C. The Recursive Segment Crawler (getFilePathAndDynamicParams)
Dinou's routing matches incoming URL paths to page layouts dynamically using the recursive crawler function getFilePathAndDynamicParams. Here is how the traversal algorithm resolves files:
- Segment Splitting: The router splits the pathname by slashes (e.g.,
/blog/post-1becomes["blog", "post-1"]) and initiates the crawler atindex = 0, starting inside thesrc/root directory. - Static Matching (Precedence 1): First, the crawler probes for an exact folder match. If
src/blogexists, it enters the directory and calls itself withindex = 1. - Route Group Resolution (Precedence 2): If no static folder exists, it checks if any directories are wrapped in parentheses (e.g.,
src/(auth)). If found, the crawler recursively walks into that folder without incrementing theindexcursor, keeping the route group invisible to the public URL. - Dynamic Single Parameter (Precedence 3): Next, it probes for folders matching the single bracket syntax (e.g.
src/[id]). It extracts the variable name ("id"), URL-decodes the active segment ("post-1"), writes it to the parameters registry (dParams.id = "post-1"), and enters the directory. - Catch-All & Optional Catch-All (Precedence 4):
- Catch-All (
[...rest]): Binds all remaining segments from the activeindexto the end of the URL array, saving them as a string array parameter. - Optional Catch-All (
[[...rest]]): Behaves identically to catch-all, but matches even if theindexhas exceeded the URL segments (returning an empty array instead of failing).
- Catch-All (
Crawler Signature & Parameter Breakdown
The recursive crawler is signatured as follows in get-file-path-and-dynamic-params.js:
function getFilePathAndDynamicParams(
reqSegments, query, currentPath, fileName = "page",
withExtension = true, finalDestination = true, lastFound = undefined,
index = 0, dParams = {}, accumulative = false, accumulate = [],
isFound = { value: false }, possibleExtensions = [".tsx", ".ts", ".jsx", ".js"]
)Each parameter drives a specific aspect of the routing and compilation lifecycle:
reqSegments(Array): The segments of the requested URL split by slashes (e.g.["blog", "hello"]).query(Object): Parsed URL query parameters (e.g.?search=react). Note: While this parameter is actively passed down through the recursive crawler walks and intogetSlots(), it is currently a vestige / dead parameter inside the framework's codebase. It is neither read for resolving slots nor passed down to page or layout components as props (components access query parameters directly using the Request Context instead).currentPath(String): The absolute file path of the directory reached at the current step of the recursive resolution.fileName(String): The file target base name we are searching for (defaults to"page"to find endpoints, but set to"layout"when compiling layout trees,"page_functions"for route configurations, or"error"/"not-found"for error boundaries).withExtension(Boolean): Toggles file extension appending. If false, searches directories or folders matching the raw name.finalDestination(Boolean): If true, returns a match only if V8 completely exhausts the URL segments cursor. If false, permits returning the nearest parent file match (e.g. locating layout wrappers).lastFound(String): A rolling accumulator that tracks the nearest ancestor matching file found during the descent. WhenfinalDestinationisfalse(such as when searching for the closesterror.tsxboundary ornot-found.tsxfallback along a route path),lastFoundacts as a bubble fallback. If the target folder at the end of the URL doesn't contain the requested file, the crawler returns the closest ancestor file recorded inlastFoundin a single pass.index(Number): The segment cursor pointer. Tells the crawler which segment of thereqSegmentsarray is currently being evaluated.dParams(Object): An accumulator dictionary of resolved dynamic route parameters (e.g.{ slug: 'hello' }).accumulative(Boolean): If true, changes the return behavior to collect all layouts found along the path instead of returning only the final leaf page.accumulate(Array): The list accumulator for Layouts. Aggregates tuples of[layoutPath, params, slots]found on the descent.isFound(Object): A mutable reference object ({ value: boolean }) shared across calls to instantly halt search branches once a route resolves.possibleExtensions(Array): Probe extensions list. Defaults to[".tsx", ".ts", ".jsx", ".js"].
D. Nested Layouts & Parallel Slots
Dinou's router operates in two modes. In accumulative mode, it descends the folder hierarchy toward the target route page. At each step, it records any co-located layout.tsx module and compiles them into a nested React component hierarchy automatically.
During this layout accumulation, the router scans directory siblings using a getSlots crawler. If it finds directories prefixed with an @ symbol (representing Parallel Slots), it compiles the slot target and instantiates it dynamically:
// get-file-path-and-dynamic-params.js -> getSlots helper
function getSlots(currentPath, reqSegments, query) {
let slots = {};
const entries = readdirSync(currentPath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.startsWith("@")) {
// Find the page file inside the slot folder
const [slotPath, slotParams] = getFilePathAndDynamicParams(
reqSegments, query, path.join(currentPath, entry.name), "page", ...
);
if (slotPath) {
const SlotComponent = require(slotPath).default;
const slotName = entry.name.slice(1); // e.g. "@sidebar" -> "sidebar"
// Dynamically instantiate the parallel slot as a React element
slots[slotName] = React.createElement(SlotComponent, {
params: slotParams,
key: slotName,
});
}
}
}
return slots;
}These instantiated slots are passed down directly as React props to the parent layout.tsx (e.g. props.sidebar).
E. Virtual Filesystem (vfs.js) in Production
Performing recursive disk reads (using fs.existsSync and fs.readdirSync) on every request would degrade server performance.
In production mode (process.env['NODE_ENV'] === "production"), Dinou initializes a Virtual Filesystem (vfs.js) at startup. It crawls the src/ directory recursively once, building a nested, in-memory directory representation. The routing engine intercepts all filesystem calls and resolves paths synchronously against this in-memory tree:
// dinou/core/vfs.js
function existsSync(filePath) {
if (isDevelopment) return fs.existsSync(filePath);
const normalized = path.resolve(filePath);
return !!vfs[normalized]; // Check in-memory index
}π 4. Two-Process SSR Pipeline
Dinou renders HTML inside a forked child process (render-html.js) rather than on the parent Express process.
This isolation is mandatory because the parent process executes components in a react-server ESM environment, whereas generating HTML shells requires the regular react-dom/server (which is incompatible with the RSC graph).
1. When a request hits, the parent process obtains the RSC tree and pipes it as a Flight stream to the child process via a dedicated file descriptor (fd: 4).
2. The child process receives the Flight stream and deserializes it using createFromNodeStream.
3. The child process streams the final compiled HTML shell back to parent stdout, which pipes it straight to the Express response.
// render-app-to-html.js (Parent forks and sets up fd:4 for Flight stream)
const child = fork(renderHtmlPath, [reqPath, ...], {
stdio: ["ignore", "pipe", "pipe", "ipc", "pipe"], // stdio[4] is the RSC pipe
});
// Parent writes RSC stream into the child's input pipe
renderToPipeableStream(jsx, baseUrl).pipe(child.stdio[4]);
// Parent pipes child's stdout (HTML) to Express client
child.stdout.pipe(res);IPC Context Proxy
Since Server Components are rendered inside the child process, actions like redirecting the client, setting status codes, or setting cookies must be relayed back to the parent Express response. Dinou achieves this by passing a proxy object via IPC:
function createResponseProxy() {
function sendCommand(command, args) {
if (typeof process.send === "function") {
process.send({ type: "DINOU_CONTEXT_COMMAND", command, args });
}
}
return {
clearCookie: (name, options) => sendCommand("clearCookie", [name, options]),
cookie: (name, value, options) => sendCommand("cookie", [name, value, options]),
setHeader: (name, value) => sendCommand("setHeader", [name, value]),
redirect: (arg1, arg2) => arg2 ? sendCommand("redirect", [arg1, arg2]) : sendCommand("redirect", [arg1]),
status: (code) => sendCommand("status", [code]),
};
}Handling Commands during Streaming
When a command is received from the child process, the parent process behaves differently depending on whether it has already started sending HTML chunks to the browser:
- Scenario A: Headers Not Sent (Before Streaming): The parent simply calls the native Express response methods directly (e.g.
res.cookie(),res.redirect(), orres.status()). - Scenario B: Headers Already Sent (During Streaming): HTTP headers cannot be mutated once they are committed. Dinou manages this via client-side JavaScript injection:
- Redirects: Parent injects a
<script>window.location.href = "/url";</script>block into the HTML stream, closes the connection, and kills the child process (child.kill()) to halt further rendering. - Cookies: Injects
<script>document.cookie = "...";</script>to set the cookie on the client browser. - Statuses: Ignored (Express logs a developer warning on console).
- Redirects: Parent injects a
β οΈ Security Limitation: HttpOnly Cookies during Streaming
HttpOnly cookie after the HTML stream has started (e.g., inside a deeply nested Server Component that renders slowly), the action will fail. Because HttpOnly cookies cannot be read or written by client-side JavaScript, Dinou cannot inject them via document.cookie. Always set critical auth or session cookies inside middlewares or page functions (which execute before headers are sent).πΎ 5. The SSG Pipeline
Dinou builds and compiles static pages inside build-static-pages.js. The compilation flow coordinates 4 distinct phases:
generateStatic()
β rmSync(dist2/) // Phase 1: Clean build folder
β buildStaticPages() // Phase 2: Traverse folders and register static routes
β generateStaticRSCs() // Phase 3: Write client RSC JSON payloads to dist2/
β generateStaticPages() // Phase 4: Compile and write static HTML files to dist2/Phase 1: Folder Traversal & Dynamic Parameter Parsing
To build a complete index of static URL targets, Dinou crawls the src/ directory recursively using the async helper collectPages(). This crawler handles the complex task of expanding dynamic routes into concrete static file paths:
π‘ Conceptually: The "Detective Explorer"
Think ofcollectPages()as an explorer mapping all routes in your project. For static folders (likesrc/about), it simply notes down/about. However, dynamic folders (likesrc/blog/[slug]) pose a problem: the compiler cannot create a physical file named[slug]/index.html. To resolve this, the explorer imports yourpage_functions.tsfile and runsgetStaticPaths(). If it returns[{ slug: 'hello' }, { slug: 'world' }], the explorer multiplies that path, translating the dynamic folder into two concrete paths:/blog/helloand/blog/world, then continues recursing down each branch.
collectPages() AlgorithmThe function takes the following parameters:
currentPath(String): The absolute folder path currently being traversed.segments(Array): The accumulated static path segments (e.g.,["blog", "first-post"]).params(Object): The resolved dynamic parameters dictionary (e.g.,{ slug: 'first-post' }).dynamicStructure(Array): The sequence of dynamic parameter names encountered along the route path (e.g.,["category", "id"]).doNotPushAtEnd(Boolean): A flag to prevent duplicate page mapping when resolving optional catch-all routes.
How it resolves dynamic directories:
- Bailout for Dynamic Routes: When the crawler enters a dynamic folder (e.g.,
src/blog/[slug]), it maps and imports its co-locatedpage_functionsfile. If the module exports a functiondynamic()returningtrue, the crawler immediately ignores the directory for SSG, marking it as a request-time SSR-only route. - Invoking
getStaticPaths(): If the route is static, the crawler executesgetStaticPaths(). This function must return an array of path parameter mappings:// Example page_functions.ts export async function getStaticPaths() { const products = await db.getProducts(); return products.map(p => ({ category: p.cat, id: p.id })); } - The Gap Check (Path Validation): For catch-all or nested dynamic segments, the crawler flattens the returned parameter values and performs a strict validation checks:If an intermediate segment parameter is missing (e.g., resolving to
// Detection of prohibited intermediate gaps: const hasGap = flatSegments.some((seg, index) => { const isUndefined = seg === undefined || seg === null || seg === ""; if (!isUndefined) return false; // Gap is invalid if a defined segment exists further to the right const remaining = flatSegments.slice(index + 1); return remaining.some(s => s !== undefined && s !== null && s !== ""); });[undefined, "something"]), the route path contains a gap and is skipped. - Recursion Expansion: The resolved segments are appended to the accumulated path, the parameters are normalized, and
collectPages()recurses into the subdirectory. - Leaf Page Yielding: When a static folder containing a
page.tsxis reached, the crawler registers the final route path along with its gathered parameters and Layout/Slot structure to the pages build list.
What happens next? The Route Processing Pipeline
Once collectPages() finishes crawling, it returns a flat array of all resolved static page configurations, mapping physical folders to URL segment structures and parameter values:
// Result from collectPages:
[
{ path: "c:/project/src/blog/[slug]", segments: ["blog", "hello"], params: { slug: "hello" } },
{ path: "c:/project/src/blog/[slug]", segments: ["blog", "world"], params: { slug: "world" } },
{ path: "c:/project/src/about", segments: ["about"], params: {} }
]Dinou's compiler loops through this collection and feeds each route configuration through a multi-step compilation pipeline:
- Mock Request Context Creation: For each URL target (e.g.
/blog/hello), the engine mocks an Express request and response wrapper so that component rendering stores have access to standard route context variables. - Bailout Proxy Spying (Phase 2): It wraps the mock request's cookies, headers, and query parameters in Javascript Proxies.
- Props Resolution & Layout Nesting: The compiler dynamically requires the page component, executes
getProps(params)to resolve its static props, and recursively wraps the page element inside all matching parent layout modules and parallel slots. - Bailout Checking: During this tree execution, if any component or middleware attempts to read from cookies or headers, the proxy spy triggers a callback setting
isStatic = false. This causes the compiler to instantly abort static compilation for this pageβmarking it to render as dynamic SSR at request time instead. - Payload Output: If the page renders cleanly without triggering a bailout, it is flagged as static:
- The nested React element tree is compiled to standard RSC JSON (Flight stream) via
asyncRenderJSXToClientJSX(jsx)and written to the build folder. - The route URL and metadata (like revalidation intervals or redirection side-effects) are registered in the global static routes registry, which is later compiled to the final index HTML files.
- The nested React element tree is compiled to standard RSC JSON (Flight stream) via
Phase 2: Dry Render with Proxy Spies
To determine whether a route can be statically pre-rendered or if it relies on request-time inputs (cookies, headers, or query parameters), Dinou performs a dry render. It feeds the page component a mock request context where these inputs are wrapped in JavaScript Proxies:
// build-static-pages.js -> createBailoutProxy
function createBailoutProxy(target, label, onBailout) {
const safeTarget = target || {};
return new Proxy(safeTarget, {
get(t, prop, receiver) {
if (typeof prop === "symbol" || ["inspect", "valueOf", "toString"].includes(prop)) {
return Reflect.get(t, prop, receiver);
}
console.log(`[StaticBailout] Access to ${label} detected: "${String(prop)}".`);
onBailout(); // Mark page as dynamic -> skip static generation!
return Reflect.get(t, prop, receiver);
},
ownKeys(t) {
onBailout();
return Reflect.ownKeys(t);
},
has(t, prop) {
onBailout();
return Reflect.has(t, prop);
}
});
}If a page reads a cookie (e.g. req.cookies.session) or checks a header (e.g. "Authorization" in req.headers), the proxy fires, executing onBailout(). Dinou immediately marks that page as dynamic and skips writing it to disk. At runtime, this page falls back to full dynamic SSR.
Phase 3: Side Effect Capturing & Script Block Injection
What happens if a static page sets a theme cookie or triggers a redirect inside a Server Component during the dry render compilation?
Dinou intercepts these side-effects inside a mock response object and saves them under the "effects" key inside metadata.json:
// dist2/blog/hello/metadata.json
{
"revalidate": 3600,
"generatedAt": 1751234567890,
"effects": {
"redirect": "/login",
"cookies": [
{ "name": "theme", "value": "dark", "options": { "path": "/" } }
]
}
}When writing the static index.html, Dinou checks for these effects and uses get-ssg-metadata.js to translate them into a self-executing JavaScript block. This script block is injected directly at the top of the static HTML file:
// get-ssg-metadata.js -> processMetadata helper
function processMetadata(effects) {
if (!effects) return "";
let script = "";
if (effects.cookies && effects.cookies.length > 0) {
effects.cookies.forEach((ck) => {
const name = JSON.stringify(ck.name);
const value = JSON.stringify(ck.value || "");
const path = JSON.stringify(ck.options?.path || "/");
script += `document.cookie = ${name} + "=" + ${value} + "; path=" + ${path} + ";";`;
});
}
if (effects.redirect) {
script += `window.location.href = "${effects.redirect}";`;
}
return script ? `<script>(function(){ ${script} })();</script>` : "";
}When a user loads the static HTML file from disk or CDN, this self-executing script block fires immediately before the browser parses or renders the HTML, replaying the side-effects (cookies or redirects) instantly on the client browser.
β‘ 6. ISG and ISR Architecture
Dinou provides native support for Incremental Static Generation (ISG) and Incremental Static Regeneration (ISR). These systems enable pages that were not pre-rendered at compile time to build on demand, and existing static pages to update asynchronously in the background.
A. Key Files & Architectural Roles
The ISR/ISG engine is distributed across five main files in dinou/core/, each handling a distinct lifecycle phase:
server.js(The Orchestrator): Intercepts requests. If an HTML cache file doesn't exist, it performs a real-time SSR render and schedules an ISG generation on response finish (res.on("finish")). If a cache file exists, it serves the file instantly and triggers a background ISR revalidation check. It also intercepts calls to serve_oldbackup assets if a compile lock is active.generating-isg.js(On-Demand compiler): Manages the first-time generation of static routes in the background. It acquires the compilation lock, copies existing files to backup names, and builds static payloads.revalidating.js(Stale-While-Revalidate Engine): Handles background revalidation. It reads the page'smetadata.json, evaluates whether the revalidation time has expired (Date.now() > generatedAt + revalidate), sets compile locks, and executes re-compilation.cache-revalidate.js(On-Demand Trigger): Provides hooks for programmatic revalidation (revalidatePathandrevalidateTag). For tag revalidation, it crawls allmetadata.jsonfiles in thedist2/cache directory, reads the tags index, and re-compiles matching paths in parallel.safe-rename.js(Atomic Committer): Ensures filesystem safety. Instead of writing directly to active files (which would cause Express to serve partial or corrupt files during compilation), the compiler writes to temporary files (e.g.index.html.tmp) and callssafeRename, which performs an atomic OS-level file replacement (fs.renameSync).
B. Concurrency Locks & Stale-While-Revalidate
To prevent race conditions where multiple requests try to compile the same page simultaneously, Dinou uses a shared regenerating Set to store active path locks:
// revalidating.js & generating-isg.js shared locking mechanism
const regenerating = new Set(); // Global compile lock
function revalidating(reqPath, isDynamicFromServer) {
if (regenerating.has(reqPath)) return; // Abort if compiler is already active
// 1. Expiration check
const isExpired = Date.now() > generatedAt + revalidate;
if (isExpired) {
// 2. Backup current stable assets
copyFileSync(htmlPath, htmlPathOld); // index.html -> index._old.html
copyFileSync(rscPath, rscPathOld); // rsc.rsc -> rsc._old.rsc
regenerating.add(reqPath); // Lock path
(async () => {
try {
const isDynamic = {};
await buildStaticPage(reqPath, isDynamic); // Compilation dry run
if (isDynamic.value) {
isDynamicFromServer.value = true;
return; // Bail out if page accesses dynamic cookies/headers
}
// 3. Compile RSC Flight stream first, commit atomically
const rscResult = await generateStaticRSC(reqPath);
await safeRename(rscResult.tempPath, rscResult.finalPath);
// 4. Compile HTML page, commit atomically
const pageResult = await generateStaticPage(reqPath);
await safeRename(pageResult.tempPath, pageResult.finalPath);
updateStatus(reqPath, pageResult.status);
} finally {
regenerating.delete(reqPath); // Release lock
}
})();
}
}Serving stale backups: While the path lock is active in regenerating, server.js directs any concurrent incoming requests to serve the backup assets (index._old.html and rsc._old.rsc). Dinou injects script markers inside the HTML head so the browser client knows it is reading temporary stale data:
<script>window.__DINOU_USE_STATIC__=true;</script>
<script>window.__DINOU_USE_OLD_RSC__=true;</script>
<script>window.__DINOU_BUILD_ID__="1751234567890";</script>C. On-Demand Revalidation (Paths & Tags)
Programmatic revalidations (triggered by revalidatePath(path) or revalidateTag(tag)) bypass the time-expiration checks and force immediate background compilations through cache-revalidate.js:
// cache-revalidate.js -> revalidateTag implementation
async function revalidateTag(tag) {
const dist2Folder = path.resolve(process.cwd(), "dist2");
const metadataFiles = await walkMetadataFiles(dist2Folder); // Crawls all metadata.json
const revalidatePromises = [];
for (const fileOfMeta of metadataFiles) {
const metadata = JSON.parse(await fs.readFile(fileOfMeta, "utf8"));
if (metadata.tags && metadata.tags.includes(tag)) {
// Convert physical folder name to relative request path
const relative = path.relative(dist2Folder, path.dirname(fileOfMeta));
const reqPath = "/" + relative.replace(/\\/g, "/");
revalidatePromises.push(revalidatePath(reqPath)); // Triggers parallel compilation
}
}
await Promise.all(revalidatePromises);
}π 7. Server Functions
Server Functions (Server Actions) are standard JavaScript functions in files prefixed with the "use server" directive. Under the hood, Dinou implements them through a secure, two-sided lifecycle that spans compilation, serialization, CSRF auditing, and streaming command protocols:
A. Compilation & Double-Sided Stubbing
Dinou compiles actions differently depending on where the code is executing:
- Client-Side Stubbing (Rollup / Webpack): During client compilation, bundler plugins (such as
rollup-plugin-server-functions.js) completely strip the server-side logic (database queries, private keys, API calls) to prevent code leakage. They replace all exports with stubs generated viacreateServerFunctionProxy("file:///src/actions.ts#exportName"). - Server-Side Registration (ESM Loader): Inside the parent Node.js Express process, the ESM loader (
babel-esm-loader.js) preserves the actual execution code but calls React's nativeregisterServerReference(fn, fileUrl, exportName)on each export. This registers the memory reference with React Server DOM.
B. Action Reference Generation
When a Server Component renders a button or form bound to an action:
// In a Server Component:
<form action={addTodo}>React's server serializer detects that the addTodo function is registered. In the RSC Flight Stream, it replaces the function with an action reference tag (e.g. $@1) mapping to the unique identifier file:///src/actions/todo.ts#addTodo. The browser client receives this payload and binds the reference to the local proxy stub created in Step A.
C. Client-Side Dispatch (The Proxy Trap)
When a user submits a form or triggers the action directly in JavaScript, the execution flow runs through a three-step client dispatch process:
- Proxy Interception: The call is intercepted by the
applytrap of the JavascriptProxywrapper associated with the action's identifier. - Argument Packaging:
- FormData Input: If the action is triggered by a form submit (passing a
FormDataobject), the proxy appends the unique action identifier to the fields (__dinou_func_id) and serializes any secondary arguments as a JSON string (__dinou_args). This allows file uploads to pass seamlessly. - Standard JS Call: If called programmatically with arguments, it formats a clean JSON payload:
{ id, args }.
- FormData Input: If the action is triggered by a form submit (passing a
- Fetch Dispatch: It attaches a mandatory CSRF security header (
x-server-function-call: 1) and issues aPOSTrequest to the framework's internal endpoint/____server_function____.
// server-function-proxy.js -> Proxy apply trap
export function createServerFunctionProxy(id) {
return new Proxy(() => {}, {
apply: async (_target, _thisArg, args) => {
let body;
const headers = { "x-server-function-call": "1" }; // CSRF prevention header
if (args[0] instanceof FormData) {
const formData = args[0];
formData.append("__dinou_func_id", id); // Inject action identifier
if (args.length > 1) {
formData.append("__dinou_args", JSON.stringify(args.slice(1)));
}
body = formData;
} else {
headers["Content-Type"] = "application/json";
body = JSON.stringify({ id, args });
}
const res = await fetch("/____server_function____", {
method: "POST",
headers,
body,
});
// ... parse stream
}
});
}D. Security Audit & Server Invocation
When the request hits app.post("/____server_function____") in server.js, it undergoes four strict security audits:
- Origin Validation: In production, the request's
Originheader is checked against the server's activeHost/x-forwarded-host. If they don't align, the request is rejected with a403 Forbiddenstatus. - CSRF Check: Rejects requests missing the
x-server-function-call: 1header. - Sandbox Traversal Prevention: Parses the action identifier. It ensures it has no
..traversals or absolute drive letters, and verifies that the resolved path lies strictly inside the project'ssrc/directory. - Export Allowance Registry: Verifies that the targeted export function is registered in the build manifest (
server-functions-manifest.json).- In Development: The Express server reads the file on the fly and runs Babel's AST parser (via
parse-exports.js) to verify the exports. - In Production: At build time, custom bundler plugins (such as
rollup-plugin-server-functions.jsfor esbuild/rollup orWebpackServerFunctionsPluginSimplefor Webpack) crawl the codebase, intercept all files with the"use server"directive, collect their exported function names, and write them toserver-functions-manifest.json. At startup, the Express server loads this manifest as a read-only registry.
- In Development: The Express server reads the file on the fly and runs Babel's AST parser (via
Once validated, the function is executed within a requestStorage.run wrapper, ensuring that calls to getContext() resolve the current request headers and cookies correctly:
result = await requestStorage.run(context, async () => {
return await fn(...args);
});E. Response Return & Stream Redirects
Dinou allows actions to return either JSON values or React Server Component nodes. The server serializes JSX results using renderToPipeableStream and sends them back as a text/x-component stream.
Handling redirects: If the action calls redirect("/path"), React throws an internal redirect exception:
- Clean Response (Scenario A): If headers have not been sent, the server responds with a JSON payload
{ redirect: '/path' }and setting theX-Dinou-Redirectheader. - Active Stream (Scenario B): If the response stream is already active, the server writes a custom command line into the stream:
D:{"type":"redirect","url":"/path"}\nand closes the socket.
The Stream Command Filtering Algorithm: Feeding Dinou's raw control commands directly to React's client-side Flight decoder (createFromFetch) would result in a rendering crash. To prevent this, the client-side proxy intercepts the HTTP response stream and wraps the stream reader in a custom ReadableStream filter that parses data chunk-by-chunk:
- Buffer Accumulation: As binary chunks (
value) are downloaded from the network viareader.read(), they are decoded to text strings and appended to a persistent local stringbuffer. - Complete Line Slicing: Network packages can easily slice a text command in half. The algorithm runs
buffer.lastIndexOf("\n")to slice out only the completely downloaded lines (completeChunk) for immediate processing, leaving any incomplete trail in thebufferfor subsequent chunks. - Prefixed Line Analysis: The chunk text is split by newlines. The parser loops through each line:
- Lines starting with
D:(Control Commands): The JSON payload is parsed (e.g.{ type: 'redirect', url: '/dashboard' }). The proxy triggers the command immediately in the browser (by routing or setting cookies) and discards the line entirely from the stream. - Standard Lines (RSC Flight Data): The line is appended to
cleanChunk.
- Lines starting with
- Enqueuing Clean Data: Only the
cleanChunkstring containing pristine, React-compliant Flight tokens is enqueued back into the stream controller (controller.enqueue). - React Decoupling: The stream is passed to
createFromFetch(Promise.resolve(new Response(readableStream))). For React, the stream is completely clean, ensuring it updates the layout seamlessly in the background.
// server-function-proxy.js -> Executed inside createServerFunctionProxy()
const readableStream = new ReadableStream({
async start(controller) {
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lastNewlineIndex = buffer.lastIndexOf("\n");
if (lastNewlineIndex !== -1) {
const completeChunk = buffer.slice(0, lastNewlineIndex + 1);
buffer = buffer.slice(lastNewlineIndex + 1);
const lines = completeChunk.split("\n");
let cleanChunk = "";
for (const line of lines) {
if (line.startsWith("D:")) {
const payload = JSON.parse(line.slice(2));
if (payload.type === "redirect") executeRedirect(payload.url);
if (payload.type === "cookie") document.cookie = payload.cookie;
} else {
cleanChunk += line + "\n"; // Enqueue clean RSC Flight data
}
}
if (cleanChunk) controller.enqueue(encoder.encode(cleanChunk));
}
}
controller.close();
}
});
return createFromFetch(Promise.resolve(new Response(readableStream)));F. Open Redirect Protection (CWE-601 Mitigation)
Attackers often abuse redirect features to redirect users from a trusted domain to external phishing links (e.g. /login?redirect=https://evil.com).
Dinou mitigates this inside its native safeRedirect helper by sanitizing all redirect inputs:
// render-app-to-html.js -> safeRedirect sanitation
const resolvedUrl = resolveRelativeUrl(targetUrl, reqPath);
let finalUrl = "/";
if (typeof resolvedUrl === "string" && resolvedUrl.startsWith("/") && !resolvedUrl.startsWith("//")) {
finalUrl = resolvedUrl;
} else {
console.warn(`[Dinou Security] Blocked unsafe redirect to: ${targetUrl}`);
}
// Perform redirect using finalUrl...Sanitation Rules:
- Relative Resolution: Uses
resolveRelativeUrlto convert relative paths (like../profile) into absolute pathnames relative to the referrer's path. - Internal Only Enforcement: Verifies that the resolved path starts with a single slash (
/) and does NOT start with a double slash (//). Double slashes are blocked because browsers treat//evil.comas a protocol-relative scheme, resolving it ashttps://evil.com. - Fallback Redirect: If these conditions fail, Dinou blocks the redirect, overrides the destination to a safe root path (
/), and outputs a warning to the server logs.
G. HttpOnly Cookie Assurance
When a component attempts to set a cookie, Dinou intercepts the call:
- If HTTP headers have not been sent, it sets the cookie securely via standard HTTP
Set-Cookieheaders. - If headers have already been sent, Dinou falls back to client-side script block injection (appending
<script>document.cookie = ...;</script>to the active stream).
However, if the requested cookie options contain httpOnly: true during the streaming phase, the server throws an error and rejects the operation. This prevents silent security failures where developers assume their HttpOnly auth/session cookies are saved, when in fact they are ignored because client-side JavaScript cannot write HttpOnly keys.
βοΈ 8. Client Entry Point & SPA Runtime (client.jsx)
The client.jsx file serves as the main entry point for the browser client. It bridges the gap between the static HTML sent by the server and the live, interactive React single-page application (SPA).
A. DOM Hydration (hydrateRoot)
Upon page load, client.jsx initializes by calling React 19's hydrateRoot(document, <Router />). This process attaches event listeners to the pre-rendered HTML sent by the server (SSR), bringing the static page to life without reconstructing the DOM from scratch.
B. SPA Navigation & Client Transitions
Dinou prevents full-page browser reloads when navigating between internal links. Instead, the client-side <Router /> manages transitions dynamically:
- Global Click Listener: Rather than attaching listeners to every individual anchor, Dinou registers a single click listener on the
documentroot. It intercepts clicks on internal local links (ignoringmailto:,tel:, external URLs, or modified clicks likeCtrl/Cmd + Click) and callse.preventDefault(). - Concurrent Transitions (
useTransition): It triggers navigation inside astartTransitionhook. This tells React 19 to download and render the target page's Server Components in the background, keeping the current page interactive and preventing UI freeze during network latency. - History Synchronization (
popstate): Listens to the browser'spopstateevents to capture history back/forward operations and update the active route in React's state. - Prefetch Hook: Exposes
window.__DINOU_PREFETCH__to pre-fetch RSC Flight payloads into the cache when users hover over links.
// client.jsx -> Global Click listener
document.addEventListener("click", (e) => {
if (e.defaultPrevented) return;
const anchor = e.target.closest("a");
if (!anchor || anchor.target || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
const href = anchor.getAttribute("href");
if (!href || href.startsWith("mailto:") || href.startsWith("tel:") || isExternalUrl(href)) return;
e.preventDefault();
const finalPath = resolveUrl(href, window.location.pathname);
window.__DINOU_ROUTER_NAVIGATE__(finalPath);
});C. RSC Payload Fetching & Promise Cache (getRSCPayload)
To render a new route, the router fetches its RSC Flight Stream from the internal endpoint /____rsc_payload____/route.
- Idempotent Promise Cache: Because React 19 may suspend components and retry rendering multiple times a second during load, standard fetching would trigger duplicate network requests. Dinou resolves this by caching the network Promise itself instead of the resolved data. Subsequent suspension retries immediately receive the same active Promise, avoiding network loops.
- Server Action Mapping: Registers a custom
callServerhandler within thecreateFromFetchdecoder. This callback routes interactive client actions back to the local action proxy stubs:
// client.jsx -> cache promise storage
const cache = new Map();
const getRSCPayload = (rscKey, isPrefetch = false) => {
const url = rscKey.split("::")[0];
if (cache.has(url)) {
return cache.get(url); // Returns identical promise -> avoids loop
}
// Check server-injected fallback flags for first page load
let payloadUrl;
if (window.__DINOU_USE_OLD_RSC__ || window.__DINOU_USE_STATIC__) {
payloadUrl = window.__DINOU_USE_OLD_RSC__
? (window.__DINOU_USE_STATIC__ ? "/____rsc_payload_old_static____" : "/____rsc_payload_old____") + url
: "/____rsc_payload_static____" + url;
window.__DINOU_USE_OLD_RSC__ = false;
window.__DINOU_USE_STATIC__ = false;
} else {
payloadUrl = "/____rsc_payload____" + url;
}
const promise = createFromFetch(
fetch(payloadUrl).then((res) => {
if (res.headers.has("x-rsc-redirect")) {
const redirectUrl = res.headers.get("x-rsc-redirect");
cache.delete(url);
window.__DINOU_ROUTER_NAVIGATE__(redirectUrl, { replace: true });
return new Promise(() => {}); // Suspends render permanently while navigating
}
return res;
}),
{
callServer: async (id, args) => {
return createServerFunctionProxy(id)(...args); // Hook actions back to stubs
}
}
);
cache.set(url, promise);
return promise;
};D. Scroll Restoration & Hash Management
Dinou provides smooth scroll behavior by hooking into the routing lifecycle via useLayoutEffect and requestAnimationFrame:
- Scroll Caching: During navigation, the router captures the current scroll height using
window.scrollYand caches it in a globalscrollCachemap using the active path as the key. - PopState Restoration: When navigating backwards or forwards via browser history (triggering
PopState), the router retrieves the cached scroll height and useswindow.scrollTo(0, savedY)within arequestAnimationFramewrapper to restore the user's exact scroll position. - Standard Navigate Reset: On standard link navigations, the router scrolls the window back to the top (
window.scrollTo(0, 0)). If the target URL contains an anchor hash (e.g.#team), it waits for the React render tree to commit and callselement.scrollIntoView()to align the viewport. - Hash-Only Navigation Bypass: If a navigation target only modifies the URL hash (e.g.
/about#team), Dinou intercepts it, pushes the history state, and scrolls directly without querying the network.
// client.jsx -> isHashChangeOnly check
if (isHashChangeOnly(finalPath)) {
window.history.pushState(null, "", finalPath);
const hash = new URL(finalPath, window.location.origin).hash;
const element = document.getElementById(hash.replace("#", ""));
if (element) {
element.scrollIntoView({ behavior: "auto" });
}
return; // Stop RSC pipeline execution
}E. Dynamic Error Boundary & Recovery
The client hydrates inside an ErrorBoundary. If a component encounters a rendering exception, the router intercepts it and fetches a formatted error page from the backend dynamically over RSC (/____rsc_payload_error____):
const getErrorRSCPayload = (route, error) => {
const payloadUrl = "/____rsc_payload_error____" + route;
return createFromFetch(
fetch(payloadUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
error: { message: error.message, name: error.name, stack: error.stack }
})
})
);
};π¦ 9. Bundler Integration
Dinou supports three bundler configurations: esbuild, Rollup, and webpack. Regardless of the bundler chosen, they all must resolve two primary constraints:
- Client Manifest Generation: Map each client component's file path to the output bundle asset path (so the RSC server can resolve references).
- Server Function Proxying: Scan and replace server action code blocks on the client with fetch stubs.
β‘ 9.1 esbuild Configuration
Dinou's esbuild integration resides inside dinou/esbuild/. It handles high-performance bundling for both client-side components and server-side assets. Since esbuild lacks native loaders for React 19 Compiler, PostCSS, or ESM-based HMR out of the box, Dinou implements a modular system composed of orchestrator scripts, helpers, custom plugins, and a custom Fast Refresh integration.
A. The Orchestrator Scripts
Dinou exposes two entry-point scripts depending on the environment:
dev.mjs(Development Server): Cleans old assets, crawls the repository to build initial entry points, and boots a Chokidar filesystem watcher on thesrc/directory. When a file is added or removed, it debounces and recreates the esbuild compilation context. When files are modified, it executes a hot rebuild and broadcasts path updates to the client browser over a WebSocket server for Hot Module Replacement (HMR).build.mjs(Production Compilation): Executes a single, production-optimized compilation pass usingesbuild.build. It triggers asset hashing, CSS extraction, minification, and outputs the production bundle under thedist3/directory.
B. Directory Crawlers & Configuration Helpers
To coordinate builds, esbuild requires precise entry points. Dinou resolves this using specialized helpers under helpers-esbuild/:
get-esbuild-entries.mjs(Entry Finder): Unlike Rollup or Webpack, esbuild does not automatically crawl dynamic imports to split client files. Dinou solves this by scanning the source directory recursively at startup. It uses Babel to detect files containing the"use client"directive, stylesheet imports, and public assets, and registers them as independent compile entry points.get-config-esbuild.mjsandget-config-esbuild-prod.mjs: Generate the build options for development and production, configuring target environments, loaders, inline sourcemaps, and global plugin chains.update-manifest-for-module.mjs: Appends or updates entries in the dynamic asset manifests during development HMR cycles.
C. Custom esbuild Plugins (plugins-esbuild/)
Dinou injects a series of custom plugins into the esbuild compilation chain to handle React, CSS, and security features:
babel-react-compiler-plugin.mjs(React 19 Compiler Bridge): Intercepts.[jt]sx?files and compiles them using Babel. It injects the official React 19 Compiler (babel-plugin-react-compiler) to automatically memoize components (removing the need for manualuseMemooruseCallback) and wires up Fast Refresh hooks.css-processor-plugin.mjs(Tailwind & CSS Modules processor): Processes CSS stylesheets using PostCSS. It integrates@tailwindcss/postcssandautoprefixer, processes scoped CSS Modules class names usingpostcss-modules, and outputs the class mappings into local JS modules while extracting the final styles intopublic/styles.css.stable-chunk-names-and-maps-plugin.mjs(Cache Cascade Protection): By default, esbuild generates shared chunks with random hashes (e.g.chunk-AJS98D.js). When files are modified, these names change, breaking browser caches. This plugin computes a stable chunk name based on its primary source path (e.g.src/utils/math.tscompiles tochunk-utils-math.js) and rewrites all import references inside the generated bundles.react-client-manifest-plugin.mjs(RSC Mapper): Runs on build end to scan generated modules. For every file containing"use client", it parses its exports and writes toreact-client-manifest.jsonso that React Server Components can map client component declarations to their compiled JavaScript bundle paths.server-functions-plugin.mjs(Server Actions Shield): Strips server-side code from client-bound bundles by replacing"use server"file exports with stubs callingcreateServerFunctionProxy, while logging action paths toserver-functions-manifest.json.assets-plugin.mjs(Asset Loader): Intercepts imports of static assets (like images or SVGs) and outputs them under theassets/folder with hashed filenames, returning their public URLs inside the client code.
D. Fast Refresh HMR Integration (react-refresh/)
Dinou provides state-preserving Hot Module Replacement in development through a custom integration under react-refresh/:
esm-hmr-plugin.mjs: Runs inside esbuild to inject HMR boundary checks and runtime scripts into client components. It registers the local WebSocket listener in the browser. When the dev server broadcasts a file change, the client uses dynamic imports to fetch the new code chunk and re-register it.react-refresh-runtime.mjs: Wires up React's official Fast Refresh runtime (react-refresh/runtime) into the HMR lifecycle. When a component file is hot-swapped, React triggers an in-place re-render of the component tree, applying the new code without resetting component state (such as inputs, form values, oruseStatehooks).
π 9.2 Rollup Configuration
Dinou's Rollup integration resides under dinou/rollup/. Unlike esbuild, Rollup natively traverses the import graph recursively starting from the client entries, so it does not require a pre-scan step. It relies on standard Node CommonJS module syntax to execute compilation configurations.
A. The Main Configuration (rollup.config.js)
The primary bundler configuration coordinates transpilation, CSS extraction, and code splitting. It enforces two strict output constraints vital for React 19's serialization:
// dinou/rollup/rollup.config.js
module.exports = {
// ... input settings
output: {
dir: outputDirectory,
format: "esm",
minifyInternalExports: false, // 1. Prevents Rollup from minifying internal chunk exports to "a", "b", etc.
},
preserveEntrySignatures: "strict", // 2. Prevents Rollup from stripping or modifying entry point exports
};Dinou sets preserveEntrySignatures: "strict" and minifyInternalExports: false because React's runtime client serializer resolves components by matching original export names (e.g. MyComponent) between the server-generated RSC Flight payload and the client's JS bundle. If Rollup minifies internal exports to single-letter variables, the client hydration fails.
B. Custom Rollup Plugins (rollup-plugins/)
To replicate React and security functionality in Rollup, Dinou registers four bespoke plugins:
rollup-plugin-react-client-manifest.js(Client Manifest & Default Export Hack): During the build, this plugin hooks into thetransformlifecycle to find files with the"use client"directive and registers them as separate code-split entry points.
The Default Export Hack: Rollup tree-shakes default exports if they are not explicitly imported by client entry points, which breaks React's runtime manifest resolution. In thegenerateBundlehook, the plugin parses the original module code via Babel AST. If a default export exists but was tree-shaken, it appends a manual alias directly to the chunk code:// rollup-plugin-react-client-manifest.js -> generateBundle hook if (chunk.facadeModuleId && !chunk.exports.includes("default")) { const originalCode = readFileSync(chunk.facadeModuleId, "utf8"); const defaultName = getDefaultExportName(originalCode); // AST search if (defaultName && chunk.exports.includes(defaultName)) { chunk.code += `\nexport { ${defaultName} as default };\n`; // Append alias chunk.exports.push("default"); } }rollup-plugin-server-functions.js(Server Actions Shield): Crawls the Rollup module graph. When it encounters a file with a"use server"header, it extracts its exports, deletes the server-side logic from the file, and replaces the module content with client proxies invokingcreateServerFunctionProxy. It also outputs theserver-functions-manifest.jsonsecurity index.dinou-asset-plugin.js(Static Asset Resolver): Intercepts imports of static resources (e.g. images, SVGs) within client files, copies them to the public directory with an asset hash, and exports their compiled URLs.manifest-generator-plugin.js(Asset Hasher Map): Runs on build end in production. It collects all output bundle chunks and writes a lookup JSON mapping original file names (likemain.js) to their compiled hashed filenames (likemain-h7d9s2.js) to coordinate static index rendering.
C. Fast Refresh HMR Integration (react-refresh/)
Like esbuild, Dinou's Rollup configuration implements state-preserving Hot Module Replacement in development through specialized wrappers:
rollup-plugin-esm-hmr.js: Establishes the WebSocket server in development and injects the ESM HMR client listener (import.meta.hot) into the client-bound JavaScript files.react-refresh-wrap-modules.js: Before compiling files, this plugin intercepts client modules and wraps their export declarations with React Fast Refresh runtime registration code.react-refresh-runtime.js,react-refresh-entry.js, andis-react-refresh-boundary.js: Integrate React's Fast Refresh registry into the ESM HMR lifecycle, ensuring that when the HMR plugin hot-swaps a module, React re-evaluates the components in-place without resetting browser state.
πΈοΈ 9.3 Webpack Configuration
Dinou's Webpack integration resides inside dinou/webpack/. Unlike esbuild and Rollup, Webpack's architecture leverages official, standard React compilation plugins to build manifests, while using custom loaders and plugins to hook Server Actions and CSS extraction.
A. The Main Configuration (webpack.config.js)
The primary configuration manages cleaning target outputs (via the cleanDir helper), compiling entries, and setting up module rules:
// dinou/webpack/webpack.config.js
const ReactServerWebpackPlugin = require("react-server-dom-webpack/plugin");
const ServerFunctionsPlugin = require("./plugins/server-functions-plugin");
module.exports = {
// ... configuration settings
plugins: [
new ReactServerWebpackPlugin({ isServer: false }), // Generates react-client-manifest.json
new ServerFunctionsPlugin(), // Consolidates actions allowlist
],
};Unlike other configurations, Dinou delegates React-specific code-splitting and client mapping to Webpack's official ReactServerWebpackPlugin, which intercepts the module graph to output react-client-manifest.json automatically.
B. Directory Crawlers (helpers/)
get-webpack-entries.js: Crawls the source directory at build start to identify CSS stylesheets and static assets, outputting a flat dictionary mapping entry point names to their absolute file paths.
C. Custom Webpack Loaders (loaders/)
server-functions-loader.js(Actions Stripper): Webpack uses loaders to transform source modules. This loader intercepts files containing the"use server"directive. It strips out the backend logic and replaces it with lazy import proxies pointing to the client proxy helper. It also emits a temporary metadata JSON file detailing the action's exports:// server-functions-loader.js -> Loader export module.exports = function (source) { if (!useServerRegex.test(source)) return source; const exports = parseExports(source); const normalizedPath = path.relative(process.cwd(), this.resourcePath).replace(/\\/g, "/"); // Dynamic stub calling createServerFunctionProxy let proxyCode = `const loadProxy = new Function('return import("/" + "__SERVER_FUNCTION_PROXY__")');\n`; for (const exp of exports) { const key = exp === "default" ? `file:///${normalizedPath}#default` : `file:///${normalizedPath}#${exp}`; proxyCode += exp === "default" ? `export default (...args) => loadProxy().then(m => m.createServerFunctionProxy("${key}")(...args));\n` : `export const ${exp} = (...args) => loadProxy().then(m => m.createServerFunctionProxy("${key}")(...args));\n`; } // Emit a temporary json record for this file's server actions this.emitFile(`server-functions/${normalizedPath}.json`, JSON.stringify({ path: normalizedPath, exports })); return proxyCode; };
D. Custom Webpack Plugins (plugins/)
Dinou registers two custom Webpack plugins to handle asset mapping and action security manifests:
server-functions-plugin.js(Actions Consolidator): Hooks into Webpack's asset generation phase. It performs two duties:- Placeholder Replacement: Scans JS assets, finds
__SERVER_FUNCTION_PROXY__placeholders, and replaces them with the final compiled, hashed name of the client proxy runtime. - Manifest Synthesis: Reads the temporary JSON files generated by
server-functions-loader.js, compiles them into a unifiedserver-functions-manifest.jsonallowlist, and deletes the temporary files from the final output assets.
- Placeholder Replacement: Scans JS assets, finds
manifest-generator-plugin.js(Asset Map): Hooks into the asset processing phase in production to compile a flat JSON lookup map linking original source filenames to their final hashed output files (e.g.main.js -> main-a7b89.js).