default transformFunctions doesn't include jest-resolve's Resolver.resolveModule
I've configured my alternate search paths as part of this module's roots (rather than in jest's config), and I get the following:
Cannot find module 'shared/selectors' from 'mock-helpers.js'
at Resolver.resolveModule (node_modules/jest-resolve/build/index.js:259:17)
at require (spec/javascripts/helpers/mock-helpers.js:158:34)
at Object.<anonymous> (spec/javascripts/helpers/setupTests.js:10:1)
This appears to be because jest-resolve's Resolver.resolveModule isn't included in the list of default transformFunctions:
- https://github.com/tleunen/babel-plugin-module-resolver/blob/master/DOCS.md#transformfunctions
- https://github.com/tleunen/babel-plugin-module-resolver/blob/master/src/normalizeOptions.js#L14-L28
Looking at normalizeTransformedFunctions I can see that our custom transformFunctions are appended to the list of defaults:
- https://github.com/tleunen/babel-plugin-module-resolver/blob/master/src/normalizeOptions.js#L127-L133
function normalizeTransformedFunctions(optsTransformFunctions) {
if (!optsTransformFunctions) {
return defaultTransformedFunctions;
}
return [...defaultTransformedFunctions, ...optsTransformFunctions];
}
Using a custom jest resolver we can log more info just before this crash happens:
// jest.config.js
resolver: '<rootDir>/jestResolverWithLogging.js',
// jestResolverWithLogging.js',
const resolverWithLogging = (path, options) => {
if (path === 'shared/selectors') {
console.trace('[jestResolverWithLogging]\n', path, '\n', options)
}
return options.defaultResolver(path, options)
}
module.exports = resolverWithLogging
Which shows the following just before it crashes:
Trace: [jestResolverWithLogging]
shared/selectors
{ basedir: '/Users/devalias/dev/REDACTED/spec/javascripts/helpers',
browser: false,
defaultResolver: [Function: defaultResolver],
extensions: [ '.js', '.json', '.jsx', '.ts', '.tsx', '.node' ],
moduleDirectory: [ 'node_modules' ],
paths: undefined,
rootDir: '/Users/devalias/dev/REDACTED' }
at resolverWithLogging (/Users/devalias/dev/REDACTED/jestResolverWithLogging.js:3:13)
at Function.findNodeModule (/Users/devalias/dev/REDACTED/node_modules/jest-resolve/build/index.js:141:14)
at resolveNodeModule (/Users/devalias/dev/REDACTED/node_modules/jest-resolve/build/index.js:203:16)
at Resolver.resolveModuleFromDirIfExists (/Users/devalias/dev/REDACTED/node_modules/jest-resolve/build/index.js:214:16)
at Resolver.resolveModule (/Users/devalias/dev/REDACTED/node_modules/jest-resolve/build/index.js:252:12)
at Resolver._getVirtualMockPath (/Users/devalias/dev/REDACTED/node_modules/jest-resolve/build/index.js:390:14)
at Resolver._getAbsolutePath (/Users/devalias/dev/REDACTED/node_modules/jest-resolve/build/index.js:376:14)
at Resolver.getModuleID (/Users/devalias/dev/REDACTED/node_modules/jest-resolve/build/index.js:348:31)
at Runtime._shouldMock (/Users/devalias/dev/REDACTED/node_modules/jest-runtime/build/index.js:942:37)
at Runtime.requireModuleOrMock (/Users/devalias/dev/REDACTED/node_modules/jest-runtime/build/index.js:595:16)
This looks as though we should be able to use add Resolver.resolveModule to our custom transformFunctions:
transformFunctions: ['Resolver.resolveModule'],
Though unfortunately this doesn't seem to work.. presumably because jest-resolve is in our node_modules, and that isn't getting transpiled by babel, so this plugin never gets a chance to hook it.
At this stage I could probably look at hacking something together using a custom resolvePath function:
- https://github.com/tleunen/babel-plugin-module-resolver/blob/master/DOCS.md#resolvepath
- https://github.com/tleunen/babel-plugin-module-resolver/blob/master/DOCS.md#for-plugin-authors
But given the project is looking for maintainers (https://github.com/tleunen/babel-plugin-module-resolver/issues/346).. it might be better for me to find an alternative solution to my needs.
For those that come across this in future, I was originally wanting centralise my webpack resolving/aliasing in babel, so it could 'just work' among webpack, jest, babel, etc.
Some things that attempt to do similar'ish:
- https://www.npmjs.com/package/jest-webpack-resolver
- https://github.com/mkg0/jest-webpack-resolver
- last updated March 2018
- seems to use some pretty outdated methods to find config files, etc
- Main functionality happening here: https://github.com/mkg0/jest-webpack-resolver/blob/master/index.js#L74-L103
- https://github.com/mkg0/jest-webpack-resolver
- https://www.npmjs.com/package/jest-webpack
- https://github.com/mzgoddard/jest-webpack
- last updated July 2018
- This seems more aimed at running jest after you run your files fully through webpack, which could be useful, but far heavier than what I wanted at this stage.
- https://github.com/mzgoddard/jest-webpack
- https://jestjs.io/docs/en/webpack
- Talks about built in jest options that can be used to get similar config as webpack options
- See also the config settings I called out in https://github.com/tleunen/babel-plugin-module-resolver/issues/346#issuecomment-558917168
- https://survivejs.com/webpack/techniques/testing/#jest
-
You can find a lot of testing tools for JavaScript. The most popular options work with webpack after you configure it right. Even though test runners work without webpack, running them through it allows you to process code the test runners do not understand while having control over the way modules are resolved. You can also use webpack's watch mode instead of relying on one provided by a test runner. -> Porting a webpack setup to Jest requires more effort especially if you rely on webpack specific features. The official guide covers quite a few of the common problems. You can also configure Jest to use Babel through babel-jest as it allows you to use Babel plugins like babel-plugin-module-resolver to match webpack's functionality. -> jest-webpack provides an integration between webpack and Jest.
-
In the end I wrote a custom jest resolver that reads from my webpack config, uses webpack-merge to merge relevant aspects of my jest config + specific custom test only overrides, and then resolves using enhanced-resolve:
- https://github.com/survivejs/webpack-merge
- https://github.com/webpack/enhanced-resolve
The implementation could definitely be cleaned up and made a bit more robust/generic, probably some generic 'xConfigAdapter' type things to map jest/babel/etc config -> the appropriate webpack-merge format, and could probably even work it into babel itself.. but here is what I came up with:
const fs = require('fs')
const path = require('path')
const webpackMerge = require('webpack-merge')
const { CachedInputFileSystem, ResolverFactory } = require('enhanced-resolve')
// TODO: If we wanted to read config out of babel..
// Ref: https://babeljs.io/docs/en/babel-core#advanced-apis
const getSanitisedNodeEnv = () => {
const nodeEnv = process.env.NODE_ENV
switch (nodeEnv) {
case 'development':
case 'test':
case 'production':
return nodeEnv
default:
return 'test'
}
}
const root = fs.realpathSync(process.cwd())
// We want to be able to import from our webpack config
// Refs:
// https://webpack.js.org/configuration/resolve/
// https://webpack.js.org/api/resolvers/
const webpackConfig = require(`${root}/config/webpack/${getSanitisedNodeEnv()}`)
const jestDefaults = require('jest-config').defaults
// We want to be able to import from our jest config
const jestConfig = require(`${root}/jest.config`)
// Ref: https://github.com/facebook/jest/blob/master/packages/jest-runtime/src/cli/index.ts#L59-L68
// TODO: Read the jest config 'properly' (including parsing everything)
// Validation Error: Module jest-localstorage-mock in the setupFiles option was not found.
// console.warn(require('jest-config').readConfig({}, root))
const jestRootDir = jestConfig.rootDir || root
const rootDirReplacer = path => path.replace('<rootDir>', jestRootDir)
const mappedJestDefaults = {
extensions: jestDefaults.moduleFileExtensions.map(ext => `.${ext}`),
}
// Map the relevant jest config options into webpack config layout
// Ref: https://jestjs.io/docs/en/configuration
const mappedJestConfig = {
modules: [
...(jestConfig.roots || []).map(rootDirReplacer),
...(jestConfig.modulePaths || []).map(rootDirReplacer),
...(jestConfig.moduleDirectories || []),
],
extensions: jestConfig.moduleFileExtensions,
}
// TODO: handle https://jestjs.io/docs/en/configuration#modulenamemapper-objectstring-string
// TODO: handle https://jestjs.io/docs/en/configuration#modulepathignorepatterns-arraystring
// Refs:
// https://webpack.js.org/configuration/resolve/#resolvealias
// Note: resolve.alias takes precedence over other module resolutions.
// https://github.com/tleunen/babel-plugin-module-resolver/blob/master/DOCS.md#alias
const jestAliases = {
alias: {
'test-helpers': path.resolve(jestRootDir, 'spec/javascripts/helpers'),
},
}
// Note: When not defined here the strategy defaults to 'append'
const mergeStrategies = {}
// Ref: https://github.com/survivejs/webpack-merge#merging-with-strategies
const mergedResolverFactoryConfig = webpackMerge.smartStrategy(mergeStrategies)(
webpackConfig.resolve,
mappedJestDefaults,
mappedJestConfig,
jestAliases
)
// Refs:
// https://github.com/webpack/enhanced-resolve
// https://github.com/webpack/enhanced-resolve#creating-a-resolver
// https://github.com/webpack/enhanced-resolve#resolver-options
// https://github.com/webpack/enhanced-resolve/wiki/Plugins
// https://github.com/webpack/enhanced-resolve/blob/master/lib/ResolverFactory.js
// https://github.com/webpack/enhanced-resolve/blob/master/lib/node.js#L76-L91
const createSyncResolver = options => {
const resolver = ResolverFactory.createResolver({
useSyncFileSystemCalls: true,
fileSystem: new CachedInputFileSystem(fs, 4000),
...options,
})
// https://github.com/webpack/enhanced-resolve/blob/master/lib/node.js#L13-L15
const context = {
environments: ['node+es3+es5+process+native'],
}
return (baseDir, thingToResolve) =>
resolver.resolveSync(context, baseDir, thingToResolve)
}
const webpackResolver = createSyncResolver(mergedResolverFactoryConfig)
// Ref: https://jestjs.io/docs/en/configuration#resolver-string
const jestWebpackResolver = (path, options) => {
let webpackResolved
let defaultResolved
try {
webpackResolved = webpackResolver(options.basedir, path)
} catch (_error) {
defaultResolved = options.defaultResolver(path, options)
console.warn(
'[JestWebpackResolver] WARNING: Failed to resolve, falling back to default jest resolver:\n',
{ path, options, webpackResolved, defaultResolved }
)
}
return webpackResolved || defaultResolved
}
module.exports = jestWebpackResolver