Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add child process environment example #130

Merged
merged 26 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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