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-client-inline-runtime.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@solidjs/vite-plugin': patch
---

In the Vitest client posture (jsdom, happy-dom), the plugin now inlines `solid-js`, `@solidjs/web` and the packages that import them without a `solid` export condition, such as `@solidjs/testing-library`. Before, they stayed external, and the host runtime resolved their own imports. Node gets the `browser` condition from Vitest's worker `--conditions`, but Deno does not apply it in worker threads (the `threads` pool) and resolves `deno`/`node`. Under Deno, `@solidjs/testing-library` loaded the server build of `@solidjs/web` and `render` failed with `isServer` true (#369). Inlining sends these imports through Vite's `browser` condition and loads one client-build instance. A `test.server.deps.inline` list is merged with the plugin's, and a `test.server.deps.external` entry for `solid-js` still opts out.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1152,7 +1152,7 @@ For other breaking changes, check [the migration guide of vite](https://vitejs.d

If you are using [vitest](https://vitest.dev/), this plugin already injects the necessary configuration for you. It even automatically detects if you have `@testing-library/jest-dom` installed in your project and automatically adds it to the `setupFiles`. All you need to add (if you want) is `globals`, `coverage`, and other testing configuration of your choice. If you can live without those, enjoy using vitest without the need to configure it yourself.

Tests default to the client posture, regardless of the app's `ssr` flag: DOM codegen, non-hydratable (nothing hydrates in a test), browser export conditions, and a `jsdom` default `test.environment`. A server-rendered app needs no `ssr: mode !== 'test'` workaround — DOM component tests just work.
Tests default to the client posture, regardless of the app's `ssr` flag: DOM codegen, non-hydratable (nothing hydrates in a test), browser export conditions, and a `jsdom` default `test.environment`. `solid-js`, `@solidjs/web` and the packages that import them (such as `@solidjs/testing-library`) are inlined, so their imports resolve the browser build through Vite on any host runtime, Deno included. A server-rendered app needs no `ssr: mode !== 'test'` workaround — DOM component tests just work.

Server-runtime unit tests (server functions, sessions, `renderToString` — anything that needs `isServer` to be `true` and the real server build of the framework) opt out per [vitest project](https://vitest.dev/guide/projects) by setting `test.environment: 'node'` (or `'edge-runtime'`) explicitly. That project gets the server posture end to end: server export conditions, ssr codegen, and the framework inlined so the whole graph — request-event storage included — resolves into one server-build instance. No inline/alias configuration needed. Both postures coexist in one workspace:

Expand All @@ -1172,7 +1172,7 @@ test: {
},
```

The posture is chosen while each project resolves its own Vite config. Vitest 5 lets inline projects reuse the root Vite server instead (`test.sharedViteServer`, on by default), which skips that step, so the plugin sets `sharedViteServer: false` when the root config declares `test.projects` — trading Vitest 5's shared-server optimization for the per-project resolution Vitest 4 always did. Setting the option yourself overrides this; with `true`, every shared project runs with the root config's posture, without the injected `jest-dom` setup file, and without the plugin's `server.deps` handling (the framework inlined for the server posture, externalized for the client one).
The posture is chosen while each project resolves its own Vite config. Vitest 5 lets inline projects reuse the root Vite server instead (`test.sharedViteServer`, on by default), which skips that step, so the plugin sets `sharedViteServer: false` when the root config declares `test.projects` — trading Vitest 5's shared-server optimization for the per-project resolution Vitest 4 always did. Setting the option yourself overrides this; with `true`, every shared project runs with the root config's posture, without the injected `jest-dom` setup file, and without the plugin's `server.deps` handling (the framework inlined in both postures, together with its consumers such as `@solidjs/testing-library` in the client one).

# Credits

Expand Down
1 change: 1 addition & 0 deletions examples/start-ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"test": "node test/run.mjs && node test/http-bridge.mjs && node test/components-warning.mjs && node test/webworker-warning.mjs && node test/dedupe.mjs && node test/ssr-inline.mjs"
},
"devDependencies": {
"@solidjs/testing-library": "^1.0.0-beta.2",
"jsdom": "^26.1.0",
"vite": "^8.2.1",
"@solidjs/vite-plugin": "workspace:*",
Expand Down
41 changes: 41 additions & 0 deletions examples/start-ssr/src/testing-library.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Client-posture regression test for runtime-consuming packages (#369). The
// vitest mode of test/run.mjs runs it with test/deno-conditions.mjs
// preloaded. `@solidjs/testing-library` has no `solid` export condition.
// Left external, its own `import "@solidjs/web"` is resolved by the host
// runtime, and under Deno that is the server build, so `render` ran with
// `isServer` true. The plugin inlines it with the runtime, so its imports go
// through Vite's `browser` condition and share the test file's instance.
import { expect, it } from 'vitest';
import { createSignal } from 'solid-js';
import { isServer } from '@solidjs/web';
import { render } from '@solidjs/testing-library';

function Counter() {
const [count, setCount] = createSignal(0);
return (
<button type="button" onClick={() => setCount(count() + 1)}>
count: {count()}
</button>
);
}

const nextTick = () => new Promise((resolve) => setTimeout(resolve, 0));

it('resolves the browser build in the test file', () => {
expect(isServer).toBe(false);
});

it('renders and updates through @solidjs/testing-library', async () => {
const { getByRole, unmount } = render(() => <Counter />);
try {
const button = getByRole('button');
expect(button.textContent).toContain('count: 0');
button.click();
for (let i = 0; i < 20 && !button.textContent!.includes('count: 1'); i++) {
await nextTick();
}
expect(button.textContent).toContain('count: 1');
} finally {
unmount();
}
});
29 changes: 29 additions & 0 deletions examples/start-ssr/test/deno-conditions.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Preloaded into the vitest workers (NODE_OPTIONS=--import) by the vitest
// mode of test/run.mjs. It makes Node resolve bare imports the way Deno does.
// In worker threads, Deno does not apply the `browser` condition that vitest
// passes to the workers as `--conditions`, so an externalized package's own
// `import "@solidjs/web"` resolves `deno`/`node`, which is the server build
// (#369).
import module from 'node:module';

const denoConditions = (context) => ({
...context,
conditions: [...context.conditions.filter((c) => c !== 'browser'), 'deno'],
});

if (module.registerHooks) {
module.registerHooks({
resolve: (specifier, context, next) => next(specifier, denoConditions(context)),
});
} else {
// Node < 22.15 has only the async loader API.
module.register(
'data:text/javascript,' +
encodeURIComponent(`
export async function resolve(specifier, context, next) {
const conditions = context.conditions.filter((c) => c !== 'browser');
return next(specifier, { ...context, conditions: [...conditions, 'deno'] });
}
`),
);
}
38 changes: 38 additions & 0 deletions examples/start-ssr/test/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6352,6 +6352,44 @@ async function runVitestMode() {
testPass ? undefined : out.slice(-2000),
);

// Runtime-consuming packages in the client posture (#369). An externalized
// package's own imports are resolved by the host runtime, not by Vite.
// Node gets the `browser` condition from vitest's worker `--conditions`;
// Deno does not, and `@solidjs/testing-library` then loaded the server
// build of `@solidjs/web`. test/deno-conditions.mjs makes the workers
// resolve the way Deno does, so this fails unless the plugin inlines the
// runtime and its consumers.
let consumersOut = '';
let consumersOk = false;
try {
consumersOut = execSync('pnpm exec vitest run src/testing-library.test.tsx --pool=forks', {
cwd: exampleDir,
stdio: 'pipe',
timeout: 180000,
env: {
...process.env,
NO_COLOR: '1',
NODE_OPTIONS: [
process.env.NODE_OPTIONS,
`--import=${pathToFileURL(path.join(exampleDir, 'test', 'deno-conditions.mjs')).href}`,
]
.filter(Boolean)
.join(' '),
},
}).toString();
consumersOk = true;
} catch (e) {
consumersOut = String(e.stdout || '') + String(e.stderr || '');
}
const consumersPass = consumersOk && /2 passed/.test(consumersOut);
record(
mode,
'runtime-consumers',
'@solidjs/testing-library renders with the browser build when the host runtime ignores `browser`',
consumersPass,
consumersPass ? undefined : consumersOut.slice(-2000),
);

// Both postures in ONE workspace (VITEST_PROJECTS=1 adds test.projects to
// the config): the jsdom project keeps the client posture while the node
// project gets the server posture end to end just by writing
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 41 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1260,6 +1260,10 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
// Names of the packages `isSemiFrameworkPkgByJson` classified, so their
// dependencies can be taken back out of `optimizeDeps.include` (see below).
const semiFrameworkPkgs = new Set<string>();
// Packages that import the Solid runtime without a `solid` export
// condition, found by the same crawl in the vitest client posture. They are
// inlined there with the runtime (see `test.server.deps` below).
const testRuntimeConsumers = new Set<string>();
const tsrxCss = new Map<string, string>();

// The client build's manifest, read back by SSR builds. In builder-mode
Expand Down Expand Up @@ -1469,10 +1473,20 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
// the same way `development` does, and an externalized consumer
// would split it just the same), never vitest (it manages inlining
// via test.server.deps).
if (!(replaceDev || observe) || isTestMode) return false;
const semi = SOLID_RUNTIME_PKGS.some(
(name) => pkgJson.dependencies?.[name] || pkgJson.peerDependencies?.[name],
);
if (isTestMode) {
// Client test posture: record the runtime consumers so
// `test.server.deps.inline` below can inline them, without
// handing them to vitefu (its `ssr.external` for their
// dependencies would reach vitest's `server.deps.external`).
if (semi && !serverTestPosture && typeof pkgJson.name === 'string') {
testRuntimeConsumers.add(pkgJson.name);
}
return false;
}
if (!(replaceDev || observe)) return false;
if (semi && typeof pkgJson.name === 'string') semiFrameworkPkgs.add(pkgJson.name);
return semi;
},
Expand Down Expand Up @@ -1581,11 +1595,36 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin[] {
test.server = { deps: { inline: [/solid-js/, /@solidjs[+/]web/] } };
}
} else if (
// `inline: true` inlines everything already (and Vite's config
// merge would turn it into `[true, ...]`, which vitest can't read).
// A user list is merged with this one; a user who externalizes
// solid-js keeps it external.
userTest.server?.deps?.inline !== true &&
!userTest.server?.deps?.external?.find((item: string | RegExp) =>
/solid-js/.test(item.toString()),
)
) {
test.server = { deps: { external: [/solid-js/] } };
// An externalized package is imported natively, and its own imports
// are resolved by the host runtime, not by Vite. Node gets the
// `browser` condition from vitest's worker `--conditions`, but Deno
// does not apply them in worker threads. It resolves `deno`/`node`
// and lands on the server build (`isServer` true inside
// `@solidjs/testing-library`'s `render`, #369). Inline the runtime and the packages that consume it, so
// every import of `solid-js` and `@solidjs/web` goes through Vite's
// `browser` condition and loads one client-build instance. A
// consumer left external would load a second, native copy, so the
// consumers found by the crawl are inlined with the runtime.
// Packages with a `solid` export condition are inlined already
// through vitefu's `ssr.noExternal`.
test.server = {
deps: {
inline: [
/[\\/]node_modules[\\/]solid-js[\\/]/,
/[\\/]node_modules[\\/]@solidjs[\\/]web[\\/]/,
...testRuntimeConsumers,
],
},
};
}
// jest-dom's DOM matchers have no place in a server-posture project;
// vitest browser mode already has bundled jest-dom assertions
Expand Down
Loading