solid-testing-library: Can't test component with Vitest

Hi,

I’m trying to test my SolidJS library with Vitest. But I’m getting the following error:

 FAIL  src/index.test.tsx [ src/index.test.tsx ]
SyntaxError: The requested module 'solid-js/web' does not provide an export named 'hydrate'
 ❯ async src/index.test.tsx:8:31
      4| test('render', async () => {
      5|   await render(() => <div>Hello</div>)
      6| })
       |   ^
      7| 

I’m using:

"solid-js": "^1.3.12"
"vite": "^2.8.6",
"vite-plugin-solid": "^2.2.6",
"vitest": "^0.6.1"
"solid-testing-library": "^0.3.0",

vite.config.js

/// <reference types="vitest" />
/// <reference types="vite/client" />

import { defineConfig } from 'vite'
import solid from 'vite-plugin-solid'

export default defineConfig({
  test: {
    environment: 'jsdom',
    transformMode: {
      web: [/\.[jt]sx?$/],
    },
    // solid needs to be inline to work around
    // a resolution issue in vitest:
    deps: {
      inline: [/solid-js/],
    },
    // if you have few tests, try commenting one
    // or both out to improve performance:
    // threads: false,
    // isolate: false,
  },
  plugins: [solid()],
  resolve: {
    conditions: ['development', 'browser'],
  },
})

I’m new to testing. Is my configuration correct or is it bug? I could not find a lot of documentation on testing SolidJS with Vitest.

Thanks.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 25 (1 by maintainers)

Commits related to this issue

Most upvoted comments

After hours and hours of debugging I finally figured out the issue! It’s been reported as https://github.com/vitest-dev/vitest/issues/1588. In the meantime, there is a workaround:

 /* @file: vite.config.js */
 
 /* snip... */
 export default defineConfig({
   /* snip... */
   test: {
     /* snip... */
     deps: {
       inline: [
        /* snip... */
+       /solid-testing-library/,
      ],
     },
   },
 });

If you do that it will work 100% of the time, regardless of which package management tool you use (npm, yarn, pnpm, etc.)


Want to know why?

If you only care about fixing the issue then you’re done with this comment. If, on the other hand, you’re curious as to what actually happened, then read on.

Why the error 'solid-js/web' does not provide an export named 'hydrate'?

First, some context. Vite, like many bundlers, supports a system called “conditional exports”, which allows you to define a set of exports from your package that should be used in different situations. solid-js/web uses this system to export different files based on whether you are on the server or the client. This is useful for keeping bundle size down without having to manually specify different imports for every place the code will be used (especially problematic in ssr environments, where the same code runs on the client and the server), and ensuring that the code actually runs (client bundles can include browser APIs and polyfills, for example, while server code doesn’t need it).

Solid’s ts-vitest template defines the resolve conditions to be ["development", "browser"], which should import Solid via the exports.browser.development.import path. When running vitest, however, the wrong package gets imported. Instead of exports.browser.development.import, the system instead imports exports.node.import. Since this is a server file it has no need for any client-side APIs, and thus doesn’t export hydrate or render.

Why did it work with pnpm then?

Ah, what an excellent question. Deep inside of Vitest’s module resolution code is a seemingly simple function: _shouldExternalize(id). This function answers a simple question: do we need to transform the file before exporting it, or can we “externalize” the import (ask Node to do it for us). The logic in there isn’t too complex, but the part we care about is this line: if (matchExternalizePattern(id, depsExternal)) return id. This says “if the id (which is a filepath) matches any of the depsExternal patterns, then use Node’s default import() rather than transforming the file ourselves”. The problem is that the default depsExternal patterns includes a match for /\.mjs$/, which means that any file that ends with .mjs is handed off to Node to resolve. This is a problem because this library, as @KaiHuebner guessed, exports a file named solid-testing-library/dist/index.mjs, which imports hydrate and render from solid-js/web. When Vitest gets to this file it matches against the depsExternal pattern which tells it to use Node’s import. Node then tries to resolve the import to solid-js/web the way it knows how: by importing the file defined in the exports.node.import key. Since this is a server file, it doesn’t contain an export named hydrate, and so the import fails.

