vitest

Supply alias extensions for `.js` files?


I am migrating to Vitest from Jest. My tsconfig.json is configured for module and module resolution nodeNext, which requires that you specify the file extension for imports statements. The compilation process doesn't modify the import statement extensions either - so if you want to import things from a .ts file in the /src folder, then you would import it as a .js file knowing that the .ts file will compile to .js.

However, Vitest doesn't compile everything before it runs it. It actually needs the uncompiled .ts extension corresponding to what is found in the /src directory, since that's where it is executing from rather than from the /dist directory. As such, I need to configure Vitest so that when it sees a .js extension in an import statement, it tries a variety of options in-order like: .ts, .tsx, etc.


Solution

  • I found a somewhat hacky solution by combining resolve.alias and resolve.extensions.

    resolve.extensions provides the behavior that I would like, with the caveat that it only applies to file imports without an extension. Since nodeNext forces me to include file extensions, this configuration does nothing in and of itself.

    However, with a little experimenting, I confirmed that resolve.extensions is applied AFTER resolve.alias. So I can use an alias to convert a .js file import into an extensionless file import, which in turn allows resolve.extensions to kick in and do its job.

    My configuration now looks like so:

    resolve: {
        extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'],
        alias: [
          {
            find: /^(.*)\.css$/,
            replacement: "$1.scss"
          },
          {
            find: /^(.*)\.(m?)js$/,
            replacement: "$1" 
          }
        ]
      }
    

    This feels incredibly hacky... but it works for now. I think a more optimal solution would be to write a custom resolver. Unfortunately, I have yet to find any real documentation on how to write a custom resolver. I can't even find a proper signature for the custom resolver...

    I will leave this question open for now in hopes of a better solution. If someone can supply a custom resolver for the job, that would be greatly appreciated!


    EDIT: I found that with the @rollup/plugin-node-resolve package I can further improve my answer. I can use it to construct a resolver that searches for a valid extension on a case-by-case basis rather than relying upon the global resolve.extensions. Below is my updated code that reflects this, with one resolver for .js files and another for .css files.

    import { defineConfig } from 'vitest/config'
    import resolve from '@rollup/plugin-node-resolve';
    
    const cssResolver = resolve({
      extensions: ['.css', '.scss', '.sass']
    });
    
    const jsResolver = resolve({
      extensions: ['.mjs', '.js', '.jsx', '.mts', '.ts', '.tsx']
    });
    
    export default defineConfig({
      ...
      resolve: {
          {
            find: /^(.*)\.css$/,
            replacement: "$1",
            customResolver: cssResolver
          },
          {
            find: /^(.*)\.(m?)js$/,
            replacement: "$1", 
            customResolver: jsResolver
          }
        ]
      }
    });
    

    While I would still prefer a customResolver that works without having to remove the file extension first, that is a minor gripe. This looks like a solid approach that should work well moving forward :)