);
}
diff --git a/docs/src/pages/en/(pages)/deploy/adapters.mdx b/docs/src/pages/en/(pages)/deploy/adapters.mdx
index 162d0744..1ff660f6 100644
--- a/docs/src/pages/en/(pages)/deploy/adapters.mdx
+++ b/docs/src/pages/en/(pages)/deploy/adapters.mdx
@@ -21,7 +21,7 @@ You can use adapters to configure your app for different deployment environments
## Configuration
-Add `adapter` entry to your `react-server.config.mjs` file. You can specify the name of a built-in adapter (`vercel`, `netlify`, `cloudflare`, `aws`, `bun`, `deno`, `azure`, `azure-swa`, `firebase`, or `docker`) as a string, or use an external adapter package.
+Add `adapter` entry to your `react-server.config.mjs` file. You can specify the name of a built-in adapter (`vercel`, `netlify`, `cloudflare`, `aws`, `bun`, `deno`, `azure`, `azure-swa`, `firebase`, `docker`, or `singlefile`) as a string, or use an external adapter package.
> **Note:** When running a production build with **Bun** or **Deno**, the corresponding adapter is automatically detected and used without any configuration. You can override this with an explicit `adapter` setting in your config or via `--adapter ` on the CLI. Use `--no-adapter` to disable auto-detection.
diff --git a/docs/src/pages/en/(pages)/deploy/singlefile.mdx b/docs/src/pages/en/(pages)/deploy/singlefile.mdx
new file mode 100644
index 00000000..5813c619
--- /dev/null
+++ b/docs/src/pages/en/(pages)/deploy/singlefile.mdx
@@ -0,0 +1,95 @@
+---
+title: Singlefile
+category: Deploy
+order: 9
+---
+
+import Link from "../../../../components/Link.jsx";
+
+# Singlefile
+
+> **Experimental:** This adapter is experimental and may change in future releases. It is intended for simple, single-page applications and static sites.
+
+The `singlefile` adapter bundles your entire statically exported React application into a single, self-contained HTML file. All CSS and JavaScript modules are inlined — no external resources are fetched at runtime.
+
+This is useful for:
+
+- **Offline-capable apps** that work from a `file://` URL or a single HTTP request
+- **Portable demos** or prototypes you can share as a single file
+- **Embedding** in environments where only one HTML file is allowed (e.g. email attachments, embedded webviews)
+
+
+## Installation
+
+
+No additional packages are needed — the adapter is built into `@lazarv/react-server`.
+
+Add the adapter to your `react-server.config.mjs` file:
+
+```mjs
+export default {
+ adapter: "singlefile",
+};
+```
+
+Or pass it via the CLI:
+
+```sh
+pnpm react-server build ./src/index.jsx --adapter singlefile
+```
+
+
+## How it works
+
+
+The singlefile adapter performs these steps during the build:
+
+1. **Static export** — The `"/"` route is statically exported to produce `index.html`
+2. **CSS inlining** — All `` tags are replaced with inline ``
+ );
+
+ // 2. Replace ALL remaining references to the CSS path (in flight data,
+ // scripts, etc.) with a data: URI so React can still load it at runtime.
+ const cssDataUri = `data:text/css;base64,${Buffer.from(cssContent).toString("base64")}`;
+ // Use a global string replace (not regex) to catch all occurrences.
+ // The CSS path may appear as "/assets/root-X.css" in various contexts
+ // like :HL["/assets/root-X.css","style"] and {"href":"/assets/root-X.css"}
+ html = html.split(cssHref).join(cssDataUri);
+ }
+ success(
+ `${cssAssetFiles.length} stylesheet${cssAssetFiles.length !== 1 ? "s" : ""} inlined`
+ );
+
+ // --- Remove modulepreload links ---
+ html = html.replace(/]*rel=["']modulepreload["'][^>]*\/?>/g, "");
+
+ // --- Remove dev-time preconnect/live-reload link ---
+ html = html.replace(/]*id=["']live-io["'][^>]*\/?>/g, "");
+
+ // --- Build module source registry ---
+ banner("inlining client modules", { emoji: "⚡" });
+ const moduleSources = {};
+ const baseEscaped = base.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+
+ for (const file of clientMjsFiles) {
+ let source = readFileSync(join(reactServerDir, file), "utf-8");
+
+ // Resolve relative imports to absolute paths first.
+ // For "client/src/App.HASH.mjs" with import "../react.HASH.mjs",
+ // we resolve relative to "/client/src/" → "/client/react.HASH.mjs"
+ const moduleDir = posix.dirname(`${base}${file}`);
+
+ const resolveRelative = (match, prefix, relPath, suffix) => {
+ const resolved = posix.resolve(moduleDir, relPath);
+ return `${prefix}${resolved}${suffix}`;
+ };
+
+ // Static imports/re-exports: from "./X.mjs" or from "../X.mjs"
+ source = source.replace(
+ /(from\s*["'])(\.\.?\/[^"']+)(["'])/g,
+ resolveRelative
+ );
+
+ // Dynamic imports: import("./X.mjs") or import("../X.mjs")
+ source = source.replace(
+ /(import\s*\(\s*["'])(\.\.?\/[^"']+)(["']\s*\))/g,
+ resolveRelative
+ );
+
+ // Side-effect imports: import "./X.mjs" or import "../X.mjs"
+ source = source.replace(
+ /(import\s+["'])(\.\.?\/[^"']+)(["'])/g,
+ resolveRelative
+ );
+
+ // Convert ALL absolute path imports to bare specifiers by stripping
+ // the leading base path (e.g. "/"). Blob: URLs can't resolve URL-like
+ // specifiers (/client/foo.mjs) through the import map because the
+ // specifier gets normalized against the blob URL, producing a mismatch.
+ // Bare specifiers (client/foo.mjs) are matched as raw strings in the
+ // import map, so they work regardless of the referrer's URL scheme.
+ const stripBase = new RegExp(
+ `((?:from|import)\\s*["'])${baseEscaped}([^"']+["'])`,
+ "g"
+ );
+ source = source.replace(stripBase, "$1$2");
+
+ // Also handle dynamic import("...") with absolute paths
+ const stripBaseDynamic = new RegExp(
+ `(import\\s*\\(\\s*["'])${baseEscaped}([^"']+["'])`,
+ "g"
+ );
+ source = source.replace(stripBaseDynamic, "$1$2");
+
+ // Use bare specifier as the key (no leading base path)
+ moduleSources[`${file}`] = source;
+
+ message(` ${file}`);
+ }
+ success(
+ `${clientMjsFiles.length} module${clientMjsFiles.length !== 1 ? "s" : ""} inlined`
+ );
+
+ // --- Find module entry points from , etc. inside JS module code.
+ const moduleSourcesB64 = {};
+ for (const [key, source] of Object.entries(moduleSources)) {
+ moduleSourcesB64[key] = Buffer.from(source).toString("base64");
+ }
+
+ // The b64 map only contains [A-Za-z0-9+/=] values — safe to JSON.stringify
+ // into a `;
+
+ // Inject boot script at the very start of (before any other scripts).
+ // IMPORTANT: Use a replacer function to avoid $-pattern interpretation
+ // in module source code (e.g. $`, $', $& from JS template literals).
+ if (html.includes("]*)>/,
+ (match, attrs) => `${bootScript}`
+ );
+ } else {
+ // Fallback: prepend to document
+ html = bootScript + html;
+ }
+
+ // Write the single HTML file
+ await mkdir(outDir, { recursive: true });
+ const outputPath = join(outDir, "index.html");
+ await writeFile(outputPath, html);
+ success(relative(cwd, outputPath));
+ },
+});
diff --git a/packages/react-server/package.json b/packages/react-server/package.json
index d6c480ba..0d3a6c46 100644
--- a/packages/react-server/package.json
+++ b/packages/react-server/package.json
@@ -150,6 +150,10 @@
"types": "./adapters/adapter.d.ts",
"default": "./adapters/docker/index.mjs"
},
+ "./adapters/singlefile": {
+ "types": "./adapters/adapter.d.ts",
+ "default": "./adapters/singlefile/index.mjs"
+ },
"./worker": {
"types": "./worker/index.d.ts",
"default": "./worker/index.mjs"