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

wip: build browser and server in parallel #350

Draft
wants to merge 12 commits into
base: wip-defer-internal
Choose a base branch
from
3 changes: 2 additions & 1 deletion packages/react-server/src/features/assets/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,8 @@ export function vitePluginServerAssets({
// ensure hmr boundary since css module doesn't have `import.meta.hot.accept`
return code + `if (import.meta.hot) { import.meta.hot.accept() }`;
}
if (manager.buildType === "client") {
if (manager.buildType) {
await manager.buildSteps.closeBundleServer;
// TODO: probe manifest to collect css?
const files = await fs.promises.readdir("./dist/rsc/assets", {
withFileTypes: true,
Expand Down
4 changes: 2 additions & 2 deletions packages/react-server/src/features/router/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function routeManifestPluginServer({
name: "server-route-manifest",
apply: "build",
async buildEnd() {
if (manager.buildType === "rsc") {
if (manager.buildType === "parallel") {
const routeFiles = await FastGlob(
"./src/routes/**/(page|layout|error).(js|jsx|ts|tsx)",
);
Expand Down Expand Up @@ -53,7 +53,7 @@ export function routeManifestPluginClient({
name: routeManifestPluginClient.name + ":bundle",
apply: "build",
generateBundle(_options, bundle) {
if (manager.buildType === "client") {
if (manager.buildType === "parallel") {
const facadeModuleDeps: Record<string, AssetDeps> = {};
for (const [k, v] of Object.entries(bundle)) {
if (v.type === "chunk" && v.facadeModuleId) {
Expand Down
37 changes: 25 additions & 12 deletions packages/react-server/src/features/server-action/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
debounce,
tinyassert,
} from "@hiogawa/utils";
import { type Plugin, type PluginOption, parseAstAsync } from "vite";
import { type Plugin, parseAstAsync } from "vite";
import type { PluginStateManager } from "../../plugin";
import {
type CustomModuleMeta,
Expand Down Expand Up @@ -62,6 +62,13 @@ const $$proxy = (id, name) => createServerReference(id + "#" + name);
id,
outCode: output.toString(),
});
if (manager.buildType === "parallel") {
tinyassert(manager.buildContextServer);
manager.buildContextServer.emitFile({
type: "chunk",
id: id.replace(/^\0/, ""),
});
}
return {
code: output.toString(),
map: output.generateMap(),
Expand Down Expand Up @@ -92,7 +99,7 @@ export function vitePluginServerUseServer({
}: {
manager: PluginStateManager;
runtimePath: string;
}): PluginOption {
}): Plugin[] {
const transformPlugin: Plugin = {
name: vitePluginServerUseServer.name,
async transform(code, id, _options) {
Expand All @@ -114,7 +121,7 @@ export function vitePluginServerUseServer({
id,
outCode: output.toString(),
});
if (manager.buildType === "rsc") {
if (manager.buildType) {
this.emitFile({
type: "chunk",
id,
Expand All @@ -138,11 +145,12 @@ export function vitePluginServerUseServer({
const virtualPlugin = createVirtualPlugin(
"server-references",
async function () {
if (manager.buildType === "scan") {
return `export default {}`;
}
tinyassert(manager.buildType === "rsc");
await this.load({ id: "\0virtual:wait-for-idle" });
// if (manager.buildType === "scan") {
// return `export default {}`;
// }
tinyassert(manager.buildType === "parallel");
await manager.buildSteps.virtualClientReferenes;
console.log("[virtual:server-references]", manager.rscUseServerIds);
let result = `export default {\n`;
for (const id of manager.rscUseServerIds) {
result += `"${hashString(id)}": () => import("${id}"),\n`;
Expand All @@ -153,19 +161,24 @@ export function vitePluginServerUseServer({
},
);

return [transformPlugin, virtualPlugin, waitForIdlePlugin()];
return [transformPlugin, virtualPlugin];
}

// https://github.com/rollup/rollup/issues/4985#issuecomment-1936333388
// https://github.com/ArnaudBarre/downwind/blob/1d47b6a3f1e7bc271d0bb5bd96cfbbea68445510/src/vitePlugin.ts#L164
function waitForIdlePlugin(): Plugin[] {
export function waitForIdlePlugin(): Plugin[] {
const idlePromise = createManualPromise<void>();
let done = false;
const notIdle = debounce((...args) => {
console.log("[wait-for-idle:done]", { args });
if (0) {
if (done) {
console.log("[wait-for-idle:done-again]");
}
console.log("[wait-for-idle:done]", { args });
}
done = true;
idlePromise.resolve();
}, 200);
}, 1000);

return [
{
Expand Down
31 changes: 25 additions & 6 deletions packages/react-server/src/features/use-client/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
createVirtualPlugin,
hashString,
} from "../../plugin/utils";
import { waitForIdlePlugin } from "../server-action/plugin";

const debug = createDebug("react-server:plugin:use-client");

Expand Down Expand Up @@ -147,11 +148,19 @@ export function vitePluginServerUseClient({
`import { registerClientReference as $$proxy } from "${runtimePath}";\n`,
);
manager.rscUseClientIds.add(id);
if (manager.buildType === "scan") {
// to discover server references imported only by client
// we keep code as is and continue crawling
return;
if (manager.buildType === "parallel") {
tinyassert(manager.buildContextBrowser);
manager.buildContextBrowser.emitFile({
type: "chunk",
// unwrap virtual
id: id.replace(/^\0/, ""),
});
}
// if (manager.buildType === "scan") {
// // to discover server references imported only by client
// // we keep code as is and continue crawling
// return;
// }
return {
code: output.toString(),
map: output.generateMap(),
Expand All @@ -163,6 +172,7 @@ export function vitePluginServerUseClient({
};
},
};

return [useClientExternalPlugin, useClientPlugin];
}

Expand Down Expand Up @@ -224,6 +234,7 @@ export function vitePluginClientUseClient({

return [
devExternalPlugin,
...waitForIdlePlugin(),

/**
* emit client-references as dynamic import map
Expand All @@ -233,8 +244,16 @@ export function vitePluginClientUseClient({
* "some-file1": () => import("some-file1"),
* }
*/
createVirtualPlugin("client-references", () => {
tinyassert(manager.buildType === "client" || manager.buildType === "ssr");
createVirtualPlugin("client-references", async () => {
tinyassert(manager.buildType);
if (manager.buildType === "parallel") {
tinyassert(manager.buildContextBrowser);
await manager.buildContextBrowser.load({
id: "\0virtual:wait-for-idle",
});
console.log("[virtual:client-references]", manager.rscUseClientIds);
manager.buildSteps.virtualClientReferenes.resolve();
}
let result = `export default {\n`;
for (let id of manager.rscUseClientIds) {
// virtual module needs to be mapped back to the original form
Expand Down
69 changes: 59 additions & 10 deletions packages/react-server/src/plugin/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fileURLToPath } from "node:url";
import { createDebug, tinyassert } from "@hiogawa/utils";
import { createDebug, createManualPromise, tinyassert } from "@hiogawa/utils";
import {
type ConfigEnv,
type InlineConfig,
Expand Down Expand Up @@ -62,9 +62,16 @@ class PluginStateManager {
config!: ResolvedConfig;
configEnv!: ConfigEnv;

buildType?: "scan" | "rsc" | "client" | "ssr";
// buildType?: "scan" | "rsc" | "client" | "ssr";
buildType?: "parallel" | "ssr";
buildContextServer?: Rollup.PluginContext;
buildContextBrowser?: Rollup.PluginContext;
buildSteps = {
buildStartBrowser: createManualPromise<void>(),
buildStartServer: createManualPromise<void>(),
virtualClientReferenes: createManualPromise<void>(),
closeBundleServer: createManualPromise<void>(),
};

routeToClientReferences: Record<string, string[]> = {};
routeManifest?: RouteManifest;
Expand Down Expand Up @@ -183,6 +190,30 @@ export function vitePluginReactServer(options?: {
},
},

// TODO:
// ensure both `buildContextBrowser` and `buildContextServer`
// are ready during `buildStart`
{
name: "build-steps-server",
apply: "build",
buildStart: {
order: "pre",
async handler() {
if (manager.buildType === "parallel") {
manager.buildContextServer = this;
manager.buildSteps.buildStartServer.resolve();
await manager.buildSteps.buildStartBrowser;
}
},
},
closeBundle: {
order: "post",
async handler() {
manager.buildSteps.closeBundleServer.resolve();
},
},
},

...(options?.plugins ?? []),
],
build: {
Expand Down Expand Up @@ -310,15 +341,19 @@ export function vitePluginReactServer(options?: {
// manager.buildType = "scan";
// await build(reactServerViteConfig);

console.log("▶▶▶ REACT SERVER BUILD (server) [2/4]");
manager.buildType = "rsc";
await build(reactServerViteConfig);
console.log("▶▶▶ REACT SERVER BUILD (server, browser) [(1,2)/3]");
manager.buildType = "parallel";
await Promise.all([build(reactServerViteConfig), build()]);

// console.log("▶▶▶ REACT SERVER BUILD (server) [2/4]");
// manager.buildType = "rsc";
// await build(reactServerViteConfig);

console.log("▶▶▶ REACT SERVER BUILD (browser) [3/4]");
manager.buildType = "client";
await build();
// console.log("▶▶▶ REACT SERVER BUILD (browser) [3/4]");
// manager.buildType = "client";
// await build();

console.log("▶▶▶ REACT SERVER BUILD (ssr) [4/4]");
console.log("▶▶▶ REACT SERVER BUILD (ssr) [3/3]");
manager.buildType = "ssr";
}
},
Expand All @@ -328,6 +363,20 @@ export function vitePluginReactServer(options?: {
return [
rscParentPlugin,
buildOrchestrationPlugin,
{
name: "build-steps-browser",
apply: "build",
buildStart: {
order: "pre",
async handler() {
if (manager.buildType === "parallel") {
manager.buildContextBrowser = this;
manager.buildSteps.buildStartBrowser.resolve();
await manager.buildSteps.buildStartServer;
}
},
},
},
vitePluginSilenceDirectiveBuildWarning(),
vitePluginClientUseServer({
manager,
Expand All @@ -350,7 +399,7 @@ export function vitePluginReactServer(options?: {
`;
}
// build
if (manager.buildType === "client") {
if (manager.buildType === "parallel") {
// import "runtime-client" for preload
return /* js */ `
import "${SERVER_CSS_PROXY}";
Expand Down