Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/vitest-node-env-browser-condition.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'vite-plugin-solid': patch
---

Load the client build of solid-js in test files that set `// @vitest-environment node` when `test.environment` is `jsdom` or `happy-dom`. The `ssr` environment keeps the `browser` condition again, and only the conditions Vitest passes to Node leave it out, so `msw/node` still loads.
22 changes: 22 additions & 0 deletions examples/vite-6/tests/jsdom/node-environment.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// @vitest-environment node
// Regression test for solidjs/solid-vite-plugin#534. This file runs in the
// `ssr` environment while the suite defaults to jsdom, and must still load
// the client build of solid-js.
import { createEffect, createRoot } from 'solid-js';
import { isServer } from 'solid-js/web';
import { expect, test } from 'vitest';

test('loads the client build of solid-js', () => {
expect(isServer).toBe(false);
});

test('runs effects', async () => {
let ran = false;
createRoot(() =>
createEffect(() => {
ran = true;
}),
);
await Promise.resolve();
expect(ran).toBe(true);
});
51 changes: 32 additions & 19 deletions examples/vite-6/tests/node-conditions.mjs
Original file line number Diff line number Diff line change
@@ -1,41 +1,54 @@
// Vitest passes the `ssr` environment's conditions to Node as `--conditions`
// for the packages it leaves external. In a jsdom test run the plugin used to
// add `browser` there, so Node-only packages failed to load. For example,
// `msw/node` imports `@mswjs/interceptors/ClientRequest`, whose `browser`
// export is `null` (#125).
// Vitest passes the resolved `ssr.resolve.conditions` to Node as
// `--conditions` for the packages it leaves external. In a jsdom test run the
// plugin used to add `browser` there, so Node-only packages failed to load.
// For example, `msw/node` imports `@mswjs/interceptors/ClientRequest`, whose
// `browser` export is `null` (#125). Files with `// @vitest-environment node`
// run in the `ssr` environment, so it keeps `browser` for Vite's own
// resolution of the inlined solid-js (#534).
//
// Checks:
// - jsdom: `client` keeps `browser`, `ssr` does not.
// - node environment: `ssr` keeps `browser`, since its tests run there.
// - jsdom: `client` and `ssr` keep `browser`, Node's conditions do not.
// - jsdom with `browser` set by the user on `ssr`: Node keeps it.
// - node environment: `ssr` and Node keep `browser`.
import { resolveConfig } from 'vite';
import solid from 'vite-plugin-solid';

