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 + } +}