Code Monkey home page Code Monkey logo

xunnamius / babel-plugin-explicit-exports-references Goto Github PK

View Code? Open in Web Editor NEW
16.0 2.0 2.0 998 KB

Transforms all internal references to a module's exports such that each reference starts with "module.exports" instead of directly referencing an internal name.

Home Page: https://www.npmjs.com/package/babel-plugin-explicit-exports-references

License: MIT License

JavaScript 50.21% TypeScript 49.79%
babel default exports explicit references same module jest test refer

babel-plugin-explicit-exports-references's Introduction

Black Lives Matter! Maintenance status Last commit timestamp Open issues Pull requests codecov Source license NPM version semantic-release

babel-plugin-explicit-exports-references

Transforms all internal references to a module's exports such that each reference starts with module.exports instead of directly referencing an internal name. This enables easy mocking of specific (exported) functions in Jest with Babel/TypeScript, even when the mocked functions call each other in the same module.

Installation and Usage

npm install --save-dev babel-plugin-explicit-exports-references

And in your babel.config.js:

module.exports = {
  plugins: ['explicit-exports-references']
};

Note: it is recommended that this plugin only be enabled when NODE_ENV is test. Using this plugin elsewhere, such as in production, can lead to increased build size. For example:

module.exports = {
  parserOpts: { ... },
  plugins: [ ... ],
  env: {
      test: {
        plugins: ['explicit-exports-references']
      }
  }
};

Finally, run Babel through your toolchain (Webpack, Jest, etc) or manually:

npx babel src --out-dir dist

AST Algorithm

To see detailed output on how this plugin transforms your code, enable debug mode: DEBUG='babel-plugin-explicit-exports-references:*' npx jest your-test-file

The plugin begins by looking for default and named export declarations in a program.

For default exports, it looks for function declarations and class declarations that have ids (i.e. variable names), like export default function main() {}, and updates any Identifiers referencing that id.

For named exports, it looks for function and class declarations too, but also variable declarations like export const foo = 5; and export { x as default, y, x as z };. Enums are explicitly ignored. Any Identifiers that reference the declaration's id or specifier are updated.

When updating references, by default only Identifiers are transformed. Assignment Expressions can also be transformed, but doing so is currently unstable. All other reference types are ignored, including TypeScript types and Identifiers within their own declarations.

The following enables transforming Assignment Expressions along with Identifiers:

module.exports = {
  plugins: [
    ['explicit-exports-references', { transformAssignExpr: true }]
  ]
};

Motivation

Suppose we have the following myModule.ts TypeScript file:

// file: myModule.ts

export function foo() {
  // This function works fine in production but throws on our local test machine
  throw new Error('failed to do expensive network stuff');
  return;
}

export function bar() {
  // ...
  foo();
  return 5;
}

export function baz() {
  // ...
  foo();
  return 50;
}

Lets say we want to unit test myModule.ts. Specifically, we want to test bar and baz. We don't want to unit test foo because a) attempting to run it on our local machine will always fail, which is why b) it is covered by our integration tests instead. We simply want to ensure bar and baz work, and that they both call foo without running foo.

If we expect a function to be called, and we want an alternative implementation run when it is called, the easy and obvious solution is to mock it.

So suppose we create the following myModule.test.ts Jest test file, mocking foo with a noop:

// file: myModule.test.ts

import * as myModule from './myModule';

it('bar does what I want', () => {
  const spy = jest.spyOn(myModule, 'foo').mockImplementation(() => undefined);

  expect(myModule.bar()).toBe(5);
  expect(spy).toBeCalled();

  spy.mockRestore();
});

it('baz does what I want', () => {
  const spy = jest.spyOn(myModule, 'foo').mockImplementation(() => undefined);

  expect(myModule.baz()).toBe(50);
  expect(spy).toBeCalled();

  spy.mockRestore();
});

This file tests that bar and baz do what we want, and whenever they call foo the dummy version is called instead and no error is thrown. Or rather, that seems like it should be the thing that happens. Unfortunately, if we run this code, the above tests will fail because foo throws failed to do expensive network stuff.

Is this a bug?

After encountering this problem over five years ago, someone posed the question to the Jest project: how do you mock a specific function in a module?, to which a contributor responded:

Supporting the above by mocking a function after requiring a module is impossible in JavaScript – there is (almost) no way to retrieve the binding that foo refers to and modify it.

However, if you change your code to this:

var foo = function foo() {};
var bar = function bar() {
  exports.foo();
};

exports.foo = foo;
exports.bar = bar;

and then in your test file you do:

var module = require('../module');
module.foo = jest.fn();
module.bar();

it will work just as expected. This is what we do at Facebook where we don't use ES2015.

While ES2015 modules may have immutable bindings for what they export, the underlying compiled code that babel compiles to right now doesn't enforce any such constraints. I see no way currently to support exactly what you are asking...

Essentially, this plugin aims to automate the suggestion above, allowing you to mock a specific module function using standard Jest spies by automatically replacing references to exported identifiers with an explicit reference of the form module.exports.${identifier}. No assembly required.

With this plugin loaded into Babel, the tests in the motivating example above pass! πŸŽ‰

Prior Art

Prior solutions include:

Further reading and additional solutions:

Documentation

Further documentation can be found under docs/.

Contributing and Support

New issues and pull requests are always welcome and greatly appreciated! 🀩 Just as well, you can star 🌟 this project to let me know you found it useful! ✊🏿 Thank you!

See CONTRIBUTING.md and SUPPORT.md for more information.

babel-plugin-explicit-exports-references's People

Contributors

dependabot[bot] avatar xunnamius avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

playvs-inc

babel-plugin-explicit-exports-references's Issues

License?

Aloha! Thank you for crafting this plugin. Extremely helpful. I was just curious if you had a particular license file in mind/plan to add to repo?

Breaks ES Modules

When transpiling to ES Modules (including setting "type": "module" in package.json) this breaks Jest tests with the error:

ReferenceError: module is not defined

It seems it doesn't take the targeted build type into account and just assumes it's building CommonJS modules. I know the options are limited and I'm not certain what the best solution is, but since ES Module adoption is only increasing it would be nice to have the option to not produce a broken build with Jest. Just wanted to at least raise the issue for consideration.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.