From 05b5bbbc5e150ac41af5a0c46cc2bd93f584dc74 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 11 Jul 2024 14:05:21 +0200 Subject: [PATCH 1/5] docs: add a note about transport between the server and the runner --- docs/guide/api-environment.md | 71 ++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/docs/guide/api-environment.md b/docs/guide/api-environment.md index 87865674decd47..b132877e7f12e5 100644 --- a/docs/guide/api-environment.md +++ b/docs/guide/api-environment.md @@ -748,7 +748,7 @@ interface RunnerTransport { } ``` -Transport object that communicates with the environment via an RPC or by directly calling the function. By default, you need to pass an object with `fetchModule` method - it can use any type of RPC inside of it, but Vite also exposes `RemoteRunnerTransport` to make the configuration easier. You need to couple it with the `RemoteEnvironmentTransport` instance on the server like in this example where module runner is created in the worker thread: +Transport object that communicates with the environment via an RPC or by directly calling the function. By default, you need to pass an object with `fetchModule` method - it can use any type of RPC inside of it, but Vite also exposes bidirectional transport interface via a `RemoteRunnerTransport` class to make the configuration easier. You need to couple it with the `RemoteEnvironmentTransport` instance on the server like in this example where module runner is created in the worker thread: ::: code-group @@ -804,7 +804,74 @@ await createServer({ ::: -`RemoteRunnerTransport` and `RemoteEnvironmentTransport` are meant to be used together. If you don't use either of them, then you can define your own function to communicate between the runner and the server. +`RemoteRunnerTransport` and `RemoteEnvironmentTransport` are meant to be used together, but you don't have to use them at all. You can define your own function to communicate between the runner and the server. For example, if you connect to the environment via an HTTP request, you can call `fetch().json()` in `fetchModule` function: + +```ts +import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' + +const runner = new ModuleRunner( + { + root: fileURLToPath(new URL('./', import.meta.url)), + transport: { + async fetchModule(id, importer) { + const response = await fetch( + `http://my-vite-server/fetch?id=${id}&importer=${importer}`, + ) + return response.json() + }, + }, + }, + new ESModulesEvaluator(), +) + +await runner.import('/entry.js') +``` + +::: warning Acessing Module on the Server +We do not want to encourage communication between the server and the runner. One of the problems that were exposed with `vite.ssrLoadModule` is over-reliance on the server state inside the processed modules. This makes it harder to implement runtime-agnostic SSR since there might be no direct access to the server APIs that are available in Node.js. + +Instead, we recommend using virtual modules to import the state and process it inside the loaded environment module: + +```ts +import { runner } from './ssr-module-runner.js' +import { processRoutes } from './routes.js' + +const { routes } = await runner.import('ssr:routes') +processRoutes(routes) +``` + +You can also use virtual modules to load HTML: + +```ts {13-21} +function vitePluginVirtualIndexHtml(): Plugin { + let server: ViteDevServer | undefined + return { + name: vitePluginVirtualIndexHtml.name, + configureServer(server_) { + server = server_ + }, + resolveId(source, _importer, _options) { + return source === 'virtual:index-html' ? '\0' + source : undefined + }, + async load(id, _options) { + if (id === '\0' + 'virtual:index-html') { + let html: string + if (server) { + this.addWatchFile('index.html') + html = await fs.promises.readFile('index.html', 'utf-8') + html = await server.transformIndexHtml('/', html) + } else { + html = await fs.promises.readFile('dist/client/index.html', 'utf-8') + } + return `export default ${JSON.stringify(html)}` + } + return + }, + } +} +``` + +::: ## ModuleRunnerHMRConnection From bf69ad14d78ba44e0a75a33e54391f39481b4891 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 11 Jul 2024 14:08:12 +0200 Subject: [PATCH 2/5] chore: cleanup --- docs/guide/api-environment.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/guide/api-environment.md b/docs/guide/api-environment.md index b132877e7f12e5..41bc77f701475b 100644 --- a/docs/guide/api-environment.md +++ b/docs/guide/api-environment.md @@ -836,7 +836,7 @@ Instead, we recommend using virtual modules to import the state and process it i import { runner } from './ssr-module-runner.js' import { processRoutes } from './routes.js' -const { routes } = await runner.import('ssr:routes') +const { routes } = await runner.import('virtual:ssr-routes') processRoutes(routes) ``` From 36030cf230068dfb512f55555242772d85283a11 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 12 Jul 2024 18:39:55 +0200 Subject: [PATCH 3/5] docs: example of coupled state --- docs/guide/api-environment.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/guide/api-environment.md b/docs/guide/api-environment.md index 41bc77f701475b..b6142b1aa17663 100644 --- a/docs/guide/api-environment.md +++ b/docs/guide/api-environment.md @@ -828,13 +828,21 @@ await runner.import('/entry.js') ``` ::: warning Acessing Module on the Server -We do not want to encourage communication between the server and the runner. One of the problems that were exposed with `vite.ssrLoadModule` is over-reliance on the server state inside the processed modules. This makes it harder to implement runtime-agnostic SSR since there might be no direct access to the server APIs that are available in Node.js. +We do not want to encourage communication between the server and the runner. One of the problems that was exposed with `vite.ssrLoadModule` is over-reliance on the server state inside the processed modules. This makes it harder to implement runtime-agnostic SSR since user environment might have no access to server APIs. For example, this code assumes that Vite server and user code can run in the same context: -Instead, we recommend using virtual modules to import the state and process it inside the loaded environment module: +```ts +const vite = createServer() +const routes = collectRoutes() + +const { processRoutes } = await vite.ssrLoadModule('./routes-processor.js') +processRoutes(routes) +``` + +This makes it impossible to run user code in the same way it might run in production (for example, on the edge) because the server state and user state are coupled. So instead, we recommend using virtual modules to import the state and process it inside the user module: ```ts import { runner } from './ssr-module-runner.js' -import { processRoutes } from './routes.js' +import { processRoutes } from './routes-processor.js' const { routes } = await runner.import('virtual:ssr-routes') processRoutes(routes) From 22c012e82095faee23e1f87eda222cfedf41686d Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Fri, 12 Jul 2024 18:50:00 +0200 Subject: [PATCH 4/5] chore: cleanup --- docs/guide/api-environment.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/guide/api-environment.md b/docs/guide/api-environment.md index b6142b1aa17663..b6fdb48f6060a1 100644 --- a/docs/guide/api-environment.md +++ b/docs/guide/api-environment.md @@ -809,7 +809,7 @@ await createServer({ ```ts import { ESModulesEvaluator, ModuleRunner } from 'vite/module-runner' -const runner = new ModuleRunner( +export const runner = new ModuleRunner( { root: fileURLToPath(new URL('./', import.meta.url)), transport: { @@ -841,6 +841,8 @@ processRoutes(routes) This makes it impossible to run user code in the same way it might run in production (for example, on the edge) because the server state and user state are coupled. So instead, we recommend using virtual modules to import the state and process it inside the user module: ```ts +// this code runs on another machine or in another thread + import { runner } from './ssr-module-runner.js' import { processRoutes } from './routes-processor.js' From f1bf2f6723a8e0363715a0362336045ff35906b1 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Thu, 18 Jul 2024 11:24:38 +0200 Subject: [PATCH 5/5] docs: add more docs to the html in ssr --- docs/guide/api-environment.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/guide/api-environment.md b/docs/guide/api-environment.md index b6fdb48f6060a1..0f682b858fbf93 100644 --- a/docs/guide/api-environment.md +++ b/docs/guide/api-environment.md @@ -834,7 +834,7 @@ We do not want to encourage communication between the server and the runner. One const vite = createServer() const routes = collectRoutes() -const { processRoutes } = await vite.ssrLoadModule('./routes-processor.js') +const { processRoutes } = await vite.ssrLoadModule('internal:routes-processor') processRoutes(routes) ``` @@ -850,7 +850,7 @@ const { routes } = await runner.import('virtual:ssr-routes') processRoutes(routes) ``` -You can also use virtual modules to load HTML: +Simple setups like in [SSR Guide](/guide/ssr) can still use `server.transformIndexHtml` directly if it's not expected that the server will run in a different process in production. However, if the server will run in an edge environment or a separate process, we recommend creating a virtual module to load HTML: ```ts {13-21} function vitePluginVirtualIndexHtml(): Plugin { @@ -860,10 +860,10 @@ function vitePluginVirtualIndexHtml(): Plugin { configureServer(server_) { server = server_ }, - resolveId(source, _importer, _options) { + resolveId(source) { return source === 'virtual:index-html' ? '\0' + source : undefined }, - async load(id, _options) { + async load(id) { if (id === '\0' + 'virtual:index-html') { let html: string if (server) { @@ -881,6 +881,26 @@ function vitePluginVirtualIndexHtml(): Plugin { } ``` +Then in SSR entry point you can call `import('virtual:index-html')` to retrieve the processed HTML: + +```ts +import { render } from 'framework' + +// this example uses cloudflare syntax +export default { + async fetch() { + // during dev, it will return transformed HTML + // during build, it will bundle the basic index.html into a string + const { default: html } = await import('virtual:index-html') + return new Response(render(html), { + headers: { 'content-type': 'text/html' }, + }) + }, +} +``` + +This keeps the HTML processing server agnostic. + ::: ## ModuleRunnerHMRConnection