diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index df27a01cb..31b7271d2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -61,7 +61,11 @@ jobs:
- run: pnpm -C packages/react-server/examples/prerender test-e2e-preview
- run: pnpm -C packages/react-server/examples/prerender cf-build
- run: pnpm -C packages/react-server/examples/prerender vc-build
-
+ - run: pnpm -C packages/react-server/examples/custom-out-dir test-e2e
+ - run: pnpm -C packages/react-server/examples/custom-out-dir build
+ - run: pnpm -C packages/react-server/examples/custom-out-dir test-e2e-preview
+ - run: pnpm -C packages/react-server/examples/custom-out-dir cf-test-e2e-preview
+
test-react-server-package:
runs-on: ubuntu-latest
steps:
diff --git a/packages/react-server-next/src/vite/adapters/cloudflare/build.ts b/packages/react-server-next/src/vite/adapters/cloudflare/build.ts
index ddfa383b2..30ed7fad7 100644
--- a/packages/react-server-next/src/vite/adapters/cloudflare/build.ts
+++ b/packages/react-server-next/src/vite/adapters/cloudflare/build.ts
@@ -3,23 +3,23 @@ import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import type { PrerenderManifest } from "@hiogawa/react-server/plugin";
-export async function build() {
- const buildDir = join(process.cwd(), "dist");
- const outDir = join(process.cwd(), "dist/cloudflare");
+export async function build({ outDir }: { outDir: string }) {
+ const buildDir = join(process.cwd(), outDir);
+ const adapterOutDir = join(process.cwd(), outDir, "cloudflare");
// clean
- await rm(outDir, { recursive: true, force: true });
- await mkdir(outDir, { recursive: true });
+ await rm(adapterOutDir, { recursive: true, force: true });
+ await mkdir(adapterOutDir, { recursive: true });
// assets
- await cp(join(buildDir, "client"), outDir, {
+ await cp(join(buildDir, "client"), adapterOutDir, {
recursive: true,
});
// worker routes
// https://developers.cloudflare.com/pages/functions/routing/#create-a-_routesjson-file
await writeFile(
- join(outDir, "_routes.json"),
+ join(adapterOutDir, "_routes.json"),
JSON.stringify(
{
version: 1,
@@ -39,7 +39,7 @@ export async function build() {
// headers
// https://developers.cloudflare.com/pages/configuration/headers/
await writeFile(
- join(outDir, "_headers"),
+ join(adapterOutDir, "_headers"),
`\
/favicon.ico
Cache-Control: public, max-age=3600, s-maxage=3600
@@ -53,7 +53,7 @@ export async function build() {
const esbuild = await import("esbuild");
const result = await esbuild.build({
entryPoints: [join(buildDir, "server/index.js")],
- outfile: join(outDir, "_worker.js"),
+ outfile: join(adapterOutDir, "_worker.js"),
bundle: true,
minify: true,
metafile: true,
diff --git a/packages/react-server-next/src/vite/adapters/index.ts b/packages/react-server-next/src/vite/adapters/index.ts
index eddd28ae3..1987323bb 100644
--- a/packages/react-server-next/src/vite/adapters/index.ts
+++ b/packages/react-server-next/src/vite/adapters/index.ts
@@ -4,6 +4,7 @@ export type AdapterType = "node" | "vercel" | "vercel-edge" | "cloudflare";
export function adapterPlugin(options: {
adapter?: AdapterType;
+ outDir: string;
}): Plugin[] {
const adapter = options.adapter ?? autoSelectAdapter();
if (adapter === "node") {
@@ -31,15 +32,15 @@ export function adapterPlugin(options: {
console.log(`▶▶▶ ADAPTER: ${adapter}`);
if (adapter === "cloudflare") {
const { build } = await import("./cloudflare/build");
- await build();
+ await build({ outDir: options.outDir });
}
if (adapter === "vercel") {
const { build } = await import("./vercel/build");
- await build({ runtime: "node" });
+ await build({ runtime: "node", outDir: options.outDir });
}
if (adapter === "vercel-edge") {
const { build } = await import("./vercel/build");
- await build({ runtime: "edge" });
+ await build({ runtime: "edge", outDir: options.outDir });
}
},
},
diff --git a/packages/react-server-next/src/vite/adapters/vercel/build.ts b/packages/react-server-next/src/vite/adapters/vercel/build.ts
index 8c02c946b..bdabeb35c 100644
--- a/packages/react-server-next/src/vite/adapters/vercel/build.ts
+++ b/packages/react-server-next/src/vite/adapters/vercel/build.ts
@@ -38,13 +38,19 @@ const vcConfig = {
type VercelRuntime = "node" | "edge";
-export async function build({ runtime }: { runtime: VercelRuntime }) {
- const buildDir = join(process.cwd(), "dist");
- const outDir = join(process.cwd(), ".vercel/output");
+export async function build({
+ runtime,
+ outDir,
+}: {
+ runtime: VercelRuntime;
+ outDir: string;
+}) {
+ const buildDir = join(process.cwd(), outDir);
+ const adapterOutDir = join(process.cwd(), ".vercel/output");
// clean
- await rm(outDir, { recursive: true, force: true });
- await mkdir(outDir, { recursive: true });
+ await rm(adapterOutDir, { recursive: true, force: true });
+ await mkdir(adapterOutDir, { recursive: true });
// overrides for ssg html
// https://vercel.com/docs/build-output-api/v3/configuration#overrides
@@ -69,20 +75,20 @@ export async function build({ runtime }: { runtime: VercelRuntime }) {
// config
await writeFile(
- join(outDir, "config.json"),
+ join(adapterOutDir, "config.json"),
JSON.stringify(configJson, null, 2),
);
// static
- await mkdir(join(outDir, "static"), { recursive: true });
- await cp(join(buildDir, "client"), join(outDir, "static"), {
+ await mkdir(join(adapterOutDir, "static"), { recursive: true });
+ await cp(join(buildDir, "client"), join(adapterOutDir, "static"), {
recursive: true,
});
// function config
- await mkdir(join(outDir, "functions/index.func"), { recursive: true });
+ await mkdir(join(adapterOutDir, "functions/index.func"), { recursive: true });
await writeFile(
- join(outDir, "functions/index.func/.vc-config.json"),
+ join(adapterOutDir, "functions/index.func/.vc-config.json"),
JSON.stringify(vcConfig[runtime], null, 2),
);
@@ -90,7 +96,7 @@ export async function build({ runtime }: { runtime: VercelRuntime }) {
const esbuild = await import("esbuild");
const result = await esbuild.build({
entryPoints: [join(buildDir, "server/index.js")],
- outfile: join(outDir, "functions/index.func/index.mjs"),
+ outfile: join(adapterOutDir, "functions/index.func/index.mjs"),
metafile: true,
bundle: true,
minify: true,
diff --git a/packages/react-server-next/src/vite/index.ts b/packages/react-server-next/src/vite/index.ts
index f4d9f0c52..b3f08c623 100644
--- a/packages/react-server-next/src/vite/index.ts
+++ b/packages/react-server-next/src/vite/index.ts
@@ -20,6 +20,7 @@ export type ReactServerNextPluginOptions = {
export default function vitePluginReactServerNext(
options?: ReactServerPluginOptions & ReactServerNextPluginOptions,
): PluginOption {
+ const outDir = options?.outDir ?? "dist";
return [
react(),
nextJsxPlugin(),
@@ -32,9 +33,9 @@ export default function vitePluginReactServerNext(
vitePluginLogger(),
vitePluginSsrMiddleware({
entry: "next/vite/entry-ssr",
- preview: path.resolve("./dist/server/index.js"),
+ preview: path.resolve(outDir, "server", "index.js"),
}),
- adapterPlugin({ adapter: options?.adapter }),
+ adapterPlugin({ adapter: options?.adapter, outDir }),
appFaviconPlugin(),
{
name: "next-exclude-optimize",
diff --git a/packages/react-server/examples/custom-out-dir/.gitignore b/packages/react-server/examples/custom-out-dir/.gitignore
new file mode 100644
index 000000000..b8a118880
--- /dev/null
+++ b/packages/react-server/examples/custom-out-dir/.gitignore
@@ -0,0 +1,4 @@
+custom-out-dir/
+.vercel/
+.wrangler/
+dist-*/
\ No newline at end of file
diff --git a/packages/react-server/examples/custom-out-dir/app/page.tsx b/packages/react-server/examples/custom-out-dir/app/page.tsx
new file mode 100644
index 000000000..34cc3da60
--- /dev/null
+++ b/packages/react-server/examples/custom-out-dir/app/page.tsx
@@ -0,0 +1,11 @@
+function page() {
+ const outDir = import.meta.env.VITE_E2E_OUT_DIR;
+ return (
+
+
Hello from custom out dir!
+ {outDir &&
{outDir}}
+
+ );
+}
+
+export default page;
diff --git a/packages/react-server/examples/custom-out-dir/e2e/basic.test.ts b/packages/react-server/examples/custom-out-dir/e2e/basic.test.ts
new file mode 100644
index 000000000..f6c8d7a1e
--- /dev/null
+++ b/packages/react-server/examples/custom-out-dir/e2e/basic.test.ts
@@ -0,0 +1,23 @@
+import fs from "node:fs";
+import { expect, test } from "@playwright/test";
+test("custom outDir app can be visited", async ({ page }) => {
+ const res = await page.goto("/");
+ expect(res?.status()).toBe(200);
+ expect(await page.textContent("h1")).toBe("Hello from custom out dir!");
+ if (process.env.VITE_E2E_OUT_DIR) {
+ expect(await page.textContent("pre")).toBe(process.env.VITE_E2E_OUT_DIR);
+ }
+});
+
+test("custom outDir is created", async () => {
+ test.skip(
+ !process.env.E2E_PREVIEW && !process.env.E2E_CF,
+ "outDir is not available in preview",
+ );
+ const outDir = process.env.VITE_E2E_OUT_DIR || "custom-out-dir";
+ expect(fs.existsSync(outDir)).toBe(true);
+});
+
+test("default outDir is not created", async () => {
+ expect(fs.existsSync("dist")).toBe(false);
+});
diff --git a/packages/react-server/examples/custom-out-dir/package.json b/packages/react-server/examples/custom-out-dir/package.json
new file mode 100644
index 000000000..c551968c4
--- /dev/null
+++ b/packages/react-server/examples/custom-out-dir/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "@hiogawa/react-server-example-custom-out-dir",
+ "version": "0.0.0",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "next dev",
+ "start": "next start",
+ "build": "next build",
+ "cf-build": "CF_PAGES=1 pnpm build",
+ "cf-preview": "wrangler pages dev ./dist-cf/cloudflare --compatibility-date=2024-01-01 --compatibility-flags=nodejs_compat",
+ "vc-build": "VERCEL=1 pnpm build",
+ "test-e2e": "playwright test",
+ "test-e2e-preview": "E2E_PREVIEW=1 playwright test",
+ "cf-test-e2e-preview": "VITE_E2E_OUT_DIR=dist-cf pnpm cf-build && VITE_E2E_OUT_DIR=dist-cf E2E_CF=1 pnpm test-e2e"
+ },
+ "dependencies": {
+ "@hiogawa/react-server": "workspace:*",
+ "next": "link:../../../react-server-next",
+ "react": "rc",
+ "react-dom": "rc",
+ "react-server-dom-webpack": "rc"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.45.1",
+ "@types/react": "latest",
+ "@types/react-dom": "latest",
+ "vite": "latest"
+ }
+}
diff --git a/packages/react-server/examples/custom-out-dir/playwright.config.ts b/packages/react-server/examples/custom-out-dir/playwright.config.ts
new file mode 100644
index 000000000..a9b8be342
--- /dev/null
+++ b/packages/react-server/examples/custom-out-dir/playwright.config.ts
@@ -0,0 +1,35 @@
+import { defineConfig, devices } from "@playwright/test";
+
+const port = Number(process.env.E2E_PORT || 6174);
+const isCloudflarePages = Boolean(process.env.CF_PAGES);
+const isPreview = Boolean(process.env.E2E_PREVIEW);
+const command = isPreview
+ ? `pnpm start --port ${port} --strict-port`
+ : `pnpm dev --port ${port} --strict-port`;
+
+const cfPreview = `pnpm run cf-preview --port ${port}`;
+
+export default defineConfig({
+ testDir: "e2e",
+ use: {
+ trace: "on-first-retry",
+ },
+ projects: [
+ {
+ name: "chromium",
+ use: {
+ ...devices["Desktop Chrome"],
+ viewport: null,
+ deviceScaleFactor: undefined,
+ },
+ },
+ ],
+ webServer: {
+ command: isCloudflarePages ? cfPreview : command,
+ port,
+ },
+ grepInvert: isPreview ? /@dev/ : /@build/,
+ forbidOnly: !!process.env["CI"],
+ retries: process.env["CI"] ? 2 : 0,
+ reporter: "list",
+});
diff --git a/packages/react-server/examples/custom-out-dir/tsconfig.json b/packages/react-server/examples/custom-out-dir/tsconfig.json
new file mode 100644
index 000000000..51a93621c
--- /dev/null
+++ b/packages/react-server/examples/custom-out-dir/tsconfig.json
@@ -0,0 +1,17 @@
+{
+ "include": ["**/*.ts", "**/*.tsx"],
+ "compilerOptions": {
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "skipLibCheck": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+ "moduleResolution": "Bundler",
+ "module": "ESNext",
+ "target": "ESNext",
+ "lib": ["ESNext", "DOM"],
+ "types": ["next", "vitest/globals"],
+ "jsx": "react-jsx"
+ }
+}
diff --git a/packages/react-server/examples/custom-out-dir/vite.config.ts b/packages/react-server/examples/custom-out-dir/vite.config.ts
new file mode 100644
index 000000000..65e519d6e
--- /dev/null
+++ b/packages/react-server/examples/custom-out-dir/vite.config.ts
@@ -0,0 +1,10 @@
+import next from "next/vite";
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ plugins: [
+ next({
+ outDir: process.env.VITE_E2E_OUT_DIR || "custom-out-dir",
+ }),
+ ],
+});
diff --git a/packages/react-server/src/entry/ssr.tsx b/packages/react-server/src/entry/ssr.tsx
index 12d974285..c2f75e7c9 100644
--- a/packages/react-server/src/entry/ssr.tsx
+++ b/packages/react-server/src/entry/ssr.tsx
@@ -81,7 +81,7 @@ export async function importReactServer(): Promise {
if (import.meta.env.DEV) {
return $__global.dev.reactServer.ssrLoadModule(ENTRY_SERVER_WRAPPER) as any;
} else {
- return import("/dist/rsc/index.js" as string);
+ return import("virtual:import-react-server");
}
}
diff --git a/packages/react-server/src/features/assets/plugin.ts b/packages/react-server/src/features/assets/plugin.ts
index 300c28641..6cf5f55cc 100644
--- a/packages/react-server/src/features/assets/plugin.ts
+++ b/packages/react-server/src/features/assets/plugin.ts
@@ -65,7 +65,7 @@ export function vitePluginServerAssets({
// TODO: (refactor) use RouteManifest?
const manifest: Manifest = JSON.parse(
await fs.promises.readFile(
- "dist/client/.vite/manifest.json",
+ path.join(manager.outDir, "client", ".vite", "manifest.json"),
"utf-8",
),
);
@@ -138,8 +138,8 @@ export function vitePluginServerAssets({
if (manager.buildType === "browser") {
for (const file of manager.serverAssets) {
await fs.promises.cp(
- path.join("dist/rsc", file),
- path.join("dist/client", file),
+ path.join(manager.outDir, "rsc", file),
+ path.join(manager.outDir, "client", file),
);
}
}
@@ -150,7 +150,9 @@ export function vitePluginServerAssets({
export function serverAssertsPluginServer({
manager,
-}: { manager: PluginStateManager }): Plugin[] {
+}: {
+ manager: PluginStateManager;
+}): Plugin[] {
// 0. track server assets during server build (this plugin)
// 1. copy all server assets to browser build (copy-build plugin)
// 2. out of those, inject links automatically (ssr-assets virtual plugin)
diff --git a/packages/react-server/src/features/next/plugin.ts b/packages/react-server/src/features/next/plugin.ts
index b2e8e08ba..b67fb9309 100644
--- a/packages/react-server/src/features/next/plugin.ts
+++ b/packages/react-server/src/features/next/plugin.ts
@@ -1,4 +1,5 @@
import { mkdir, writeFile } from "node:fs/promises";
+import path from "node:path";
import type { Rollup } from "vite";
// ensure `.js` extension even if project root is cjs
@@ -8,7 +9,7 @@ export const OUTPUT_SERVER_JS_EXT = {
chunkFileNames: "assets/[name]-[hash].js",
} satisfies Rollup.OutputOptions;
-export async function createServerPackageJson() {
- await mkdir("dist", { recursive: true });
- await writeFile("dist/package.json", `{ "type": "module" }`);
+export async function createServerPackageJson(outDir: string) {
+ await mkdir(outDir, { recursive: true });
+ await writeFile(path.join(outDir, "package.json"), `{ "type": "module" }`);
}
diff --git a/packages/react-server/src/features/prerender/plugin.ts b/packages/react-server/src/features/prerender/plugin.ts
index 3b275d2d2..7abc7541d 100644
--- a/packages/react-server/src/features/prerender/plugin.ts
+++ b/packages/react-server/src/features/prerender/plugin.ts
@@ -39,7 +39,7 @@ export function prerenderPlugin({
writeBundle: {
sequential: true,
handler() {
- return processPrerender(prerender);
+ return processPrerender(prerender, manager.outDir);
},
},
},
@@ -66,10 +66,13 @@ function urlPathToHtmlPath(pathname: string) {
return pathname + (pathname.endsWith("/") ? "index.html" : ".html");
}
-async function processPrerender(getPrerenderRoutes: PrerenderFn) {
+async function processPrerender(
+ getPrerenderRoutes: PrerenderFn,
+ outDir: string,
+) {
console.log("▶▶▶ PRERENDER");
const entry: typeof import("../../entry/ssr") = await import(
- path.resolve("dist/server/__entry_ssr.js")
+ path.resolve(path.join(outDir, "server", "__entry_ssr.js"))
);
const { router } = await entry.importReactServer();
const presets = createPrerenderPresets(router.manifest);
@@ -87,11 +90,11 @@ async function processPrerender(getPrerenderRoutes: PrerenderFn) {
const data = Readable.from(stream as any);
const htmlFile = urlPathToHtmlPath(route);
const dataFile = route + RSC_PATH;
- await mkdir(path.dirname(path.join("dist/client", htmlFile)), {
+ await mkdir(path.dirname(path.join(outDir, "client", htmlFile)), {
recursive: true,
});
- await writeFile(path.join("dist/client", htmlFile), html);
- await writeFile(path.join("dist/client", dataFile), data);
+ await writeFile(path.join(outDir, "client", htmlFile), html);
+ await writeFile(path.join(outDir, "client", dataFile), data);
manifest.entries.push({
route,
html: htmlFile,
@@ -99,7 +102,7 @@ async function processPrerender(getPrerenderRoutes: PrerenderFn) {
});
}
await writeFile(
- "dist/client/__prerender.json",
+ path.join(outDir, "client", "__prerender.json"),
JSON.stringify(manifest, null, 2),
);
}
diff --git a/packages/react-server/src/features/router/plugin.ts b/packages/react-server/src/features/router/plugin.ts
index 204a4b2d5..c058e7e60 100644
--- a/packages/react-server/src/features/router/plugin.ts
+++ b/packages/react-server/src/features/router/plugin.ts
@@ -24,7 +24,10 @@ import { createFsRouteTree } from "./tree";
export function routeManifestPluginServer({
manager,
routeDir,
-}: { manager: PluginStateManager; routeDir: string }): Plugin[] {
+}: {
+ manager: PluginStateManager;
+ routeDir: string;
+}): Plugin[] {
return [
{
name: "server-route-manifest",
@@ -60,7 +63,9 @@ export function routeManifestPluginServer({
export function routeManifestPluginClient({
manager,
-}: { manager: PluginStateManager }): Plugin[] {
+}: {
+ manager: PluginStateManager;
+}): Plugin[] {
return [
{
name: routeManifestPluginClient.name + ":bundle",
@@ -96,7 +101,10 @@ export function routeManifestPluginClient({
const source = `${JSON.stringify(data, null, 2)}`;
const sourceHash = hashString(source).slice(0, 8);
const url = `/assets/route-manifest-${sourceHash}.js`;
- writeFileSync(`dist/client${url}`, `export default ${source}`);
+ writeFileSync(
+ path.join(manager.outDir, `client${url}`),
+ `export default ${source}`,
+ );
// give asset url and manifest to ssr
return `export default ${JSON.stringify(
diff --git a/packages/react-server/src/plugin/index.ts b/packages/react-server/src/plugin/index.ts
index 48c88babf..5f57e9256 100644
--- a/packages/react-server/src/plugin/index.ts
+++ b/packages/react-server/src/plugin/index.ts
@@ -75,6 +75,8 @@ class PluginStateManager {
config!: ResolvedConfig;
configEnv!: ConfigEnv;
+ outDir!: string;
+
buildType?: "scan" | "server" | "browser" | "ssr";
routeToClientReferences: Record = {};
@@ -123,6 +125,7 @@ export type ReactServerPluginOptions = {
entryBrowser?: string;
entryServer?: string;
routeDir?: string;
+ outDir?: string;
noAsyncLocalStorage?: boolean;
};
@@ -134,6 +137,7 @@ export function vitePluginReactServer(
const entryServer =
options?.entryServer ?? "@hiogawa/react-server/entry/server";
const routeDir = options?.routeDir ?? "src/routes";
+ const outDir = options?.outDir ?? "dist";
const reactServerViteConfig: InlineConfig = {
customLogger: createLogger(undefined, {
@@ -149,7 +153,6 @@ export function vitePluginReactServer(
},
plugins: [
...(options?.plugins ?? []),
-
vitePluginSilenceDirectiveBuildWarning(),
// expose server reference to react-server itself
@@ -168,28 +171,28 @@ export function vitePluginReactServer(
createVirtualPlugin("server-routes", () => {
return `
- const glob = import.meta.glob(
- "/${routeDir}/**/(page|layout|error|not-found|loading|template|route).(js|jsx|ts|tsx)",
- { eager: true },
- );
- export default Object.fromEntries(
- Object.entries(glob).map(
- ([k, v]) => [k.slice("/${routeDir}".length), v]
- )
- );
-
- const globMiddleware = import.meta.glob("/middleware.(js|jsx|ts|tsx)", { eager: true });
- export const middleware = Object.values(globMiddleware)[0];
- `;
+ const glob = import.meta.glob(
+ "/${routeDir}/**/(page|layout|error|not-found|loading|template|route).(js|jsx|ts|tsx)",
+ { eager: true },
+ );
+ export default Object.fromEntries(
+ Object.entries(glob).map(
+ ([k, v]) => [k.slice("/${routeDir}".length), v]
+ )
+ );
+
+ const globMiddleware = import.meta.glob("/middleware.(js|jsx|ts|tsx)", { eager: true });
+ export const middleware = Object.values(globMiddleware)[0];
+ `;
}),
createVirtualPlugin(
ENTRY_SERVER_WRAPPER.slice("virtual:".length),
() => `
- import "virtual:inject-async-local-storage";
- export { handler } from "${entryServer}";
- export { router } from "@hiogawa/react-server/entry/server";
- `,
+ import "virtual:inject-async-local-storage";
+ export { handler } from "${entryServer}";
+ export { router } from "@hiogawa/react-server/entry/server";
+ `,
),
// make `AsyncLocalStorage` available globally for React.cache from edge build
@@ -199,9 +202,9 @@ export function vitePluginReactServer(
return "export {}";
}
return `
- import { AsyncLocalStorage } from "node:async_hooks";
- Object.assign(globalThis, { AsyncLocalStorage });
- `;
+ import { AsyncLocalStorage } from "node:async_hooks";
+ Object.assign(globalThis, { AsyncLocalStorage });
+ `;
}),
validateImportPlugin({
@@ -243,7 +246,7 @@ export function vitePluginReactServer(
ssr: true,
manifest: true,
ssrEmitAssets: true,
- outDir: "dist/rsc",
+ outDir: path.join(outDir, "rsc"),
rollupOptions: {
input: {
index: ENTRY_SERVER_WRAPPER,
@@ -285,7 +288,7 @@ export function vitePluginReactServer(
},
build: {
manifest: true,
- outDir: env.isSsrBuild ? "dist/server" : "dist/client",
+ outDir: path.join(outDir, env.isSsrBuild ? "server" : "client"),
rollupOptions: env.isSsrBuild
? {
input: options?.prerender
@@ -303,6 +306,7 @@ export function vitePluginReactServer(
},
configResolved(config) {
manager.config = config;
+ manager.outDir = outDir;
},
async configureServer(server) {
manager.server = server;
@@ -373,7 +377,7 @@ export function vitePluginReactServer(
apply: "build",
async buildStart(_options) {
if (!manager.buildType) {
- await createServerPackageJson();
+ await createServerPackageJson(manager.outDir);
console.log("▶▶▶ REACT SERVER BUILD (scan) [1/4]");
manager.buildType = "scan";
await build(
@@ -426,6 +430,13 @@ export function vitePluginReactServer(
"client-only": true,
"server-only": `'server-only' is included in client build`,
}),
+
+ createVirtualPlugin("import-react-server", () => {
+ return `
+ export * from "/${outDir}/rsc/index.js";
+ `;
+ }),
+
createVirtualPlugin(ENTRY_BROWSER_WRAPPER.slice("virtual:".length), () => {
// dev
if (!manager.buildType) {
diff --git a/packages/react-server/src/plugin/virtual.d.ts b/packages/react-server/src/plugin/virtual.d.ts
index e3cf4f693..d25e6f1ff 100644
--- a/packages/react-server/src/plugin/virtual.d.ts
+++ b/packages/react-server/src/plugin/virtual.d.ts
@@ -6,3 +6,7 @@ declare module "virtual:server-routes" {
| import("../features/next/middleware").MiddlewareModule
| undefined;
}
+
+declare module "virtual:import-react-server" {
+ export const { handler, router }: typeof import("../entry/server");
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bba7a4690..5d65a50ff 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -234,6 +234,37 @@ importers:
specifier: ^5.3.3
version: 5.3.3(@types/node@20.14.10)(terser@5.31.2)
+ packages/react-server/examples/custom-out-dir:
+ dependencies:
+ '@hiogawa/react-server':
+ specifier: workspace:*
+ version: link:../..
+ next:
+ specifier: link:../../../react-server-next
+ version: link:../../../react-server-next
+ react:
+ specifier: 19.0.0-rc-df5f2736-20240712
+ version: 19.0.0-rc-df5f2736-20240712
+ react-dom:
+ specifier: 19.0.0-rc-df5f2736-20240712
+ version: 19.0.0-rc-df5f2736-20240712(react@19.0.0-rc-df5f2736-20240712)
+ react-server-dom-webpack:
+ specifier: 19.0.0-rc-df5f2736-20240712
+ version: 19.0.0-rc-df5f2736-20240712(react-dom@19.0.0-rc-df5f2736-20240712(react@19.0.0-rc-df5f2736-20240712))(react@19.0.0-rc-df5f2736-20240712)(webpack@5.93.0(@swc/core@1.6.13(@swc/helpers@0.5.12))(esbuild@0.23.0))
+ devDependencies:
+ '@playwright/test':
+ specifier: ^1.45.1
+ version: 1.45.1
+ '@types/react':
+ specifier: 18.3.3
+ version: 18.3.3
+ '@types/react-dom':
+ specifier: 18.3.0
+ version: 18.3.0
+ vite:
+ specifier: ^5.3.3
+ version: 5.3.3(@types/node@20.14.10)(terser@5.31.2)
+
packages/react-server/examples/minimal:
dependencies:
'@hiogawa/react-server':