> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/cloudflare/vinext/llms.txt
> Use this file to discover all available pages before exploring further.

# Virtual Module System

> Virtual modules, entry generation, and module resolution in Vinext

# Virtual Module System

Vinext uses Vite's virtual module system to generate dynamic entry points for different runtime environments. This guide explains how virtual modules work, how they're generated, and the resolution patterns used.

## What Are Virtual Modules?

Virtual modules are modules that don't exist on disk. They're generated at runtime by Vite plugins and resolved via the `resolveId` and `load` hooks.

**Why use virtual modules?**

* Generate entry points based on file-system routing
* Embed route metadata at build time
* Create environment-specific entries (RSC vs SSR vs Client)
* Avoid writing generated files to disk

## Virtual Module IDs

Vinext defines several virtual modules:

### Pages Router

```typescript theme={null}
const VIRTUAL_SERVER_ENTRY = "virtual:vinext-server-entry";
const RESOLVED_SERVER_ENTRY = "\0" + VIRTUAL_SERVER_ENTRY;

const VIRTUAL_CLIENT_ENTRY = "virtual:vinext-client-entry";
const RESOLVED_CLIENT_ENTRY = "\0" + VIRTUAL_CLIENT_ENTRY;
```

### App Router

```typescript theme={null}
const VIRTUAL_RSC_ENTRY = "virtual:vinext-rsc-entry";
const RESOLVED_RSC_ENTRY = "\0" + VIRTUAL_RSC_ENTRY;

const VIRTUAL_APP_SSR_ENTRY = "virtual:vinext-app-ssr-entry";
const RESOLVED_APP_SSR_ENTRY = "\0" + VIRTUAL_APP_SSR_ENTRY;

const VIRTUAL_APP_BROWSER_ENTRY = "virtual:vinext-app-browser-entry";
const RESOLVED_APP_BROWSER_ENTRY = "\0" + VIRTUAL_APP_BROWSER_ENTRY;
```

**The `\0` prefix** is a Rollup convention indicating a resolved virtual module. It prevents other plugins from attempting to resolve it further.

## Resolution Hooks

From `packages/vinext/src/index.ts`:

```typescript theme={null}
resolveId(id, importer, options) {
  // Handle build-time root prefix
  // Vite prefixes virtual module IDs with project root when resolving SSR entries
  const normalized = id.replace(/^\0/, "").replace(root + "/", "");

  if (normalized === VIRTUAL_SERVER_ENTRY) return RESOLVED_SERVER_ENTRY;
  if (normalized === VIRTUAL_CLIENT_ENTRY) return RESOLVED_CLIENT_ENTRY;
  if (normalized === VIRTUAL_RSC_ENTRY) return RESOLVED_RSC_ENTRY;
  if (normalized === VIRTUAL_APP_SSR_ENTRY) return RESOLVED_APP_SSR_ENTRY;
  if (normalized === VIRTUAL_APP_BROWSER_ENTRY) return RESOLVED_APP_BROWSER_ENTRY;

  // Handle next/* shim resolution
  const shimId = resolveNextShim(id);
  if (shimId) return shimId;

  return null;
},

load(id) {
  if (id === RESOLVED_SERVER_ENTRY) {
    return generateServerEntry();
  }
  if (id === RESOLVED_CLIENT_ENTRY) {
    return generateClientEntry();
  }
  if (id === RESOLVED_RSC_ENTRY) {
    return generateRscEntry(
      appDir,
      routes,
      middlewarePath,
      metadataRoutes,
      globalErrorPath,
      basePath,
      trailingSlash,
      config,
    );
  }
  // ...
  return null;
},
```

### Resolution Quirks

<Warning>
  Vite has several edge cases in virtual module resolution that must be handled.
</Warning>

**1. Build-time root prefix**

During SSR builds, Vite prefixes virtual module IDs with the project root path:

