diff --git a/package.json b/package.json
index db7463ad..9a28043a 100644
--- a/package.json
+++ b/package.json
@@ -21,10 +21,6 @@
"patchedDependencies": {
"vite@4.3.9": "patches/vite@4.3.9.patch",
"@tanstack/react-start@0.0.1-beta.111": "patches/@tanstack__react-start@0.0.1-beta.111.patch"
- },
- "overrides": {
- "h3": "npm:@vinxi/h3@1.8.6",
- "listhen": "npm:@vinxi/listhen"
}
},
"dependencies": {
diff --git a/packages/vinxi/lib/build.js b/packages/vinxi/lib/build.js
index 039ee880..593e9a27 100644
--- a/packages/vinxi/lib/build.js
+++ b/packages/vinxi/lib/build.js
@@ -297,7 +297,6 @@ async function createRouterBuild(app, router) {
ssrManifest: true,
rollupOptions: {
input: { handler: router.handler },
- external: ["h3"],
},
target: "esnext",
outDir: join(router.outDir + "_entry"),
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 29f60526..2f5bcde7 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -4,10 +4,6 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
-overrides:
- h3: npm:@vinxi/h3@1.8.6
- listhen: npm:@vinxi/listhen
-
patchedDependencies:
'@tanstack/react-start@0.0.1-beta.111':
hash: pe7vlff7zvtgxhebldgtqibm6a
@@ -806,14 +802,14 @@ importers:
specifier: ^6.1.2
version: 6.1.2
h3:
- specifier: npm:@vinxi/h3@1.8.6
- version: /@vinxi/h3@1.8.6
+ specifier: 1.8.0
+ version: 1.8.0
http-proxy:
specifier: ^1.18.1
version: 1.18.1
listhen:
- specifier: npm:@vinxi/listhen
- version: /@vinxi/listhen@1.5.5
+ specifier: ^1.5.0
+ version: 1.5.5
micromatch:
specifier: ^4.0.5
version: 4.0.5
@@ -1098,6 +1094,49 @@ importers:
specifier: ^18.2.7
version: 18.2.7
+ test/templates/react-rsc:
+ dependencies:
+ '@picocss/pico':
+ specifier: ^1.5.10
+ version: 1.5.10
+ '@vinxi/plugin-references':
+ specifier: 0.0.20
+ version: link:../../../packages/vinxi-references
+ '@vinxi/react':
+ specifier: 0.0.10
+ version: link:../../../packages/vinxi-react
+ '@vinxi/react-server-dom':
+ specifier: 0.0.3
+ version: 0.0.3(react-dom@0.0.0-experimental-035a41c4e-20230704)(react@0.0.0-experimental-035a41c4e-20230704)(vite@4.4.9)
+ '@vitejs/plugin-react':
+ specifier: ^4.0.4
+ version: 4.0.4(vite@4.4.9)
+ acorn-loose:
+ specifier: ^8.3.0
+ version: 8.3.0
+ autoprefixer:
+ specifier: ^10.4.15
+ version: 10.4.16(postcss@8.4.30)
+ react:
+ specifier: 0.0.0-experimental-035a41c4e-20230704
+ version: 0.0.0-experimental-035a41c4e-20230704
+ react-dom:
+ specifier: 0.0.0-experimental-035a41c4e-20230704
+ version: 0.0.0-experimental-035a41c4e-20230704(react@0.0.0-experimental-035a41c4e-20230704)
+ tailwindcss:
+ specifier: ^3.3.3
+ version: 3.3.3
+ vinxi:
+ specifier: 0.0.30
+ version: link:../../../packages/vinxi
+ devDependencies:
+ '@types/react':
+ specifier: ^18.2.21
+ version: 18.2.22
+ '@types/react-dom':
+ specifier: ^18.2.7
+ version: 18.2.7
+
packages:
/@aashutoshrathi/word-wrap@1.2.6:
@@ -7097,6 +7136,32 @@ packages:
dependencies:
duplexer: 0.1.2
+ /h3@1.8.0:
+ resolution: {integrity: sha512-057VY83X7Tg5n4XU2GV9M3dsCWUU4jtw2Lc/r4GjAcf9Jb6GI1mD5F8TCQHUcvLMEgtx6lbfobOFu7Vdmejihg==}
+ dependencies:
+ cookie-es: 1.0.0
+ defu: 6.1.2
+ destr: 2.0.1
+ iron-webcrypto: 0.8.2
+ radix3: 1.1.0
+ ufo: 1.3.0
+ uncrypto: 0.1.3
+ unenv: 1.7.4
+ dev: false
+
+ /h3@1.8.1:
+ resolution: {integrity: sha512-m5rFuu+5bpwBBHqqS0zexjK+Q8dhtFRvO9JXQG0RvSPL6QrIT6vv42vuBM22SLOgGMoZYsHk0y7VPidt9s+nkw==}
+ dependencies:
+ cookie-es: 1.0.0
+ defu: 6.1.2
+ destr: 2.0.1
+ iron-webcrypto: 0.8.2
+ radix3: 1.1.0
+ ufo: 1.3.0
+ uncrypto: 0.1.3
+ unenv: 1.7.4
+ dev: false
+
/hard-rejection@2.1.0:
resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==}
engines: {node: '>=6'}
@@ -7441,6 +7506,10 @@ packages:
/iron-webcrypto@0.10.1:
resolution: {integrity: sha512-QGOS8MRMnj/UiOa+aMIgfyHcvkhqNUsUxb1XzskENvbo+rEfp6TOwqd1KPuDzXC4OnGHcMSVxDGRoilqB8ViqA==}
+ /iron-webcrypto@0.8.2:
+ resolution: {integrity: sha512-jGiwmpgTuF19Vt4hn3+AzaVFGpVZt7A1ysd5ivFel2r4aNVFwqaYa6aU6qsF1PM7b+WFivZHz3nipwUOXaOnHg==}
+ dev: false
+
/is-absolute-url@4.0.1:
resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -7917,6 +7986,29 @@ packages:
/lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
+ /listhen@1.5.5:
+ resolution: {integrity: sha512-LXe8Xlyh3gnxdv4tSjTjscD1vpr/2PRpzq8YIaMJgyKzRG8wdISlWVWnGThJfHnlJ6hmLt2wq1yeeix0TEbuoA==}
+ hasBin: true
+ dependencies:
+ '@parcel/watcher': 2.3.0
+ '@parcel/watcher-wasm': 2.3.0
+ citty: 0.1.4
+ clipboardy: 3.0.0
+ consola: 3.2.3
+ defu: 6.1.2
+ get-port-please: 3.1.1
+ h3: 1.8.1
+ http-shutdown: 1.2.2
+ jiti: 1.20.0
+ mlly: 1.4.2
+ node-forge: 1.3.1
+ pathe: 1.1.1
+ std-env: 3.4.3
+ ufo: 1.3.0
+ untun: 0.1.2
+ uqr: 0.1.2
+ dev: false
+
/load-yaml-file@0.2.0:
resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==}
engines: {node: '>=6'}
@@ -12233,7 +12325,7 @@ packages:
esbuild: 0.18.20
fast-glob: 3.3.1
get-port: 6.1.2
- h3: /@vinxi/h3@1.8.6
+ h3: 1.8.0
http-proxy: 1.18.1
listhen: /@vinxi/listhen@1.5.5
micromatch: 4.0.5
diff --git a/test/rsc.test.ts b/test/rsc.test.ts
new file mode 100644
index 00000000..cba69699
--- /dev/null
+++ b/test/rsc.test.ts
@@ -0,0 +1,59 @@
+import { expect, test } from "@playwright/test";
+
+import type { AppFixture, Fixture } from "./helpers/create-fixture.js";
+import { createFixture, js } from "./helpers/create-fixture.js";
+import {
+ PlaywrightFixture,
+ prettyHtml,
+ selectHtml,
+} from "./helpers/playwright-fixture.js";
+
+test.describe("rsc", () => {
+ let fixture: Fixture;
+ let appFixture: AppFixture;
+
+ test.beforeAll(async () => {
+ fixture = await createFixture({
+ files: {},
+ template: "react-rsc",
+ });
+
+ appFixture = await fixture.createServer();
+ });
+
+ test.afterAll(async () => {
+ await appFixture.close();
+ });
+
+ let logs: string[] = [];
+
+ test.beforeEach(({ page }) => {
+ page.on("console", (msg) => {
+ logs.push(msg.text());
+ });
+ });
+
+ test("spa", async ({ page }) => {
+ let app = new PlaywrightFixture(appFixture, page);
+ await app.goto("/", true);
+
+ expect(await app.getHtml("[data-test-id=title]")).toBe(
+ prettyHtml(`
Hello from Vinxi
`),
+ );
+ expect(await app.getHtml("[data-test-id=counter]")).toBe(
+ prettyHtml(``),
+ );
+ expect(await app.getHtml("[data-test-id=server-count]")).toBe(
+ prettyHtml(`0`),
+ );
+
+ await app.clickElement("[data-test-id=counter]");
+
+ expect(await app.getHtml("[data-test-id=counter]")).toBe(
+ prettyHtml(``),
+ );
+ expect(await app.getHtml("[data-test-id=server-count]")).toBe(
+ prettyHtml(`1`),
+ );
+ });
+});
diff --git a/test/templates/react-rsc/.gitignore b/test/templates/react-rsc/.gitignore
new file mode 100644
index 00000000..e985853e
--- /dev/null
+++ b/test/templates/react-rsc/.gitignore
@@ -0,0 +1 @@
+.vercel
diff --git a/test/templates/react-rsc/CHANGELOG.md b/test/templates/react-rsc/CHANGELOG.md
new file mode 100644
index 00000000..f948ac32
--- /dev/null
+++ b/test/templates/react-rsc/CHANGELOG.md
@@ -0,0 +1,72 @@
+# app
+
+## null
+
+### Patch Changes
+
+- Updated dependencies [fd06048]
+- Updated dependencies [0f14555]
+- Updated dependencies [82267c5]
+ - vinxi@0.0.30
+ - @vinxi/plugin-references@0.0.20
+ - @vinxi/react@0.0.10
+
+## null
+
+### Patch Changes
+
+- Updated dependencies [8058084]
+ - vinxi@0.0.29
+ - @vinxi/plugin-references@0.0.19
+
+## null
+
+### Patch Changes
+
+- Updated dependencies [b934e84]
+- Updated dependencies [17693dc]
+- Updated dependencies [d6305b8]
+- Updated dependencies [cb91c48]
+- Updated dependencies [085116d]
+- Updated dependencies [f1ee5b8]
+ - vinxi@0.0.28
+ - @vinxi/plugin-references@0.0.18
+
+## null
+
+### Patch Changes
+
+- Updated dependencies [7803042]
+ - vinxi@0.0.27
+ - @vinxi/plugin-references@0.0.17
+
+## null
+
+### Patch Changes
+
+- Updated dependencies [2b17e0d]
+ - vinxi@0.0.26
+ - @vinxi/plugin-references@0.0.16
+
+## null
+
+### Patch Changes
+
+- Updated dependencies [552d8ca]
+ - vinxi@0.0.25
+ - @vinxi/plugin-references@0.0.15
+
+## null
+
+### Patch Changes
+
+- Updated dependencies [47abc3c]
+ - @vinxi/plugin-references@0.0.14
+ - vinxi@0.0.24
+
+## null
+
+### Patch Changes
+
+- Updated dependencies [ec37a2e]
+ - @vinxi/react@0.0.9
diff --git a/test/templates/react-rsc/app.config.js b/test/templates/react-rsc/app.config.js
new file mode 100644
index 00000000..27756195
--- /dev/null
+++ b/test/templates/react-rsc/app.config.js
@@ -0,0 +1,59 @@
+import { references } from "@vinxi/plugin-references";
+import reactRefresh from "@vitejs/plugin-react";
+import { createApp } from "vinxi";
+
+export default createApp({
+ server: {
+ plugins: [references.serverPlugin],
+ virtual: {
+ [references.serverPlugin]: references.serverPluginModule({
+ routers: ["server", "rsc"],
+ }),
+ },
+ },
+ routers: [
+ {
+ name: "public",
+ mode: "static",
+ dir: "./public",
+ },
+ {
+ name: "rsc",
+ worker: true,
+ mode: "handler",
+ base: "/_rsc",
+ handler: "./app/react-server.tsx",
+ target: "server",
+ plugins: () => [references.serverComponents(), reactRefresh()],
+ },
+ {
+ name: "client",
+ mode: "spa",
+ handler: "./index.ts",
+ target: "browser",
+ plugins: () => [
+ references.clientRouterPlugin({
+ runtime: "@vinxi/react-server-dom/runtime",
+ }),
+ reactRefresh(),
+ references.clientComponents(),
+ ],
+ base: "/",
+ },
+ {
+ name: "server",
+ worker: true,
+ mode: "handler",
+ base: "/_server",
+ handler: "./app/server-action.tsx",
+ target: "server",
+ plugins: () => [
+ references.serverRouterPlugin({
+ resolve: {
+ conditions: ["react-server"],
+ },
+ }),
+ ],
+ },
+ ],
+});
diff --git a/test/templates/react-rsc/app/Counter.tsx b/test/templates/react-rsc/app/Counter.tsx
new file mode 100644
index 00000000..84657b21
--- /dev/null
+++ b/test/templates/react-rsc/app/Counter.tsx
@@ -0,0 +1,18 @@
+"use client";
+
+import { useState } from "react";
+
+export function Counter({ onChange }) {
+ const [count, setCount] = useState(0);
+ return (
+
+ );
+}
diff --git a/test/templates/react-rsc/app/actions.tsx b/test/templates/react-rsc/app/actions.tsx
new file mode 100644
index 00000000..a843ead4
--- /dev/null
+++ b/test/templates/react-rsc/app/actions.tsx
@@ -0,0 +1,12 @@
+"use server";
+
+let store = { count: 0 };
+export function sayHello() {
+ console.log("Hello World");
+ store.count++;
+ return store.count;
+}
+
+export function getStore() {
+ return store.count;
+}
diff --git a/test/templates/react-rsc/app/app.tsx b/test/templates/react-rsc/app/app.tsx
new file mode 100644
index 00000000..418914da
--- /dev/null
+++ b/test/templates/react-rsc/app/app.tsx
@@ -0,0 +1,23 @@
+import React from "react";
+
+import { Counter } from "./Counter";
+import { getStore, sayHello } from "./actions";
+import "./style.css";
+
+export default function App({ assets }) {
+ return (
+
+
+
+ {assets}
+
+
+
+ Hello from Vinxi
+ {getStore()}
+
+
+
+
+ );
+}
diff --git a/test/templates/react-rsc/app/client.tsx b/test/templates/react-rsc/app/client.tsx
new file mode 100644
index 00000000..0e2f9a49
--- /dev/null
+++ b/test/templates/react-rsc/app/client.tsx
@@ -0,0 +1,17 @@
+///
+import { createModuleLoader } from "@vinxi/react-server-dom/runtime";
+import { createRoot } from "react-dom/client";
+import "vinxi/client";
+
+import { ServerComponent } from "./server-component";
+
+globalThis.__vite__ = createModuleLoader({
+ loadModule: async (id) => {
+ return import(
+ /* @vite-ignore */ import.meta.env.MANIFEST["client"].chunks[id].output
+ .path
+ );
+ },
+});
+
+createRoot(document).render();
diff --git a/test/templates/react-rsc/app/fetchServerAction.tsx b/test/templates/react-rsc/app/fetchServerAction.tsx
new file mode 100644
index 00000000..95838ec0
--- /dev/null
+++ b/test/templates/react-rsc/app/fetchServerAction.tsx
@@ -0,0 +1,26 @@
+import {
+ createFromFetch,
+ encodeReply,
+} from "@vinxi/react-server-dom/client";
+
+export async function fetchServerAction(
+ base,
+ id,
+ args,
+ callServer = (id, args) => {
+ throw new Error("No server action handler");
+ },
+) {
+ const response = fetch(base, {
+ method: "POST",
+ headers: {
+ Accept: "text/x-component",
+ "server-action": id,
+ },
+ body: await encodeReply(args),
+ });
+
+ return await createFromFetch(response, {
+ callServer,
+ });
+}
diff --git a/test/templates/react-rsc/app/react-server.tsx b/test/templates/react-rsc/app/react-server.tsx
new file mode 100644
index 00000000..54de84e1
--- /dev/null
+++ b/test/templates/react-rsc/app/react-server.tsx
@@ -0,0 +1,111 @@
+import { renderAsset } from "@vinxi/react";
+import { renderToPipeableStream } from "@vinxi/react-server-dom/server";
+import { Suspense } from "react";
+import { eventHandler } from "vinxi/server";
+
+import App from "./app";
+
+export default eventHandler(async (event) => {
+ console.log("event", event);
+ async function loadModule(id) {
+ if (import.meta.env.DEV) {
+ console.log(import.meta.env.MANIFEST["rsc"].chunks[id].output.path);
+ return await import(
+ import.meta.env.MANIFEST["rsc"].chunks[id].output.path
+ );
+ }
+
+ console.log(id, globalThis.$$chunks);
+ if (globalThis.$$chunks[id + ".js"]) {
+ return globalThis.$$chunks[id + ".js"];
+ }
+ return await import(import.meta.env.MANIFEST["rsc"].chunks[id].output.path);
+ }
+ if (event.node.req.method === "POST") {
+ const {
+ renderToPipeableStream,
+ decodeReply,
+ decodeReplyFromBusboy,
+ decodeAction,
+ } = await import("@vinxi/react-server-dom/server");
+ console.log(event.node.req.headers);
+ const serverReference = event.node.req.headers["server-action"];
+ if (serverReference) {
+ // This is the client-side case
+ const [filepath, name] = serverReference.split("#");
+ const action = (await loadModule(filepath))[name];
+ // Validate that this is actually a function we intended to expose and
+ // not the client trying to invoke arbitrary functions. In a real app,
+ // you'd have a manifest verifying this before even importing it.
+ if (action.$$typeof !== Symbol.for("react.server.reference")) {
+ throw new Error("Invalid action");
+ }
+
+ let args;
+ // if (req.is('multipart/form-data')) {
+ // // Use busboy to streamingly parse the reply from form-data.
+ // const bb = busboy({headers: req.headers});
+ // const reply = decodeReplyFromBusboy(bb, moduleBasePath);
+ // req.pipe(bb);
+ // args = await reply;
+ // } else {
+ const text = await new Promise((resolve) => {
+ const requestBody = [];
+ event.node.req.on("data", (chunks) => {
+ console.log(chunks);
+ requestBody.push(chunks);
+ });
+ event.node.req.on("end", () => {
+ resolve(requestBody.join(""));
+ });
+ });
+ console.log(text);
+
+ args = await decodeReply(text);
+ console.log(args, action);
+ // }
+ const result = action.apply(null, args);
+ try {
+ // Wait for any mutations
+ await result;
+ } catch (x) {
+ // We handle the error on the client
+ }
+ // Refresh the client and return the value
+ // return {};
+ } else {
+ throw new Error("Invalid request");
+ }
+ }
+ console.log("rendering");
+ const reactServerManifest = import.meta.env.MANIFEST["rsc"];
+ const serverAssets = await reactServerManifest.inputs[
+ reactServerManifest.handler
+ ].assets();
+ const clientManifest = import.meta.env.MANIFEST["client"];
+ const assets = await clientManifest.inputs[clientManifest.handler].assets();
+
+ const events = {};
+ const stream = renderToPipeableStream(
+
+ {serverAssets.map((m) => renderAsset(m))}
+ {assets.map((m) => renderAsset(m))}
+
+ }
+ />,
+ );
+
+ // @ts-ignore
+ stream._read = () => {};
+ // @ts-ignore
+ stream.on = (event, listener) => {
+ events[event] = listener;
+ };
+
+ event.node.res.setHeader("Content-Type", "text/x-component");
+ event.node.res.setHeader("Router", "rsc");
+
+ return stream;
+});
diff --git a/test/templates/react-rsc/app/server-action.tsx b/test/templates/react-rsc/app/server-action.tsx
new file mode 100644
index 00000000..bdb0b38c
--- /dev/null
+++ b/test/templates/react-rsc/app/server-action.tsx
@@ -0,0 +1,124 @@
+// import { renderAsset } from "@vinxi/react";
+// import { renderToPipeableStream } from "@vinxi/react-server-dom/server";
+// import React, { Suspense } from "react";
+import { eventHandler, sendStream } from "vinxi/server";
+
+// import App from "./app";
+
+export default eventHandler(async (event) => {
+ console.log("event", event);
+ async function loadModule(id) {
+ if (import.meta.env.DEV) {
+ console.log(import.meta.env.MANIFEST["rsc"].chunks[id].output.path);
+ return await import(
+ import.meta.env.MANIFEST["rsc"].chunks[id].output.path
+ );
+ }
+
+ console.log(id, globalThis.$$chunks);
+ if (globalThis.$$chunks[id + ".js"]) {
+ return globalThis.$$chunks[id + ".js"];
+ }
+ return await import(import.meta.env.MANIFEST["rsc"].chunks[id].output.path);
+ }
+ if (event.node.req.method === "POST") {
+ const {
+ renderToPipeableStream,
+ decodeReply,
+ decodeReplyFromBusboy,
+ decodeAction,
+ } = await import("@vinxi/react-server-dom/server");
+ const serverReference = event.node.req.headers["server-action"];
+ if (serverReference) {
+ // This is the client-side case
+ const [filepath, name] = serverReference.split("#");
+ const action = (await loadModule(filepath))[name];
+ // Validate that this is actually a function we intended to expose and
+ // not the client trying to invoke arbitrary functions. In a real app,
+ // you'd have a manifest verifying this before even importing it.
+ if (action.$$typeof !== Symbol.for("react.server.reference")) {
+ throw new Error("Invalid action");
+ }
+
+ let args;
+ // if (req.is('multipart/form-data')) {
+ // // Use busboy to streamingly parse the reply from form-data.
+ // const bb = busboy({headers: req.headers});
+ // const reply = decodeReplyFromBusboy(bb, moduleBasePath);
+ // req.pipe(bb);
+ // args = await reply;
+ // } else {
+ const text = await new Promise((resolve) => {
+ const requestBody = [];
+ event.node.req.on("data", (chunks) => {
+ console.log(chunks);
+ requestBody.push(chunks);
+ });
+ event.node.req.on("end", () => {
+ resolve(requestBody.join(""));
+ });
+ });
+ console.log(text);
+
+ args = await decodeReply(text);
+ console.log(args, action);
+ // }
+ const result = action.apply(null, args);
+ try {
+ // Wait for any mutations
+ await result;
+ const events = {};
+ const stream = renderToPipeableStream(result);
+
+ // @ts-ignore
+ stream._read = () => {};
+ // @ts-ignore
+ stream.on = (event, listener) => {
+ events[event] = listener;
+ };
+
+ event.node.res.setHeader("Content-Type", "application/json");
+ event.node.res.setHeader("Router", "server");
+
+ return stream;
+ } catch (x) {
+ // We handle the error on the client
+ }
+ // Refresh the client and return the value
+ // return {};
+ } else {
+ throw new Error("Invalid request");
+ }
+ }
+ console.log("rendering");
+ // const reactServerManifest = import.meta.env.MANIFEST["rsc"];
+ // const serverAssets = await reactServerManifest.inputs[
+ // reactServerManifest.handler
+ // ].assets();
+ // const clientManifest = import.meta.env.MANIFEST["client"];
+ // const assets = await clientManifest.inputs[clientManifest.handler].assets();
+
+ // const events = {};
+ // const stream = renderToPipeableStream(
+ //
+ // {serverAssets.map((m) => renderAsset(m))}
+ // {assets.map((m) => renderAsset(m))}
+ //
+ // }
+ // />,
+ // );
+
+ // // @ts-ignore
+ // stream._read = () => {};
+ // // @ts-ignore
+ // stream.on = (event, listener) => {
+ // events[event] = listener;
+ // };
+
+ // event.node.res.setHeader("Content-Type", "text/x-component");
+ // event.node.res.setHeader("Router", "rsc");
+
+ // return result;
+});
diff --git a/test/templates/react-rsc/app/server-component.tsx b/test/templates/react-rsc/app/server-component.tsx
new file mode 100644
index 00000000..21746b12
--- /dev/null
+++ b/test/templates/react-rsc/app/server-component.tsx
@@ -0,0 +1,67 @@
+import { createFromFetch } from "@vinxi/react-server-dom/client";
+import * as React from "react";
+import { startTransition, use, useState } from "react";
+import ReactDOM from "react-dom/client";
+
+import { fetchServerAction } from "./fetchServerAction";
+
+let updateRoot;
+declare global {
+ interface Window {
+ init_server: ReadableStream | null;
+ chunk(chunk: string): Promise;
+ }
+}
+
+export function getServerElementStream(url: string) {
+ let stream;
+ // Ideally we should have a readable stream inlined in the HTML
+ if (window.init_server) {
+ stream = { body: window.init_server };
+ self.init_server = null;
+ } else {
+ stream = fetch(`/_rsc${url}`, {
+ headers: {
+ Accept: "text/x-component",
+ "x-navigate": url,
+ },
+ });
+ }
+
+ return stream;
+}
+
+export function ServerComponent({ url }: { url: string }) {
+ const [root, setRoot] = useState(use(useServerElement(url)));
+ updateRoot = setRoot;
+ return root;
+}
+
+export const serverElementCache = /*#__PURE__*/ new Map<
+ string,
+ React.Thenable
+>();
+
+export function createCallServer(base) {
+ return async function callServer(id, args) {
+ const root = await fetchServerAction(base, id, args, callServer);
+ // Refresh the tree with the new RSC payload.
+ startTransition(() => {
+ updateRoot(root);
+ });
+ // return returnValue;
+ };
+}
+
+const callServer = createCallServer("/_rsc");
+export function useServerElement(url: string) {
+ if (!serverElementCache.has(url)) {
+ serverElementCache.set(
+ url,
+ createFromFetch(getServerElementStream(url), {
+ callServer,
+ }),
+ );
+ }
+ return serverElementCache.get(url)!;
+}
diff --git a/test/templates/react-rsc/app/server.tsx b/test/templates/react-rsc/app/server.tsx
new file mode 100644
index 00000000..cecb0eb7
--- /dev/null
+++ b/test/templates/react-rsc/app/server.tsx
@@ -0,0 +1,107 @@
+///
+import viteServer from "#vite-dev-server";
+import { renderAsset } from "@vinxi/react";
+import * as ReactServerDOM from "@vinxi/react-server-dom/client";
+import { createModuleLoader } from "@vinxi/react-server-dom/runtime";
+import React, { Suspense } from "react";
+import { renderToPipeableStream } from "react-dom/server";
+import { H3Event, eventHandler, fetchWithEvent } from "vinxi/server";
+
+import { Readable, Writable } from "node:stream";
+
+import App from "./app";
+
+export default eventHandler(async (event) => {
+ globalThis.__vite__ = createModuleLoader(viteServer);
+
+ const readable = new Readable({
+ objectMode: true,
+ });
+ readable._read = () => {};
+ readable.headers = {};
+
+ const writableStream = new Writable({
+ write(chunk, encoding, callback) {
+ console.log("chunk", chunk);
+
+ readable.push(chunk);
+ callback();
+ },
+ });
+ writableStream.setHeader = () => {};
+
+ writableStream.on("finish", () => {
+ // parentPort?.postMessage(
+ // JSON.stringify({
+ // chunk: "end",
+ // id: rest.id,
+ // })
+ // );
+ console.log("finish");
+
+ readable.push(null);
+ readable.destroy();
+ });
+
+ event.node.req.url = `/_rsc` + event.node.req.url;
+
+ $handle(new H3Event(event.node.req, writableStream));
+
+ const clientManifest = import.meta.env.MANIFEST["client"];
+
+ const events = {};
+ console.log("element", "here");
+
+ const element = await ReactServerDOM.createFromNodeStream(readable);
+
+ console.log("element", element);
+
+ const stream = renderToPipeableStream(element, {
+ bootstrapModules: [
+ clientManifest?.inputs[clientManifest.handler].output.path,
+ ].filter(Boolean) as string[],
+ bootstrapScriptContent: `
+ window.base = "${import.meta.env.BASE_URL}";`,
+ // {
+ // onAllReady: () => {
+ // events["end"]?.();
+ // },
+ // bootstrapModules: [
+ // clientManifest.inputs[clientManifest.handler].output.path,
+ // ],
+ // bootstrapScriptContent: `window.manifest = ${JSON.stringify(
+ // await clientManifest.json(),
+ // )}`,
+ // },
+ });
+
+ // const clientManifest = import.meta.env.MANIFEST["client"];
+ // const assets = await clientManifest.inputs[clientManifest.handler].assets();
+ // const events = {};
+ // const stream = renderToPipeableStream(
+ // {assets.map((m) => renderAsset(m))}} />,
+ // {
+ // onAllReady: () => {
+ // events["end"]?.();
+ // },
+ // bootstrapModules: [
+ // clientManifest.inputs[clientManifest.handler].output.path,
+ // ],
+ // bootstrapScriptContent: `window.manifest = ${JSON.stringify(
+ // await clientManifest.json(),
+ // )}`,
+ // },
+ // );
+
+ // @ts-ignore
+ stream.on = (event, listener) => {
+ console.log("on", "event", event);
+
+ events[event] = listener;
+ };
+
+ console.log("render");
+
+ event.node.res.setHeader("Content-Type", "text/html");
+ return stream;
+});
diff --git a/test/templates/react-rsc/app/style.css b/test/templates/react-rsc/app/style.css
new file mode 100644
index 00000000..b1284a35
--- /dev/null
+++ b/test/templates/react-rsc/app/style.css
@@ -0,0 +1,7 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+* {
+ color: red;
+}
\ No newline at end of file
diff --git a/test/templates/react-rsc/index.ts b/test/templates/react-rsc/index.ts
new file mode 100644
index 00000000..a5af66aa
--- /dev/null
+++ b/test/templates/react-rsc/index.ts
@@ -0,0 +1,29 @@
+import { eventHandler, toWebRequest } from "vinxi/server";
+
+export default eventHandler((event) => {
+ console.log(toWebRequest(event));
+ return new Response(
+ `
+
+
+
+
+
+ Document
+
+
+
+
+
+
+
+
+ `,
+ {
+ status: 200,
+ headers: {
+ "Content-Type": "text/html",
+ },
+ },
+ );
+});
diff --git a/test/templates/react-rsc/package.json b/test/templates/react-rsc/package.json
new file mode 100644
index 00000000..9cb48862
--- /dev/null
+++ b/test/templates/react-rsc/package.json
@@ -0,0 +1,28 @@
+{
+ "name": "test-react-rsc",
+ "type": "module",
+ "private": true,
+ "version": null,
+ "scripts": {
+ "dev": "vinxi dev",
+ "build": "vinxi build",
+ "start": "node .output/server/index.mjs"
+ },
+ "dependencies": {
+ "@picocss/pico": "^1.5.10",
+ "@vinxi/plugin-references": "0.0.20",
+ "@vinxi/react": "0.0.10",
+ "@vinxi/react-server-dom": "0.0.3",
+ "@vitejs/plugin-react": "^4.0.4",
+ "acorn-loose": "^8.3.0",
+ "autoprefixer": "^10.4.15",
+ "react": "0.0.0-experimental-035a41c4e-20230704",
+ "react-dom": "0.0.0-experimental-035a41c4e-20230704",
+ "tailwindcss": "^3.3.3",
+ "vinxi": "0.0.30"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.21",
+ "@types/react-dom": "^18.2.7"
+ }
+}
diff --git a/test/templates/react-rsc/postcss.config.cjs b/test/templates/react-rsc/postcss.config.cjs
new file mode 100644
index 00000000..33ad091d
--- /dev/null
+++ b/test/templates/react-rsc/postcss.config.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/test/templates/react-rsc/public/favicon.ico b/test/templates/react-rsc/public/favicon.ico
new file mode 100644
index 00000000..129ee136
Binary files /dev/null and b/test/templates/react-rsc/public/favicon.ico differ
diff --git a/test/templates/react-rsc/react-server.js b/test/templates/react-rsc/react-server.js
new file mode 100644
index 00000000..429c1d0d
--- /dev/null
+++ b/test/templates/react-rsc/react-server.js
@@ -0,0 +1,27 @@
+import { chunksServerVirtualModule } from "@vinxi/plugin-references/chunks";
+import { client } from "@vinxi/plugin-references/client";
+import { clientComponents } from "@vinxi/plugin-references/client-components";
+import { server } from "@vinxi/plugin-references/server";
+import { serverComponents } from "@vinxi/plugin-references/server-components";
+import { transformReferences } from "@vinxi/plugin-references/transform-references";
+import { fileURLToPath } from "url";
+
+export const references = {
+ serverPlugin: "#server-chunks",
+ serverPluginModule: chunksServerVirtualModule,
+ transformReferences,
+ clientRouterPlugin: client,
+ clientComponents,
+ serverComponents,
+ serverRouterPlugin: server,
+ serverRouter: () => ({
+ name: "server",
+ mode: "handler",
+ base: "/_server",
+ handler: fileURLToPath(new URL("./server.js", import.meta.url)),
+ build: {
+ target: "server",
+ plugins: () => [references.serverRouterPlugin()],
+ },
+ }),
+};
diff --git a/test/templates/react-rsc/tailwind.config.cjs b/test/templates/react-rsc/tailwind.config.cjs
new file mode 100644
index 00000000..e1e70f61
--- /dev/null
+++ b/test/templates/react-rsc/tailwind.config.cjs
@@ -0,0 +1,8 @@
+/** @type {import('tailwindcss').Config} */
+module.exports = {
+ content: ["./app/**/*.tsx", "./app/**/*.ts", "./app/**/*.js"],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+};
diff --git a/test/templates/react-rsc/tsconfig.json b/test/templates/react-rsc/tsconfig.json
new file mode 100644
index 00000000..f5570281
--- /dev/null
+++ b/test/templates/react-rsc/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "node",
+ "allowSyntheticDefaultImports": true,
+ "esModuleInterop": true,
+ "jsx": "react-jsx",
+ "allowJs": true,
+ "checkJs": true,
+ "noEmit": true,
+ "types": ["vinxi/client", "react/experimental"],
+ "isolatedModules": true
+ }
+}