The reason it works when installed via pnpm is because of another check in earlier in _shouldExternalize(). Vitest provides an option called deps which allows you to override which modules should and should not be externalized. The second piece of the puzzle is again the ts-vitest template, which sets deps.internalize to [/solid-js/]. This is intended to force solid-js to be transformed, rather than going through Node’s import, which is does, but only when solid-js is itself imported from a transformed file. The reason it works with pnpm though is because of the install paths. When using npm, solid-testing-library is imported from node_modules/solid-testing-library/dist/index.mjs. But when using pnpm it’s imported from node_modules/.pnpm/solid-testing-library@0.3.0_solid-js@1.4.5/node_modules/solid-testing-library/dist/index.mjs. If you look closely, you can see that path contains “solid-js”, which means that it is caught by the /solid-js/ regex and told to internalize. When installed via npm, the path doesn’t contain solid-js, and thus doesn’t get told to externalize.

This is why the short fix is to add /solid-testing-library/ to the deps.internalize array fixes the issue, it tells vitest to always internalize solid-testing-library, thus ensuring it reads the right exports and thus finds the hydrate and/or render functions it needs.

@atk Not quite. In order for this to work we need _shouldExternalize(id) to return false. There are only 3 cases where that happens:

  1. If the id matches deps.inline
  2. If the id matches defaultInline
  3. If all other checks fail

Option 1 is the quick fix proposed above, but it requires manual configuration. Option 2 is out because we can’t influence defaultInline, nor can we make the import be one of those options. So we’re left with option 3: bypass all the other checks.

In order to reach the end of _shouldExternalize() we need the following:

  1. isNodeBuiltin(id) must be false – this is easy, since it’s not a node builtin.
  2. id must NOT be a data: url – also easy, it’s a file
  3. id must NOT match deps.external – again, easy
  4. id must NOT match the depsExternal pattern – this means the file can’t end in .cjs.js or .mjs
  5. EITHER:
    • id MUST NOT be in **/dist/** (easy) OR **/node_modules/** (impossible)
    • OR isValidNodeImport(id) must return false

We can control everything up to the final check, but since the file will always be inside of node_modules we are forced to go a step further: ensure that isValidNodeImport("path/to/solid-testing-library/index/file") returns false. So lets look at when it does so:

  1. If id has a protocol that isn’t in the list of allowed protocols (defaults to node:, file:, or data:)
  2. If the extension is anything other than .js
  3. If the path ends with .es.js or .esm.js
  4. If the actual code has signs of ESM syntax but NOT signs of CJS syntax

Option 1 is ruled out because we can’t control the protocol. Option 2 is possible, but the only extensions options that I know of are .ts, .tsx, .js, .jsx, .cjs and .esm, and of those all but .cjs and .js are already ruled out, and you can’t (or at least really shouldn’t) put ESM code in a .cjs file, so that leaves just .js. Option 4 is also ruled out because just before it is a check for package.type === "module", which would cause it to return true.

All of this comes together to mean that the only way I can see to get around this without needing to set deps.inline manually is to have the file end in .esm.js. However I can’t comfortably recommend this solution. That behavior is several dependencies deep and is NOT one of the API guarantees of either package, so while it should work for now, I don’t feel confident that it will continue working in the future.

I think the most reliable way forwards is to have prominent documentation here in solid-testing-library showing how to set it up with vitest (specify deps.inline either to include solid-testing library, to include all of node_modules, or to just be true which inlines everything), and hope that the vitest team comes up with some solution on their end for this to be nicer in the future.

I’ve run into the same issue and cannot use pnpm (I’m behind a corporate proxy on Windows, and can’t get it to install)

I would appreciate if this could be reopened and further investigated, please

I had the same issue with the ts/vitest template. npm test would fail. Tests pass with pnpm.

I am investigating further, but this issue seems to be on the side of vitest, not solid.

Hi guys, thanks for the quick response.

Yes, I installed jsdom version 19.0.0.

I’m not using any server side rendering, so I actually don’t need the hydrate function.

I was thinking it has something to do with the import statement in: node_modules/solid-testing-library/dist/index.mjs

When I change line 2 to: import { render as solidRender } from "solid-js/web";

Then I get a different error message: SyntaxError: The requested module 'solid-js/web' does not provide an export named 'render'