Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

default transformFunctions doesn't include jest-resolve's Resolver.resolveModule #375

Open
0xdevalias opened this issue Nov 27, 2019 · 1 comment

Comments

@0xdevalias
Copy link

0xdevalias commented Nov 27, 2019

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:

Looking at normalizeTransformedFunctions I can see that our custom transformFunctions are appended to the list of defaults:

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:

But given the project is looking for maintainers (#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:

@0xdevalias
Copy link
Author

0xdevalias commented Nov 28, 2019

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:

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant