Skip to content

Commit 75cc89d

Browse files
authored
fix(vite): proxy nitro/* imports from service environments (#4152)
1 parent feaa125 commit 75cc89d

6 files changed

Lines changed: 130 additions & 2 deletions

File tree

src/build/vite/env.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { EnvironmentOptions, RollupCommonJSOptions } from "vite";
1+
import type { EnvironmentOptions, RollupCommonJSOptions, Plugin as VitePlugin } from "vite";
22
import type { NitroPluginContext, ServiceConfig } from "./types.ts";
33

44
import type { RunnerName } from "env-runner";
@@ -68,7 +68,10 @@ export function createServiceEnvironment(
6868
return {
6969
consumer: "server",
7070
build: {
71-
rollupOptions: { input: { index: serviceConfig.entry } },
71+
rollupOptions: {
72+
input: { index: serviceConfig.entry },
73+
external: [/^nitro(\/|$)/],
74+
},
7275
minify: ctx.nitro!.options.minify,
7376
sourcemap: ctx.nitro!.options.sourcemap,
7477
outDir: join(ctx.nitro!.options.buildDir, "vite/services", name),
@@ -183,6 +186,49 @@ async function _loadRunner(ctx: NitroPluginContext, manager: RunnerManager) {
183186
await manager.reload(runner);
184187
}
185188

189+
// Service environments (e.g. SSR) must not bundle their own copy of `nitro/*`
190+
// runtime modules. In dev, imports are proxied to the Nitro environment via
191+
// __VITE_ENVIRONMENT_RUNNER_IMPORT__. In prod, they are externalized (see createServiceEnvironment).
192+
const NITRO_PROXY_PREFIX = "\0nitro-env-proxy:";
193+
194+
export function nitroServiceProxy(): VitePlugin {
195+
return {
196+
name: "nitro:service-proxy",
197+
enforce: "pre",
198+
applyToEnvironment: (env) => env.name !== "nitro" && env.config.consumer === "server",
199+
apply: (_config, configEnv) => configEnv.command === "serve",
200+
201+
resolveId: {
202+
filter: { id: /^nitro(\/|$)/ },
203+
handler(id) {
204+
if (id === "nitro" || id.startsWith("nitro/")) {
205+
return { id: NITRO_PROXY_PREFIX + id, moduleSideEffects: false };
206+
}
207+
},
208+
},
209+
210+
load: {
211+
filter: { id: /^\0nitro-env-proxy:/ },
212+
handler(id) {
213+
if (!id.startsWith(NITRO_PROXY_PREFIX)) {
214+
return;
215+
}
216+
const originalId = id.slice(NITRO_PROXY_PREFIX.length);
217+
// __vite_ssr_exportAll__ is provided by the module runner execution context.
218+
// It re-exports all enumerable own properties (except "default") from the source module.
219+
return {
220+
code: [
221+
`const _mod = await globalThis.__VITE_ENVIRONMENT_RUNNER_IMPORT__("nitro", ${JSON.stringify(originalId)});`,
222+
`__vite_ssr_exportAll__(_mod);`,
223+
`export default _mod.default;`,
224+
].join("\n"),
225+
map: null,
226+
};
227+
},
228+
},
229+
};
230+
}
231+
186232
// workerd-based runners (miniflare) cannot handle CJS externals via import(),
187233
// so all dependencies must be processed through Vite's transform pipeline.
188234
function _isWorkerdRunner(ctx: NitroPluginContext): boolean {

src/build/vite/plugin.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
createNitroEnvironment,
1919
createServiceEnvironments,
2020
createServiceEnvironment,
21+
nitroServiceProxy,
2122
} from "./env.ts";
2223
import { configureViteDevServer } from "./dev.ts";
2324
import { runtimeDir } from "nitro/meta";
@@ -50,6 +51,7 @@ export function nitro(pluginConfig: NitroPluginConfig = {}): VitePlugin[] {
5051
nitroMain(ctx),
5152
nitroPrepare(ctx),
5253
nitroService(ctx),
54+
nitroServiceProxy(),
5355
nitroPreviewPlugin(ctx),
5456
pluginConfig.experimental?.vite?.assetsImport !== false &&
5557
assetsPlugin({
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { useStorage } from "nitro/storage";
2+
3+
export default () => {
4+
const storage = useStorage();
5+
return storage.get("test:key");
6+
};
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { useStorage } from "nitro/storage";
2+
import { useRuntimeConfig } from "nitro/runtime-config";
3+
4+
export default {
5+
async fetch() {
6+
const storage = useStorage();
7+
const config = useRuntimeConfig();
8+
await storage.set("test:key", "value-from-ssr");
9+
const value = await storage.get("test:key");
10+
return Response.json({
11+
storage: value,
12+
config: config.nitro?.envPrefix ?? "NITRO_",
13+
});
14+
},
15+
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { defineConfig } from "vite";
2+
import { nitro } from "nitro/vite";
3+
4+
export default defineConfig({
5+
plugins: [nitro({ serverDir: "./" })],
6+
});

test/vite/app.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { fileURLToPath } from "node:url";
2+
import type { ViteDevServer } from "vite";
3+
import { describe, test, expect, beforeAll, afterAll } from "vitest";
4+
5+
const { createServer } = (await import(
6+
process.env.NITRO_VITE_PKG || "vite"
7+
)) as typeof import("vite");
8+
9+
describe("vite:app", () => {
10+
let server: ViteDevServer;
11+
let serverURL: string;
12+
13+
const rootDir = fileURLToPath(new URL("./app-fixture", import.meta.url));
14+
15+
const originalCwd = process.cwd();
16+
17+
beforeAll(async () => {
18+
process.chdir(rootDir);
19+
server = await createServer({ root: rootDir });
20+
await server.listen("0" as unknown as number);
21+
const addr = server.httpServer?.address() as {
22+
port: number;
23+
address: string;
24+
family: string;
25+
};
26+
serverURL = `http://${addr.family === "IPv6" ? `[${addr.address}]` : addr.address}:${addr.port}`;
27+
}, 30_000);
28+
29+
afterAll(async () => {
30+
await server?.close();
31+
process.chdir(originalCwd);
32+
});
33+
34+
test("SSR entry can use nitro/storage (shared with nitro env)", async () => {
35+
const res = await fetch(serverURL);
36+
const data = (await res.json()) as { storage: string; config: string };
37+
expect(data.storage).toBe("value-from-ssr");
38+
});
39+
40+
test("SSR entry can use nitro/runtime-config", async () => {
41+
const res = await fetch(serverURL);
42+
const data = (await res.json()) as { storage: string; config: string };
43+
expect(data.config).toBe("NITRO_");
44+
});
45+
46+
test("storage is shared between SSR and nitro environments", async () => {
47+
// SSR entry writes to storage, API route reads it
48+
await fetch(serverURL);
49+
const res = await fetch(`${serverURL}/api/storage`);
50+
const value = await res.text();
51+
expect(value).toBe("value-from-ssr");
52+
});
53+
});

0 commit comments

Comments
 (0)