Skip to content

Commit 228540f

Browse files
Merge pull request #80 from robbchar/fix/typeof-window-swc-env-detection
Fix/typeof window swc env detection
2 parents e9d54b8 + 52540be commit 228540f

File tree

2 files changed

+151
-6
lines changed

2 files changed

+151
-6
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
import type loadJsConfig from "next/dist/build/load-jsconfig.js";
4+
import type { NextConfigComplete } from "next/dist/server/config-shared.js";
5+
6+
vi.mock("next/dist/build/load-jsconfig.js", () => ({
7+
default: vi.fn(),
8+
}));
9+
10+
vi.mock("next/dist/build/swc/index.js", () => ({
11+
transform: vi.fn(),
12+
}));
13+
14+
vi.mock("../../utils/nextjs", () => ({
15+
findNextDirectories: vi.fn(),
16+
loadClosestPackageJson: vi.fn(),
17+
loadSWCBindingsEagerly: vi.fn(),
18+
}));
19+
20+
vi.mock("../../utils/swc/transform", () => ({
21+
getVitestSWCTransformConfig: vi.fn(),
22+
}));
23+
24+
const createPromiseWithResolvers = <T>() => {
25+
let resolve!: (value: T | PromiseLike<T>) => void;
26+
let reject!: (reason?: unknown) => void;
27+
const promise = new Promise<T>((res, rej) => {
28+
resolve = res;
29+
reject = rej;
30+
});
31+
32+
return { promise, resolve, reject };
33+
};
34+
35+
const nextConfig: NextConfigComplete = {
36+
// Only the fields read by our code under test are populated here.
37+
experimental: {
38+
swcPlugins: [],
39+
},
40+
modularizeImports: undefined,
41+
compiler: undefined,
42+
distDir: ".next",
43+
// biome-ignore lint/suspicious/noExplicitAny: we only need a partial shape for this test
44+
} as any;
45+
46+
describe("vitePluginNextSwc env detection", () => {
47+
const setupMocks = async () => {
48+
const loadJsConfigModule = await import("next/dist/build/load-jsconfig.js");
49+
vi.mocked(loadJsConfigModule.default).mockResolvedValue({
50+
useTypeScript: true,
51+
jsConfig: { compilerOptions: {} },
52+
resolvedBaseUrl: undefined,
53+
} as unknown as Awaited<ReturnType<typeof loadJsConfig>>);
54+
55+
const NextUtils = await import("../../utils/nextjs");
56+
vi.mocked(NextUtils.findNextDirectories).mockReturnValue({
57+
pagesDir: "/pages",
58+
appDir: "/app",
59+
});
60+
vi.mocked(NextUtils.loadClosestPackageJson).mockResolvedValue({
61+
type: "module",
62+
});
63+
vi.mocked(NextUtils.loadSWCBindingsEagerly).mockResolvedValue(undefined);
64+
65+
const swc = await import("next/dist/build/swc/index.js");
66+
vi.mocked(swc.transform).mockResolvedValue({
67+
code: "export {}",
68+
map: null,
69+
});
70+
71+
const swcTransform = await import("../../utils/swc/transform");
72+
vi.mocked(swcTransform.getVitestSWCTransformConfig).mockReturnValue(
73+
{} as ReturnType<typeof swcTransform.getVitestSWCTransformConfig>,
74+
);
75+
};
76+
77+
it("treats Storybook-like Vite config (no `test` field) as browser, not server", async () => {
78+
// In Storybook, VITEST is not set; in our unit test runner it is, so we explicitly simulate Storybook.
79+
process.env.VITEST = "false";
80+
vi.resetModules();
81+
await setupMocks();
82+
83+
const { vitePluginNextSwc } = await import("./plugin");
84+
85+
const nextConfigResolver = createPromiseWithResolvers<NextConfigComplete>();
86+
nextConfigResolver.resolve(nextConfig);
87+
88+
const plugin = vitePluginNextSwc("/root", nextConfigResolver);
89+
90+
await plugin.config?.({}, { mode: "development" } as never);
91+
92+
await plugin.transform?.call(
93+
{ getCombinedSourcemap: () => null } as unknown as ThisParameterType<
94+
NonNullable<typeof plugin.transform>
95+
>,
96+
"export const x = typeof window;",
97+
"/src/example.ts",
98+
);
99+
100+
const swcTransform = await import("../../utils/swc/transform");
101+
const lastCallArg = vi
102+
.mocked(swcTransform.getVitestSWCTransformConfig)
103+
.mock.calls.at(-1)?.[0];
104+
105+
expect(lastCallArg?.isServerEnvironment).toBe(false);
106+
});
107+
108+
it("treats Vitest node environment as server", async () => {
109+
process.env.VITEST = "true";
110+
vi.resetModules();
111+
await setupMocks();
112+
113+
const { vitePluginNextSwc } = await import("./plugin");
114+
115+
const nextConfigResolver = createPromiseWithResolvers<NextConfigComplete>();
116+
nextConfigResolver.resolve(nextConfig);
117+
118+
const plugin = vitePluginNextSwc("/root", nextConfigResolver);
119+
120+
await plugin.config?.({ test: { environment: "node" } }, {
121+
mode: "development",
122+
} as never);
123+
124+
await plugin.transform?.call(
125+
{ getCombinedSourcemap: () => null } as unknown as ThisParameterType<
126+
NonNullable<typeof plugin.transform>
127+
>,
128+
"export const x = typeof window;",
129+
"/src/example.ts",
130+
);
131+
132+
const swcTransform = await import("../../utils/swc/transform");
133+
const lastCallArg = vi
134+
.mocked(swcTransform.getVitestSWCTransformConfig)
135+
.mock.calls.at(-1)?.[0];
136+
137+
expect(lastCallArg?.isServerEnvironment).toBe(true);
138+
});
139+
});

src/plugins/next-swc/plugin.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { NextConfigComplete } from "next/dist/server/config-shared.js";
44
import { resolve } from "pathe";
55
import { type Plugin, createFilter } from "vite";
66

7+
import { isVitestEnv } from "../../utils";
78
import * as NextUtils from "../../utils/nextjs";
89
import { getVitestSWCTransformConfig } from "../../utils/swc/transform";
910
import { isDefined } from "../../utils/typescript";
@@ -49,12 +50,17 @@ export function vitePluginNextSwc(
4950
const serverWatchIgnored = config.server?.watch?.ignored;
5051
const isServerWatchIgnoredArray = Array.isArray(serverWatchIgnored);
5152

52-
if (
53-
config.test?.environment === "node" ||
54-
config.test?.environment === "edge-runtime" ||
55-
config.test?.browser?.enabled !== false
56-
) {
57-
isServerEnvironment = true;
53+
// Default to browser-mode (Storybook preview / Vite dev).
54+
isServerEnvironment = false;
55+
56+
// In Vitest, default to server-mode unless browser mode is explicitly enabled.
57+
// This avoids Next/SWC folding `typeof window` to `"undefined"` in Storybook's browser preview
58+
// when `config.test` is absent.
59+
if (isVitestEnv) {
60+
isServerEnvironment =
61+
config.test?.environment === "node" ||
62+
config.test?.environment === "edge-runtime" ||
63+
config.test?.browser?.enabled !== true;
5864
}
5965

6066
return {

0 commit comments

Comments
 (0)