async function conditions(test) {
async function conditions(test, extra = {}) {
const resolved = await resolveConfig(
{ mode: 'test', configFile: false, logLevel: 'silent', plugins: [solid()], test },
{ mode: 'test', configFile: false, logLevel: 'silent', plugins: [solid()], test, ...extra },
'serve',
);
return {
client: resolved.environments.client.resolve.conditions,
ssr: resolved.environments.ssr.resolve.conditions,
node: resolved.ssr.resolve.conditions,
};
}

const failures = [];
const jsdom = await conditions({ environment: 'jsdom' });
if (!jsdom.client.includes('browser')) {
failures.push(`jsdom: client conditions lack browser: ${JSON.stringify(jsdom.client)}`);
}
if (jsdom.ssr.includes('browser')) {
failures.push(`jsdom: ssr conditions include browser: ${JSON.stringify(jsdom.ssr)}`);
function expectBrowser(label, list, expected) {
if (list.includes('browser') !== expected) {
failures.push(`${label} ${expected ? 'lacks' : 'includes'} browser: ${JSON.stringify(list)}`);
}
}

const jsdom = await conditions({ environment: 'jsdom' });
expectBrowser('jsdom: client conditions', jsdom.client, true);
expectBrowser('jsdom: ssr environment conditions', jsdom.ssr, true);
expectBrowser('jsdom: Node conditions', jsdom.node, false);

const userSsr = await conditions(
{ environment: 'jsdom' },
{ environments: { ssr: { resolve: { conditions: ['browser', 'node', 'import'] } } } },
);
expectBrowser('jsdom + user ssr browser: Node conditions', userSsr.node, true);

const node = await conditions({ environment: 'node' });
if (!node.ssr.includes('browser')) {
failures.push(`node: ssr conditions lack browser: ${JSON.stringify(node.ssr)}`);
}
expectBrowser('node: ssr environment conditions', node.ssr, true);
expectBrowser('node: Node conditions', node.node, true);

if (failures.length) {
for (const failure of failures) console.error(`FAIL: ${failure}`);
process.exit(1);
}
console.log('PASS: jsdom tests keep browser out of the ssr conditions');
console.log('PASS: jsdom tests keep browser out of the Node conditions only');
28 changes: 17 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,10 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin {
// by the crawl in test mode. They are inlined with solid-js (see
// `test.server.deps` below).
const testRuntimeConsumers = new Set<string>();
// True when a DOM test run inlines the Solid runtime. The `ssr`
// environment then does not get the `browser` condition (see
// `configEnvironment` below).
// True when a DOM test run inlines the Solid runtime. Node then does not
// get the `browser` condition (see `configResolved` below).
let domTestRuntimeInlined = false;
let ssrBrowserAdded = false;

return {
name: 'solid',
Expand All @@ -285,6 +285,7 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin {
projectRoot = userConfig.root;
isTestMode = userConfig.mode === 'test';
domTestRuntimeInlined = false;
ssrBrowserAdded = false;

// Leave the user's `resolve.alias` as it is. Turning an object into an
// array changes how Vite merges it with aliases from later plugins:
Expand Down Expand Up @@ -488,18 +489,12 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin {
config.resolve.conditions = [...defaultServerConditions];
}
}
// Vitest passes the `ssr` environment's conditions to Node as
// `--conditions` for the packages it leaves external. With `browser`
// there, Node-only packages fail to load. For example, `msw/node`
// imports `@mswjs/interceptors/ClientRequest`, whose `browser`
// export is `null` (#125). DOM tests run in the `client` environment,
// and the Solid runtime is inlined in that case, so the `ssr`
// environment does not need `browser`.
const addBrowser =
isTestMode &&
!opts.isSsrTargetWebworker &&
!options.ssr &&
!(domTestRuntimeInlined && name === 'ssr');
!config.resolve.conditions.includes('browser');
if (name === 'ssr') ssrBrowserAdded = addBrowser;
config.resolve.conditions = [
'solid',
// A server build with `ssr.target: 'webworker'` starts from Vite's
Expand Down Expand Up @@ -533,6 +528,17 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin {
config.command === 'serve' &&
config.mode !== 'production' &&
options.hot !== false;

// Vitest sends only this copy to Node; `browser` there breaks `msw/node` (#125, #534).
const ssrResolve = config.ssr?.resolve;
if (
domTestRuntimeInlined &&
ssrBrowserAdded &&
ssrResolve?.conditions &&
ssrResolve !== config.environments?.ssr?.resolve
) {
ssrResolve.conditions = ssrResolve.conditions.filter((c) => c !== 'browser');
}
},

resolveId(id) {
Expand Down
41 changes: 41 additions & 0 deletions src/test-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,44 @@ describe('test config', () => {
expect(test?.server).toBeUndefined();
});
});

describe('test conditions', () => {
async function conditions(test: Record<string, unknown>, extra: Record<string, unknown> = {}) {
const { resolveConfig } = await import('vite');
const resolved = await resolveConfig(
{
mode: 'test',
configFile: false,
logLevel: 'silent',
plugins: [solidPlugin()],
test,
...extra,
} as any,
'serve',
);
return {
ssr: resolved.environments.ssr.resolve.conditions,
node: resolved.ssr.resolve!.conditions!,
};
}

it('keeps browser on the ssr environment but not in the Node conditions for DOM tests', async () => {
const { ssr, node } = await conditions({ environment: 'happy-dom' });
expect(ssr).toContain('browser');
expect(node).not.toContain('browser');
});

it('keeps a browser condition the user set on the ssr environment', async () => {
const { node } = await conditions(
{ environment: 'jsdom' },
{ environments: { ssr: { resolve: { conditions: ['browser', 'node', 'import'] } } } },
);
expect(node).toContain('browser');
});

it('keeps browser in the Node conditions for the node environment', async () => {
const { ssr, node } = await conditions({ environment: 'node' });
expect(ssr).toContain('browser');
expect(node).toContain('browser');
});
});
Loading