* Dev: `virtual:vinext-server-entry`
* Build: `/home/user/project/virtual:vinext-server-entry`

The `resolveId` hook strips the root prefix before matching.

**2. `\0` prefix in client environment**

When the RSC plugin generates its browser entry, it imports virtual modules using the already-resolved `\0`-prefixed ID. Vite's `import-analysis` plugin can't resolve this.

Fix: Strip the `\0` prefix before pattern matching:

```typescript theme={null}
const normalized = id.replace(/^\0/, "");
```

**3. Absolute paths required**

Virtual modules have no real file location. All imports within them must use **absolute paths**:

```typescript theme={null}
// ❌ Wrong - relative import
import { createElement } from "react";

// ✅ Correct - absolute path
import { createElement } from "/home/user/project/node_modules/react/index.js";
```

Vinext uses `import.meta.url` and path resolution to generate absolute paths:

```typescript theme={null}
const absPath = route.filePath.replace(/\\/g, "/");
imports.push(`import * as mod_${i} from ${JSON.stringify(absPath)};`);
```

## Server Entry Generation

The Pages Router server entry (`virtual:vinext-server-entry`) is a self-contained SSR handler:

```typescript theme={null}
async function generateServerEntry(): Promise<string> {
  const pageRoutes = await pagesRouter(pagesDir);
  const apiRoutes = await apiRouter(pagesDir);

  // Generate import statements using absolute paths
  const pageImports = pageRoutes.map((r: Route, i: number) => {
    const absPath = r.filePath.replace(/\\/g, "/");
    return `import * as page_${i} from ${JSON.stringify(absPath)};`;
  });

  const apiImports = apiRoutes.map((r: Route, i: number) => {
    const absPath = r.filePath.replace(/\\/g, "/");
    return `import * as api_${i} from ${JSON.stringify(absPath)};`;
  });

  // Build route table
  const pageRouteEntries = pageRoutes.map((r: Route, i: number) => {
    const absPath = r.filePath.replace(/\\/g, "/");
    return `  { 
      pattern: ${JSON.stringify(r.pattern)}, 
      isDynamic: ${r.isDynamic}, 
      params: ${JSON.stringify(r.params)}, 
      module: page_${i}, 
      filePath: ${JSON.stringify(absPath)} 
    }`;
  });

  // Embed config at build time
  const vinextConfigJson = JSON.stringify({
    basePath: nextConfig?.basePath ?? "",
    trailingSlash: nextConfig?.trailingSlash ?? false,
    redirects: nextConfig?.redirects ?? [],
    rewrites: nextConfig?.rewrites ?? { beforeFiles: [], afterFiles: [], fallback: [] },
    headers: nextConfig?.headers ?? [],
    i18n: nextConfig?.i18n ?? null,
  });

  return `
import React from "react";
import { renderToReadableStream } from "react-dom/server.edge";
import { resetSSRHead, getSSRHeadHTML } from "next/head";
import { setSSRContext } from "next/router";
import { getCacheHandler } from "next/cache";
// ... more imports

${pageImports.join("\n")}
${apiImports.join("\n")}

const pageRoutes = [
${pageRouteEntries.join(",\n")}
];

export const vinextConfig = ${vinextConfigJson};

// ... route matching, ISR, rendering logic

export async function renderPage(request, url, manifest) {
  // Full SSR implementation
}
  `;
}
```

**Key features:**

* All page and API routes are imported and registered
* Config is embedded at build time (no runtime file I/O)
* Middleware matching logic is inlined
* ISR cache helpers are included
* Supports both development and production

## RSC Entry Generation

The App Router RSC entry (`virtual:vinext-rsc-entry`) is the request handler:

From `packages/vinext/src/server/app-dev-server.ts`:

