Skip to content

Commit

Permalink
refactor: ChildProcessFetchDevEnvironment
Browse files Browse the repository at this point in the history
  • Loading branch information
hi-ogawa committed Oct 4, 2024
1 parent f6f9625 commit a6a5fc4
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 121 deletions.
8 changes: 4 additions & 4 deletions examples/fetch/src/entry-ssr.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from "react";
import ReactDomServer from "react-dom/server.edge";
import ReactClient from "react-server-dom-webpack/client.edge";
import type { ViteDevServer } from "vite";
import type { ChildProcessFetchDevEnvironment } from "../vite.config";
import type { StreamData } from "./entry-rsc";

export default async function handler(request: Request): Promise<Response> {
Expand Down Expand Up @@ -38,8 +39,7 @@ export default async function handler(request: Request): Promise<Response> {
declare const __vite_server__: ViteDevServer;

async function handleRsc(request: Request): Promise<Response> {
return (__vite_server__.environments["rsc"] as any).dispatchFetch(
"/src/entry-rsc.tsx",
request,
);
return (
__vite_server__.environments["rsc"] as ChildProcessFetchDevEnvironment
).dispatchFetch("/src/entry-rsc.tsx", request);
}
240 changes: 123 additions & 117 deletions examples/fetch/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,143 +37,149 @@ export default defineConfig((_env) => ({
],
environments: {
rsc: {
// TODO: avoid mixed condition for ssr/rsc envs https://github.com/vitejs/vite/issues/18222
// TODO: for now, we need this to avoid mixed condition for ssr/rsc envs
// https://github.com/vitejs/vite/issues/18222
webCompatible: true,
resolve: {
externalConditions: ["react-server"],
},
dev: {
// TODO: refactor to factory
createEnvironment(name, config, _context) {
// const command = [
// "bun",
// "run",
// "--conditions",
// "react-server",
// join(import.meta.dirname, "./src/lib/vite/runtime/bun.js"),
// ];
const command = [
"node",
"bun",
"run",
"--conditions",
"react-server",
join(import.meta.dirname, "./src/lib/vite/runtime/node.js"),
join(import.meta.dirname, "./src/lib/vite/runtime/bun.js"),
];
// const command = [
// "node",
// "--conditions",
// "react-server",
// join(import.meta.dirname, "./src/lib/vite/runtime/node.js"),
// ];
return new ChildProcessFetchDevEnvironment(
{ command },
name,
config,
{
// TODO
hot: false,
},
);
},
},
},
},
}));

// TODO
// can we abstract away child process? FetchBridgeDevEnvironment?
// multiple children per env (like Vitest)? need different API?
class ChildProcessFetchDevEnvironment extends DevEnvironment {
public bridge!: http.Server;
public bridgeUrl!: string;
public child!: childProcess.ChildProcess;
public childUrl!: string;
public childUrlPromise!: PromiseWithResolvers<string>;
// TODO
// can we abstract away child process? FetchBridgeDevEnvironment?
// multiple children per env (like Vitest)? need different API?
export class ChildProcessFetchDevEnvironment extends DevEnvironment {
public bridge!: http.Server;
public bridgeUrl!: string;
public child!: childProcess.ChildProcess;
public childUrl!: string;
public childUrlPromise!: PromiseWithResolvers<string>;

override init: DevEnvironment["init"] = async (...args) => {
await super.init(...args);
constructor(
public extraOptions: { command: string[] },
...args: ConstructorParameters<typeof DevEnvironment>
) {
super(...args);
}

const listener = webToNodeHandler(async (request) => {
const url = new URL(request.url);
// TODO: other than json?
if (url.pathname === "/rpc") {
const { method, args } = await request.json();
assert(method in this);
const result = await (this as any)[method]!(...args);
return Response.json(result);
}
return undefined;
});
override init: DevEnvironment["init"] = async (...args) => {
await super.init(...args);

const bridge = http.createServer((req, res) => {
listener(req, res, (e) => {
console.error(e);
res.statusCode = 500;
res.end("Internal server error");
});
});
this.bridge = bridge;
const listener = webToNodeHandler(async (request) => {
const url = new URL(request.url);
// TODO: other than json?
if (url.pathname === "/rpc") {
const { method, args } = await request.json();
assert(method in this);
const result = await (this as any)[method]!(...args);
return Response.json(result);
}
return undefined;
});

await new Promise<void>((resolve, reject) => {
bridge.listen(() => {
const address = bridge.address();
assert(address && typeof address !== "string");
this.bridgeUrl = `http://localhost:${address.port}`;
resolve();
});
bridge.on("error", (e) => {
console.error(e);
reject(e);
});
});
const bridge = http.createServer((req, res) => {
listener(req, res, (e) => {
console.error(e);
res.statusCode = 500;
res.end("Internal server error");
});
});
this.bridge = bridge;

// TODO: separate child process concern?
this.childUrlPromise = PromiseWithReoslvers();
const child = childProcess.spawn(
command[0]!,
[
...command.slice(1),
JSON.stringify({
bridgeUrl: this.bridgeUrl,
root: this.config.root,
}),
],
{
stdio: ["ignore", "inherit", "inherit"],
},
);
this.child = child;
await new Promise<void>((resolve, reject) => {
child.on("spawn", () => {
resolve();
});
child.on("error", (e) => {
reject(e);
});
});
this.childUrl = await this.childUrlPromise.promise;
console.log("[environment.init]", {
bridgeUrl: this.bridgeUrl,
childUrl: this.childUrl,
});
};
await new Promise<void>((resolve, reject) => {
bridge.listen(() => {
const address = bridge.address();
assert(address && typeof address !== "string");
this.bridgeUrl = `http://localhost:${address.port}`;
resolve();
});
bridge.on("error", (e) => {
console.error(e);
reject(e);
});
});

override close: DevEnvironment["close"] = async (...args) => {
await super.close(...args);
this.child?.kill();
this.bridge?.close();
};
// TODO: separate child process concern?
this.childUrlPromise = PromiseWithReoslvers();
const command = this.extraOptions.command;
const child = childProcess.spawn(
command[0]!,
[
...command.slice(1),
JSON.stringify({
bridgeUrl: this.bridgeUrl,
root: this.config.root,
}),
],
{
stdio: ["ignore", "inherit", "inherit"],
},
);
this.child = child;
await new Promise<void>((resolve, reject) => {
child.on("spawn", () => {
resolve();
});
child.on("error", (e) => {
reject(e);
});
});
this.childUrl = await this.childUrlPromise.promise;
console.log("[environment.init]", {
bridgeUrl: this.bridgeUrl,
childUrl: this.childUrl,
});
};

async dispatchFetch(
entry: string,
request: Request,
): Promise<Response> {
const headers = new Headers(request.headers);
headers.set(
"x-vite-meta",
JSON.stringify({ entry, url: request.url }),
);
const url = new URL(request.url);
const childUrl = new URL(this.childUrl);
url.host = childUrl.host;
return fetch(new Request(url, { ...request, headers }));
}
override close: DevEnvironment["close"] = async (...args) => {
await super.close(...args);
this.child?.kill();
this.bridge?.close();
};

/** @internal rpc for runner */
async register(childUrl: string) {
this.childUrlPromise.resolve(childUrl);
return true;
}
}
async dispatchFetch(entry: string, request: Request): Promise<Response> {
const headers = new Headers(request.headers);
headers.set("x-vite-meta", JSON.stringify({ entry, url: request.url }));
const url = new URL(request.url);
const childUrl = new URL(this.childUrl);
url.host = childUrl.host;
return fetch(new Request(url, { ...request, headers }));
}

return new ChildProcessFetchDevEnvironment(name, config, {
// TODO
hot: false,
});
},
},
},
},
}));
/** @internal rpc for runner */
async register(childUrl: string) {
this.childUrlPromise.resolve(childUrl);
return true;
}
}

function PromiseWithReoslvers<T>(): PromiseWithResolvers<T> {
let resolve: any;
Expand Down

0 comments on commit a6a5fc4

Please sign in to comment.