Skip to content

Commit

Permalink
feat: add child process environment example (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa authored Oct 6, 2024
1 parent a01d82b commit 9a756a1
Show file tree
Hide file tree
Showing 19 changed files with 565 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.1.29
- run: corepack enable
- run: pnpm i
- run: pnpm lint-check
Expand All @@ -31,6 +34,7 @@ jobs:
- run: pnpm -C examples/react-server test-e2e-preview
- run: pnpm -C examples/react-server cf-build
- run: pnpm -C examples/react-server test-e2e-cf-preview
- run: pnpm -C examples/child-process test-e2e
# vitest not working
# - run: pnpm -C examples/react-server test
- run: pnpm -C examples/vue-ssr test-e2e
Expand Down
14 changes: 14 additions & 0 deletions examples/child-process/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# child-process

Running module runner inside child process (e.g. node, bun) with `--conditions react-server`, which allows `react` to be externalized.

```sh
pnpm dev
```

## related

- https://github.com/netlify/netlify-vite-environment
- https://github.com/flarelabs-net/vite-environment-providers
- https://github.com/flarelabs-net/vite-plugin-cloudflare
- https://github.com/vitejs/vite/discussions/18191
6 changes: 6 additions & 0 deletions examples/child-process/e2e/basic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { test } from "@playwright/test";

test("basic", async ({ page }) => {
await page.goto("/");
await page.getByText("Bun.version").click();
});
18 changes: 18 additions & 0 deletions examples/child-process/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "@hiogawa/vite-environment-examples-child-process",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build --app",
"preview": "vite preview",
"test-e2e": "playwright test",
"test-e2e-preview": "E2E_PREVIEW=1 playwright test"
},
"devDependencies": {
"@types/bun": "^1.1.10"
},
"volta": {
"extends": "../../package.json"
}
}
28 changes: 28 additions & 0 deletions examples/child-process/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { defineConfig, devices } from "@playwright/test";

const port = Number(process.env["E2E_PORT"] || 6174);
const isPreview = Boolean(process.env["E2E_PREVIEW"]);
const command = isPreview
? `pnpm preview --port ${port} --strict-port`
: `pnpm dev --port ${port} --strict-port`;

export default defineConfig({
testDir: "e2e",
use: {
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: devices["Desktop Chrome"],
},
],
webServer: {
command,
port,
},
grepInvert: isPreview ? /@dev/ : /@build/,
forbidOnly: !!process.env["CI"],
retries: process.env["CI"] ? 2 : 0,
reporter: "list",
});
10 changes: 10 additions & 0 deletions examples/child-process/src/entry-browser.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import "./lib/polyfill-webpack";
import ReactDomClient from "react-dom/client";
import ReactClient from "react-server-dom-webpack/client.browser";

async function main() {
ReactDomClient;
ReactClient;
}

main();
27 changes: 27 additions & 0 deletions examples/child-process/src/entry-rsc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import "./lib/polyfill-webpack";
import ReactServer from "react-server-dom-webpack/server.edge";
import Page from "./routes/page";

export type StreamData = React.ReactNode;

export default function handler(request: Request) {
const url = new URL(request.url);
if (url.searchParams.has("crash-rsc-handler")) {
throw new Error("boom");
}
const root = (
<html>
<head></head>
<body>
<pre>url: {request.url}</pre>
<Page url={url} />
</body>
</html>
);
const stream = ReactServer.renderToReadableStream<StreamData>(root, {}, {});
return new Response(stream, {
headers: {
"content-type": "text/x-component;charset=utf-8",
},
});
}
55 changes: 55 additions & 0 deletions examples/child-process/src/entry-ssr.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import "./lib/polyfill-webpack";
import assert from "node:assert";
import React from "react";
import ReactDomServer from "react-dom/server.edge";
import ReactClient from "react-server-dom-webpack/client.edge";
import type { StreamData } from "./entry-rsc";
import type { ChildProcessFetchDevEnvironment } from "./lib/vite/environment";

export default async function handler(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.searchParams.has("crash-ssr-handler")) {
throw new Error("boom");
}

const response = await handleRsc(request);
if (!response.ok) {
return response;
}

if (url.searchParams.has("__f")) {
return response;
}

assert(response.body);
const [rscStream1, rscStream2] = response.body.tee();

const rscPromise = ReactClient.createFromReadableStream<StreamData>(
rscStream1,
{
ssrManifest: {},
},
);

function Root() {
return (
<>
<meta name="node-version" content={process.version} />
{React.use(rscPromise)}
</>
);
}

const ssrStream = await ReactDomServer.renderToReadableStream(<Root />, {
bootstrapModules: [],
});

rscStream2;
return new Response(ssrStream, { headers: { "content-type": "text/html" } });
}

declare const __vite_environment_rsc__: ChildProcessFetchDevEnvironment;

async function handleRsc(request: Request): Promise<Response> {
return __vite_environment_rsc__.dispatchFetch("/src/entry-rsc.tsx", request);
}
25 changes: 25 additions & 0 deletions examples/child-process/src/lib/ambient-react.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
declare module "react-dom/server.edge" {
export * from "react-dom/server";
}

declare module "react-server-dom-webpack/server.edge" {
export function renderToReadableStream<T>(
data: T,
bundlerConfig: unknown,
opitons?: unknown,
): ReadableStream<Uint8Array>;
}

declare module "react-server-dom-webpack/client.edge" {
export function createFromReadableStream<T>(
stream: ReadableStream<Uint8Array>,
options?: unknown,
): Promise<T>;
}

declare module "react-server-dom-webpack/client.browser" {
export function createFromReadableStream<T>(
stream: ReadableStream<Uint8Array>,
options?: unknown,
): Promise<T>;
}
3 changes: 3 additions & 0 deletions examples/child-process/src/lib/polyfill-webpack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Object.assign(globalThis, {
__webpack_require__: () => {},
});
60 changes: 60 additions & 0 deletions examples/child-process/src/lib/vite/bridge-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// @ts-check

import assert from "node:assert";
import { ESModulesEvaluator, ModuleRunner } from "vite/module-runner";

/**
* @param {import("./types").BridgeClientOptions} options
*/
export function createBridgeClient(options) {
/**
* @param {string} method
* @param {...any} args
* @returns {Promise<any>}
*/
async function rpc(method, ...args) {
const response = await fetch(options.bridgeUrl + "/rpc", {
method: "POST",
body: JSON.stringify({ method, args }),
});
assert(response.ok);
const result = response.json();
return result;
}

const runner = new ModuleRunner(
{
root: options.root,
sourcemapInterceptor: "prepareStackTrace",
transport: {
fetchModule: (...args) => rpc("fetchModule", ...args),
},
hmr: false,
},
new ESModulesEvaluator(),
);

// TODO: move this out
/**
* @param {Request} request
* @returns {Promise<Response>}
*/
async function handler(request) {
try {
const headers = request.headers;
// @ts-ignore
const meta = JSON.parse(headers.get("x-vite-meta"));
headers.delete("x-vite-meta");
const mod = await runner.import(meta.entry);
return mod.default(new Request(meta.url, { ...request, headers }));
} catch (e) {
console.error(e);
const message =
"[bridge client handler error]\n" +
(e instanceof Error ? `${e.stack ?? e.message}` : "");
return new Response(message, { status: 500 });
}
}

return { runner, rpc, handler };
}
Loading

0 comments on commit 9a756a1

Please sign in to comment.