```typescript theme={null}
export function generateRscEntry(
  appDir: string,
  routes: AppRoute[],
  middlewarePath?: string | null,
  metadataRoutes?: MetadataFileRoute[],
  globalErrorPath?: string | null,
  basePath?: string,
  trailingSlash?: boolean,
  config?: AppRouterConfig,
): string {
  // Build import map for all route modules
  const imports: string[] = [];
  const importMap: Map<string, string> = new Map();
  let importIdx = 0;

  function getImportVar(filePath: string): string {
    if (importMap.has(filePath)) return importMap.get(filePath)!;
    const varName = `mod_${importIdx++}`;
    const absPath = filePath.replace(/\\/g, "/");
    imports.push(`import * as ${varName} from ${JSON.stringify(absPath)};`);
    importMap.set(filePath, varName);
    return varName;
  }

  // Pre-register all modules
  for (const route of routes) {
    if (route.pagePath) getImportVar(route.pagePath);
    if (route.routePath) getImportVar(route.routePath);
    for (const layout of route.layouts) getImportVar(layout);
    for (const tmpl of route.templates) getImportVar(tmpl);
    // ... register all route modules
  }

  // Build route table as serialized JS
  const routeEntries = routes.map((route) => {
    const layoutVars = route.layouts.map((l) => getImportVar(l));
    const templateVars = route.templates.map((t) => getImportVar(t));
    return `  {
    pattern: ${JSON.stringify(route.pattern)},
    isDynamic: ${route.isDynamic},
    params: ${JSON.stringify(route.params)},
    page: ${route.pagePath ? getImportVar(route.pagePath) : "null"},
    routeHandler: ${route.routePath ? getImportVar(route.routePath) : "null"},
    layouts: [${layoutVars.join(", ")}],
    templates: [${templateVars.join(", ")}],
    // ... all route metadata
  }`;
  });

  // Embed metadata files as base64
  const metaRouteEntries = (metadataRoutes || []).map((mr) => {
    if (mr.isDynamic) {
      return `  {
    type: ${JSON.stringify(mr.type)},
    isDynamic: true,
    servedUrl: ${JSON.stringify(mr.servedUrl)},
    contentType: ${JSON.stringify(mr.contentType)},
    module: ${getImportVar(mr.filePath)},
  }`;
    }
    // Static: read and embed as base64
    let fileDataBase64 = "";
    try {
      const buf = fs.readFileSync(mr.filePath);
      fileDataBase64 = buf.toString("base64");
    } catch {}
    return `  {
    type: ${JSON.stringify(mr.type)},
    isDynamic: false,
    servedUrl: ${JSON.stringify(mr.servedUrl)},
    contentType: ${JSON.stringify(mr.contentType)},
    fileDataBase64: ${JSON.stringify(fileDataBase64)},
  }`;
  });

  return `
import {
  renderToReadableStream,
  decodeReply,
  loadServerAction,
  createTemporaryReferenceSet,
} from "@vitejs/plugin-rsc/rsc";
import { createElement, Suspense, Fragment } from "react";
import { setNavigationContext, getNavigationContext } from "next/navigation";
import { setHeadersContext, headersContextFromRequest, runWithHeadersContext } from "next/headers";
import { NextRequest } from "next/server";
import { ErrorBoundary, NotFoundBoundary } from "vinext/error-boundary";
import { MetadataHead, ViewportHead } from "vinext/metadata";
// ... more imports

${imports.join("\n")}

const routes = [
${routeEntries.join(",\n")}
];

const metadataRoutes = [
${metaRouteEntries.join(",\n")}
];

// ... route matching, component tree building, rendering

export default async function handler(request) {
  return runWithHeadersContext(headersContextFromRequest(request), async () => {
    return runWithFetchCache(async () => {
      return _handleRequest(request);
    });
  });
}
  `;
}
```

**Key features:**

* All routes are discovered and imported at build time
* Metadata files are embedded as base64 (works on Workers with no filesystem)
* Full middleware, rewrite, redirect, and header logic is inlined
* Component tree building logic is included
* Server action handling is built-in

