diff --git a/.changeset/stale-badgers-cover.md b/.changeset/stale-badgers-cover.md new file mode 100644 index 00000000..d85c8b6f --- /dev/null +++ b/.changeset/stale-badgers-cover.md @@ -0,0 +1,7 @@ +--- +"@vinxi/react": patch +"@vinxi/solid": patch +"vinxi": patch +--- + +added `import()` helpers to manifest diff --git a/.gitignore b/.gitignore index bfd6e13c..84f7cb66 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ dist docs/.vitepress/dist docs/.vitepress/cache .vitepress +tmp diff --git a/examples/react/rsc/spa/app/client.tsx b/examples/react/rsc/spa/app/client.tsx index d8b1be8e..ba2063da 100644 --- a/examples/react/rsc/spa/app/client.tsx +++ b/examples/react/rsc/spa/app/client.tsx @@ -18,10 +18,7 @@ document.addEventListener("click", async (e) => { globalThis.__vite__ = createModuleLoader({ loadModule: async (id) => { - return import( - /* @vite-ignore */ import.meta.env.MANIFEST["client"].chunks[id].output - .path - ); + return import.meta.env.MANIFEST["client"].chunks[id].import(); }, }); diff --git a/examples/react/rsc/spa/app/react-server.tsx b/examples/react/rsc/spa/app/react-server.tsx index bb322a22..3eb42849 100644 --- a/examples/react/rsc/spa/app/react-server.tsx +++ b/examples/react/rsc/spa/app/react-server.tsx @@ -1,23 +1,14 @@ import { renderAsset } from "@vinxi/react"; import { renderToPipeableStream } from "@vinxi/react-server-dom/server"; import { Suspense } from "react"; -import { eventHandler } from "vinxi/server"; +import { eventHandler, setHeaders } from "vinxi/server"; import App from "./app"; export default eventHandler(async (event) => { - async function loadModule(id) { - if (import.meta.env.DEV) { - return await import( - import.meta.env.MANIFEST["rsc"].chunks[id].output.path - ); - } + const reactServerManifest = import.meta.env.MANIFEST["rsc"]; + const clientManifest = import.meta.env.MANIFEST["client"]; - 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, @@ -25,11 +16,13 @@ export default eventHandler(async (event) => { decodeReplyFromBusboy, decodeAction, } = await import("@vinxi/react-server-dom/server"); - const serverReference = event.node.req.headers["server-action"]; + const serverReference = event.headers.get("server-action"); if (serverReference) { // This is the client-side case const [filepath, name] = serverReference.split("#"); - const action = (await loadModule(filepath))[name]; + const action = (await reactServerManifest.chunks[filepath].import())[ + 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. @@ -69,14 +62,13 @@ export default eventHandler(async (event) => { throw new Error("Invalid request"); } } - 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( { />, ); - // @ts-ignore - stream._read = () => {}; - // @ts-ignore - stream.on = (event, listener) => { - events[event] = listener; - }; + // // @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"); + setHeaders(event, { + "Content-Type": "text/x-component", + Router: "rsc", + }); return stream; }); diff --git a/examples/react/rsc/spa/app/server-action.tsx b/examples/react/rsc/spa/app/server-action.tsx index a673a712..f10927de 100644 --- a/examples/react/rsc/spa/app/server-action.tsx +++ b/examples/react/rsc/spa/app/server-action.tsx @@ -1,35 +1,20 @@ -// 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"; +import { eventHandler, readRawBody, setHeader } from "vinxi/server"; export default eventHandler(async (event) => { - async function loadModule(id) { - if (import.meta.env.DEV) { - return await import( - import.meta.env.MANIFEST["rsc"].chunks[id].output.path - ); - } - - 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") { + if (event.method === "POST") { const { renderToPipeableStream, decodeReply, decodeReplyFromBusboy, decodeAction, } = await import("@vinxi/react-server-dom/server"); - const serverReference = event.node.req.headers["server-action"]; + const serverReference = event.headers.get("server-action"); if (serverReference) { // This is the client-side case const [filepath, name] = serverReference.split("#"); - const action = (await loadModule(filepath))[name]; + const action = ( + await import.meta.env.MANIFEST["server"].chunks[filepath].import() + )[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. @@ -45,16 +30,7 @@ export default eventHandler(async (event) => { // 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("")); - }); - }); + const text = await readRawBody(event); console.log(text); args = await decodeReply(text); @@ -74,11 +50,12 @@ export default eventHandler(async (event) => { events[event] = listener; }; - event.node.res.setHeader("Content-Type", "application/json"); - event.node.res.setHeader("Router", "server"); + setHeader(event, "Content-Type", "application/json"); + setHeader(event, "Router", "server"); return stream; } catch (x) { + console.error(x); // We handle the error on the client } // Refresh the client and return the value @@ -87,35 +64,4 @@ export default eventHandler(async (event) => { 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/examples/react/rsc/spa/tmp/count b/examples/react/rsc/spa/tmp/count index bf0d87ab..301160a9 100644 --- a/examples/react/rsc/spa/tmp/count +++ b/examples/react/rsc/spa/tmp/count @@ -1 +1 @@ -4 \ No newline at end of file +8 \ No newline at end of file diff --git a/packages/vinxi-react/lazy-route.jsx b/packages/vinxi-react/lazy-route.jsx index c779b7a7..5d18b72d 100644 --- a/packages/vinxi-react/lazy-route.jsx +++ b/packages/vinxi-react/lazy-route.jsx @@ -22,9 +22,7 @@ export default function lazyRoute( if (import.meta.env.DEV) { let manifest = import.meta.env.SSR ? serverManifest : clientManifest; - const mod = await import( - /* @vite-ignore */ manifest.inputs[component.src].output.path - ); + const mod = await manifest.inputs[component.src].import(); invariant( mod[exported], `Module ${component.src} does not export ${exported}`, diff --git a/packages/vinxi-solid/lazy-route.jsx b/packages/vinxi-solid/lazy-route.jsx index 2bfca5fd..5b9af6ba 100644 --- a/packages/vinxi-solid/lazy-route.jsx +++ b/packages/vinxi-solid/lazy-route.jsx @@ -17,9 +17,7 @@ export default function lazyRoute( if (import.meta.env.DEV) { let manifest = import.meta.env.SSR ? serverManifest : clientManifest; - const mod = await import( - /* @vite-ignore */ manifest.inputs[component.src].output.path - ); + const mod = await manifest.inputs[component.src].import(); invariant( mod[exported], `Module ${component.src} does not export ${exported}`, diff --git a/packages/vinxi/lib/dev-server.js b/packages/vinxi/lib/dev-server.js index 327be535..e8d04e90 100644 --- a/packages/vinxi/lib/dev-server.js +++ b/packages/vinxi/lib/dev-server.js @@ -1,6 +1,9 @@ import { inspect } from "@vinxi/devtools"; +import { fileURLToPath } from "node:url"; + import { consola, withLogger } from "./logger.js"; +import { normalize } from "./path.js"; export * from "./router-dev-plugins.js"; @@ -65,6 +68,9 @@ export async function createViteHandler(router, app, serveConfig) { router, app, server: { + fs: { + allow: [normalize(fileURLToPath(new URL("../", import.meta.url))), "."], + }, middlewareMode: true, hmr: { port, diff --git a/packages/vinxi/lib/manifest/client-manifest.js b/packages/vinxi/lib/manifest/client-manifest.js index 127d231f..cfc1c163 100644 --- a/packages/vinxi/lib/manifest/client-manifest.js +++ b/packages/vinxi/lib/manifest/client-manifest.js @@ -25,6 +25,9 @@ const manifest = new Proxy( ? join(import.meta.env.BASE_URL, "@fs", chunk) : join(import.meta.env.BASE_URL, chunk + ".js"); return { + import() { + return import(/* @vite-ignore */ outputPath); + }, output: { path: outputPath, }, @@ -42,6 +45,9 @@ const manifest = new Proxy( ? join(import.meta.env.BASE_URL, "@fs", input) : window.manifest[input].output; return { + async import() { + return import(/* @vite-ignore */ outputPath); + }, async assets() { if (import.meta.env.DEV) { const assetsPath = diff --git a/packages/vinxi/lib/manifest/dev-server-manifest.js b/packages/vinxi/lib/manifest/dev-server-manifest.js index 788f673a..c224eba8 100644 --- a/packages/vinxi/lib/manifest/dev-server-manifest.js +++ b/packages/vinxi/lib/manifest/dev-server-manifest.js @@ -61,7 +61,6 @@ export function createDevManifest(app) { "No manifest for static router", ); - let relativePath = relative(app.config.root, chunk); if (router.target === "browser") { return { output: { @@ -70,6 +69,11 @@ export function createDevManifest(app) { }; } else { return { + import() { + return router.internals.devServer?.ssrLoadModule( + /* @vite-ignore */ absolutePath, + ); + }, output: { path: join(absolutePath), }, @@ -146,6 +150,11 @@ export function createDevManifest(app) { if (router.target === "browser") { return { + import() { + return router.internals.devServer?.ssrLoadModule( + /* @vite-ignore */ join(absolutePath), + ); + }, async assets() { return [ ...(viteServer @@ -186,6 +195,11 @@ export function createDevManifest(app) { }; } else { return { + import() { + return router.internals.devServer?.ssrLoadModule( + /* @vite-ignore */ join(absolutePath), + ); + }, async assets() { return [ ...(viteServer @@ -208,7 +222,7 @@ export function createDevManifest(app) { ]; }, output: { - path: join(router.base, "@fs", absolutePath), + path: absolutePath, }, }; } diff --git a/packages/vinxi/lib/manifest/prod-server-manifest.js b/packages/vinxi/lib/manifest/prod-server-manifest.js index d11b3eec..7b464819 100644 --- a/packages/vinxi/lib/manifest/prod-server-manifest.js +++ b/packages/vinxi/lib/manifest/prod-server-manifest.js @@ -56,9 +56,20 @@ export function createProdManifest(app) { { get(target, chunk) { invariant(typeof chunk === "string", "Chunk expected"); + const chunkPath = join( + router.outDir, + router.base, + chunk + ".js", + ); return { + import() { + if (globalThis.$$chunks[chunk + ".js"]) { + return globalThis.$$chunks[chunk + ".js"]; + } + return import(/* @vite-ignore */ chunkPath); + }, output: { - path: join(router.outDir, router.base, chunk + ".js"), + path: chunkPath, }, }; }, @@ -115,6 +126,14 @@ export function createProdManifest(app) { ? virtualId(handlerModule(router)) : input; return { + import() { + return import( + /* @vite-ignore */ join( + router.base, + bundlerManifest[id].file, + ) + ); + }, assets() { return findAssetsInViteManifest(bundlerManifest, id) .filter((asset) => asset.endsWith(".css")) diff --git a/packages/vinxi/types/manifest.d.ts b/packages/vinxi/types/manifest.d.ts index 8bba87fd..e567fcd2 100644 --- a/packages/vinxi/types/manifest.d.ts +++ b/packages/vinxi/types/manifest.d.ts @@ -30,6 +30,7 @@ export type Manifest = { chunks: { [key: string]: { assets(): Promise; + import(): Promise; output: { path: string; }; diff --git a/test/templates/react-rsc/app/client.tsx b/test/templates/react-rsc/app/client.tsx index 0e2f9a49..2edbf6d0 100644 --- a/test/templates/react-rsc/app/client.tsx +++ b/test/templates/react-rsc/app/client.tsx @@ -7,10 +7,7 @@ import { ServerComponent } from "./server-component"; globalThis.__vite__ = createModuleLoader({ loadModule: async (id) => { - return import( - /* @vite-ignore */ import.meta.env.MANIFEST["client"].chunks[id].output - .path - ); + return import.meta.env.MANIFEST["client"].chunks[id].import(); }, }); diff --git a/test/templates/react-rsc/app/react-server.tsx b/test/templates/react-rsc/app/react-server.tsx index 8c1fe094..a5eea5f1 100644 --- a/test/templates/react-rsc/app/react-server.tsx +++ b/test/templates/react-rsc/app/react-server.tsx @@ -1,30 +1,25 @@ import { renderAsset } from "@vinxi/react"; import { renderToPipeableStream } from "@vinxi/react-server-dom/server"; import { Suspense } from "react"; -import { eventHandler } from "vinxi/server"; +import { eventHandler, readRawBody, setHeader } from "vinxi/server"; import App from "./app"; export default eventHandler(async (event) => { - async function loadModule(id) { - if (import.meta.env.DEV) { - return await import( - import.meta.env.MANIFEST["rsc"].chunks[id].output.path - ); - } - - if (globalThis.$$chunks[id + ".js"]) { - return globalThis.$$chunks[id + ".js"]; - } - return await import(import.meta.env.MANIFEST["rsc"].chunks[id].output.path); - } + const reactServerManifest = import.meta.env.MANIFEST["rsc"]; + const clientManifest = import.meta.env.MANIFEST["client"]; if (event.method === "POST") { - const { decodeReply } = await import("@vinxi/react-server-dom/server"); + const { + renderToPipeableStream, + decodeReply, + decodeReplyFromBusboy, + decodeAction, + } = await import("@vinxi/react-server-dom/server"); const serverReference = event.headers.get("server-action"); if (serverReference) { // This is the client-side case - const [filepath, name] = serverReference.split("#"); - const action = (await loadModule(filepath))[name]; + const [chunk, name] = serverReference.split("#"); + const action = (await reactServerManifest.chunks[chunk].import())[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. @@ -33,18 +28,16 @@ export default eventHandler(async (event) => { } let args; - const text = await new Promise((resolve) => { - const requestBody = []; - event.node.req.on("data", (chunks) => { - requestBody.push(chunks); - }); - event.node.req.on("end", () => { - resolve(requestBody.join("")); - }); - }); + // 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 readRawBody(event); args = await decodeReply(text); - // } const result = action.apply(null, args); try { // Wait for any mutations @@ -58,11 +51,9 @@ export default eventHandler(async (event) => { throw new Error("Invalid request"); } } - 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 stream = renderToPipeableStream( @@ -76,7 +67,8 @@ export default eventHandler(async (event) => { />, ); - event.node.res.setHeader("Content-Type", "text/x-component"); - event.node.res.setHeader("Router", "rsc"); + setHeader(event, "Content-Type", "text/x-component"); + setHeader(event, "Router", "rsc"); + return stream; }); diff --git a/test/templates/react-rsc/app/server-action.tsx b/test/templates/react-rsc/app/server-action.tsx index e9d54aaa..30d7d29b 100644 --- a/test/templates/react-rsc/app/server-action.tsx +++ b/test/templates/react-rsc/app/server-action.tsx @@ -1,35 +1,20 @@ -// 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"; +import { eventHandler, readRawBody, setHeader } from "vinxi/server"; export default eventHandler(async (event) => { - async function loadModule(id) { - if (import.meta.env.DEV) { - return await import( - import.meta.env.MANIFEST["rsc"].chunks[id].output.path - ); - } - - 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") { + if (event.method === "POST") { const { renderToPipeableStream, decodeReply, decodeReplyFromBusboy, decodeAction, } = await import("@vinxi/react-server-dom/server"); - const serverReference = event.node.req.headers["server-action"]; + const serverReference = event.headers.get("server-action"); if (serverReference) { // This is the client-side case const [filepath, name] = serverReference.split("#"); - const action = (await loadModule(filepath))[name]; + const action = ( + await import.meta.env.MANIFEST["server"].chunks[filepath].import() + )[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. @@ -38,17 +23,19 @@ export default eventHandler(async (event) => { } let args; - const text = await new Promise((resolve) => { - const requestBody = []; - event.node.req.on("data", (chunks) => { - requestBody.push(chunks); - }); - event.node.req.on("end", () => { - resolve(requestBody.join("")); - }); - }); + // 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 readRawBody(event); + console.log(text); args = await decodeReply(text); + console.log(args, action); + // } const result = action.apply(null, args); try { // Wait for any mutations @@ -56,8 +43,15 @@ export default eventHandler(async (event) => { const events = {}; const stream = renderToPipeableStream(result); - event.node.res.setHeader("Content-Type", "application/json"); - event.node.res.setHeader("Router", "server"); + // @ts-ignore + stream._read = () => {}; + // @ts-ignore + stream.on = (event, listener) => { + events[event] = listener; + }; + + setHeader(event, "Content-Type", "application/json"); + setHeader(event, "Router", "server"); return stream; } catch (x) {