babel-plugin-rewire-exports

Babel plugin for stubbing (ES6, ES2015) module exports

Usage no npm install needed!

<script type="module">
  import babelPluginRewireExports from 'https://cdn.skypack.dev/babel-plugin-rewire-exports';
</script>

README

babel-plugin-rewire-exports

NPM Version NPM Downloads Build Status

Babel plugin for stubbing (ES6, ES2015) module exports. It allows to rewire the exported values in all the importing modules. Unlike babel-plugin-rewire it doesn't modify the module internals (e.g. imports and top-level variables and functions). See How it works section for implementation details.

Exports

Plugin transforms module exports in such a way that they can be stubbed (or "rewired") via the following API:

  • default export - plugin exports additional rewire(stub) function that allows to replace the original
  • named exports - for each export (e.g. export var foo) an additional function rewire$foo(stub) is exported
  • restore() function allows to restore the exports to their original values
  • if there are existing rewire or restore top-level identifiers, the generated exports will be named rewire$default and restore$rewire respectively

Example

Named export:

//------ text.js ------
export let message = 'Hello world!'

//------ logger.js ------
import {message} from './text.js'

export default function () {
  console.log(message)
}

//------ main.js ------
import {rewire$message, restore} from './text.js'
import logger from './logger.js'

logger() // 'Hello world!'
rewire$message('I am now rewired')
logger() // 'I am now rewired'
restore()
logger() // 'Hello world!'

Default export:

//------ fetch.js ------
export default function (url) {
  // perform some expensive remote call
}

//------ adapter.js ------
import fetch from './fetch.js'

export function fetchItems() {
  return fetch('/items')
}

//------ test.js ------
import {rewire, restore} from './fetch.js'
import {fetchItems} from './adapter.js'

// Jasmine example
describe('adapter', function () {
  beforeEach(function () {
    rewire(this.spy = jasmine.createSpy('fetch'))
  })
  afterAll(restore)
  
  it('should call fetch', function () {
    fetchItems()
    expect(this.spy).toHaveBeenCalledWith('/items')
  })
})

// Mocha/Chai and Sinon example
describe('adapter', function () {
  var spy

  beforeEach(function () {
    rewire(spy = sinon.spy())
  })
  after(restore)
  
  it('should call fetch', function () {
    fetchItems()
    expect(spy.withArgs('/items').calledOnce).to.be.true
  })
})

Compatibility

How it works

In ES6, imports are live read-only views on exported values:

//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}

//------ main1.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

// The imported value can’t be changed
counter++; // TypeError

This allows for any exports to be overwritten from within the module - and imports will be automatically updated via their bindings.

Transformations

Here's how various kinds of export declarations are transformed:

  • Literals (export default 'foo') - the original value is copied to a variable to be stubbed and restored later: export {_default as default}

  • Variables:

    • named exports (export var foo, export let bar or export {baz}) are left intact, but their initial values are similarly copied to temp variables.
    • default export (export default foo) is converted to a named export to enable live binding: export {foo as default}
  • Constants (export const foo = 'bar' or export default foo) are treated similar to variables, but their values are not modified within the module (since they are read-only) - only exported values are rewired:

    • named exports: export { _foo as foo }
    • default export: export { _default as default }

    You can use unsafeConst option to convert const to let in order to enable live binding.

  • Functions (export default function () {…} or export function foo() {…}) are converted into a function expression and exported variable by the same name. The variable initialization is hoisted to the top of the scope to preserve existing behavior.

  • Classes (export default class foo {…} or export class foo {…}) are handled similarly to functions except the variables are not hoisted (again to preserve the existing behavior).

  • Re-exports (export * from './foo.js' or export {bar} from 'baz') are ignored. They can be rewired in the original modules.

  • Immutable values such as undefined, globals and imports are copied similar to literals.

Installation

$ npm install babel-plugin-rewire-exports

Usage

Via .babelrc (Recommended)

.babelrc

// without options
{
  "plugins": ["rewire-exports"]
}

// with options
{
  "plugins": [
    ["rewire-exports", {
      "unsafeConst": true
    }]
  ]
}

Via CLI

$ babel --plugins rewire-exports script.js

Via Node API

require("@babel/core").transform("code", {
  plugins: ["rewire-exports"]
});

Options

unsafeConst

boolean, defaults to false.

Constants cannot be rewired if you're targeting ES2015+, because the plugin relies on variables being assign-able in order to work. However setting unsafeConst: true will convert export const foo = 'bar' to export let foo = 'bar'. This will allow to treat constant exports as regular variables. This is potentially unsafe if your code relies on constants being read-only.