## Client Entry Generation

The Pages Router client entry (`virtual:vinext-client-entry`) bootstraps React hydration:

```typescript theme={null}
function generateClientEntry(): string {
  return `
import { hydrateRoot } from "react-dom/client";
import React from "react";

// Hydrate the page
const root = document.getElementById("__next");
if (root) {
  const pageData = window.__NEXT_DATA__;
  const { page, props } = pageData;
  
  // Dynamically import the page component
  import(/* @vite-ignore */ page).then((mod) => {
    const PageComponent = mod.default;
    hydrateRoot(root, React.createElement(PageComponent, props));
  });
}
  `;
}
```

The App Router browser entry (`virtual:vinext-app-browser-entry`) is generated by the RSC plugin and consumes the RSC stream for hydration.

## SSR Entry Generation

The App Router SSR entry (`virtual:vinext-app-ssr-entry`) consumes the RSC stream and renders HTML:

```typescript theme={null}
export function generateSsrEntry(): string {
  return `
import { renderToReadableStream } from "react-dom/server.edge";
import { createFromReadableStream } from "react-server-dom-webpack/client.edge";
import { setNavigationContext } from "next/navigation";
import { createElement } from "react";

export async function handleSsr(rscStream, navContext, fontData) {
  // Set navigation context in SSR environment
  setNavigationContext(navContext);
  
  // Consume RSC stream to build React tree
  const tree = await createFromReadableStream(rscStream);
  
  // Inject font styles
  const head = [
    ...fontData.styles.map(s => createElement("style", { 
      key: s.href, 
      dangerouslySetInnerHTML: { __html: s.content } 
    })),
  ];
  
  const app = createElement("html", null,
    createElement("head", null, ...head),
    createElement("body", null,
      createElement("div", { id: "__next" }, tree)
    )
  );
  
  // Render to HTML stream
  return renderToReadableStream(app);
}
  `;
}
```

**Why separate SSR entry?**

The RSC and SSR environments have different import conditions:

* RSC uses `react-server` (server-only APIs)
* SSR uses `node` (React client APIs for SSR)

They must use separate module graphs to avoid mixing incompatible APIs.

## Shim Resolution

All `next/*` imports are resolved to shim modules:

```typescript theme={null}
function resolveNextShim(id: string): string | null {
  // Strip .js extension (some libraries import "next/navigation.js")
  const normalized = id.replace(/\.js$/, "");
  
  if (nextShimMap[normalized]) {
    return nextShimMap[normalized];
  }
  
  return null;
}

// In config hook:
nextShimMap = {
  "next/link": path.resolve(shimsDir, "link.ts"),
  "next/image": path.resolve(shimsDir, "image.ts"),
  "next/head": path.resolve(shimsDir, "head.ts"),
  "next/router": path.resolve(shimsDir, "router.ts"),
  "next/navigation": path.resolve(shimsDir, "navigation.ts"),
  "next/server": path.resolve(shimsDir, "server.ts"),
  "next/headers": path.resolve(shimsDir, "headers.ts"),
  "next/dynamic": path.resolve(shimsDir, "dynamic.ts"),
  "next/script": path.resolve(shimsDir, "script.ts"),
  // ... 33 total shims
};
```

Shims are resolved to absolute paths so they work from any importer location.

## Next Steps

<CardGroup cols={2}>
  <Card title="Architecture Deep Dive" icon="sitemap" href="/advanced/architecture-deep-dive">
    Core architecture and patterns
  </Card>

  <Card title="RSC Integration" icon="server" href="/advanced/rsc-integration">
    React Server Components integration
  </Card>

  <Card title="Build Pipeline" icon="hammer" href="/advanced/build-pipeline">
    Production build pipeline
  </Card>

  <Card title="Contributing" icon="code" href="/contributing/overview">
    Contribute to Vinext
  </Card>
</CardGroup>
